Workflows
Declarative multi-step agent chains. A workflow is a YAML file in workflows/
that names an ordered set of steps; each step runs one agent (or mounts a
sub-workflow), and routing between steps is driven by next and conditions.
The full key-by-key grammar lives in
reference.md — Workflow YAML Grammar.
How Declarative Workflows Work
Section titled “How Declarative Workflows Work”A workflow defines who runs, in what order, and what can make the chain branch or stop:
name: my-workflowtrigger: agents: [architect] # @@architect in an issue body can start itsteps: - name: analyze agent: architect next: plan - name: plan agent: architect conditions: # evaluated in order - on_result: approved next: implement - on_result: rejected next: analyze # loop back - name: implement agent: dev next: null # terminal- Steps execute sequentially;
next: nullends the workflow. conditions[]give the agent a choice of where to go, based on the result it reports in its dispatch (status/task_hint). Always include a catch-all (e.g.,default) so routing stays deterministic.skill: [...]attaches extra prompt context fromdefaults/skills/to that step; each entry is a bare name (meansinject) or- inject: [names]/- discover: [names](issue #955).prompt:injects a standing instruction into that step’s agent context (see Step Instructions).complexity: low|medium|highdeclares the complexity tier for that step’s agent invocation (see Per-step Complexity).prompt_replace: truemakes the stepprompt:replace the role prompt entirely, so the role’s generic flow cannot redefine the step’s scope (see Step Instructions).human_gate: truemarks a step as a security boundary: the workflow is held after that step until a human releases it (see Human Gates).- Workflows are started by GitHub CI (an issue comment/opened event) or by a
/nebuko run workflow --name <name>command, which enters at the workflow’s first step. Adispatchfield pointing at a workflow also routes to it. Human commands override state-based routing — see the routing cascade in architecture.md.
Step Instructions (prompt)
Section titled “Step Instructions (prompt)”A step can carry a prompt: field with a standing instruction that is
injected into that step’s agent context every time the step runs — useful for
repetitive per-workflow directives such as output-format contracts or analysis
requirements.
steps: - name: analyze agent: architect prompt: | Analyze the transcription and provide me: a short summary (~5-10 sentences) 5 Key points A possible link/impact/benefits to the https://github.com/your/repo project next: contextSemantics & precedence. The step prompt is a standing instruction for
that step — it governs the format and behavior of that step. The issue body
remains the task-specific content and wins on task specifics; the role prompt
stays the base identity. Injection order in the agent context:
role identity → step instruction → issue body/history (section header:
### 🔴 STEP INSTRUCTION (from workflow.yaml)).
Mechanics. The resolver writes the prompt verbatim to
/tmp/step_prompt.md and emits STEP_PROMPT_FILE (path only — the prompt may
contain newlines). When the step has no prompt, the variable is empty and any
stale file is removed, so existing workflows are unaffected. Multi-line YAML
block scalars (| / >) are supported.
Replacement mode (prompt_replace: true). By default the step prompt is
additive: the role prompt stays the base identity. Setting
prompt_replace: true alongside prompt: makes the step prompt the entire
agent context — the role prompt (e.g. dev.md) and its generic
Understand / Plan / Implement / Verify flow are suppressed. Use it to
structurally scope a step to a single job that the role’s default flow would
otherwise broaden (e.g. a scaffold step that must materialize a plan and not
implement it). It is ignored when prompt: is unset, so existing workflows are
unaffected.
prompt: vs skill:. Use prompt: for short, self-contained,
per-step directives that should live inline in the workflow. Use
skill: [...] for richer, multi-file, or reusable knowledge files in
defaults/skills/. They compose: a step may have both.
inject vs discover. An injected skill’s whole body is rendered into the
step prompt, so it costs context on every turn — use it for rules the agent must
always have (the posting contract, a mode’s absolute constraints). A discovered
skill only shows its name and one-line summary; the agent pulls the body in with
invoke_skill when it needs it (see
agents-and-skills.md). A bare
- name entry is injected.
Per-step Complexity
Section titled “Per-step Complexity”A step can declare the complexity tier for its own agent invocation with
complexity: low|medium|high. This lets a workflow author cheaply price each
step instead of paying the default medium on every one — for example a
triage/analyze step that only drafts a context query can run low, while an
architecture plan step can run high:
steps: - name: analyze agent: architect complexity: low # triage + context-query drafting is cheap next: context
- name: plan agent: architect complexity: high # architecture work deserves the top tier next: stubThe step selects the tier; the agent config defines it. What a tier
means (model swap, temperature floor, reasoning effort) lives in
agents.<name>.complexity_overrides.<tier> — see
configuration.md.
Precedence (highest first):
- an explicit
complexityin the agent’sdispatch_next(the agent in the loop knows best), then - the step’s
steps[].complexity, then medium.
The resolved tier is exported to the run as CONFIG_COMPLEXITY and drives the
tier machinery at LLM-config resolution time. Note that the first step of a
workflow has no preceding dispatch, so its declared complexity is the only
thing that can lower its cost — this is the motivating case for the field
(issue #787).
Human Gates
Section titled “Human Gates”human_gate: true is a security boundary enforced by the engine — not a
routing choice made by the agent. It guarantees that no LLM can advance the
workflow past a gated step on its own; only an identity-verified human
command releases it.
A gate release is an engine-owned state mutation (GateDecision), never an
agent action. The releasing command is recognized at comment-ingestion time and
keyed on the GitHub event author — never on comment-body markers, which are
agent-writable.
Mechanics:
-
The gated step’s agent finishes and emits
advance(or any other completing dispatch). The engine intercepts it and recordshold(reasonhuman_gate:<step>). The workflow stops; the step’s output stays posted on the issue for human review. Every advancing dispatch is intercepted — the gate has no bypass. A held state also carries no forward edge: the intercept emptiesresolution.next_stepand drops any agent-namedfunction_call, and on any later bot trigger the resolver treats thehuman_gatehold as higher precedence than the workflow’s next step or acall_agenthand-off, so neither a staleplan -> implementedge nor a persisted hand-off can advance the run (issues #885, #887). -
An authorized human (GitHub
OWNER/MEMBER/COLLABORATOR) reviews the output and runs one of:/nebuko approve gate [--feedback "..."]/nebuko reject gate [--feedback "..."]The
--feedbackvalue may span the command line and every following line of the comment body (verbatim, newlines preserved); a trailing--feedbackwith no value is still rejected (issue #919).The command is handled inline by the engine and is never routed to an agent.
-
The engine records a
GateDecision(status,decided_by,decided_at,feedback, and the exact hold instance it consumes) — the only artifact that can move the workflow past the gate — and routes from it:- approved → the gated step’s
on_result: approvedsuccessor; - rejected with feedback → the gated step is re-run with the feedback injected into its context; the gate re-arms when that run completes, so fresh human validation is required;
- rejected without feedback → the
on_result: rejectedsuccessor.
- approved → the gated step’s
Gate verdict routing (issue #1274)
Section titled “Gate verdict routing (issue #1274)”How to use. A gated step may declare conditional edges alongside
human_gate: true, exactly like any other step:
- name: plan agent: architect human_gate: true next: implement conditions: - on_result: approved next: implement - on_result: rejected next: context(This is the shape shipped by defaults/workflows/default.yaml: a rejected plan
sends the run back to context for fresh facts.)
How it works — verdict routing. When a gated step’s agent ends its turn with a verdict, the gate guard routes on it before parking:
| Agent verdict | Gate has on_result: rejected edge? | What happens |
|---|---|---|
rejected | yes | The conditional reject edge is routed automatically (including backward edges). No human ping, no hold. The armed GateLock is consumed on that route so the next turn is not re-parked; the gate re-arms naturally when the gated step completes again. |
rejected | no | Falls back to the human hold — it never silently follows the forward next edge. |
approved / default / absent | — | Unchanged: parked in human_gate_hold until /nebuko approve gate / /nebuko reject gate. The gate pings the human only before moving forward. |
Notes:
- The consult budget (
call_agentrounds while armed) does not apply to the rejected route; it stays bound to the hold path. - The reject route is still bounded: after
MAX_CONSECUTIVE_AUTO_REJECTS = 3consecutive auto-routed rejects on the same gated step (constant insrc/nebuko/lib/resolve_config/features/resolve_route/resolve_route.py), the run falls back to the human hold with the reason in the hold note. The counter resets on any human gate decision or on anapprovedverdict for the gated step. - Human gate command semantics are untouched: a rejected-with-feedback command still re-runs the gated step with the feedback injected.
The example above mirrors the shape shipped in
defaults/workflows/default.yaml
(the plan step).
- Decisions are single-shot: a
GateDecisionbinds to exactly one(workflow, step, position)hold instance and is consumed on routing, so replaying an old command is a no-op.
Agents complete a gated step with advance, never hold. The gate is
released only by the human command above, so the agent’s job is to deliver its
output and advance; the engine converts that completing dispatch into the hold.
An explicit hold would stop before the gate and require a second, non-gate
resume — the double-approval anti-pattern.
Non-authoritative agent classification. Agents may still read and classify
human intent for UX (a role prompt may note “the human approved”), but that
classification is not what releases a gate — only the engine-recorded
GateDecision does.
Identity is authoritative. A gate command is accepted only when its GitHub
event author is a non-bot user whose author_association is
OWNER / MEMBER / COLLABORATOR, and the comment body carries no machine
token (<!-- bot-... -->, AGENT_RUN_CONFIG, …). A comment authored by the CI
bot that merely contains /nebuko approve gate is inert, and so is a
privileged human echoing agent output that carries a machine token.
/nebuko resume is unchanged for non-gated holds. It still releases a
soft_kill exhaustion halt (issue #704), but on a human_gate:* hold it is a
no-op that points at the gate commands — one release path per hold class.
Engine-owned human notification (issue #795). Agents never ping a human.
When a run stops for human attention the engine itself posts one
HumanActionNotice comment naming the workflow starter
(@started_by), so the person who launched the workflow is asked for
validation. The notice is classified from the run outcome:
| Stop | Notice |
|---|---|
hold, reason human_gate:<step> | awaiting approval |
hold, any other reason (incl. a rudderless no-action default, §5 of handoff-and-callback.md) | awaiting input |
| terminal step, on the run that produced its output | workflow complete |
soft_kill while the soft-retry budget remains | none (transient retry) |
terminal SKIP_AGENT on a follow-up run | none (nothing new to act on) |
| gate command with nothing it may decide (issue #1012) | none — the engine posts the skip reason (skipping agent — <reason>) and exits 0 |
The recipient is the workflow starter (WorkflowState.started_by, captured
from the first trigger and carried forward), falling back to the issue author
and then the trigger author. Agent-authored comment bodies are made
notification-inert by the publish path, so an agent mentioning @someone is
readable but does not notify; fenced blocks and inline code are left verbatim
(GitHub never notifies for a mention there, so rewriting one would only corrupt
the snippet).
The gate intercepts only advance (and an absent dispatch). terminate is
never intercepted, and loop retries within a step are not intercepted.
Script Steps
Section titled “Script Steps”A step can be a deterministic script run instead of an agent: the engine
executes a command, posts its raw output as a comment, and advances to the next
step. The script: key is itself the step source (the nested object name is
the kind — no separate kind field), like agent: or workflow:. The fields
follow the Kubernetes container schema (command + args).
steps: - name: run-tests script: command: uv # required; executable or repo-relative script args: ["run", "pytest", "-q"] # optional argv list (no shell interpolation) working_dir: src # optional, repo-relative (default: repo root) env: # optional, overlaid on the engine environment PYTHONWARNINGS: error timeout_minutes: 15 # optional, >= 1 (default 30, capped at 60) next: reviewThere is no shell: command is executed directly with args as its argv
([command, *args]), so metacharacters and quoting are never interpreted. Use
command: sh with args: ["-c", "..."] when you actually want a shell, and
remember the whole script is argv — an argument containing spaces or quotes
survives the round trip to the child process intact. working_dir is resolved
against the session checkout (absolute paths and .. segments are rejected)
and env overrides inherited variables with the same key.
Mechanics:
-
The resolver detects the
script:source and emits the execution contract (RUN_SCRIPT=true,SKIP_AGENT=true,SCRIPT_PATH,SCRIPT_ARGSas a JSON array,SCRIPT_WORKING_DIR,SCRIPT_ENVas a JSON object,SCRIPT_TIMEOUT_MINUTES,WORKFLOW_STEP) instead of a task assignment; no agent is run for that step.SCRIPT_PATHcarries thecommandverbatim andSCRIPT_ARGSis decoded withjson.loads, so argv round-trips losslessly. -
The engine runs the script from
[phase:action](bounded by the optionaltimeout_minutes), capturing stdout and stderr merged, and posts two comments — the same comment lifecycle as an agent step:- a start notice on entry:
<step> command started— View workflow run logs. It carries no state block and no<!-- workflow-continue -->token (a token here would re-trigger the gate before the command finished); - a completion comment carrying the workflow state
(
<!-- bot-workflow-terminal -->+<!-- bot-command-<step> -->+<!-- workflow-continue -->+ the embedded state block). This comment is the step’s registered output.
- a start notice on entry:
-
The completion body is a
<step> command succeeded/command failedbanner — the step name, exit code and duration only, never the command line (command/args may carry secrets) — followed by a collapsed<details><summary>command output</summary>fenced block with the captured output (so trigger-like strings in the output stay inert). Oversized output is truncated head+tail with a marker to stay under GitHub’s comment-size limit. -
The completion comment is registered as the step output (
current_context.step_outputs→{step, comment_id}), so a downstreaminputs: [<step>]binds to it exactly like an agent answer (ADR-0013), and the<!-- workflow-continue -->token re-triggers the workflow gate so the next step runs — in its own engine invocation. The script’s distinctbot-command-*tag is never counted as an agent turn by the circuit breaker.Exactly one script step runs per invocation (issue #994): the completion comment is the only thing that advances the workflow, so two independent advance mechanisms can never fire for the same edge. To chain several commands, put them in one step —
command: sh, args: ["-c", "a && b"]. Cross-step state does not survive, so a step must not depend on a file, ref or virtualenv an earlier step produced.
Result semantics (feeds the existing conditions mechanism):
- Exit
0→ resultpass; non-zero exit or timeout → resultfail. - If the step declares
conditions[], routing uses the matching branch (e.g.on_result: fail → dev); otherwise it follows the declarednext. - A failing script still advances the workflow in v1 — it does not hold
or terminate. The successor (usually an agent) receives the raw failure
output and reacts. A
stop_on_failpolicy switch is a deferred enhancement.
Security notes:
- A repo-relative
command/working_diris re-verified at runtime (realpath containment inside the checkout); absolute paths and..segments are rejected at parse time. - The command line (
command+args) is deliberately not echoed in the posted comments; only the step name, exit code and duration appear in the banner. The captured output is posted as-is — the same trust level as an agent answer. - The script runs in the same CI job with the job’s full environment and
permissions — the same trust level as the workflow YAML itself, plus the
step’s own
env. Secret scrubbing / sandboxed execution are deferred enhancements.
Step Hooks (pre_hook / post_hook)
Section titled “Step Hooks (pre_hook / post_hook)”An agent step can declare two deterministic hooks that run in the session
workspace around the agent invocation (issue #884): pre_hook before the
agent (e.g. initialise a dev environment) and post_hook after the agent
attempt (e.g. gather file/statistics). Each hook is a repo-local command — the
same shape as a script step — nested under its key:
steps: - name: implement agent: dev pre_hook: path: scripts/init-dev-env.sh # required, repo-relative args: ["--full"] # optional argv list (no shell interpolation) timeout_minutes: 10 # optional, >= 1 (default 30, capped at 60) post_hook: path: scripts/collect-stats.sh next: reviewMechanics:
- The resolver emits the step’s declared hooks as a
STEP_HOOKSJSON contract (separate from the step’s task assignment); the engine decodes it intoHookSpecvalues before the agent phase runs. pre_hookruns after the pre-agent hand-off is staged and immediately before the agent is invoked, in the session workspace (the repo root) with the agent’s environment. It is fail-closed: a non-zero exit or a timeout skips the agent entirely and fails the step through the normal failure path (the same handling as an agent exception).post_hookruns after the agent attempt in a finally position — on success, on failure, and even when the agent raised or was skipped. It is fail-open: its failure never changes the step outcome or routing, but on a non-zero exit or timeout apost_hook failed: ...note (including the captured output tail) is recorded underpost_hook_failurein the persisted workflow state, so a broken hook stays visible on the thread.- Hooks are only valid on
agentsteps. Ascriptstep has no agent session and aworkflow/repostep dispatches elsewhere; declaring a hook on any of them is rejected at parse time.
Security notes:
pathis repo-relative; absolute paths and..segments are rejected at parse time, mirroringscript.path.- Hooks run in the same CI job with the job’s environment and permissions — the same trust level as the workflow YAML itself. A future sandboxed session can move hook execution behind the agent host without touching the schema.
v1 boundaries (deferred): string shorthand (pre_hook: scripts/x.sh), ordered
lists of hooks, hooks on script/workflow/repo steps, an on_fail policy
switch, and posting hook output as separate comments.
Typed Inputs and Step Outputs
Section titled “Typed Inputs and Step Outputs”A workflow is a typed function (issue #813): it declares inputs (its
parameters) and outputs (its return values), and the caller supplies concrete
values at each workflow call site. This is how the engine guarantees a step
receives the artifacts it needs — the plan, a git diff, test results —
deterministically, without the model spending turns fetching them or
hallucinating their absence (see ADR-0009).
# leaf.yaml — declares what it needs and what it returnsname: leafinputs: - name: run_test kind: exec # exec = a deterministic provider command the engine runs - name: greeting kind: str # str = a constant passed throughoutputs: - implementsteps: - name: implement agent: dev inputs: run_test: input.run_test # materialized just before this agent runs greeting: input.greeting next: null# caller.yaml — binds the arguments at the call sitename: callersteps: - name: build workflow: leaf inputs: run_test: exec: ["pytest", "."] # a literal command binding (block style) greeting: str: "hello"Mechanics:
- Call-site binding — a
workflowcall step passes one value per declared input. A binding has exactly one source key:exec(argv),str(constant), or a bare string meaning aref(diff: input.diff). - Kind-preserving lazy forwarding — a
refto anexecinput forwards the command spec, not its output. A super-parent can push a command through a middle workflow to a leaf; materialization happens exactly once, at the deepest consuming step, so freshness is depth-invariant. - Mandatory injection — right before the consuming agent step, the engine
runs each
execand injects the captured stdout as a labeled context block.required: true(default) fails the step before the agent is invoked if the command fails;required: falseinjects_(unavailable: …)_. - Bounded nesting — the call-frame chain is capped (
MAX_CALL_FRAMES); an over-deep chain fails the step deterministically before the agent runs. The frame chain is derived from the run’s carried-forward mount position and decoded at the engine’s input-materialization seam, where the cap is enforced.
Wiring is checked offline by python -m engine lint (rules
L8–L11): every declared input bound exactly once per
call site, kinds matched, references resolvable in scope, every declared
input consumed or forwarded, and every prompt reference naming a declared step.
Step Outputs — A Step’s Output Is Its Comment
Section titled “Step Outputs — A Step’s Output Is Its Comment”Data flow between steps of the same workflow needs no declaration: a step’s output is the comment it posts, addressed by the step name (issue #756, ADR-0013). A downstream step consumes an upstream step’s comment through either surface, both resolved by the engine before the agent runs:
- the deterministic
inputsseam —inputs: [plan](list shorthand) or the alias mappinginputs: {reviewed_plan: plan}; and {{ <step> }}insidesteps[].prompt.
steps: - name: plan agent: architect next: implement
- name: implement agent: dev inputs: [plan] # the plan comment, materialized
- name: review agent: review inputs: [plan, implement] # fan-in: plan + the dev output prompt: | Review the implementation against the plan. Plan under review: {{ plan }} conditions: - on_result: approved next: null - on_result: implement next: plan- The value is the referenced step’s full comment, state-stripped — exactly what agents see in the thread. There is no truncation; the only bounds are the pre-existing comment-size limits.
- Only a locator (
{step, comment_id}) is persisted, undercurrent_context["step_outputs"]in theWORKFLOW_STATEv1 comment — never the body. The body is read back from the bounded thread snapshot. - Resolution is single-pass: a substituted body is never re-scanned, so a
step that quotes
{{ … }}cannot induce recursion. - An unresolved reference degrades to
_(unavailable: <step>)_; the linter (L10-input-ref,L11-prompt-ref) reports it ahead of the run. inputs: [plan]is a required binding:input.<name>workflow parameters from ADR-0009 are unchanged and take precedence in the scope.
Status. Both paths are shipped: the input path (declared inputs,
call-site binding, kind-preserving forwarding, just-in-time exec
materialization, mandatory injection, MAX_CALL_FRAMES) and the step-output
path (inputs: [<step>], {{ <step> }} prompt references, locator storage,
locator backfill at publish and read time). This supersedes the earlier
“output path is forward-looking” note: the named-output grammar
(outputs: / <step>.<name>) was removed in favour of step-name
references; a leftover steps[].outputs: key fails at load time with a #756
migration hint. The dotted form is valid again only for a field a step
declares in its output: block (issue #973, below): {{ plan.headline }} and
inputs: {summary: plan.headline} resolve to that one section, while a dotted
ref to an undeclared field is reported by the linter (L10-input-ref,
L11-prompt-ref).
Declared Step Output — The output: Shape (issue #973)
Section titled “Declared Step Output — The output: Shape (issue #973)”A step may additionally declare the shape of its own posted comment with an
output: block. This is opt-in: a step without output: keeps the legacy
finish / dispatch_next terminal contract byte-for-byte.
steps: - name: implement agent: dev next: review output: fields: - name: headline # the comment's `#` lead-in title: true - name: changes # a `## Changes` section (required) description: What changed, with file paths. - name: verification # optional: absent ⇒ no heading, no markers required: false - name: matrix # a table field type: table columns: [case, result]Field keys:
| Key | Type | Default | Meaning |
|---|---|---|---|
name | ^[a-z][a-z0-9_]*$ | — | Section id; also the marker name. |
type | text | table | text | A table field requires columns. |
description | string | "" | Shown in the prompt block’s field list. |
heading | string | capitalized name | The rendered ## heading. |
title | bool | false | At most one text field; its body is the # lead-in. |
collapsed | bool | false | Wrap the body in <details>. |
required | bool | true | A missing required section fails the turn. |
columns | list | [] | Column names for a table field. |
Terminal contract. For a step that declares output:, the engine appends
the section prompt block to the step prompt and exposes the terminal finalize
tool (with call_agent for delegation; finish/dispatch_next are hidden).
The turn carries exactly two artifacts:
- the report — the turn’s markdown text, written as section bodies between the exact markers the prompt block shows; and
- the route — the
finalizetool’s typedrouteargument (status/agent/complexity/branch/pin/reason), exposed to the model as the tool’sresponse_schemaso a long markdown payload can never truncate the routing decision.
The engine — never the model — owns the markdown wrappers (# title, ##
headings, <details>), extracts the bodies, validates them (known names,
required present, no <!-- injection) and renders the posted comment. A
malformed answer gets exactly one retry naming the offending field; a second
failure salvages the completed sections and logs loudly. A truncated turn is
salvaged the same way and routes hold.
See .nebuko/workflows/default-with-output.yaml
for a runnable copy of default that declares output: on its report-posting
steps (analyze, plan, implement, review); context stays undeclared
because its comment is the free-form data payload downstream steps consume.
Built-in Workflows
Section titled “Built-in Workflows”| Workflow | File | One-line description |
|---|---|---|
default | default.yaml | Standard dev chain: analyze → context → plan → implement → review |
think | think.yaml | Thinking only — analyze + context + plan, no implementation |
review-only | review-only.yaml | Standalone single-shot code review |
dev-review-loop | dev-review-loop.yaml | Reusable sub-workflow: one plan step with review, loops on rejection |
plan-orchestration | plan-orchestration.yaml | Full multi-step plan lifecycle sharing one PR |
spec-driven | spec-driven.yaml | Spec → stub (human-gated) → implement |
spec-driven-impl | spec-driven-impl.yaml | Plan → stub (human-gated) → implement |
demo-composition | demo-composition.yaml | Demo of sub-workflow composition |
default
Section titled “default”analyze (architect) → context (context) → plan (architect) ──approved──► implement (dev) → review (review) ◆ │rejected └────────► context (loop)Trigger: @@architect. The standard development cycle used when a human hands a
problem to the system. The plan step can loop back to context when the
plan is rejected (insufficient context).
The plan step is a single human_gate:plan: @@architect emits one planning
artifact — visible Context (root cause), Decision (design + Object Design
Anchor), an Approaches considered table, and the Object Design Contract, with
the Implementation Plan collapsed in a <details> block — and ends with
advance. The engine intercepts that advance into the gate hold, so
the human runs exactly one /nebuko approve gate before @@dev is routed.
Example trigger:
@@architect implement retry logic in the circuit breakeranalyze (architect) → context (context) → plan (architect) ◆ └─ rejected ─► context (loop)Trigger: @@architect. Pure analysis — ends after the plan/answer is produced;
nothing is implemented. Use for “help me understand / define X” requests.
review-only
Section titled “review-only”review (review) ◆Trigger: @@review. Standalone review of code without a prior implementation
step in the same workflow — used for ad-hoc review requests and nightly review
(issue #161).
dev-review-loop
Section titled “dev-review-loop”implement (dev) → review (review) ──approved──► ◆ └─rejected─► implement (loop)Trigger: @@dev. A reusable sub-workflow: it executes a single plan
step with review and loops until the reviewer approves. It is not typically
started directly — it is referenced per-step from a dynamically generated
tmp_impl/workflow.yaml (see plan-orchestration).
plan-orchestration
Section titled “plan-orchestration”architect (architect) → context (context) → plan (architect) ──approved──► scaffold (dev, prompt_replace) │rejected │ └────► plan (loop) ▼ sub_impl (workflow: tmp_impl/workflow.yaml) │ ▼ sub_clean (dev, skill: plan-cleanup) │ ┌────────────────────▼────────────────────┐ ▼ │ final_review (review) ──rejected──► final_fix (dev, skill: final-fix) │approved │ └►◆◄────────────────┘Trigger: @@architect. The full lifecycle for a multi-step plan that shares a
single PR (issue #462):
architect/context/plan— architect produces a structured plan; a human approves it (reject → re-plan loop).scaffold— dev converts the approved<planWorkflow>JSON intotmp_impl/workflow.yaml, a per-step sub-workflow. The step setsprompt_replace: true, so thedev.mdrole prompt and its generic Implement flow are suppressed and the step scope is exactly the scaffold instruction.scaffold_review(review) verifiestmp_impl/workflow.yamlfaithfully materializes the approved<planWorkflow>block (same steps, same order; nothing implemented outsidetmp_impl/); a mismatch loops back toscaffold.sub_impl— mounts the generated sub-workflow as one step. Each plan step runs throughdev-review-loop(implement → review, loop on rejection).sub_clean— dev removes thetmp_impl/scaffolding (skill:plan-cleanup).final_review↔final_fix— one whole-PR review; rejections are addressed across all plan steps (skill:final-fix) and re-reviewed.
This is the main demonstration of sub-workflow composition — a step
declaring workflow: instead of agent:, with a runtime-generated path.
Entering mid-lifecycle (issue #789). When a plan already exists on the
issue (for example from an earlier deep-thinking run), you can enter this
workflow without replaying architect:
/nebuko run workflow --name plan-orchestration --step planUse --step plan to have architect convert/re-validate the existing plan and
re-arm the human gate; --step scaffold converts it directly (bypassing the
gate — run is human-only, so that is an explicit human choice). --step sub_impl requires tmp_impl/workflow.yaml to be scaffolded already, else the
entry fails loudly rather than starting from the first step.
Restarting at a nested plan step (issue #822). A dotted selector descends
the mount chain, so you can re-enter the scaffolded plan at a specific item
without replaying the ones already done. For issue #648 — where plan step_1
and step_2 are complete — restart at step_3:
/nebuko run workflow --name plan-orchestration --step sub_impl.step_3This seeds a fresh run at plan-orchestration/sub_impl → tmp-impl/step_3 → dev-review-loop/implement, so it runs step_3…N then sub_clean →
final_review; step_1 and step_2 are never executed. Add one more segment
to re-enter at a leaf inside the per-step loop — e.g. re-run only step_3’s
review once its implementation is done:
/nebuko run workflow --name plan-orchestration --step sub_impl.step_3.reviewEach segment must match the scaffolded step name verbatim (step_3, not
step3). An unknown segment, a malformed path, or a mount whose
tmp_impl/workflow.yaml has not been scaffolded fails loudly rather than
silently starting from the first step.
spec-driven
Section titled “spec-driven”spec (po) ──rejected──► spec │approved ▼architect_prepare_context (architect) → context_architect (context) → architect_review_context (architect) │rejected └► context_architect (loop) │approved/default ▼stub (dev, skill: stub-only) ◄──rejected── stub_review (review) │ │approved/default ▼ ▼ stub_approval (architect, human_gate) ◄──rejected │approved/default │ ▼ ▼implement (dev) ──rejected── review (review) → ◆ ▲______________________________│Trigger: @@po. Two human gates: the spec must be approved before any
architecture work, and the stubs must be approved before real implementation.
The stub step is constrained by the stub-only skill (stubs/tests only, no
logic). Review rejects send the dev back to fixing stubs or implementation.
spec-driven-impl
Section titled “spec-driven-impl”analyze (architect) → context (context) → plan (architect) ──approved──► stub (dev, stub-only) │rejected │ └► context ▼ stub_review (review) │ stub_approval (architect, human_gate) │approved ▼ implement (dev) ◆Trigger: @@architect. Same gate structure as spec-driven, but starts from the
standard analyze/context/plan chain instead of a PO spec. The stub review +
human approval is the quality gate before implementation.
demo-composition
Section titled “demo-composition”analyze (architect) → subtask (workflow: review-only) → finalize (architect) ◆Trigger: @@architect. Minimal example of sub-workflow composition: the
subtask step mounts the entire review-only workflow as one step of the
parent.
Writing a Custom Workflow
Section titled “Writing a Custom Workflow”Create workflows/<name>.yaml:
name: my-workflowdescription: "What this workflow does"trigger: agents: [architect]steps: - name: step_one agent: architect next: step_two - name: step_two agent: dev skill: [python] next: nullRules:
namemust match the filename (without.yaml).- Start by typing
@@<trigger agent>in an issue, or use/nebuko my-workflowto force it on an existing issue. - Local files take precedence over remotely fetched workflows of the same name (zero-copy fetch — see features.md).
Reusable sub-workflows are just workflows referenced by workflow: in a
step — dev-review-loop and review-only are the built-in examples. The
referenced path may be a workflow name (resolved against workflows/) or a
relative file path (e.g., tmp_impl/workflow.yaml), which enables
runtime-generated workflows.
Nested sub-workflows: a step that references a workflow whose first step is
itself a workflow: reference is supported (e.g. tmp_impl → dev-review-loop).
On entry, the resolver descends to the innermost runnable leaf
(agent: or script: step), up to a depth guard with cycle detection. skill:
values declared on the referencing parent step propagate down to the leaf.
Each sub-workflow’s terminal return resumes only its immediate parent; an
outer parent pair already on record survives nested re-entry, so the chain
unwinds one level per return.
Position (the nested call-stack). The engine tracks where a run is as a
position — the ordered stack of (workflow, step) frames from the
outermost workflow down to the step actually executing (persisted as
WorkflowState.position). It is carried forward across transitions and
mutated by exactly one frame per move: entry appends a frame, an
intra-workflow step move replaces the leaf, and return-to-parent pops it.
This is what keeps a plan-orchestration → tmp_impl → dev-review-loop run
inside tmp_impl/step_1 while implement → review happens, and what lets the
terminal review resume tmp_impl/step_2 instead of collapsing to the outer
sub_clean. See
ADR-0006.