Deliverables and the journal
Every task completion produces a structured handoff — in the deliverables array, on the assignment row, and in /task/JOURNAL.md.
When a coordinator marks a task completed, three things happen at once:
- An entry appends to the board item's
deliverablesarray. - The handoff fields merge into the assignment row's
result_payload, andresult_summarymirrors to its own column. - An entry appends to
/task/JOURNAL.md.
This page covers that handoff — what it carries, where it lands, and which surface to read for which purpose.
The handoff payload
The coordinator agent reports a completion by calling update_project_task with status="completed". The runtime validates the payload before accepting the transition:
| Field | Required | Constraint |
|---|---|---|
result_summary | yes | ≥ 30 characters after trimming |
artifacts | yes | at least one entry; every declared path must exist on disk |
findings | optional | ≤ 200 characters, single line |
cautions | optional | ≤ 200 characters, single line |
next | optional | ≤ 200 characters, single line |
artifacts is a list of objects, each carrying a path. Existence is checked per path at completion time — a path that names a file the agent never wrote rejects the transition. Paths are conventionally under /task/artifacts/, but the check is existence, not prefix.
A payload that fails any constraint rejects the status change with a BadRequest. The agent sees the error and corrects it on the next iteration. The same validation applies to the other terminal statuses that require a summary (failed, blocked, rejected, needs_clarification); only completed also requires artifacts.
Where the handoff lands
The deliverables array on the board item
{
"id": "board_item_id",
"title": "30s teaser for launch",
"status": "completed",
"deliverables": [
{
"at": "2026-05-20T14:33:12Z",
"assignment_id": "1234567890",
"by_thread_id": "987654",
"by_agent_name": "video-coordinator",
"result_summary": "Rendered 30s teaser with 4 cuts and music bed.",
"artifacts": [{ "path": "/task/artifacts/teaser-final.mp4" }],
"findings": "Source clips needed -2dB normalization.",
"cautions": "Audio drift after 25s — re-encode source if reusing.",
"next": "Run color grade pass before publish."
}
]
}Each completion appends a new entry. Reopen and recomplete a task and you get multiple entries — the array is the audit trail. This is the surface your UI should read: stable, ordered, and queryable.
The assignment's result_payload
findings, cautions, and next also shallow-merge into the assignment row's result_payload JSONB, and result_summary mirrors to the assignment's result_summary column. Read these when you are querying assignment-level history — one row per executor run — rather than task-level deliverables.
/task/JOURNAL.md
A markdown entry appends to the journal on every completion:
## [2026-05-20T14:33:12Z] · video-coordinator · completed
outcome: Rendered 30s teaser with 4 cuts and music bed.
findings: Source clips needed -2dB normalization.
cautions: Audio drift after 25s — re-encode source if reusing.
artifacts: /task/artifacts/teaser-final.mp4
next: Run color grade pass before publish.
<!-- assignment:1234567890 -->The <!-- assignment:N --> marker makes the append idempotent. If the same handoff fires twice, the second append short-circuits — the marker is already present.
The journal is the agent's working memory across iterations. At prompt-build time the runtime injects the tail of JOURNAL.md — the last 60 lines, drawn from the most recent 16 KB — so an agent with an active board item sees recent handoffs without querying. This is plumbing the agent reads, not a surface for your UI.
Surfacing deliverables in your UI
"use client";
import { useProjectTaskBoardItem } from "@wacht/nextjs";
export function DeliverablesPanel({ projectId, taskId }: { projectId: string; taskId: string }) {
const { item } = useProjectTaskBoardItem(projectId, taskId);
const deliverables = item?.deliverables ?? [];
if (deliverables.length === 0) {
return <div>No deliverables yet. The agent is still working.</div>;
}
return (
<ol>
{deliverables.map((d, i) => (
<li key={i}>
<header>
<strong>{d.by_agent_name}</strong>
<time>{d.at}</time>
</header>
<p>{d.result_summary}</p>
{d.findings && <p><b>Findings:</b> {d.findings}</p>}
{d.cautions && <p><b>Cautions:</b> {d.cautions}</p>}
{d.next && <p><b>Next:</b> {d.next}</p>}
<ul>
{d.artifacts.map((a) => (
<li key={a.path}>
<a href={`/api/task/${taskId}/file?path=${encodeURIComponent(a.path)}`}>{a.path}</a>
</li>
))}
</ul>
</li>
))}
</ol>
);
}useProjectTaskBoardItem(projectId, taskId) revalidates on a 15-second interval, so a new deliverable surfaces within that window and the list re-renders. Each artifact is an object with a path — iterate over a.path, not the entry directly.
Reading the journal directly
To show the markdown narrative — a "history" tab, say — read /task/JOURNAL.md through the workspace file method on the same hook:
"use client";
import { useEffect, useState } from "react";
import { useProjectTaskBoardItem } from "@wacht/nextjs";
export function JournalView({ projectId, taskId }: { projectId: string; taskId: string }) {
const { getTaskWorkspaceFile } = useProjectTaskBoardItem(projectId, taskId);
const [text, setText] = useState<string | null>(null);
useEffect(() => {
getTaskWorkspaceFile("/task/JOURNAL.md").then((res) => setText(res.data?.content ?? ""));
}, [getTaskWorkspaceFile]);
if (text === null) return <div>Loading…</div>;
return <pre>{text}</pre>;
}getTaskWorkspaceFile is a method on the hook, not a hook of its own — call it on demand rather than subscribing. Render the raw text through react-markdown if you want formatting. Remember the journal is the agent's memory; don't put it where a user expects to see results.
Multi-agent handoffs
In a multi-lane flow an executor finishes its assignment without marking the task completed — other lanes may still be running. The executor's handoff records on its assignment row (result_payload), but it does not append to deliverables yet.
The deliverable lands on the array only when the coordinator (or a conversation thread) calls update_project_task(status="completed", ...). The coordinator's result_summary summarizes the whole task; per-lane summaries live on each assignment.
So:
deliverables[]— task-level summaries, one per round trip through completion.result_payloadon assignments — per-lane summaries, one per executor run.
Read deliverables[] for the headline. Read both when you want the full picture of who did what.
When to use which surface
| You want | Read |
|---|---|
| The latest output | deliverables[last].artifacts |
| Every completion, structured | deliverables (the array) |
| The narrative history | /task/JOURNAL.md |
| Findings/cautions per assignment | result_payload on the assignment row |
| The summary line in a task list | result_summary on the assignment row |
The data is duplicated across these places on purpose — each is optimized for a different access pattern. Most UIs need only deliverables.
Where to go next
- Multi-agent orchestration — how lanes coordinate their handoffs
- Realtime UI — surfacing deliverables as they land
- Workspace and artifacts — the file side of handoffs