Skip to content

Build your first workflow

In this tutorial you build build-change, a workflow that takes a brief for a small change to your repository and:

  1. has an agent plan the change and save the plan as an artifact;
  2. pauses until you approve the plan, or cancels if you reject it;
  3. has an agent implement the approved plan;
  4. runs your test command and records whether it passed;
  5. has a fresh agent fix the code if the tests failed.

Before you run it, you check it three ways that cost nothing: archon validate workflows, a dry run with stub outputs, and fixtures that archon workflow test replays on every change.

You need Archon installed and a provider added; see the Quickstart. Work in a git repository that has a test command.

Archon’s rule for a workflow is YAML coordinates. Code computes. Agents judge. build-change follows it:

  • Agents judge. Planning a change, implementing it and working out why a test fails all need judgment, so each is an agent node. When a later node needs the agent’s decision, the agent returns it as a small typed result through output_format, never as prose for code to parse.
  • Scripts give facts and act at boundaries. Whether the tests pass is a fact, so a script runs the test command and reports the exit code and output. It does not interpret the failure; that is the fixing agent’s job. Use a bash: or script: node only for work with a fixed rule like this one.
  • YAML coordinates. The file holds the order, the approval gate and the when: conditions on typed fields. It decides nothing on its own.

Tests follow the same split. The fixtures you write later check outcomes at those seams: which nodes run, whether the run completes or is cancelled, and whether the test output reached the fixing agent. They never pin what an agent writes.

A workflow lives in its own folder under .archon/workflows/<pack>/<workflow>/, with the commands and fixtures it uses beside it. Create this layout in your repository:

.archon/workflows/my-team/build-change/
├── build-change.yaml
├── commands/
│ └── fix-tests.md
└── fixtures/

my-team is the pack name; choose your own. A command or script that build-change.yaml names resolves only from its own commands/ and scripts/ folders, so two workflows can each have a fix-tests.md without colliding.

A command is a markdown file of instructions for an agent. Use one when the instructions are long or shared by several nodes. Create commands/fix-tests.md:

---
description: Find why the tests fail after a change and fix the cause
---
# Fix failing tests
The change described in $ARTIFACTS_DIR/plan.md was implemented, and the
project's tests now fail. The test output is:
$INPUTS.test_output
Decide whether the failure comes from the change or from a test that the plan
says should change. Fix the cause, not the symptom: do not delete or skip a
test to make it pass. Run the tests again, then commit the fix.

$ARTIFACTS_DIR is a directory Archon creates for each run, outside your repository. $INPUTS.test_output is a value the workflow passes to this command; the next section shows where it comes from.

Create build-change.yaml:

name: build-change
description: |
Plan a change from a brief, wait for you to approve the plan, implement it,
run the tests, and fix the code if they fail.
Use when: you have a small, well-described change for this repository.
inputs:
test_command:
default: npm test
description: the command that runs this project's tests
nodes:
- id: plan
prompt: |
Read the brief below and the code it touches. Write an implementation
plan to $ARTIFACTS_DIR/plan.md: the files you will change, the approach,
and how you will know it works. Do not change any files yet.
Brief:
$ARGUMENTS
output_format:
type: object
properties:
summary: { type: string }
required: [summary]
additionalProperties: false
- id: approve-plan
depends_on: [plan]
wait:
attention: |
$plan.output.summary
Read the full plan with `archon workflow artifacts <run-id> plan.md`.
Approve to implement it, or reject with what to change.
form: approval
- id: stop
depends_on: [approve-plan]
when: "$approve-plan.output.decision == 'reject'"
cancel: The plan was rejected.
- id: implement
depends_on: [approve-plan]
when: "$approve-plan.output.decision == 'approve'"
prompt: |
Implement the plan in $ARTIFACTS_DIR/plan.md. Reviewer notes, if any:
$approve-plan.output.text
Commit your changes with a clear message.
- id: test
depends_on: [implement]
runtime: bun
script: |
const run = Bun.spawnSync(['sh', '-c', process.env.INPUTS_TEST_COMMAND ?? '']);
const output = (run.stdout.toString() + run.stderr.toString()).slice(-4000);
console.log(JSON.stringify({ passed: run.exitCode === 0, output }));
output_format:
type: object
properties:
passed: { type: boolean }
output: { type: string }
required: [passed, output]
additionalProperties: false
- id: fix
depends_on: [test]
when: "$test.output.passed == false"
command: fix-tests
context: fresh
with:
test_output: $test.output.output

Each node does one job:

  • plan is an agent node. $ARGUMENTS is the brief you pass to the run. output_format makes the agent return JSON matching the schema, so the gate can show $plan.output.summary. The node fails if the agent returns anything else.
  • approve-plan is the gate. A wait: node with attention: pauses the run until someone answers it, and form: approval means the answer is approve or reject plus optional text. A paused run holds no process; answer it minutes or days later. Pause only where a person should decide. A check that code or another agent can make belongs in a node.
  • stop cancels the run when you reject. A rejection is an answer, not a cancellation, so the workflow decides what happens next.
  • implement runs only after an approval. $approve-plan.output.text passes on any notes you gave with the answer.
  • test is a script node. It runs the command from the test_command input, which defaults to npm test, and prints passed and the last 4,000 characters of output as JSON. output_format on a script certifies its stdout: output that does not match the schema fails the node.
  • fix runs only when test reports a failure. It loads commands/fix-tests.md, and with: binds the test output to the command’s $INPUTS.test_output. context: fresh starts a new agent session, so the agent works from the real test output instead of what the implementer believed it wrote.

