Pre-release Harn is pre-1.0 — the language, standard library, and CLI may change between releases. See the release notes

Build your first workflow

By the end of this page you will have built an agent that fixes a failing test and checks its own work. You will start with a single model call, grow it into an agent that can use tools, and finish with a two-stage workflow that verifies the result and retries with feedback when the check fails.

No prior agent-orchestration background is assumed. Every block on this page is a complete program you can save and run. We use the built-in mock provider so everything runs offline with no API key. The mock provider returns canned text, so it won't actually repair code, but it lets you watch the whole machine wire up and run. When you want a model that does real work, change one word (provider) and read LLM providers.

What you need

A working harn binary. Check it:

harn --version

That's the only prerequisite. The mock provider ships in the binary.

Step 1: one model call

The smallest useful thing is a single request. Save this as fix.harn:

const answer = llm_call(
  "The test test_add expects add(2, 2) == 4 but got 5. What's the likely bug?",
  "You are a careful Rust engineer.",
  {provider: "mock"},
)
log(answer.text)

Run it:

harn run fix.harn

llm_call sends one prompt and returns a result dict. The text is in answer.text; token counts, the model name, and the full transcript are on the same dict when you need them.

One call, one answer. There is no loop and no way for the model to look at a file or run the test. That is the right tool when the whole job is "read this, tell me that." Ours needs more: to fix the bug, the agent has to read the code, edit it, run the test, and react to what the test says. That is a loop.

Step 2: an agent that uses tools

agent_loop runs a model in a loop: it calls the model, dispatches any tools the model asked for, feeds the results back, and repeats until the model reports it's done. You give it a task, a system prompt, and a set of tools. It owns the loop, the budget, and deciding when "done" means done.

const result = agent_loop(
  "Fix the failing test test_add in src/math.rs, then run the test to confirm.",
  "You are a senior engineer. Make the smallest change that turns the test green.",
  {provider: "mock", loop_until_done: true},
)
log(result.status)          // "done", "stuck", "budget_exhausted", ...
log(result.text)            // the agent's final output
log(result.llm.iterations)  // how many model round-trips it took

loop_until_done: true tells the loop to keep going until the model signals completion rather than stopping after the first turn. The status tells you how it ended: done when the model finished cleanly, stuck or budget_exhausted when it ran out of road.

On the mock provider this returns almost immediately with canned text. On a real provider you would also pass tools: so the agent can read and edit files and run the test. See Agent tools for the ready-made write_file / edit_file / run set; the shape is:

import { agent_edit_tools } from "std/agent/host_tools"

const result = agent_loop(task, system, {
  provider: "ollama",
  model: "qwen3-coder",
  tools: agent_edit_tools(),
  loop_until_done: true,
})

One agent_loop is one goal, run to completion. It's the right rung for "make this one thing happen." But notice what it doesn't do: nothing outside the model checks whether the test actually passes at the end. The agent decides it's done; we take its word. For real work you want an independent gate, and a way to hand the agent another attempt when the gate says no. That is a workflow.

Step 3: verify the result

A workflow runs stages in order. Each stage is a step: an agent doing work, or a plain command that checks the work. Here we wire two stages: an act stage that tries the fix, and a verify stage that runs the test and reads the exit code.

import { workflow_stages } from "std/workflow/patterns"

const graph = workflow_stages({
  name: "fix-the-test",
  stages: [
    {id: "act", kind: "stage", mode: "agent", model_policy: {provider: "mock"}},
    {id: "check", kind: "verify", mode: "command",
      verify: {command: "cargo test test_add --quiet", expect_status: 0}},
  ],
})

const run = workflow_execute("Fix the failing test test_add.", graph)
log(run.status)
log(run.path)

The act stage is an agent, like Step 2. The check stage runs a real command and passes only when it exits 0. The verifier is separate from the agent, so "the agent thinks it's done" and "the test is actually green" are two different facts, decided by two different things. That separation is the entire point of lifting to a workflow.

workflow_execute runs the graph and writes a run record to run.path. Inspect it with:

harn runs view --json <run.path>

Right now, if check fails, the workflow stops. The agent gets one shot. The last piece is giving it another shot, with the failure in hand.

Step 4: retry with feedback

Add a retry_policy to the acting stage. Two keys turn a single attempt into a repair loop: max_attempts caps how many tries the stage gets, and feedback: true threads the previous failure's findings into the next attempt's prompt. The agent's second try starts with the first try's error, not a blank slate.

import { workflow_stages } from "std/workflow/patterns"

const graph = workflow_stages({
  name: "fix-the-test",
  stages: [
    {id: "act", kind: "stage", mode: "agent",
      model_policy: {provider: "mock"},
      retry_policy: {max_attempts: 3, feedback: true}},
    {id: "check", kind: "verify", mode: "command",
      verify: {command: "cargo test test_add --quiet", expect_status: 0}},
  ],
})

const run = workflow_execute("Fix the failing test test_add.", graph)
log(run.status)

Now the stage tries up to three times. After a failed attempt, the next prompt carries Previous attempt N failed: <findings>, where the findings are the verifier's output. The agent reads what went wrong and adjusts. It stops the moment an attempt passes, and every attempt is recorded in the run so you can replay exactly what it tried.

That is the self-verifying agent from the top of the page: it does the work, an independent check grades it, and a failed grade becomes the input to the next attempt.

On the mock provider the canned reply never actually fixes the code, so you'll watch all three attempts run and the stage exhaust its retries, the machine working exactly as designed. Point model_policy at a real model and give the act stage edit and run tools, and the same graph fixes the test for real.

What you built, and where to go next

You climbed the three rungs of the abstraction ladder:

  • llm_call — one request, one answer.
  • agent_loop — one goal, run to completion with tools.
  • workflow_stages — more than one attempt, with an independent verify gate and retry-with-feedback.

The rule that keeps you on the right rung: use the lowest one that covers the job, and lift only when the shape of the work changes: a second goal, a verify stage, a retry loop, a different model per stage.

From here: