Start in the parent chat. Describe the outcome, constraints, agents, and review depth you need; Kiro can generate a workflow for the task at hand. Save the graph as a recipe when it becomes useful to repeat, review, or share.
A workflow recipe is a JSON or YAML file that declares inputs and a tree of nodes. Store project recipes in .kiro/workflows/ with one of these suffixes:
*.workflow.json*.workflow.yaml*.workflow.ymlKiro discovers these files at runtime. If files with the same stem use more than one supported suffix, the JSON file takes precedence and Kiro reports the other file as a conflict.
Store personal recipes that should be available across workspaces in ~/.kiro/workflows/. Recipe-name precedence is project, user, synchronized account recipes, then bundled recipes. An invalid higher-priority recipe does not hide a valid lower-priority recipe. Available synchronized and bundled recipes can vary by account and client version.
In Kiro Web, upload one supported recipe from Settings → Workflows. To import a complete .kiro/workflows/ folder, use Configuration Sync.
Create .kiro/workflows/plan-and-implement.workflow.json:
{ "name": "plan-and-implement", "description": "Plan a change, then implement it", "inputs": { "task": "prompt" }, "steps": [ { "type": "step", "id": "plan", "agent": "wf-planner", "prompt": "Create an implementation plan for: {{task}}. Do not edit files." }, { "type": "step", "id": "implement", "agent": "wf-coder", "prompt": "Implement this plan, then run relevant tests:\n\n{{previous.output}}" } ] }
The plan step's final output becomes {{previous.output}} for implement. Before launching, ask Kiro to validate the saved recipe:
Run `validate_workflow` on `.kiro/workflows/plan-and-implement.workflow.json`. Report every error and warning, and do not launch the Workflow.
Fix any reported errors, then launch the recipe through your client. In IDE, ask Kiro in the parent chat to run the saved recipe and describe the task; then open Workflows above the parent chat and select the plan step to open its dedicated Workflow step tab. In CLI, run /workflow run plan-and-implement --task "Describe the change"; if you omit a declared input, Kiro opens a form to collect it. Select the plan step in the monitor to follow its session. In Kiro Web, upload the recipe under Settings → Workflows, ask Kiro in a cloud session to run it and describe the task, then open Workflows and select the plan step to follow or message it.
These agents ship with Kiro for use in recipes. They are available to steps only while workflows are enabled and do not appear in the /agent picker; the exact list your client offers is the one Kiro shows when it describes run_workflow:
| Agent | Role |
|---|---|
wf-planner | Read-only investigation and planning; writes reports and plans |
wf-coder | Implements changes, runs tests, commits |
wf-design | Writes technical designs |
wf-design-reviewer | Reviews a design against requirements |
wf-review-aggregator | Merges several review reports into one |
wf-pr-submitter | Opens or reuses a pull request and records its URL |
wf-pr-responder | Responds to pull request feedback |
wf-auto-researcher | Proposes, applies, and measures one experiment per iteration |
wf-workflow-creator | Drafts a recipe from a task description; used by the orchestrator, not in your steps |
semantic_reviewer | Behavioral code review; registered as both a workflow step agent and a general sub-agent |
A step's tool access comes from its agent definition, including its tools and excludedTools settings. Use narrowly scoped agents for unattended steps and review the permissions implied by each agent before launching the recipe.
| Field | Required | Description |
|---|---|---|
name | Yes | Stable recipe name |
description | No | Purpose shown by recipe selectors |
inputs | No | Map of input names to free-form type hints such as prompt, file, or string; defaults to {} |
modelId | No | Default model for steps; auto or omission inherits from the parent session |
effortLevel | No | Default reasoning effort for steps |
steps | Yes | Array of workflow nodes; top-level nodes run in order |
Input type hints help a launch interface collect values, but Kiro does not enforce them at runtime. Define every bare template variable in inputs and supply every value at launch.
A recipe is a tree. The root steps array is an implicit sequence; containers nest to compose control flow, and step and watch nodes are the leaves that do work.
| Type | Purpose | Required fields |
|---|---|---|
step | Run one named agent in its own session | id, agent, prompt |
sequence | Run child nodes in order | id, steps |
repeat | Run child nodes until a condition matches or a cap is reached | id, steps, maxIterations, onMaxIterations |
parallel | Schedule independent branches and join their results | id, branches, joinPolicy |
watch | Poll an external system through a handler without using model turns while idle | id, handler, config |
The tree below places one of each node type in a small generic recipe. Select any row to see how that type behaves at runtime; the badges are the same ones used in every later figure. The gate badge is not a node type: it marks a stopCondition, the check a repeat evaluates after each iteration, described under Define stop conditions.
A recipe is a tree of executable leaves and containers
Select any row to inspect the fields and runtime rule behind it.
indentation and trunk lines encode parent-child relationships; badges identify executable leaves, recursive containers, and the conditions that let a repeat exit or a step complete; the detail pane follows the selected row
Every node id must be unique across the static recipe. A recipe can contain at most 50 step nodes and can nest nodes to a maximum depth of 8. Repeat iterations do not consume additional static step slots.
A step supports these optional fields in addition to id, agent, and prompt:
| Field | Behavior |
|---|---|
artifacts | Maps logical names to files produced by the step |
captureOutput | Captures the step's final assistant message for later templates; defaults to true |
completion | Keeps the step interactive until a stop condition matches |
modelId | Overrides the workflow and parent-session model |
effortLevel | Overrides the workflow and parent-session reasoning effort |
Make the requested deliverable the step's final response. That response is what {{step-id.output}} and {{previous.output}} pass downstream.
A step agent signals its outcome by calling the send_message tool. The tool posts a short note to the parent session, and its severity doubles as the step's lifecycle signal:
severity | Recorded signal | Effect on the step |
|---|---|---|
success | success | Step completes; output is captured; the workflow advances |
warning | need_input | Step and run pause for user input; the step stays paused after each turn until it signals success or error |
error | error | Step fails; the failure propagates up the tree |
info | none | Progress note only; no lifecycle effect |
Kiro injects this protocol into every step session automatically, so you do not need to explain it in your prompt. It does help to tell the agent when to signal, for example "Finish with send_message severity success once the tests pass." A turn that ends without any send_message call, and without an unmet completion condition, completes the step.
send_message is the outward half of a two-way channel: guidance can also reach a step while it runs or waits. Write prompts that leave room for that guidance rather than assuming the step runs unattended; see Respond when a step needs input.
Use artifacts when a file is the durable handoff:
{ "type": "step", "id": "design", "agent": "wf-planner", "prompt": "Write the design for {{task}} to .kiro/workflow-output/design.md.", "artifacts": { "design": ".kiro/workflow-output/design.md" } }
A later step can reference the registered path as {{artifacts.design}}. Relative artifact paths resolve from the primary workspace root.
Every value a step can read arrives through a {{...}} template. Inputs come from launch, captured outputs come from the last message of an earlier step or the JSON payload of a watch, and artifacts come from files an earlier step declared. Templates work in step prompts, artifact paths, stop-condition file paths, and string values in a watch node's config.
The prompt is the wiring. Pick a step below to see every value it reads, which step produced each one, and what it publishes for later steps. Step the run back with the transport and a value whose producer has not finished yet turns amber, which is the ordering rule the rest of this section spells out:
Select a step to see what it reads and writes. Step back through the run to catch a value before its producer has finished.
plannot started · wf-planner
reads
{{task}}launch input{{run_dir}}launch inputwrites
{{plan.output}}read by implement{{artifacts.plan}}read by implement01Launch. You supply the task and run directory as declared inputs.
the tree on the left is the recipe structure; the list on the right names every value the selected step reads and where it comes from, then what it writes and who reads it. A value whose producer has not finished shows in amber
| Form | Value |
|---|---|
{{previous.output}} | Captured output of the most recent completed sibling in the same container |
{{step-id.output}} | Captured output of a prior step or watch by ID |
{{steps.step-id.output}} | Legacy alias for {{step-id.output}} |
{{artifacts.name}} | Path registered under name by a prior step |
{{input-name}} | Launch input declared in inputs |
Structured references must point to a producer that runs before the consumer. They fail the consuming step if Kiro cannot resolve them. In particular:
{{previous.output}} in the first node of a container.{{previous.output}} inside a parallel branch; branches have no guaranteed order.Unknown bare inputs behave differently. {{unknown}} remains literal text and produces a validation warning rather than an error. Kiro provides no implicit variables. Every bare {{name}} must be declared in inputs and supplied at launch.
Use sequence to name and group nodes that must run in order. The root steps array is already an implicit sequence, so add an explicit sequence only when nesting makes the plan clearer.
A repeat always runs its body at least once. After each completed iteration, Kiro evaluates the stop condition; a match completes the repeat before another iteration begins. For fileCheck, Kiro reads the JSON file at path and compares the value at jsonPath with value. Relative paths resolve from the primary workspace root. A missing or unreadable file, invalid JSON, missing JSON path, or value mismatch does not match, so the repeat continues.
{ "type": "repeat", "id": "implementation-loop", "maxIterations": 10, "onMaxIterations": "pause", "stopCondition": { "fileCheck": { "path": ".kiro/workflow-output/status.json", "jsonPath": "complete", "value": true } }, "steps": [ { "type": "step", "id": "implement-next-item", "agent": "wf-coder", "prompt": "Implement the next incomplete item and update .kiro/workflow-output/status.json." } ] }
A repeat may omit both stopCondition and stopWhen; it then runs to maxIterations.
Choose what happens at the cap:
onMaxIterations | Result |
|---|---|
abort | Abort the repeat and the run |
continue | Complete the repeat and move to its next sibling |
pause | Pause for a human decision |
Resuming a repeat paused at its cap does not add iterations; it pauses again immediately. Revise the remaining plan, or stop the run and launch the corrected definition as a new run.
A parallel node schedules each branch independently:
{ "type": "parallel", "id": "reviews", "joinPolicy": "allSettled", "branches": [ { "type": "step", "id": "security-review", "agent": "semantic_reviewer", "prompt": "Review the current changes for security issues." }, { "type": "step", "id": "test-review", "agent": "semantic_reviewer", "prompt": "Review the current changes for missing tests." } ] }
Three policies decide when the parallel node settles and whether one branch's failure cancels its siblings: all, allSettled, and any. A paused branch never cancels its siblings; a branch can pause when, for example, a nested repeat reaches onMaxIterations: "pause". Pick a situation below to see how each policy would resolve it; the difference is timing and cancellation.
Two branches. What should the parent do when…
branch 1 failedbranch 2 running
allfailed nowreports the failure immediately and cancels branch 2allSettledstill runningwaits for branch 2 to finish, then reports failed with every result keptanystill runningbranch 2 can still complete and rescue the parentUse all when one failure makes the rest pointless. Use allSettled when you need every branch's evidence, such as independent reviews. Use any when the first good result is enough.
pick what happened to the two branches; each line shows how one joinPolicy resolves it, including which siblings it cancels
Interactive steps with completion cannot be nested inside parallel.
A watch node polls an external system through a handler without spending model turns while nothing changes. When the handler reports new activity, its JSON payload becomes {{watch-id.output}} for the next step. A terminal result stays latched for stopWhen: "watch-id.terminal". Two handlers are built in: github-pr watches a pull request, and command runs a program you write, so a workflow can wait on anything a script or CLI can read.
idleTimeoutSec sits on the watch node itself, not in config, and applies to either handler. When set, a watch that stays idle for that many seconds ends with a terminal outcome and its output becomes {"outcome": "idle-timeout", "idleTimeoutSec": <seconds>}, so stopWhen: "watch-id.terminal" fires and any following step still runs; write that step's prompt to handle a timeout as well as real activity. The idle clock restarts each time a repeat re-enters the watch, and the repeat's maxIterations does not bound the idle polls within one entry. Unset means the watch waits indefinitely.
Point the handler at a URL, or at a workspace JSON file whose top-level url field contains the pull request URL. The file form is the usual pattern when an earlier step opens the PR and records where it went:
{ "type": "watch", "id": "wait-for-pr", "handler": "github-pr", "config": { "prRef": ".kiro/workflow-output/pr.json", "pollIntervalSec": 60 }, "idleTimeoutSec": 3600 }
config field | Purpose |
|---|---|
prRef or url | One is required: a workspace JSON file with a top-level url, or the PR URL directly |
pollIntervalSec | Poll cadence; default 60, minimum 30 |
includeOwnActivity | Let comments by the authenticated gh identity wake the watch; default false |
ignoreAuthors | Logins whose activity never wakes the watch, such as a bot that rewrites a marker comment |
commandTimeoutSec | Optional timeout for each underlying gh call |
The watch reports terminal-state when the PR is merged or closed. On new activity, {{wait-for-pr.output}} is a JSON object your responder prompt can rely on; every key is always present:
{ "url": "https://github.com/org/repo/pull/42", "state": "OPEN", "newComments": [], "newReviews": [], "inlineComments": [], "excludedComments": [], "excludedReviews": [], "excludedInlineComments": [], "inlineCommentsFetch": "ok", "backlog": { "remaining": 0 }, "headSha": "4b715b8b", "newFailedChecks": [], "passingCheckCount": 12, "pendingCheckCount": 3 }
state is OPEN, MERGED, or CLOSED. The new* arrays hold unseen activity that woke the watch; the excluded* arrays hold unseen activity from ignored authors, for context only. Each item wakes the watch at most once; edits and deletions do not wake it. Delivery is budgeted at 10 wake-relevant items per poll, oldest first; anything beyond that drains on later polls and is counted in backlog.remaining. The terminal payload is exempt from the budget and carries every remaining item.
A failed CI check also wakes the watch. headSha is the head commit the checks ran on, or null when unknown. Each newFailedChecks entry carries name, workflowName, conclusion, detailsUrl, runId, startedAt, and completedAt, and a failure is reported once per head commit, check name, details URL, and completion time, so a check that stays red does not re-wake every poll while a new head or a rerun does. Failed checks are never budgeted, and a failed Actions check is held until its run finishes so one wake carries every failure of that run. passingCheckCount and pendingCheckCount summarize the rest; skipped, cancelled, neutral, and stale checks count toward neither. When a responder needs full check details, have it run gh pr view <url> --json statusCheckRollup.
Across an interruption and recovery, delivery is at least once, so write responder prompts that tolerate a repeated item.
prRef paths are confined to the workspace roots the same way fileCheck paths are. Kiro validates handler configuration when the run is created; the standalone validate_workflow tool cannot perform that registry-dependent check.
Use handler: "command" when a script or CLI can tell you whether something changed: a ticket in your issue tracker, a deployment, a build in another CI system, a review in a tool without a built-in handler. Kiro starts your program once per poll; the program checks for changes, prints one result, and exits. An idle result keeps the watch polling with no agent turn. A new-activity or terminal-state result completes that watch entry, and the next step reads the payload from {{watch-id.output}}.
{ "type": "watch", "id": "review", "handler": "command", "config": { "command": "node watch-review.mjs", "reviewFile": "mock-review.json", "pollIntervalSec": 10, "commandTimeoutSec": 5 } }
config field | Purpose |
|---|---|
command | Required. One static command line, run by the session's default shell in the run's workspace |
pollIntervalSec | Poll cadence; default 60, minimum 10 |
commandTimeoutSec | Optional per-poll timeout in seconds; positive, no minimum |
| anything else | Your own keys, passed to the program as data |
Rules that go with those fields:
$SHELL elsewhere.command string. There is no args key, and a config that carries one is rejected. A {{...}} template anywhere in command is rejected at submission.commandTimeoutSec lets a poll wait indefinitely. A timeout ends the command's whole process tree and counts as an idle poll with the old cursor, so set it long enough for one poll's work. The node's idleTimeoutSec is checked between polls and does not interrupt a hung command.Each poll writes one JSON object to the program's standard input, then closes it. config holds every watch config key except command; workspacePath is the primary workspace and the process's working directory; additionalDirectories lists the other workspace roots. The first poll of a run has cursor: null.
{ "cursor": null, "config": { "reviewFile": "mock-review.json", "pollIntervalSec": 10, "commandTimeoutSec": 5 }, "workspacePath": "/path/to/project", "additionalDirectories": [] }
The program writes exactly one JSON object to standard output and exits 0:
{ "outcome": "new-activity", "cursor": { "revision": 1 }, "payload": "{\"state\":\"OPEN\",\"summary\":\"Please add a test.\"}" }
outcome is idle, new-activity, or terminal-state.cursor is required and can be any JSON value, including an explicit null; it must never be omitted. Kiro stores it opaquely, on idle polls too, hands it back on the next poll, carries it across repeat iterations, and restores it when a run resumes. Your program decides what counts as new; Kiro never compares payloads or event IDs, and a new run starts from null.payload is an optional string. Use JSON.stringify when the next step needs structured data, and supply it on both new-activity and terminal-state results; an idle payload is not captured.targetId is an optional stable string naming the watched resource. Kiro uses it to derive a run label when none was supplied, not to deduplicate events.Exit codes decide what a failed poll means. Exit 0 with a valid result is the poll result; exit 0 with malformed JSON, an invalid outcome, or a missing cursor fails the watch node. Exit 2 fails the node as invalid input, and exits 126 and 127 fail it because the shell could not execute or could not find the program, so no later poll would succeed either. Any other non-zero exit, including a commandTimeoutSec kill, is an idle poll, so a program that is briefly unhappy retries on the next poll. PowerShell does not preserve these reserved codes; on Windows they can arrive as a generic non-zero exit and keep the watch polling, so end a watch there through the result instead: return terminal-state with an explanatory payload and exit 0.
Every spawn goes through the permission policy of the session that launched the run. A rule that asks sends the prompt to that session, and a denial fails the watch node; pausing or cancelling the watch withdraws an open prompt, so a late answer has no effect. The command line runs the way an execute_bash command runs for that session: inside its sandbox when one is active, and not at all when a configured sandbox cannot be prepared, in which case the node fails with the reason rather than running unconfined. A run whose launching session is not loaded waits for it, pausing with the detail code ParentSessionUnavailable and continuing on its own when the session loads; a deleted launching session fails the node.
Keep standard output free of logs, banners, and progress messages, including output from any CLI you call, and send diagnostics to standard error; keep credentials out of both. Kiro imposes no byte cap on the streams, the cursor, or the payload and no item cap, so bound them in your program: the payload is stored with the run and may enter the next agent's prompt. For a real event stream, return a small batch plus IDs or a file reference for the rest, and advance the cursor only past items you delivered or deliberately excluded. Delivery is at least once across an interruption, so the responder must tolerate duplicates.
The example below needs no network. Save this snapshot as mock-review.json in your workspace; revision is a counter you bump whenever the summary or state changes:
{ "revision": 1, "state": "OPEN", "summary": "Please add a test." }
Save the watcher as watch-review.mjs beside it. It reports the snapshot on the first poll, stays idle while the revision is unchanged, reports a higher revision as new activity, and reports a non-OPEN state as terminal:
import { readFile } from 'node:fs/promises'; import { resolve } from 'node:path'; try { let input = ''; for await (const chunk of process.stdin) input += chunk; const { cursor, config, workspacePath } = JSON.parse(input); const review = JSON.parse( await readFile(resolve(workspacePath, config.reviewFile), 'utf8') ); const seen = cursor?.revision ?? 0; let result = { outcome: 'idle', cursor }; if (review.state !== 'OPEN' || review.revision > seen) { result = { outcome: review.state === 'OPEN' ? 'new-activity' : 'terminal-state', cursor: { revision: review.revision }, payload: JSON.stringify({ state: review.state, summary: review.summary }) }; } process.stdout.write(JSON.stringify(result) + '\n'); } catch { process.stderr.write('Invalid input or unreadable review file\n'); process.exitCode = 2; }
Test it by hand before putting it in a recipe: save the stdin object above as poll-input.json with your workspace's absolute path, then run node watch-review.mjs < poll-input.json in a POSIX shell, or Get-Content -Raw poll-input.json | node watch-review.mjs in PowerShell. The first poll prints the new-activity result shown earlier. Copy {"revision":1} into the input's cursor and run again: the program prints {"outcome":"idle","cursor":{"revision":1}}. Change the snapshot to revision 2 and run once more with the same input, and the cursor advances with the new summary in the payload. A real watcher should distinguish a bad configuration (exit 2) from a temporary service failure (any other non-zero exit), which this local example does not need to.
Put the watch and its responder inside a repeat, with stopWhen on the repeat rather than the watch. The responder also runs for the terminal payload, because the repeat checks stopWhen after its body finishes:
{ "name": "mock-review-watch", "steps": [ { "type": "repeat", "id": "review-loop", "maxIterations": 5, "onMaxIterations": "pause", "stopWhen": "review.terminal", "steps": [ { "type": "watch", "id": "review", "handler": "command", "config": { "command": "node watch-review.mjs", "reviewFile": "mock-review.json", "pollIntervalSec": 10, "commandTimeoutSec": 5 } }, { "type": "step", "id": "respond-to-review", "agent": "wf-coder", "prompt": "Summarize this mock review without changing files. If MERGED or CLOSED, report that it is finished.\n\n{{review.output}}" } ] } ] }
Set the snapshot's state to MERGED and bump its revision to exercise the terminal result and watch the loop stop.
The gate rows in the node-type tree above are stopCondition objects. Use stopCondition on a repeat or as a step's completion. When an object includes more than one field, any single match stops the loop or completes the step.
| Field | Matches when |
|---|---|
containsText | The latest captured output contains the given text |
completionSignal | The latest step signal is success, need_input (sent as send_message severity warning), or error |
fileCheck | A JSON value at jsonPath deep-equals value |
A fileCheck path outside every allowed workspace root fails the node.
If value is an array, Kiro treats it as an any-of list. Wrap a literal array in another array:
{ "fileCheck": { "path": "result.json", "jsonPath": "labels", "value": [["ready", "reviewed"]] } }
A repeat can use stopWhen instead of stopCondition:
{ "stopWhen": "wait-for-pr.terminal" }
{ "stopWhen": "{{aggregate.output}} contains VERDICT: APPROVED" }
Do not set both forms on the same repeat.
Each step resolves its model and effort in this order:
Use modelId: "auto" or omit modelId to inherit. Effort values depend on the selected model. An unsupported effort value is reconciled to that model's default when the step session is created; an unknown model passes validation with only a warning, then fails at the step's first model call with no fallback, so a guessed model id fails the run mid-flight.
Choose configurations for the role each step performs:
Workflow shape and model choice can be tested together. Run the same task set through alternative plan → execute → verify graphs, record quality, elapsed time, and estimated usage as artifacts, then compare them with a declared evaluator. A model that performs well with one broad prompt may behave differently when work is decomposed into planning, execution, critique, and revision steps.
Do not assume automatic provider selection, fallback, or performance optimization. Provider, model, and effort availability varies by account and client. Validate every modelId before launch and see Available models for current public options. Workflow examples shows the comparison shape.
After saving the complete JSON or YAML recipe, ask Kiro to run validate_workflow on the file before you launch it. The tool reports the load-time constraints a recipe violates, including:
stopWhen expressions;A passing result does not prove that the run can start. The standalone validator cannot reject an unknown custom-agent name or validate runtime watch-handler availability and configuration. Kiro performs those checks when it creates the run.
maxIterations on every repeat and choose the cap behavior deliberately.
Author workflows