cbc / codebuddy) exposes the progress and lifecycle of Dynamic Workflow over the stdio stream-json channel, as well as the alignment between this protocol and Claude Code 2.1.220. The goal is to enable any SDK / daemon / CI implemented according to the official Claude Code stream-json protocol to consume cbc workflow events without code changes.cbc --input-format stream-json --output-format stream-json (that is, headless stdio). The same wire format is also used in scenarios with -p / --print and --output-format=stream-json.system/task_* message family and does not introduce a separate top-level type. cbc follows the same convention:Focus | Online Form (Consistent with Claude Code 2.1.220 and cbc) |
Workflow run declaration | {"type":"system","subtype":"task_started","task_type":"local_workflow","workflow_name":"...", ...} |
Phase/sub-agent progress | {"type":"system","subtype":"task_progress","workflow_progress":[{"type":"workflow_phase",...}, {"type":"workflow_agent",...}], ...} |
Status transition | {"type":"system","subtype":"task_updated","patch":{"status":"running|completed|failed|killed", ...}} |
Final state | {"type":"system","subtype":"task_notification","status":"completed|failed|stopped", ...} |
Interrupt/cancel | {"type":"control_request","request":{"subtype":"interrupt", ...}} (session-level) |
type:"workflow" event. Any consumer that speaks the Claude Code stream-json protocol dialect can seamlessly drive and observe cbc workflows.\\n. It is not SSE.control_request + user messages.system (including subtype), assistant, user, result, control_response.Trigger Condition | Description |
CODEBUDDY_DISABLE_WORKFLOWS=1(env) | Disable the entire Workflow feature. |
settings.json: { "disableWorkflows": true } | Same as above, but disabled at the user/project granularity. |
Workflow tool is not registered and does not produce workflow-shaped task_started / task_progress events. Downstream consumers only see ordinary system/task_* messages from other background tasks.// 1. initialize{"type":"control_request","request_id":"init","request":{"subtype":"initialize","hooks":{},"capabilities":{},"hasPrompt":true}}// 2. Have the agent run a workflow once (the model will call the `Workflow` tool).{"type":"user","message":{"role":"user","content":"run /deep-research topic=foo"}}// 3. Stop everything (abort both the workflow and all sub-agents).{"type":"control_request","request_id":"stop-1","request":{"subtype":"interrupt","session_id":"<sessionId>","reason":"user cancel"}}
session_id of interrupt is taken from the earlier stdout system/init event.interrupt aborts both the workflow and in-flight sub-agent model streams simultaneously.control_request + subtype: "interrupt" from stdin. There is no command to "cancel a single workflow". The interrupt aborts the entire session, which is also the semantics that the CLI actually implements end-to-end.{"type": "control_request","request_id": "<client-selected string>","request": {"subtype": "interrupt","session_id": "<sessionId>","reason": "user cancel"}}
task_notification for that workflow task is received, interrupt is a no-op for the workflow state.session_id is session-scoped. Aborting one session does not affect other sessions in the same process.control_response one-time ACK:{"type":"control_response","response":{"subtype":"success","request_id":"stop-1"}}
system/task_updated The patch.status of the workflow task changes to "killed", and patch.end_time is set to the timestamp:{"type":"system","subtype":"task_updated","task_id":"<workflowTaskId>","patch":{"status":"killed","end_time":1785...}}
system/task_notification is sent only once, with status:"stopped" (internal killed / cancelled are both mapped to this value, see §5.4):{"type":"system","subtype":"task_notification","task_id":"<workflowTaskId>","status":"stopped","summary":"...","output_file":"..."}
system/task_progress events (a superset of Claude Code in cbc). When each sub-agent finishes, its workflow_agent entry in task_progress.workflow_progress[] changes the state from "start" to "error" and carries error: "subagent aborted: ...". Claude Code does not guarantee this step. cbc emits it additionally for observability. Consumers that rely only on Claude Code-guaranteed events should treat task_notification.status="stopped" as the only authoritative terminal state.import { spawn } from 'node:child_process';const cbc = spawn('cbc', ['--input-format', 'stream-json','--output-format', 'stream-json','--permission-mode', 'bypassPermissions',], { stdio: ['pipe', 'pipe', 'pipe'] });const send = obj => cbc.stdin.write(JSON.stringify(obj) + '\\n');let buf = '';let sessionId;const tasks = new Map(); // task_id -> { workflow_progress, status, notified }cbc.stdout.on('data', chunk => {buf += chunk;const lines = buf.split('\\n'); buf = lines.pop();for (const line of lines) {if (!line.trim()) continue;let msg; try { msg = JSON.parse(line); } catch { continue; }if (msg.type === 'system' && msg.subtype === 'init') sessionId = msg.session_id;if (msg.type !== 'system') continue;if (msg.subtype === 'task_started' && msg.task_type === 'local_workflow') {tasks.set(msg.task_id, { workflow_progress: [], status: 'running', notified: false });}if (msg.subtype === 'task_progress' && Array.isArray(msg.workflow_progress)) {const t = tasks.get(msg.task_id);if (t) t.workflow_progress = msg.workflow_progress; // Authoritative snapshot}if (msg.subtype === 'task_updated') {const t = tasks.get(msg.task_id);if (t && msg.patch?.status) t.status = msg.patch.status;}if (msg.subtype === 'task_notification') {const t = tasks.get(msg.task_id);if (t) { t.status = msg.status; t.notified = true; }}}});send({ type: 'control_request', request_id: 'init',request: { subtype: 'initialize', hooks: {}, capabilities: {}, hasPrompt: true } });send({ type: 'user', message: { role: 'user', content: '/deep-research topic=foo' } });async function cancel() {if (!sessionId) return;send({ type: 'control_request', request_id: 'stop-' + Date.now(),request: { subtype: 'interrupt', session_id: sessionId, reason: 'user cancel' } });await new Promise(resolve => {const check = () => {const stillRunning = [...tasks.values()].some(t => !t.notified);if (!stillRunning) resolve();else setTimeout(check, 200);};check();});}
session_id from system/init as the target of the interrupt.task_notification (instead of the control_response ACK) as the criterion for "run finished".task_progress.workflow_progress as the authoritative snapshot: a later task_progress overwrites the previous state for the same task_id.task_started{"type": "system","subtype": "task_started","task_id": "<uuid>","tool_use_id": "<toolUseId>","task_type": "local_workflow", // ← Discriminator for workflow tasks"workflow_name": "path:simple.workflow.js","description": "<workflow description>","uuid": "<messageUuid>","session_id": "<sessionId>"}
task_type: "local_workflow" identifies workflow tasks. Other background tasks retain values such as "Agent" / "Bash" / "PowerShell".workflow_name corresponds to the meta.name declared by the workflow (falling back to path:<basename> when the script is started by path). Claude Code additionally includes the full workflow JS source code in the prompt field, while cbc generally omits prompt.task_progress (workflow Form)workflow_progress for that task_id overwrites the previous one.{"type": "system","subtype": "task_progress","task_id": "<uuid>","tool_use_id": "<toolUseId>","description": "<workflow description>","usage": { "total_tokens": 0, "tool_uses": 0, "duration_ms": 0 },"workflow_progress": [{ "type": "workflow_phase", "index": 1, "title": "Gather" },{ "type": "workflow_agent", "index": 1, "agentId": "v2:<hash>","state": "start", "startedAt": 178547..., "label": "child-1","phaseTitle": "Gather", "phaseIndex": 1 }],"uuid": "<messageUuid>","session_id": "<sessionId>"}
workflow_phase: the phase declared by the workflow script, with the structure { type, index, title }.workflow_agent:{ type, index, agentId, state, startedAt, endedAt?, label?, phaseIndex?, phaseTitle?, tokens?, resultPreview? }state:"start" → "done" | "error" | "cached".agentId: a content-addressed key in the format v<schema>:<sha256>. It remains stable across reruns, making it easy for consumers to deduplicate or map to cached artifacts.task_updatedpatch to observe the incremental changes.{"type":"system","subtype":"task_updated","task_id":"<id>","patch":{"status":"completed","end_time":178547...}}
status values: pending, running, paused, completed, failed, killed.task_notification{"type":"system","subtype":"task_notification","task_id":"<id>","status":"completed", // or "failed" / "stopped""summary":"<summary>","output_file":"...","usage":{...}}
status is mapped from the internal task state: both killed and cancelled converge to "stopped". This is the authoritative terminal state signal — do not derive completion determination from other events.task_started / task_updated / task_notification are critical events: they are guaranteed to arrive in the order they occurred and will not be missed before the task reaches a terminal state.task_progress event of the Workflow class provides a delivery guarantee for phase / sub-agent lifecycle changes: before a workflow task enters a terminal state, the workflow_agent entry of every non-cached sub-agent appears at least once in a terminal state (done / error / cached).task_progress events (typically changing workflow_agent.state to "error") may arrive later than task_notification. Consumers should treat task_notification.status="stopped" as the terminal state signal, but keep the task_id for a short grace window (5 seconds recommended) to capture late task_progress updates.task_progress for the same task_id overwrites the previous workflow_progress snapshot. Consumers do not need to accumulate diffs and can simply use the latest payload as the source of truth.system/task_started form (where task_id, tool_use_id, task_type: "local_workflow", workflow_name, description, uuid, and session_id are all identical).system/task_progress.workflow_progress[] element form: workflow_phase and workflow_agent, with field names (agentId, state, startedAt, endedAt, phaseIndex, phaseTitle, label, tokens, resultPreview) and the state enum (start / done / error / cached) being identical.system/task_updated.patch and system/task_notification.status.task_progress messages to change the workflow_agent.state of in-flight sub-agents from "start" to "error". Claude Code does not guarantee this step.path:<basename> for workflow_name (Claude Code uses meta.name from the workflow source code).type values may be added under workflow_progress. Consumers MUST ignore unrecognized types.task_notification. Consumers can treat it as a terminal-state signal.control_response of initialize contains account.token (Keycloak JWT), the enterprise id, and the email address. Do not archive or publicly share stdout dumps without filtering.task_started.workflow_name, task_progress.workflow_progress[].label, and task_progress.workflow_progress[].resultPreview may contain user input or sub-agent output fragments, and must be treated as untrusted when rendered downstream.Was this page helpful?
You can also Contact sales or Submit a Ticket for help.
Help us improve! Rate your documentation experience in 5 mins.
Feedback