Nodes without a dependency between them run at the same time. Here every node depends on the one before it, so they run in order.

Terminal window
archon validate workflows build-change
build-change ok
Results: 1 valid, 0 with errors

Validation loads the workflow the way a run would. It reports a misspelt key, a when: that references a field the producer never declares, a command file that does not exist, and a key that the node’s type cannot use. For example, timeout: on a prompt node fails:

ERROR [validation_error] DAG node validation failed: Node 'plan': 'timeout' is only supported on bash, loop, loop_group or script nodes, not on prompt nodes.

A dry run walks the graph with outputs you supply instead of calling a model or running scripts. It never creates a run, so it is free. Generate a stub file that has one entry per node that needs one:

Terminal window
archon workflow run build-change --dry-run --stubs-init stubs.yaml

Edit the values to describe the route you want to see. This one simulates failing tests:

plan: { summary: "Add a --version flag that prints the version from package.json." }
implement: "Added the flag and a test."
test: { passed: false, output: "version.test.ts: expected 1.2.0, got undefined" }
fix: "Read the version from package.json at startup."

Run the simulation:

Terminal window
archon workflow run build-change --dry-run --stubs stubs.yaml "Add a --version flag"
STUBBED plan (prompt)
PAUSED approve-plan (wait) — durable wait
Simulation outcome: paused

The simulation stops at the gate, as a real run would. A stub for the wait node stands in for your answer. Add one and run it again:

approve-plan: { decision: approve, text: "" }
STUBBED plan (prompt)
STUBBED approve-plan (wait) — answered by stub
SKIPPED stop (cancel) — when_condition_false
STUBBED implement (prompt)
STUBBED test (script)
STUBBED fix (command)
Simulation outcome: completed

The trace also prints each agent node’s resolved prompt and the provider and model it will run on. A dry run checks the wiring: routing, gates, when: conditions and variable substitution. It does not check what the model will write. To run the test script for real while the agents stay stubbed, delete its stub and add --exec-code --input test_command="npm test". That runs your test command on your machine.

A fixture is a stub file with an expected result, saved in the workflow’s fixtures/ folder. archon workflow test replays every fixture, so a later edit that breaks a route fails the test. Add one fixture per route.

fixtures/approved.stubs.yaml: the tests pass, so fix must not run.

# The tests pass, so `fix` is skipped. It has no stub, so the fixture fails if it runs.
plan: { summary: "Add a --version flag that prints the version from package.json." }
approve-plan: { decision: approve, text: "" }
implement: "Added the flag and a test."
test: { passed: true, output: "12 pass, 0 fail" }
fixture:
expect: completed
reached: [implement, test]

fixtures/red.stubs.yaml: the tests fail, so fix runs and receives the test output.

# The tests fail, so `fix` runs and receives the test output.
plan: { summary: "Add a --version flag that prints the version from package.json." }
approve-plan: { decision: approve, text: "" }
implement: "Added the flag."
test: { passed: false, output: "version.test.ts: expected 1.2.0, got undefined" }
fix: "Read the version from package.json at startup."
fixture:
expect: completed
reached: [fix]
resolved-text-contains:
fix: "expected 1.2.0, got undefined"

fixtures/rejected.stubs.yaml: you reject the plan, so the run is cancelled before any code changes.

# You reject the plan: the run is cancelled before any code changes.
# `implement` has no stub, so the fixture fails if it runs.
plan: { summary: "Rewrite the CLI parser." }
approve-plan: { decision: reject, text: "Too broad. Only add the flag." }
fixture:
expect: cancelled
reached: [stop]

The fixture: block holds the expectations. expect is the simulation outcome: completed, failed, paused or cancelled. reached lists nodes that must run. resolved-text-contains checks that a fact reached a node: here, that the failing test’s output reached the fixing agent’s instructions. A node the route reaches without a stub fails the fixture, so leaving out the stub for fix in approved is what proves fix stays skipped.

Terminal window
archon workflow test build-change
✔ my-team/build-change/fixtures/approved.stubs.yaml → build-change
Simulation outcome: completed
✔ my-team/build-change/fixtures/red.stubs.yaml → build-change
Simulation outcome: completed
✔ my-team/build-change/fixtures/rejected.stubs.yaml → build-change
Simulation outcome: cancelled
3 passed, 0 failed

workflow test never creates a run or calls a provider. Run it after every edit to the workflow, and in CI.

The brief is the input the whole run is measured against. A vague brief gets a confident plan for a change you didn’t want. State the problem, why it matters, the outcome you want, what must not change, and how you will know it is done; see The input is the contract.

Terminal window
archon workflow run build-change --branch feat/version-flag --input test_command="npm test" "$(cat <<'BRIEF'
Problem: the CLI has no way to print its version, so bug reports never say
which release they were filed against.
Outcome: `mycli --version` prints the version from package.json and exits 0.
Invariants: no new dependency; every other flag behaves as before.
Acceptance: a test runs `mycli --version` and checks the output.
BRIEF
)"

The run works in its own git worktree on the branch feat/version-flag, so its edits stay out of your checkout. After plan finishes, the command prints Workflow paused — waiting for approval. and returns. Read the plan and answer:

Terminal window
archon workflow attention # paused runs that need you, with their IDs
archon workflow artifacts <run-id> plan.md # print the plan
archon workflow approve <run-id> "Keep the flag handling in cli.ts"

Approving continues the run in your terminal until it finishes. To stop it instead, run archon workflow reject <run-id> "Too broad". The answer’s text reaches the next node as $approve-plan.output.text. Afterwards, archon workflow get <run-id> shows each node’s status, and the commits are on feat/version-flag.