← All docs

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.toml and step.toml. satelle help workflow-convert is the guide for a repo still on the retired markdown route source — or still carrying a retired DOT graph — and the key-by-key mapping that converts them.

The two halves

done.toml — what DONE means. One category table per story category, each an ordered list of obligations, plus the exits the binary synthesises. ["*"] governs any category with no table of its own.

[meta]
name = "done"
type = "workflow"
scope = "project"
description = "What done means per category."

["*"]
obligations = ["raised", "coded", "closed"]
park = { state = "blocked", gate = "satelle-story-blocked-review" }
cancel = { state = "cancelled", gate = "satelle-story-cancel-review" }

[docs]
obligations = ["raised", "doc-authored", "docs-verified"]
cancel = { state = "cancelled", gate = "satelle-story-cancel-review" }

step.toml — what discharges each obligation. One obligation-keyed table per step, one [[gate]] entry per always-on gate. The table key IS the obligation the step discharges — there is no provides key.

[meta]
name = "step"
type = "workflow"
scope = "project"
description = "Step catalogue and always-on gates."

[coded]
status = "in_progress"
agent = "executor"
skills = ["code"]
reviewers = ["satelle-story-plan-review", "satelle-story-architecture-review"]
reviewer_agent = "reviewer"
parallel = 0
requires = ["raised"]

Both files carry a [meta] table (name, type = "workflow", scope, description) and must not carry applies_to — done.toml's category tables are the selector, and a second one would be a second precedence rule. A lifecycle hook (create_review / [[meta.hooks]]) rides on done.toml's [meta].

The binary owns topology. Order is a topological sort of requires against the obligation each step's table key discharges; 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.toml 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 workflow:default 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:

[integrated]
status = "integration"
reviewers = ["satelle-code-ac-review"]
reviewer_agent = "reviewer"
requires = ["coded"]
  • 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 parallel runs the list concurrently with no short-circuit, so a rejected round spends tokens on every reviewer. Set parallel = 0 for sequential, or parallel = 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]] entry with on is an always-on gate for every entry into the steps it names:

[[gate]]
skill = "satelle-estimate-actual-review"
on = ["in_progress", "done"]
for = ["*"]

[[gate]]
skill = "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]] entry | on a status change | a reviewer skill's verdict (or a coded ```check) | | Lifecycle hooks | done.toml's [meta] (create_review / [[meta.hooks]]) | 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:

# Shorthand — default agent = reviewer:
[meta]
name = "done"
type = "workflow"
scope = "project"
create_review = "my-create-review"

# Or the array form, when a non-default agent is needed (do not set both):
# [[meta.hooks]]
# operation = "create_review"
# skill     = "my-create-review"
# agent     = "strict-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 skipped when 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.