CodyCody

Authoring workflows

Step types

The fourteen step types — what each one's config means, which ones the agent performs, and the contracts the engine enforces around them.

Five step types are performed by the agent; nine run inside the engine. Each has a config whose shape is given here.

Agent-facing steps

instruction — a unit of work

{
  "prompt": "Run the test suite for {{input.target}} and summarize failures.\nReport in output: { \"failures\": [ { \"file\": \"\", \"test\": \"\", \"error\": \"\" } ] }",
  "instructionFileIds": ["if_abc123"],  // optional; contents appended to the prompt
  "isolation": "subagent"               // optional; "subagent" (default) | "inline"
}

The prompt is a template delivered verbatim to the agent. End every prompt with an explicit contract for output; later steps can only use what the agent reports.

Output location. The engine appends an output-location block to every instruction and review step: files the agent produces that are neither source nor documentation — reports, analyses, exports — go to {outputDir}/{package}, where outputDir comes from config.json and the package is the one the workflow lives in. Only spell out a path when a step must write somewhere else.

Context isolation. isolation: "subagent" asks the orchestrating agent to run the step's work inside one isolated subagent and let only the structured result enter its own conversation; "inline" runs it in the orchestrator's conversation. Defaults: instruction and reviewsubagent; decision and shellinline. Because a subagent does not remember earlier steps, a step must never rely on the agent "remembering" — pass everything it needs through templates.

decision — a branch point the agent chooses

{
  "prompt": "Inspect {{steps.Run Tests.output}}. Did all tests pass?\nRespond with branchLabel \"PASS\" or \"FAIL\".",
  "branches": [
    { "label": "PASS", "transitionId": "tr_pass" },
    { "label": "FAIL", "transitionId": "tr_fail", "isDefault": true }
  ],
  "allowSummaryFallback": false        // always set false
}

The agent inspects the recorded output and picks one label from a closed set. Branch selection order at runtime: result.transitionIdresult.branchLabel (case-insensitive) → output.transitionId / output.branch → the isDefault branch → the instance fails.

Rules: every branch's transitionId must be a real outgoing transition of this step; mark exactly one branch isDefault as the safe fallback; set allowSummaryFallback: false — summary matching is a prompt-injection surface, since data quoted in a summary can match a label; and list the allowed labels verbatim in the prompt.

gate — human approval

{ "message": "Approve deployment of {{input.target}} to production?" }

Pauses the instance. The engine returns the resolved message with status: "waiting" instead of a next step; the agent puts the question to the person in its session and reports their answer with ccw_continue. Use a gate before anything irreversible — deploys, sends, deletes. One outgoing transition; approval unblocks it.

shell — a command under a structured contract

{
  "command": "npm test -- --filter {{input.target}}",
  "expectedExitCodes": [0],            // default [0]
  "timeoutSeconds": 300,               // optional
  "captureStdout": true,               // default true
  "stdoutMaxBytes": 65536,             // default 64 KB; 0 disables inline capture
  "stderrMaxBytes": 65536,
  "captureToFile": "/tmp/{{instanceId}}-{{stepId}}.log",  // optional
  "verifyHash": true                   // optional; see below
}

The engine renders the command and instructs the agent to run it in its own shell and report output.stdout, stderr, exitCode and durationMs — plus capturedFile and outputHash when file capture is on — with status: "success" only for an expected exit code.

Interpolated {{var}} values are POSIX-shell-quoted automatically; a variable cannot break out of the command. {{!raw var}} bypasses quoting — only for values you fully control, never for anything that came from untrusted data.

verifyHash. The engine wraps the command so its combined output is written to a file, then appends sha256sum of that file and preserves the original exit code. The agent must report the printed hash as output.outputHash. A later step can re-read the file and compare — which is how a report that does not match what actually ran is caught. It implies file capture; without captureToFile the engine uses a default path under /tmp.

Prefer shell over an instruction that says "run this command": it gives deterministic quoting, exit-code checking, truncation limits, and with verifyHash tamper-evident output.

review — a bounded quality loop

{
  "prompt": "Review the implementation for correctness, style, and test coverage.",
  "maxIterations": 3,                  // optional; default 3
  "instructionFileIds": ["if_abc123"]  // optional
}

One input, two labelled outputs. The engine appends the output of the step feeding into the review — the subject — to the prompt inside <user-data> tags, and instructs the agent to report output.verdict as "approved" or "revise" with output.findings.

  • revise follows the transition labelled REVISE; point it back at the step under review. That step re-executes and can read the findings via {{steps.<review name>.output.findings}} — put that template in its prompt so revisions use the feedback.
  • approved follows APPROVED. The review step's stored result is rewritten so {{steps.<review name>.output}} resolves to the reviewed step's output; the review report itself moves to {{steps.<review name>.review}} (verdict, findings, iterations, forcedApproval).

maxIterations bounds the loop: once the review has run that many times, the engine follows APPROVED regardless and logs forcedApproval: true. A review can never loop indefinitely.

Engine-executed steps

start — required, exactly one

No config. Exactly one outgoing transition.

end — required, at least one

{ "summaryTemplate": "Fixed {{fixCount}} issues in {{input.target}}" }  // optional

Completes the instance; the resolved template is the completion message.

stop — abort

{ "message": "Aborted: {{steps.Safety Check.output.reason}}" }  // optional

Cancels the entire instance hierarchy — root and all children. Use as the target of a decision branch for unrecoverable situations. Distinct from end.

set_variable

{ "variableName": "fixCount", "valueTemplate": "{{steps.Apply Fixes.output.count}}" }

Resolves the template, tries JSON.parse so objects, arrays, numbers and booleans survive as real values, falls back to the raw string. Available afterwards as {{fixCount}}. Use it to pin a value under a stable name, or to snapshot one before a loop overwrites the step result.

create_task_list — fan-out source

{ "source": "{{steps.Scan Repository.output.issues}}", "name": "issues" }

The resolved source must be a JSON array of objects or the instance fails. Each element becomes a task { id, title: item.title, data: item, status: "pending" }. Design the producing step to output exactly that: an array, each item with a title and everything a worker needs.

get_next_task — loop head

{ "taskListName": "issues" }   // optional; defaults to the most recent list

Requires exactly two outgoing transitions labelled HAS_TASK and EMPTY. Validation does not catch a missing one; the instance fails at runtime. On HAS_TASK the next pending task's data becomes {{currentTask}}; on EMPTY every task is done.

complete_task — loop tail

No config. Marks the current task completed and clears {{currentTask}}. Route its transition back to get_next_task to close the loop:

create_task_list → get_next_task ──HAS_TASK──▶ [work steps] → complete_task ─┐
                        ▲                                                     │
                        └─────────────────────────────────────────────────────┘
                        └──EMPTY──▶ next step / end

sub_workflow — composition

{
  "workflowId": "wf_child123",
  "inputMapping": { "target": "{{input.target}}", "issue": "{{steps.Triage.output.id}}" }
}

Starts the referenced workflow as a child; the parent resumes when it completes, and the child's result is stored like a step result. Enforced at publish: the child must exist and be published, and there are no recursion cycles. Maximum nesting depth at runtime: 10.

execute_task_list — one child run per task

{
  "taskListName": "issues",            // optional
  "workflowId": "wf_fix_issue",
  "inputMapping": { "issue": "{{currentTask}}" }
}

Runs the referenced published workflow once per task — the compact form of the manual loop. Use the manual loop when you need extra steps between tasks; use this when each task is exactly one child run.