# Octomind 0.32.0: Workflows You Run by Name

> Octomind 0.32.0 turns workflows from handwritten TOML into a system you invoke: parallel fan-out, runtime branches, a spending cap, and ten pipelines.

![Animated Octomind icon — purple pixel-art octopus](/octomind-icon-animated.gif)

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](https://octomind.run/blog/octomind-0-31-0-release) 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:

```
$ 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:

```
$ 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:

```
$ 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:

```
$ 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](https://octomind.run/blog/octomind-0-31-0-release)._
