On this page
Run and write workflows
A workflow is a script that runs several agents in stages: for example, three reviewers in parallel, then one agent that checks their findings. It runs on the workspace server, so it keeps going if you close the terminal.
Use a workflow for broad work with several stages. For a focused change, just ask the agent.
Workflows run with Bun: from the command line and the terminal UI it must be on
your PATH. Desktop brings its own copy.
Run a workflow
haarness workflow list
haarness workflow run audit --args '{"scope":"server"}'list shows each workflow's name, description and file, and haarness workflow source audit
prints its code. run waits until the workflow finishes and prints the run's outcome and result;
add --json for the whole run record.
Every workflow is also a slash command in the terminal UI. /audit check the server runs the
audit workflow, and the text after the name becomes its input argument.
Run one from a script
haarness run --workflow audit --workflow-arg scope=server --workflow-arg reviewers=3Values that look like JSON (numbers, true, lists) are passed as JSON; anything else is a string.
The prompt text, if you give one, becomes the input argument. This form exits with an error when
the run does not complete, which is what a CI job usually wants.
Start a run and check on it later
haarness workflow run audit --detach # print the run and return
haarness workflow runs # recent runs in this folder
haarness workflow inspect <run-id> # status, phase and result/workflow in the terminal UI opens a dashboard of workflows and recent runs. In Desktop,
/workflow and /workflow run <name> answer in a tray above the message box, which follows the
run until it ends and shows the run ID that /workflow cancel, pause and resume take.
Pause, resume or cancel a run
haarness workflow pause <run-id>
haarness workflow resume <run-id>
haarness workflow cancel <run-id>
haarness workflow delete <run-id>Pausing stops the run. Resuming starts it again from the beginning, with the same source and
arguments. A run that was going when Haarness stopped is marked interrupted, and you resume it
the same way. delete removes the run from the list.
Write a workflow
Save a file in .haarness/workflows/ in your project, for example .haarness/workflows/audit.ts.
The workflow's name becomes its command.
import { workflow } from "@haarness-ai/plugin"
export default workflow({
name: "audit",
description: "Review a folder for security and reliability problems",
phases: [{ title: "scan" }, { title: "verify" }],
arguments: {
scope: { type: "string", default: "." },
},
async run(args, ctx) {
ctx.setPhase("scan")
const review = (topic: string) =>
ctx.agent({ prompt: `Review ${args.scope} for ${topic} problems`, label: topic })
const reports = await ctx.parallel([
() => review("security"),
() => review("reliability"),
])
ctx.setPhase("verify")
const findings = reports.map((r) => r?.text).join("\n")
const check = await ctx.agent({ prompt: `Keep only confirmed findings:\n${findings}` })
return check?.text
},
})You do not install @haarness-ai/plugin; Haarness provides it. Run haarness workflow list to
check that the file was found. Workflows that plugins ship are named plugin-name:workflow-name.
To use a workflow in every project, save it in the workflows folder of your global Haarness
folder instead: ~/.config/haarness/workflows/ on every system (see
Where Haarness keeps its files). A
project's workflow wins over a global one with the same name.
You can also ask the agent to write one: "create a workflow that reviews each crate in parallel".
It saves the file in .haarness/workflows/.
What a workflow can call
| Call | Does |
|---|---|
ctx.agent({ prompt, … }) |
Runs a child agent; returns a reply with .text |
reply.followup(prompt), reply.close() |
Continues or ends that child |
ctx.parallel(tasks, { concurrencyLimit }) |
Runs tasks at once; a failed task gives null |
ctx.pipeline(items, ...stages) |
Passes each item through the stages |
ctx.tool(name, args) |
Calls a Haarness tool |
ctx.shell(command) |
Runs a shell command; returns output and exit code |
ctx.workflow(name, args) |
Runs another workflow inside this one |
ctx.setPhase(name), ctx.log(value) |
Reports progress |
ctx.agent also takes agent, model, variant, label, skills, files and schema. With
schema (a JSON Schema), the child must answer in that shape and the value arrives as
reply.data.
A restricted form for generated workflows
When a workflow was generated rather than written by you, prefer the restricted form. It has no access to the file system, network or processes except through Haarness, so every action goes through your normal permissions.
export const meta = { name: "portable-audit", phases: ["scan", "verify"] }
phase("scan")
const reports = await parallel([
() => agent({ prompt: "Inspect authentication", label: "auth" }),
() => agent({ prompt: "Inspect file-system boundaries", label: "filesystem" }),
])
phase("verify")
const manifest = await tool("read", { path: "Cargo.toml" })
return { reports, manifest }The same calls are available without ctx.: args, phase, log, agent, tool, shell,
workflow, parallel and pipeline.
Limits worth knowing
- By default a run stops after 30 minutes.
--timeout-msonly limits how long the command waits; the run itself keeps going. - Workflow code cannot ask you questions. It fails instead of waiting.
- The agents, tools and shell commands it runs follow your permissions. Their requests ask you
like a chat's do.
haarness workflow run --yoloapproves them, once each, while the command waits; it cannot be combined with--detach. - A workflow written as ordinary TypeScript is not sandboxed. It runs with your user's access, like any script. Only add workflows you trust.
- With
--disable-project-config, project workflows are not loaded.
Reference
Flag (workflow run) |
Does |
|---|---|
--args '<json>' |
Arguments, as one JSON value |
--agent, --model, --variant |
Defaults for the child agents |
--detach |
Return as soon as the run starts |
--timeout-ms |
Stop waiting after this long |
--json |
Print the whole run record as JSON |