Workflows
A workflow-file is an optional YAML document that layers project-specific
behavior on top of a compiled pipeline at run time. A workflow can inject a
top-level prompt, override the agent or prompt of specific stages, expand
a stage into N chained copies via loop, skip (delete) a stage,
declare per-stage auto-approval via approve, force or cancel a stage's
manual launch mode via manual, attach note buttons to a stage via
notes, declaratively add new stages to the pipeline via extend, and
configure project-memory participation via the top-level memory block
and the per-stage reflect / memory instructions (see
Project memory (memory, reflect)).
Stage names in workflow.stages are matched strictly: a name that does not
match any pipeline step or extend-stage is a compile error. Workflows that
reference names absent from the target pipeline must be split or pruned.
Workflow-files live at:
<cwd>/.goga/workflows/<name>.yml
They are project-only — there is no user-level workflow directory. The
name must be a bare filename resolved inside .goga/workflows/; path
traversal via .. or an absolute prefix is rejected.
Document shape
A workflow-file is a YAML mapping with up to four top-level keys:
prompt: |
Top-level prompt injected as the first directive of the compiled pipeline.
stages:
<stage-name>:
agent: codex # optional per-stage agent override
prompt: | # optional per-stage prompt override
Additional per-stage instruction.
loop: 2 # optional iteration count (>= 1)
skills: [web-search] # optional skills merged with the pipeline stage's skills
approve: auto # optional auto-approval directive: auto | plan | dialog
notes: # optional note buttons attached to the stage
fix: Fix the failure and continue
reflect: # optional memory-reflection instruction (reflect method only)
file: shared.md
memory: true # optional memory participation (alignment method only; never together with reflect)
memory: # optional workflow-memory configuration block
method: reflect # reflect | alignment (the instruction vocabulary selector)
max_rules: 25 # optional rule cap (>= 1)
extend:
<new-stage-name>:
after: [propose] # position the new stage relative to existing ones
title: Warmup # optional display label; defaults to the entry key
prompt: | # any other stage field passes through verbatim
Bootstrap instruction for the new stage.
| Key | Type | Required | Description |
|---|---|---|---|
prompt |
string | no* | Top-level prompt emitted as the first key of the output. |
stages |
map | no* | Per-stage override instructions keyed by stage name. |
extend |
map | no* | New stages to add to the pipeline, keyed by new stage name. |
memory |
map | no* | Workflow-memory configuration (see Project memory). |
* At least one of prompt, a non-empty stages block, a non-empty
extend block, or a memory block must be present; an empty workflow is
rejected with a structural error.
Unknown top-level keys are rejected with
unknown key in workflow: <KEY>; valid keys: prompt, stages, extend, memory.
Stage entries
Each entry under stages is keyed by stage name and accepts up to ten
fields:
| Field | Type | Default | Description |
|---|---|---|---|
agent |
string | — | CLI agent name (e.g. codex, claude, cursor, opencode, qwen). Selects which agent runs this stage. See Workflow agent — choosing the CLI agent. |
prompt |
string | — | Per-stage context prompt. Lower precedence than the stage's own prompt — closer to a section description than to a direct instruction. See Workflow prompt — context, not command. |
loop |
int | — | Positive iteration count (>= 1). When >= 2, the stage is expanded into N chained copies. |
skills |
string list | — | Skill names merged with the pipeline stage's own skills (pipeline-first, deduplicated by value). See Skills merge. |
skip |
bool | — | When true, the compiler DELETES this stage from the compiled pipeline (the stage is absent from the flow-file entirely). Dependents of the skipped stage are transparently reconnected to its predecessors (no dangling references). false (or an absent key) leaves the stage in place. skip is allowed ONLY in the stages block — it is a structural error under extend. skip wins over agent/prompt/loop/skills overrides on the same entry. |
approve |
string | — | Auto-approval directive. Accepted values are auto, plan, and dialog; any other value (or a non-string) is a structural error. Each value drives a subset of two INDEPENDENT effects the compiler applies to the stage body (see Auto-approval (approve: auto/plan/dialog)): (1) communication effect — if the body has communication: true, the stage's interactive output is SUPPRESSED (omitted, not false); (2) roles effect — if the body's raw roles contain planner, the stage emits auto_approve: true. auto drives BOTH effects; plan drives only the communication effect; dialog drives only the roles effect. Allowed in both stages and extend (inline default override; a stages entry wins per-field). |
manual |
bool | — | Manual-launch instruction, stages block only. true forces the manual launch mode for the stage, overriding any authored trigger in its body. false cancels a manual state coming from either body source (a pipeline-file trigger: manual or an extend body trigger: manual) and is a structural error (manual: false on non-manual stage <NAME>) when the stage is not manual. An absent key means no instruction — the stage's own trigger decides. The three states (true/false/absent) are distinct; a non-bool value (including null) is a structural error. Allowed ONLY in stages — it is a structural error under extend (a new stage's launch mode is authored in its body via trigger). skip wins over manual: a skipped stage is removed before the manual instruction is applied. See Manual launch (manual and trigger). |
notes |
map of str→str | — | Note buttons — a map of "note name → prompt text" attached verbatim to the stage. Single-line texts serialize as plain scalars, multi-line texts as block literals. An empty map equals absence (no buttons emitted). Allowed ONLY in stages — it is a structural error under extend (an extend-stage receives its buttons through the stages block by name). Every loop-expanded copy carries the same buttons; skip wins over notes. How the buttons surface during the run is owned by the pipeline runtime — the compiler only assembles them. See Note buttons (notes). |
reflect |
map | — | Memory-reflection instruction (reflect method): {file: <path>, mode: r\|w\|rw} naming which memory file the stage reflects into. file is required and must be a relative, non-escaping path shape; mode defaults to rw. Allowed ONLY in stages — it is a structural error under extend. See Project memory (memory, reflect). |
memory |
bool | — | Memory-participation instruction (alignment method): true marks the stage as participating in project memory. An explicit false equals absence. Allowed ONLY in stages — it is a structural error under extend. See Project memory (memory, reflect). |
Rules:
- Only
agent,prompt,loop,skills,skip,approve,manual,notes,reflect,memoryare valid. An unknown key is rejected withunknown key in workflow.stages.<NAME>: <KEY>; valid keys: agent, prompt, loop, skills, skip, approve, manual, notes, reflect, memory. loopmust be an int>= 1. Zero, negative values, and non-int types raise a structural error.skillsmust be alist[str]. A non-list (or a list with non-string elements) raisesnon-list-of-str skills in workflow.stages.<NAME>.skip(when present) must be abool. A non-bool value raisesnon-bool value in workflow.stages.<NAME>.skip.skipis allowed only in thestagesblock — it is a structural error underextend(see Skipping a stage).approve(when present) must be one of the stringsauto,plan, ordialog. A non-string value raisesnon-str value in workflow.stages.<NAME>.approve; any other string raisesapprove must be one of: auto, plan, dialog in workflow.stages.<NAME>(see Auto-approval (approve: auto/plan/dialog)).manual(when present) must be abool. A non-bool value (including an explicitnull) raisesnon-bool value in workflow.stages.<NAME>.manual.manualis allowed only in thestagesblock — it is a structural error underextend.notes(when present) must be a mapping with string values. A non-mapping value (including an explicitnull) raisesnon-mapping notes in workflow.stages.<NAME>; a non-string value raisesnon-str value in workflow.stages.<NAME>.notes.<KEY>. An empty map is treated as absence.notesis allowed only in thestagesblock — it is a structural error underextend(see Note buttons (notes)).reflect(when present) must be a mapping with a key set within{file, mode};fileis required and must be a valid path shape,modeone ofr/w/rw.reflectis allowed only in thestagesblock — it is a structural error underextend(see Project memory (memory,reflect)).memory(when present) must be a bool; an explicitfalseequals absence.memoryis allowed only in thestagesblock — it is a structural error underextend(see Project memory (memory,reflect)).- The stage value must be a mapping. Non-mapping values raise
non-mapping stage <NAME> in workflow.stages. - Stage names are validated against the target pipeline: a name that does not
match any step in the pipeline or any
extend-stage is a structural error. agentis not validated against a known agent set. Absence of the corresponding wrapper file is surfaced at run time.
Workflow agent — choosing the CLI agent
Two different
agentconcepts. The pipeline-file stage fieldrolesnames the roles that organize the work inside a stage —planner,executor,reviewer(plus the always-onsummaryreport). See stage modes. The workflow-file stage fieldagent(singular) is a different concept: it names the CLI agent that runs the stage —claude,codex,cursor,opencode, or any other installed wrapper. The two are orthogonal.
A workflow's per-stage agent field lets the same pipeline run different
stages on different CLI agents. This is what makes a pipeline portable
across CLI tools: you keep one pipeline-file (the work to be done) and
express "stage X should run on codex, stage Y on claude" in a
project-level workflow-file, without forking the pipeline.
Each agent value resolves to a wrapper script installed in the goga image
at /home/goga/bin/<agent>-as-claude.sh. The wrapper presents the named
CLI agent to the pipeline runner in a uniform invocation shape, so the
pipeline itself does not care which concrete CLI is underneath.
The canonical baseline wrappers shipped with the image — claude, codex,
cursor, opencode, qwen — and their per-agent environment variables are
documented in Agents under
the Configuration reference. This page keeps the workflow-scoped agent
semantics (per-stage override, inline-extend agent); the wrapper set
itself is shared with build and pipeline.
Any other value is permitted as long as the corresponding wrapper file exists in the image — absence is surfaced at run time, not at validation time.
When a stage does not specify an agent in the workflow, the pipeline
uses its global default agent — typically claude. Setting agent
per-stage overrides that default for the named stage only; other stages
keep the global default.
Workflow prompt — context, not command
The per-stage prompt in a workflow-file is not a direct instruction
to the agent. Its weight is lower than the stage's own prompt field
declared in the pipeline-file, and it is
closer in role to a section description: it sets context, frames the
stage's intent, and gives the agent additional background to interpret the
stage's main prompt against.
Two consequences follow from this lower precedence:
- The workflow
promptdoes not override or replace the stage's ownprompt. The stage's declared prompt remains the authoritative instruction; the workflowpromptsits beside it as additional context. - The workflow
promptdoes not by itself command the agent to take specific actions. Free-form prose is interpreted as background, not as a directive.
To make a workflow prompt carry actual requirements, follow the
explicit labeled format the goga prompts already use — sections like
Requirements:, Constraints:, Algorithm:. Labeled blocks are
recognized as instructions and are honored as such; unstructured prose is
not.
Example — descriptive context only (no enforceable requirements):
stages:
propose:
prompt: |
This stage formalizes the user's request into a task document.
It runs early in the lifecycle and shapes the rest of the pipeline.
Example — context with enforceable requirements (the labeled block is honored as an instruction):
stages:
propose:
prompt: |
Task formalization process.
Requirements:
- Examine all link connections between cells carefully.
- Do not write code examples in the task.
Constraints:
- Do not build architecture in the task.
In the second form, the Requirements: and Constraints: blocks are the
load-bearing parts — the leading paragraph is still just context.
Skipping a stage
A stages entry may set skip: true to remove the named stage from the
compiled pipeline. The stage disappears from the flow-file entirely, and its
dependents are transparently reconnected: any stage that depended on the
skipped stage instead depends on the skipped stage's own predecessors. Chains
are resolved transitively — in A → B → C, skipping B makes C depend on
A; skipping both A and B makes C depend on whatever A depended on
(nothing, in the limit). For a phases pipeline, removal is positional (the
next stage simply chains to the new predecessor). skip wins over
agent/prompt/loop/skills overrides on the same entry, and removing
every stage is a structural error (empty body).
stages:
task-review:
skip: true
Auto-approval (approve: auto, plan, dialog)
A stages (or extend) entry may set an approve directive to drive the
stage's auto-approval behavior at compile time. The directive is one of
three values — auto, plan, or dialog — chosen from a closed set, not a
free-form flag, so any other string (or a non-string) is a structural error.
The compiler applies two independent effects to the stage body, and each directive drives a subset of them:
- User-input suppression (the communication effect) — if the stage
body has
communication: true, the stage's user-input pause is SUPPRESSED entirely. This makes an otherwise user-prompting stage run non-interactively.communication: false(or nocommunicationkey) is unaffected — suppression fires only whencommunication is True. - Auto-approval (the roles effect) — if the stage body's raw
roleslist containsplanner, the stage's agent actions are auto-approved. The match is against the authored role aliasplanner.
approve |
communication effect (suppress user input) | roles effect (auto-approve actions) |
|---|---|---|
| (absent) | — | — |
auto |
✓ | ✓ |
plan |
✓ | — |
dialog |
— | ✓ |
So auto is the full directive (both effects, as before); plan keeps the
communication effect but turns the roles effect OFF (a planner stage does
NOT auto-approve); dialog keeps the roles effect but turns the
communication effect OFF (communication: true still pauses for input).
The two effects are independent: each fires on its own trigger AND its
own directive subset. With neither trigger present, the directive is a no-op on
the body (it still threads through; it just has nothing to act on).
stages:
deploy:
approve: auto # or plan, or dialog
Under extend, approve is an inline default override exactly like inline
agent/loop (see Inline agent, loop, and approve overrides):
it is extracted from the entry (never reaches the body), and a stages entry
naming the same stage wins per-field.
Manual launch (manual and trigger)
A stage's launch mode has two authoring surfaces that meet in the compiler:
- The stage body (a pipeline-file stage or an
extendbody) carries atriggerfield —on_success(the default) ormanual.trigger: manualforces the manual launch mode (see Pipeline files): the stage pauses when reached and runs only when launched manually. An authoringauto_runkey in a body is a structural error — the launch mode is assembled by the compiler, never authored. - The workflow
stagesblock carriesmanual— a force/cancel instruction applied ON TOP of the body, without editing the pipeline-file:
manual |
Effect on the stage |
|---|---|
| (absent) | No instruction — the body's own trigger decides. |
true |
Forces manual launch, overriding any authored trigger. Idempotent on a stage that is already manual. |
false |
Cancels manual: an effective trigger: manual (from the pipeline-file body or an extend body) is rewritten to on_success, so the stage launches automatically. A structural error when the stage is NOT manual (manual: false on non-manual stage <NAME>) — cancelling a state that does not exist is an authoring mistake, not a no-op. |
stages:
deploy:
manual: true # hold the deploy stage for manual launch
manual is allowed ONLY in the stages block — under extend it is a
structural error (manual is forbidden in workflow.extend.<NAME>): a new
stage's launch mode is authored in its own body via trigger, never via a
workflow instruction. skip wins over manual: a skipped stage is removed
before the manual instruction is applied, so skip: true + manual: true
on one entry simply deletes the stage. On a loop-expanded stage every copy
carries the launch mode of the original.
Note buttons (notes)
A workflow's per-stage notes field attaches note buttons to a stage: a
map of "note name → prompt text" that the compiler attaches verbatim to
the stage as its note buttons.
stages:
deploy:
notes:
fix: Fix the failure and continue
investigate: |
Gather diagnostics for the failure.
Include the last 50 log lines.
- The map passes through verbatim — keys and values unchanged, authoring order preserved. Single-line texts serialize as plain scalars (quoted as needed); multi-line texts serialize as block literals.
- Buttons are authored ONLY through this instruction. An authoring
buttonskey in a stage body (a pipeline-file stage or anextendbody) is a structural error (buttons key is forbidden in stage body; use notes in workflow.stages) — there is exactly one authoring source, so the compiler-assembled field can never collide with an authored one. notesis allowed ONLY in thestagesblock — underextendit is a structural error (notes is forbidden in workflow.extend.<NAME>). An extend-stage receives its buttons through thestagesblock by its name, like any other stage.- An empty map (
notes: {}) equals absence — nobuttonskey is emitted.noteschanges nothing else: a workflow whose entries are all empty compiles byte-identically to compiling without a workflow, while any other instruction (prompt,agent,loop,skip, ...) changes the output as usual — notes or not. - Every
loop-expanded copy carries the same buttons, andskipwins overnotes: a skipped stage is removed before the buttons are resolved, so its notes never leak into a survivor. - How the buttons surface during the run is owned by the pipeline runtime — the compiler only assembles them.
Project memory (memory, reflect)
A workflow can wire its stages into the runner's project memory: a
top-level memory block selects the method and the block-level settings,
and the per-stage instructions mark which stages participate.
memory:
method: reflect # reflect (default) | alignment
max_rules: 40 # rule cap, >= 1, default 25
commit: false # whether memory changes are committed
stages:
brainstorm:
reflect: # reflect method: which file the stage reflects into
file: shared.md
mode: rw # r | w | rw, default rw
accept:
reflect:
file: shared.md
Under method: alignment the participating stages carry memory: true instead — see the key table below.
The top-level block accepts these keys:
| Key | Type | Default | Description |
|---|---|---|---|
method |
string | reflect |
The instruction vocabulary: reflect pairs with the per-stage reflect instruction, alignment with the per-stage memory instruction. Never part of any output. |
path |
string | — | Suffix inside the fixed memory root .goga/memory. Must be a relative, non-escaping path shape. |
max_rules |
int | 25 |
The maximum number of memory rules (>= 1). |
commit |
bool | false |
Whether memory changes are committed. |
mode |
string | rw under alignment |
The project-memory access mode (r/w/rw). Authored ONLY under method: alignment — an authored mode together with method: reflect is a structural error. |
Behavior rules:
- The method selects the instruction vocabulary. Under
reflect(the default when no block is authored) a stage participates by carrying areflect: {file, mode?}instruction; underalignmentit participates by carryingmemory: true. The two vocabularies never mix: areflectinstruction underalignment, or amemory: trueinstruction underreflect(including with no block at all), is a structural error. - The block is emitted iff at least one stage participates. A memory
configuration without participation is a silent no-op — no block, no stage
keys, not even an opting-out stage key. A workflow consisting of the
memoryblock alone is still valid (it counts as content). - Participation is counted over the working body — after skip removal
and loop expansion, embedded extend-stages included. A skipped stage's
instructions die with it: skipping the only participating stage disables
the block entirely. Every
loop-expanded copy carries the same memory keys as its original. - Project memory lives under the fixed root
.goga/memory(plus the authoredpathsuffix when one is set). How the memory settings and the per-stage participation are encoded into the compiled pipeline is an internal contract of the compiler; this documentation intentionally does not pin the compiled form. - Both instructions are allowed ONLY in the
stagesblock — underextendthey are structural errors. A new stage participates through astages-block entry authored under its name. Authoring areflectormemory_usekey in any stage body (a pipeline-file stage or an extend body) is likewise a structural error — the memory stage keys come from the workflow instructions alone. - The memory mechanism is executed by the pipeline runtime inside the
container — goga authors and compiles the instructions. If your image
predates the mechanism, refresh it with
--update.
Extending the pipeline with new stages
The stages block only overrides stages that already exist in the target
pipeline. The extend block does the complementary thing: it adds brand-new
stages that are not in the pipeline-file at all. Each extend entry is one
new stage, positioned relative to existing stages via before / after:
extend:
<new-stage-name>:
before: [plan] # place this new stage BEFORE the named stage(s)
after: [propose] # place this new stage AFTER the named stage(s)
agent: codex # optional: default agent override for the new stage
loop: 2 # optional: default loop override (>= 2 expands)
title: Warmup # optional; any other stage field passes through
prompt: | # verbatim stage body — same fields as a pipeline stage
Bootstrap the environment before the pipeline runs.
Positioning (before / after), default overrides (agent / loop /
approve), and the stage body are separate concerns:
beforeandafterare lists of existing stage names. The new stage is declared to run before thebeforenames and after theafternames. At least one of the two must be present — an entry with neither is rejected.agent,loop, andapproveare optional default overrides extracted from the entry (see Inline agent, loop, and approve overrides).- Everything else in the entry (
title,prompt,skills,roles,communication,trigger, or any other stage field) is the verbatim body of the new stage. It is carried through unchanged and embedded as an ordinary stage in the compiled output.before,after,agent,loop,approve, anddepends_onare never part of the body —agent/loop/approveare extracted as override fields, anddepends_onis forbidden here (positioning is declared structurally, not as a dependency edge).triggerIS part of the body: a new stage's launch mode is authored right there (trigger: manualcompiles toauto_run: false), which is why the workflowmanualkey is forbidden in anextendentry (see Manual launch (manualandtrigger));skipis likewise forbidden (a new stage has nothing to skip). - The
titlefield, when omitted, falls back to the entry key — so a stage declared underextend: warmup:without atitleis still labeledwarmupin the output.
Extend-stage names are first-class members of the workflow's name set:
they are valid targets for stages entries and for other extend-entries'
before/after refs (cross-references between extend-stages resolve). The
new stage is always inserted; if a name collides with an existing stage, the
duplicate surfaces at run time. Dangling before/after refs —
names in neither the original pipeline body nor any extend-stage — are
structural errors raised before the embed (see
How the compiler applies a workflow — Validate references).
Positioning semantics by body format
How before / after translate into run order depends on the compiled
body format of the target pipeline:
- Stages format — the compiler derives
depends_onedges from the names. Eachaftername is added to the new stage'sdepends_on(so the new stage runs after it), and the new stage's name is added to thedepends_onof eachbeforetarget (so they run after it). Existingdepends_onon the targets is preserved — the new edge is appended, not overwritten. Execution order is then governed entirely bydepends_on, exactly as for any other stage. - Phases format — the compiler inserts the new stage positionally:
immediately after the last
aftertarget and/or immediately before the firstbeforetarget. Run order comes from list position, notdepends_on. Whenafterandbeforetargets place the stage inconsistently, theafterposition wins. Everybefore/aftertarget is guaranteed to exist by the reference validation (see How the compiler applies a workflow).
Inline agent, loop, and approve overrides
Alongside positioning, an extend entry may carry inline agent, loop, and
approve fields. They are extracted from the body exactly like
before/after — they never reach the compiled stage as separate fields — and
act as default overrides for the new stage, mirroring what a stages entry
provides for an existing stage:
agent(a string) is composed into the stage'scommandwrapper path by the same template as astages-blockagent(/home/goga/bin/<agent>-as-claude.sh).loop(an int>= 1;>= 2expands) expands the new stage into<name>-1..Nchained copies by the same rules as astages-blockloop, including the externalbefore/afterreference rewrite to the last expanded id.approve(one ofauto/plan/dialog) drives the stage's auto-approval effects, same as astages-blockapprove(see Auto-approval (approve: auto/plan/dialog)).
An inline agent/loop/approve is a default: if a stages entry also
names the same stage, the stages value wins per field — an unset stages
field falls back to the inline one. This lets extend declare a sensible
default that a stages override can selectively tighten. An inline
agent: null, loop: null, or approve: null is a structural type error, not
an absence — omit the key to express absence (symmetric with the per-stage
agent/loop/approve).
Examples
A STAGES pipeline propose → review with a new warmup stage that runs after
propose, and a new extra stage that runs before review:
extend:
warmup:
after: [propose]
title: Warmup
prompt: |
Bootstrap instruction.
extra:
before: [review]
title: Extra
prompt: |
Additional pass before review.
Compiled effect (stages format): warmup depends on propose, and review
gains extra as an additional dependency —
propose → warmup, extra → review.
The same idea for a PHASES pipeline [a, b, c] — insert x after b:
extend:
x:
after: [b]
title: X
prompt: |
Stage inserted between b and c.
Compiled effect (phases format): the run order becomes [a, b, x, c].
A workflow can consist of extend alone — this is a valid, non-empty workflow:
extend:
teardown:
after: [summary]
title: Teardown
prompt: |
Final cleanup after the summary stage.
Rules and anti-patterns
depends_onis forbidden inside an extend entry. Positioning is declared viabefore/after; the compiler derives the dependency edges.- At least one of
before/aftermust be present. An entry with neither is rejected withextend entry <NAME> requires at least one of before/after. beforeandaftermust each be alist[str]when present. A scalar or a list with non-string elements is rejected withnon-list-of-str before in workflow.extend.<NAME>/non-list-of-str after in workflow.extend.<NAME>.- An inline
agent(when present) must be a string; an inlineloop(when present) must be an int>= 1; an inlineapprove(when present) must be one ofauto/plan/dialog— the same type rules as thestagesblock. A non-stringagentraisesnon-str value in workflow.extend.<NAME>.agent; a non-int or< 1loopraisesnon-int value in workflow.extend.<NAME>.loop/loop must be >= 1 in workflow.extend.<NAME>; a non-stringapproveraisesnon-str value in workflow.extend.<NAME>.approve, and any other string raisesapprove must be one of: auto, plan, dialog in workflow.extend.<NAME>. Inlineagent/loop/approveare default overrides — astagesentry for the same name wins per field. - An extend entry that names a
before/aftertarget that does not exist in the pipeline (and is not another extend-stage) is a dangling reference — a structural error raised by the reference validation (unknown stage name in workflow.extend.<NAME>.before: <REF>or the.aftervariant). - Extend-stages are embedded into the compiled
FlowDocument.stagesonly. The originalPipelineDocument.bodyis never modified —extendis a run-time layering, like the rest of a workflow-file.
How the compiler applies a workflow
When a workflow is in scope, the compiler reconstructs the parsed body in
a fixed sequence of steps before building the output stages:
references are validated, extend-stages are embedded, skipped stages are
removed and their dependents reconnected, per-stage overrides are applied,
loops are expanded, external depends_on references are rewritten, agent
modes are resolved, and memory participation is computed over the finished
working body. Embedding early means a per-stage override or a loop
expansion can also target a stage introduced by extend, by name.
Step 1 — Validate references
Before anything is moved, the compiler validates every name a workflow could dangle on — existence only; cycles, self-references, and duplicate refs surface at run time:
- Every
before/afterref inworkflow.extendmust exist in the union of the original pipeline step names and the extend-stage names. A dangling ref is a structural error (unknown stage name in workflow.extend.<NAME>.before: <REF>or the.aftervariant). - Every name in
workflow.stagesmust exist in the same union. An absent name is a structural error (unknown stage name in workflow.stages: <name>). A stage that exists — even if also markedskip: true— is not flagged.
Both checks run before skip removal, so a name or ref pointing at a stage
that also carries skip: true still validates: the stage exists at
validation time and is removed later, at Step 3.
Step 2 — Embed extend-stages
For each entry in workflow.extend, the compiler appends a new stage to
the working step sequence — the verbatim entry body minus title, labeled
with the entry's title (or, falling back, the entry key). Positioning is
then applied by body format:
- Stages format — each
aftername becomes adepends_onentry on the new stage, and the new stage's name is appended to thedepends_onof eachbeforetarget (existing entries preserved; idempotent). Danglingbefore/aftertargets were already rejected at Step 1, so this step only ever sees resolvable names. - Phases format — the new stage is inserted positionally after the
last
aftertarget and/or before the firstbeforetarget, with no explicitdepends_on. Inconsistentafter/beforepositions fall back to theafterposition.
After this step the extend-stages live in the working sequence alongside the originals, so every later step applies to them by the same generic rules.
Step 3 — Remove skipped stages and reconnect dependents
Stages with skip: true are removed from the working body. Dependents'
depends_on are reconnected to the skipped stage's predecessors
(transitively for chains; positional collapse for phases). skip wins
over other overrides — removal precedes the override step. If the
reconstructed body is empty, the compiler raises empty body.
Step 4 — Apply per-stage overrides
The compiler first resolves one effective override map keyed by stage
name, shared by the override and loop-expansion steps: each extend
entry seeds a default override from its inline agent/loop/approve,
and each stages entry then overlays per-field, winning whenever its
field is not None (the inline value is the fallback). A name that
appears only in stages is used verbatim; a name that appears only in
extend carries just its inline agent/loop/approve.
For each (stage_name, effective_stage) pair in the map:
- Find the step in the body whose
nameor id equalsstage_name. - If not found — silent (only an intentionally skipped stage removed at Step 3 reaches here; unknown names already errored at Step 1).
- If found:
- When
effective_stage.agentis not None — set the stage'scommandfield to the composed wrapper path/home/goga/bin/<agent>-as-claude.sh. This applies the inline-extendagenttoo, via the effective map. - When
effective_stage.promptis not None — attach the prompt as per-stage context for the agent running the stage. The prompt has lower precedence than the stage's ownpromptfield and is treated as ambient guidance, not as a direct command. See Workflowprompt— context, not command. - When
effective_stage.skillsis not None — merge the workflow skills with the stage's existingskills(pipeline-first, deduplicated). See Skills merge. - The effective
approvedirective is threaded into the step body under an internal sentinel key (one ofauto/plan/dialog, orNone); it is read and consumed during canonical field assembly to drive the two approve effects (each on its own directive subset) and never reaches the output. See Auto-approval (approve: auto/plan/dialog). - The effective
manualinstruction is applied to the WORKING copy of the body (the parsedPipelineDocumentis never mutated):truesets the working body's trigger tomanualover any authored value (idempotent on an already-manual stage);falserewrites an effectivetrigger: manualback toon_successor raisesmanual: false on non-manual stage <NAME>when the stage is not manual; absent is a no-op. The resulting trigger is translated during canonical field assembly into the stage's launch mode. See Manual launch (manualandtrigger).
Skills merge
A stages-block skills override is merged with the pipeline-file
skills of the matched stage, not replaced:
- Pipeline-file skills come first (their relative order is preserved), then the workflow skills, with any value already seen dropped (deduplication is by value, first-occurrence order).
- A stage with no pipeline
skillsgets the workflow list as-is; a workflow override of[](or a stage with neither side carrying skills) leaves theskillskey absent rather than emitting an empty list. - An
extendentry's own bodyskillsare the new stage's pipeline-side skills — when astagesentry also names that stage, the merge combines the extend-body skills with thestagesoverride. With no matchingstagesentry the extend-body skills pass through verbatim.
Step 5 — Expand loops
For each step, determine loop_count from the effective override's loop
for that stage name when set, else 1. The effective loop folds an
inline-extend loop together with a stages-block loop (the stages
value wins per-field when both are present).
When loop_count >= 2, the compiler appends N copies with ids
<name>-1, ..., <name>-N, each subsequent copy depending on the previous
one. The compiler records an expanded_ids map from base-name to the list
of ids produced.
Expansion interacts with body format:
- Phases format — the expanded copies chain naturally via list position. The first copy inherits the original position; subsequent copies depend on their predecessor; the next original step depends on the last expanded copy.
- Stages format —
depends_onis otherwise passed through as-is. See Step 6 for the external-reference rewrite.
Step 6 — Rewrite external depends_on (stages format only)
For each stage in stages format with a non-empty depends_on, the
compiler replaces any reference to a base name whose loop_count >= 2
with expanded_ids[ref][-1] — i.e. the last expanded id.
For example, if stage review has loop: 2 and another stage declares
depends_on: [review], the compiled output carries
depends_on: [review-2].
Step 7 — Resolve agent modes
After overrides and expansion, the compiler resolves the agent mode for
every stage the same way the no-workflow path does: a stage without an
authored non-empty roles value runs in autonomous mode, and a stage
with an authored non-empty roles value runs in coordinated mode. See
stage modes for the functional description
of each mode.
A workflow-applied command override (from a per-stage agent field) and
the stage's own agent-mode resolution are independent — the override
selects which agent binary runs the stage, while the roles field
selects how the work is organized inside it.
Step 8 — Compute memory participation
After the working body is final (skip removal, loop expansion, and the
external depends_on rewrite have all run), the compiler computes memory
participation from the workflow's effective memory configuration — the
authored memory block when present, else the materialized defaults
(method: reflect, max_rules: 25, commit: false).
- Under the reflect method a stage participates when it carries a
reflectinstruction; under the alignment method when it carriesmemory: true. Participation is looked up per base name in the working body, so everyloop-expanded copy inherits its base's verdict and a skipped stage never counts. - The memory settings apply iff at least one stage participates — a configuration without participants is a silent no-op: the compiled output carries no memory keys at all.
- How the configuration and the per-stage participation are encoded into the compiled pipeline is an internal contract of the compiler — this documentation intentionally does not pin the compiled form.
- Memory application is output-side only — the source pipeline-file is
never touched, and an authoring
reflectormemory_usekey in any stage body is a structural error. A workflow without memory participation compiles byte-identically to the same workflow compiled before the mechanism existed.
Invocation modes
A pipeline run picks up a workflow in one of three mutually exclusive modes. The launcher communicates the chosen mode to the container through env-file entries.
| Mode | Invocation | Env-file entry | Behavior |
|---|---|---|---|
| Auto-match | goga pipeline deploy |
(neither) | If <cwd>/.goga/workflows/deploy.yml exists, it is applied silently. |
| Explicit override | goga pipeline deploy --workflow custom |
GOGA_WORKFLOW_NAME=custom |
Apply <cwd>/.goga/workflows/custom.yml. Host validates existence first. |
| Disable | goga pipeline deploy --no-workflow |
GOGA_WORKFLOW_DISABLED=1 |
Disable workflow application entirely. |
In auto-match mode no host-side validation runs — the workflow-file is opened and parsed inside the container, and a missing file is silently treated as "no workflow". In explicit-override mode the host validates the file exists before launch (exit 1 if missing).
--workflow and --no-workflow are mutually exclusive — passing both
exits with code 1 before launch.
The card form honors the same three modes: goga pipeline deploy --info
[-w <wf> | --no-workflow] resolves the workflow through the identical
rule set, with the same host-side validation. The decision travels as
docker run argv (not the env-file), and the stage list the card prints
is exactly the composition a run with the same flags executes. See
pipeline.
Log line
When a workflow will actually be applied (explicit --workflow, or an
auto-match file that exists), the launcher prints a single line to stdout:
Pipeline running with workflow "<name>"
When no workflow applies, the launcher prints no workflow line.
Example
A workflow that layers a Russian-language answer directive and two per-stage prompts on top of a pipeline:
prompt: |
Answer in Russian language
stages:
propose:
prompt: |
Task formalization process.
Requirements:
- When setting the task, it is necessary to develop stack technologies more carefully.
- Carefully examine all the link connections between cells
Constraints:
- Don't write code examples in the task
- Don't build architecture in the task
brainstorm:
prompt: |
Architectural design process.
Requirements:
- Annotations describe the high-level order of actions
- Every usage file is connected through Imports and referenced in annotations
- Usage files are self-contained
- Footer Description describes the responsibility zone
Constraints:
- Annotations must not reference previous functionality
- Annotations do not contain implementation details
- Annotations must not use "X from Imports" phrasing
- Footer Description does not contain details
A workflow that expands a propose-review stage into two passes and pins
its agent to claude:
stages:
propose-review:
loop: 2
agent: claude
Compiled effect: the original propose-review stage is replaced by
propose-review-1 and propose-review-2, each depending on the previous
one, both running with the claude wrapper.
A workflow that runs different stages on different CLI agents — authoring
on codex, review on claude — without touching the pipeline-file:
stages:
propose:
agent: codex
brainstorm:
agent: codex
architecture-review:
agent: claude
plan-review:
agent: claude
Here codex does the heavy authoring (propose, brainstorm) and claude
runs the reviews. The underlying pipeline-file stays unchanged — every
project can pin its own agent-per-stage matrix in its workflow-file.
A workflow that adds stages which are not in the pipeline-file at all — a
warmup that runs after propose, and an extra review that runs before
plan-review:
extend:
warmup:
after: [propose]
title: Warmup
prompt: |
Boot up tooling context before the pipeline runs.
extra:
before: [plan-review]
title: Extra review
prompt: |
An additional review pass before the plan is finalized.
Compiled effect: two stages absent from the pipeline-file now appear in the
run. In stages format warmup depends on propose, and plan-review gains
extra as an additional dependency; in phases format warmup is inserted
after propose and extra before plan-review. The pipeline-file itself is
untouched — extend layers new stages on top at run time.
See also
- Pipeline File — the base document a workflow layers on top of.
goga pipelineCLI reference — invocation flags for--workflow/--no-workflowand exit codes.