Workflows — how a story's lifecycle is chosen
A workflow is a story's lifecycle: the steps it moves through and the
reviewers that gate entry to each. satelle does not hardcode a lifecycle — the
operator authors it as substrate under .satelle/workflows, and satelle enforces
it.
The authored form is a derived route: two files, done.md and step.md.
satelle help workflow-convert is the key-by-key reference (and the guide for
converting a repo that still carries a retired DOT graph).
The two halves
done.md — what DONE means. One ## <category> section per story category,
each an ordered list of obligations, plus the exits the binary synthesises. ## *
governs any category with no section of its own.
## *
- raised
- coded
- closed
park: blocked @satelle-story-blocked-review
cancel: cancelled @satelle-story-cancel-review
+ surface:ui design-reviewed
step.md — what discharges each obligation. One ## <name> section per
step, one ## gate <skill> per always-on gate.
## in_progress
agent: executor
skills: code
reviewers: satelle-story-plan-review, satelle-story-architecture-review
reviewer_agent: reviewer
parallel: 0
provides: coded
requires: raised
Both files carry ordinary frontmatter (name, type: workflow, scope,
description) and must not carry applies_to — done.md's sections are the
selector, and a second one would be a second precedence rule. A lifecycle hook
(hooks: / the create_review: shorthand) rides on done.md.
The binary owns topology. Order is a topological sort of requires /
provides; cancel from every non-terminal step, park from anywhere, backward
movement and park → cancel are all synthesised. Authoring a cancelled or
blocked step by hand is the most common conversion mistake.
Precedence
A repo's own route governs the categories its done.md claims. The route the
binary SHIPS is order zero: it governs a category only when no authored workflow
claims it, so upgrading the binary never re-routes a repo behind its back.
satelle workflow list --category <category>
The head of that list is the active choice. A workflows doc that declares no
route governs nothing: satelle REFUSES transitions under it, naming
satelle help workflow-convert, rather than falling back and silently dropping
every gate the repo authored.
The choice is stamped
At create, the governing lifecycle is stamped on the story — a
workflow:<name> tag plus a workflow_stamped ledger entry — so the trail
records what governed. With a derived route the stamp is done.md+step.md for
every category; which LANE applies is chosen by the story's category, and
satelle story restamp re-resolves it after a re-categorisation.
Avoiding misconfiguration
satelle workflow validate flags what the operator should fix:
- An unresolved reviewer skill — a step or gate names a reviewer that does not resolve in the substrate. WARN, not FAIL, on the named form: a repo mid-authoring writes its route before its gate skills.
- An unresolved executor rubric — a step's
skills:that does not resolve. That IS a hard failure: the step cannot be performed, so the story can never reach its terminal state.
That command also prints each gate's effective model (the binding's model, or
the CLI default when the binding pins none). satelle agent validate prints the
same surface under its grant listing. To review one gate on a different model,
define a second role = "reviewer" binding in .satelle/workflows/agents.toml and name it
as that step's reviewer_agent: — see satelle help agent-dispatch and the
satelle-route-standard principle.
Binding a reviewer: a step's reviewers: vs an always-on ## gate
How a reviewer is bound matters as much as which skill it runs.
A step's reviewers: — gate-specific reviewers (prefer this)
A gate belongs to the step it ADMITS. Bind a gate-specific reviewer to that step:
## integration
reviewers: satelle-code-ac-review
reviewer_agent: reviewer
- List order = execution order (and ledger order). By default reviewers run
sequentially, all-must-accept, with first-reject short-circuit
(later reviewers are not invoked once one rejects) — but only when the step
says so with
parallel: 0. - Concurrency is the default for 2+ reviewers. Unset
parallelruns the list concurrently with no short-circuit, so a rejected round spends tokens on every reviewer. Setparallel: 0for sequential, orparallel: N(cap 4) to bound the fan-out. Aggregation stays all-must-accept in the binary; a multi-reject refusal names every rejecting reviewer.
An always-on ## gate — multi-step only
A ## gate <skill> section with on: is an always-on gate for every entry into
the steps it names:
## gate satelle-estimate-actual-review
on: in_progress, done
for: *
## gate satelle-step-summary
agent: reviewer
mandatory: true
for: *
Use it only when the gate genuinely belongs on every entry into those steps
(estimate at begin-work and close; step summaries). Authoring a gate-specific
check as a single-step ## gate is the common misuse.
for: names the categories whose route the gate belongs to. In a shared
catalogue, omitting it fires the gate on every lane — including ones with no
release to verify.
The over-fire trap
A gate on one step matches every transition into it, including recovery
(integration → in_progress). The gate then re-fires on every fix-loop
re-entry — an extra reviewer invocation per rework cycle. Bind it to the step's
own reviewers: instead unless always-on is what you mean.
See the embedded satelle-workflow-change-review gate for a content judgment of
route edits; structural validate stays PASS/FAIL only.
Four things a lifecycle governs — don't confuse them
They differ in WHERE they are declared, WHEN they fire, and WHO decides:
| | Declared in | Fires | Decided by |
| --- | --- | --- | --- |
| Step gates | a step's reviewers:, or a ## gate section | on a status change | a reviewer skill's verdict (or a coded ```check) |
| Lifecycle hooks | done.md frontmatter (hooks: / the create_review: shorthand) | outside the status graph — at story creation today | a reviewer skill's verdict |
| Deterministic structure checks | nowhere — they are the binary's contract | always, before anything else | code (internal/structure); no LLM, never flaky |
| Agent judgments | a reviewer skill's rubric | wherever a gate or hook names it | an isolated agent returning accept/reject |
Step gates are the status graph. They can only judge a move from one state to another, so anything off the graph is out of their reach.
Lifecycle hooks exist for exactly that gap. Story creation has no from
state — the item does not exist yet — so it cannot be a transition. A hook
declares the operation, the skill that judges it, and the logical agent that runs
the skill:
hooks:
- operation: create_review
skill: my-create-review
agent: strict-reviewer # optional; defaults to reviewer
The scalar create_review: my-create-review is the documented shorthand for the
same thing with the default agent. A hook declares who runs a skill, never
how: model, effort, command, transport and tool grant stay in
.satelle/workflows/agents.toml. satelle workflow show done prints each hook's full
resolved allocation; satelle agent validate refuses one whose agent is missing,
is not role = "reviewer", or is command = "in-loop".
Deterministic structure checks are not configuration at all — a story needs a clear goal and at least one numbered acceptance criterion, judged by code so the result is harness-independent and identical every run. A structural failure pre-empts: no hook or gate reviewer is reached on a malformed draft.
Agent judgments are the rubrics themselves. A gate or hook says which skill judges; the skill says what the judgment is. That split is why adding a criterion is a markdown edit rather than a release.
Beyond validate: the semantic review
workflow validate is deterministic structure only. The judgment it deliberately
does not make — is each performing step's agent: allocation (and its binding's
model) deliberate; does each performing step have a reviewer on the step that
follows it; is a dispatched binding's grant scoped to its step; is a dispatched
implementation step self-sufficient (an isolated agent sees only the item and its
rubric, never the conversation); is each gate bound where it belongs — is the
agent's review, guided by the embedded satelle-workflow-advisor skill
(satelle doc get skills satelle-workflow-advisor). Its findings are ADVICE to
the operator; the coded structural check stays the only hard rule.
For the full contract a dispatched (agent: <name>) step runs under — how it is
briefed, how it pulls the story/documents/ledger by id, what makes it
self-sufficient, and why the step that follows it must carry the review — see
satelle help agent-dispatch.
Reading a story's route — satelle story route <id>
The two halves answer "what is the lifecycle". They do not answer "what is THIS story's lifecycle" — which tag-scoped gates are on for it, where it is now, and why each gate it has already passed decided as it did.
satelle story route <id> is that answer, as one artifact:
- the plan half — the ordered steps between the story and done, each with the
obligation it discharges, who performs it under which rubrics, and the
reviewers gating entry, marked when a gate is present only because the story
carries a tag (and marked
skippedwhen it is absent for want of one); - the outcome half — appended as each step resolves: every reviewer's verdict, its reasoning, and a pointer to the full output on the ledger.
They are deliberately the same document. A route read separately from the verdicts is two things that drift.
The route renders live before a story has moved, so it is answerable from backlog, and it never requires opening a workflow file. That matters because the graph is no longer authored: there is no diagram to read, and refusals carry the same weight — an engine refusal names the rule that fired, why it fired on this story, and the legal moves it leaves open.
Mirrored from satelle’s built-in help. Read it in the binary with
satelle help workflows, or see the canonical source in the
satelle repo.