Animated Octomind icon — purple pixel-art octopus

Multi-stage work has always put you in a bad spot. Cram "spec it, build it, test it, review it" into one session and you watch context rot set in halfway through — the model forgets the spec it wrote forty turns ago. Split it across separate octomind run calls in a shell script and you get clean sessions, but you lose every number that matters: per-step cost, live progress, and where the spend went.

0.30.0 gave you a way out of that: a standalone workflow runner — a TOML file that chains octomind run invocations, each a fresh session, outputs flowing between steps by name. 0.31.0 added the Supervisor so a single session actually finishes what it started. 0.32.0 is the next floor: workflows stop being a file you hand-write and become a system — with parallelism, runtime-selected branches, a spending ceiling for the whole run — and a library you invoke by name.


Run a workflow by name

The headline change is that you no longer have to author a workflow to use one. The muvon tap now ships a library of them. List what you have:

text
$ octomind workflow

available workflows
  content         Turn a brief into a researched, audited, publish-ready article…  (muvon/tap)
  debug           Reproduce a bug and locate its root cause in the current repo…   (muvon/tap)
  develop         Spec-driven feature development: context → developer/evaluator…  (muvon/tap)
  document        Classify the diff (SemVer + change buckets), draft docs in…       (muvon/tap)
  launch          Take a product idea from market exploration to an honest go/no-go… (muvon/tap)
  localize        Transcreate one input into several locales in parallel, then QA…  (muvon/tap)
  plan-and-build  Draft an implementation spec, then implement it and verify…       (muvon/tap)
  research        Investigate from background/evidence/counter angles in parallel…  (muvon/tap)
  review          Review the current changes, verify each finding, branch on a…     (muvon/tap)
  seo             Audit a site across technical/on-page/off-page/GEO lenses, then…   (muvon/tap)

Then run one. Input comes from stdin; per-step progress streams to stderr:

bash
echo "Add a --json flag to the export command" | octomind workflow plan-and-build

Resolution mirrors octomind run <tag>. A bare name (plan-and-build) is fetched from your taps — first tap wins, the built-in muvon tap last. An existing path or anything ending in .toml is loaded as a local file. No argument at all lists everything available across your taps.

There's one rule that makes a fetched workflow portable: public roles only. Every step's role in a tap workflow must be a category:variant tag installed via taps — developer:general, assistant:researcher, ai:evals. That guarantees anyone with the same taps can run the same workflow and get the same agents. Local files you write yourself have no such restriction — use whatever roles your config defines.

The ten workflows split into two groups. Five operate on the current directory — develop, debug, review, document, plan-and-build — and read your repo as context. Five are driven purely by a stdin goal — launch, content, research, localize, seo. Most don't just generate and stop: they chain a gate, an evaluator-optimizer loop, or an independent verifier. The output gets checked against a machine-readable verdict before you see it.

Want to see the shape before spending a token? --dry-run validates the file and prints the execution graph:

text
$ octomind workflow review --dry-run

workflow: review
  1. review  [sequential]   role: developer:general
  2. verify  [sequential]   role: developer:general
  3. verdict [conditional]
     condition:   output="verify"  matches="(?m)^VERDICT: APPROVED"
     on_match:    ["approve"]
     on_no_match: ["fixes"]

review reads the diff, then a separate step independently re-checks every finding against the actual code, then a conditional branches on a deterministic verdict — an approval note if it's clean, a prioritized fix list if it isn't. That second-pass verification is the whole point: the reviewer doesn't get to mark its own homework.


Fan-out: many branches, one answer

A workflow step used to do one thing at a time. 0.32.0 adds real parallelism. Mark a step parallel = true and its sub-steps run concurrently; the next top-level step starts only once they've all finished.

The obvious use: run the same task across several models and let a judge synthesize the best result:

toml
[[steps]]
name        = "candidates"
parallel    = true
min_success = 2                     # one model may fail; two is enough

  [[steps.run]]
  name   = "opus"
  role   = "developer:general"
  model  = "anthropic:claude-opus-4-8"
  prompt = "Solve this. Be complete and correct:\n{{input}}"

  [[steps.run]]
  name   = "gpt"
  role   = "developer:general"
  model  = "openai:gpt-5"
  prompt = "Solve this. Be complete and correct:\n{{input}}"

  [[steps.run]]
  name   = "gemini"
  role   = "developer:general"
  model  = "google:gemini-3-pro"
  prompt = "Solve this. Be complete and correct:\n{{input}}"

[[steps]]
name   = "judge"
role   = "developer:general"
prompt = "Independent solutions to the same task:\n\n{{candidates}}\n\nPick the strongest, fix its flaws, produce one final answer."

Two new block fields keep a fan-out from failing in real conditions. min_success lets the block pass even if a branch fails — two of three is enough above, so a flaky provider doesn't sink the run. max_parallel caps how many branches run at once, so a wide fan-out doesn't open thirty sessions simultaneously.

Aggregation is handled by names. {{candidates}} — the block's own name — expands to every branch's output joined under ── opus ──, ── gpt ──, ── gemini ── headers, so the judge can reference the whole set at once. Each branch is also addressable on its own ({{opus}}). And if you want best-of-N from a single model and prompt, you don't copy-paste — set count = 3 on one sub-step and it runs three times unchanged; the model's non-determinism gives you three different attempts to pick from.


Dynamic fan-out: the branch count decided at runtime

Static fan-out is fixed in the file. But often you don't know how many branches you need until a planning step has run — a researcher breaks a question into sub-questions, and you want one branch per sub-question, however many that turns out to be.

Add a match regex to a parallel block and it flips to dynamic. The regex runs against the previous step's output; each match becomes one branch. This is exactly how the shipped research workflow works:

