Skip to content
DocsHaarness CLI
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=3

Values 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-ms only 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 --yolo approves 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