Skip to main content
A workflow is a small JavaScript program that dispatches sub-agents and combines what they send back. agent(prompt) starts one and resolves to its final text, so running ten reviewers at once is await parallel([...]) — ordinary code, with the agents doing the thinking. There is no flag to set and no keyword to say. Helix reaches for a workflow on its own when the work is a shape — the same operation over a list, independent pieces that could run at once — and you can save that shape in your repo so it becomes something you run by name.

A workflow you can run

Save this as .mutagent/workflows/review-changed.js:
.mutagent/workflows/review-changed.js
Then ask for it:
Three reviewers start at the same time; when all three are back, a fourth agent merges their notes into one list. You get the merged list, and a row in the transcript that showed you both phases while they were happening.

When a workflow is the right tool

Reach for one

The same operation repeats over a list · independent readers can run at once · each item flows through the same few stages · you want to run it again next week without retyping it.

Don't

The work is one question — ask it directly · each step depends on what the last one found · you’d be writing the analysis into the script instead of letting the agents do it.
A single prompt is usually better. Every agent() call is a full model run with its own context, so a workflow that fans out five ways costs roughly five times what one agent would, plus the merge. That is a good trade when five agents genuinely read five different things, and a bad one when you split a question that only had one answer. The script is control flow, not analysis. Keep it short; let the agents think.

Where workflows live

Files in .mutagent/workflows/ are ordinary source files: commit them, review them in a pull request, share them with your team. Both .js and .ts files are picked up.
The body is plain JavaScript whatever you name the file — it runs in an isolated realm, not through your bundler. No import statements and no TypeScript type annotations: either is a syntax error that fails the run before its first agent starts.

The meta block

A saved workflow declares a meta object so it can be listed and described without being run.
Two shapes matter, because meta is read by pattern rather than executed:
  • Write it at the top level as export const meta = { … }, with the closing } at the start of its own line.
  • Give name and description plain string literals — not a variable, not a template with a substitution in it.
A file with no readable meta is skipped by the listing rather than run, and /workflows says how many were skipped. Nothing in .mutagent/workflows/ is ever executed just to find out what it is.

Running one

Ask for a saved workflow by name and Helix passes the values you named through as args:
A workflow does not have to be a file. Describe a shape and Helix writes and runs the script inline for a one-off:

args

args arrives in the script verbatim — whatever Helix passed in. It is the whole of the difference between a script and a reusable script, so read it at the top and give it a shape you can rely on:

The script API

Six things are in scope. There is nothing else — no require, no timers, no file system, no network. Return a value from the top level and it becomes the workflow’s result. console is available for debugging.

agent(prompt, opts)

Workflow agents are dispatched onto the same crew as any other agent in the session: they appear in the fleet list alongside anything you started by hand.

Failure is a value, not an exception

Nothing in a workflow throws at you from a distance:
  • An agent() whose run fails resolves to a string beginning [agent failed: …]. Your script keeps going, and can branch on it.
  • A thunk that throws inside parallel() becomes null in that slot; the others are unaffected.
  • A stage that throws inside pipeline() drops that item to null; the other items keep flowing.
  • A script that throws, or has a syntax error, fails the run — never the session — and every agent it had already dispatched is still reported.
So check for null and for [agent failed: before feeding a result into the next prompt.

Two rules worth knowing before you write one

Math.random() and Date.now() throw

Both are removed inside a workflow, and calling either fails the run with a message saying so. A workflow is meant to be re-runnable: the same script with the same args should describe the same run. A script that rolls a die or reads the clock takes a different branch every time, and the record of what it did stops matching what it would do again. If you need a seed or a timestamp, decide it outside and pass it through args:
new Date() is not blocked, but it is the same trap wearing a different hat. Take the time from args too.

Label every agent

opts.label is what the live view is keyed on. Give one to every agent() call, and make it say which piece of work this is:
Without a label the row falls back to the agent’s dispatch number and the clipped first line of its prompt. It is readable, but three near-identical prompts give you three near-identical rows to tell apart at a glance — exactly when you most want to know which one is stuck.

What a run looks like

A workflow renders as a live tool row that fills in as it goes: phases open and close, each agent gets a line with its state and how long it has been running, and the last line is the run’s shape.
Past eight agents in a phase the extra rows are counted (+ 4 more), never dropped, and on a narrow terminal the block collapses to fewer, denser lines rather than losing information. The row stays in your transcript after the run ends, which is usually when you want to read it.

Limits, honestly

There is no budget cap and no runaway backstop. Nothing counts what a workflow spends, and nothing stops a script that dispatches far more agents than you meant. parallel(items.map(…)) over a thousand-item list will try to start a thousand agents. Bound the list in the script.
  • One agent gets ten minutes. After that its call gives up and resolves to a failure string; the rest of the run continues.
  • The run itself is not time-limited. It ends when the script returns.
  • Nothing is persisted. The result and its digest live in the transcript; there is no saved run history and no resume — running a workflow again runs it from the top.
  • A workflow cannot reach the disk or the network itself. Only the agents can, through their own tools.
  • If the crew is unavailable, the workflow refuses up front and says why, rather than hanging on its first agent() call.

Sub-agents

The fleet list and the viewer — where the agents a workflow dispatches show up.