text
$ octomind workflow research --dry-run

  1. angles  [parallel]  sub-steps=3
     background / evidence / counter   role=assistant:researcher
  2. report  [loop]  max_iterations=2
     exit_when: output="judge"  matches="(?m)^VERDICT: GROUNDED"
     synthesizer  role=assistant:researcher  session=Continue
     judge        role=ai:evals             session=Fresh

research attacks a question from three fixed angles in parallel — background, evidence, counter-arguments — then loops a synthesizer against an independent groundedness judge until every claim checks out against its sources (VERDICT: GROUNDED), capped at two iterations. The fixed three-way split is static fan-out; a planner that emitted <task> blocks would be dynamic.

Getting the names right matters here, because two of them play two different roles. The block has exactly one sub-step — the per-item template. The block's name is the loop variable: inside the template it resolves to this branch's matched item. The sub-step's name is the accumulator: a later step reads it to get every branch's output joined. Item text comes from capture group 1 of your regex. The block can't be the first step (there's nothing to match against), and concurrency and spend are bounded by max_parallel and the run-wide cost cap below.


A Spending Cap for the Entire Run

The catch with all this composition: every step is a separate octomind run subprocess with its own session. Any per-request or per-session spending threshold from your config resets on every step. A loop that runs two steps for ten iterations can quietly spend up to ~20× a per-session cap, and a dynamic fan-out's branch count isn't even known until runtime.

max_cost is the answer — a single hard ceiling for the whole workflow:

toml
name     = "research"
max_cost = 5.00    # USD; abort once total spend crosses this

The check runs after each step's cost is folded into the running total, so it stops spend before the next step — including between loop iterations and after a parallel block. The step that crosses the line still finishes; then the workflow exits non-zero with workflow cost budget exceeded: spent $X exceeds max_cost $cap. It's the workflow-level analogue of Octomind's per-session spending control, and for any fan-out or loop it's the only number that actually bounds the bill. The shipped research and content workflows set it by default because their branch counts are open-ended.

One subtlety that got fixed along the way: a session = "continue" step reports cumulative session cost every time it resumes. Left alone, an N-iteration refine loop would over-count its spend ~N× and trip max_cost early. The orchestrator now subtracts each step's running baseline so every turn's spend counts exactly once — in the per-step line, the footer total, and the cap.


Output you can build on

Two features make a workflow's result something a script can consume, not just a human can read.

A plain workflow run writes nothing to stdout — the human view, with per-step boxes, spinners, and a cost footer, is all on stderr. Pass --format jsonl and stdout carries one assistant JSON event per step as it completes (the last is the final result), followed by a single cost event with the aggregated session_tokens and session_cost. That's what you pipe into the next thing.

For a single octomind run, the new --schema flag goes further and constrains the model's reply to a JSON Schema you provide:

bash
echo "List the top 3 TODOs in the auth module" \
  | octomind run developer:general --format jsonl --schema todos.schema.json

The schema applies to every assistant reply for the session's lifetime — across turns, resumes, and daemon mode — while tool calls still flow normally underneath; only the final text is constrained. It's a runtime override like --model: not persisted, so pass it again on resume. And it fails fast if the model can't actually enforce it, rather than silently returning prose:

text
$ echo "List 2 todos" | octomind run --schema todos.schema.json -m anthropic:claude-haiku-4-5
Error: Model 'anthropic:claude-haiku-4-5' (provider 'anthropic') does not support
structured output — a JSON schema cannot be enforced. Use a structured-output-capable model.

Structured output is an OpenAI-family capability today; Anthropic models reject the schema up front so you find out before you spend, not after. A ready-to-use todos.schema.json ships in config-templates/.

Steps also gained a workdir field this release — point a single step at a subdirectory (workdir = "./packages/api") without affecting the rest of the run. Useful when one stage of a workflow operates on a sub-project.


What got fixed

Three reliability fixes in 0.32.0 remove real friction.

The truncated-tool-result retry loop. When an MCP tool returned more than the cap and Octomind truncated it, the model would sometimes see the cut-off output, assume the call failed, and run it again — and again. The truncation handling now tags the result idempotently, attaches a stop directive when the same output keeps coming back truncated, and gives tool-specific guidance on how to narrow the query instead of re-running it blindly.

The task that evaporated mid-loop. During a long autonomous tool loop, context compression could drop the active task from the continuation wrapper — the agent would compact, lose its current task, and drift. Compression now extracts and re-anchors the active task across compactions, so it survives even repeated re-compaction inside a tool loop.

Guardrail spawners that hung or leaked. Hook, validator, and pipe spawners could hang a turn or leave orphaned processes behind. That's fixed, and the loader now rejects duplicate validator/pipe names up front instead of letting an ambiguous config through. Alongside it, the WebSocket server resets its per-request spending checkpoint each turn so spend is attributed to the right request.


Upgrading

The workflow library needs no configuration — the muvon tap is built in, so octomind workflow lists the ten workflows immediately and octomind workflow <name> runs one. If you've written your own workflow files, they keep working unchanged; the new parallel fan-out fields, match, max_cost, and workdir are all additive.

--schema is the one feature with a hard requirement: a structured-output-capable model. Point it at an OpenAI-family model; Anthropic models will fail fast with the error above rather than ignore the schema.

Everything else carries forward from 0.31.0 without changes.


0.30.0 built the orchestration above the session. 0.31.0 built the control plane beside it. 0.32.0 makes many sessions compose — fan out, branch on runtime conditions, stay under one budget, and check their own work against a verdict before they answer. The session still does the work. The workflow makes a dozen of them add up to one verified result. And the library means you run it by name.

Octomind is a session-based AI development assistant built in Rust. Missed the last release? Read about the Supervisor control plane in 0.31.0.