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:
- has an agent plan the change and save the plan as an artifact;
- pauses until you approve the plan, or cancels if you reject it;
- has an agent implement the approved plan;
- runs your test command and records whether it passed;
- 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.
Who does what
Section titled “Who does what”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:orscript: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.
Lay out the workflow folder
Section titled “Lay out the workflow folder”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.
Write a command
Section titled “Write a command”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 theproject'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 plansays should change. Fix the cause, not the symptom: do not delete or skip atest 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.
Write the workflow
Section titled “Write the workflow”Create build-change.yaml:
name: build-changedescription: | 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.outputEach node does one job:
planis an agent node.$ARGUMENTSis the brief you pass to the run.output_formatmakes 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-planis the gate. Await:node withattention:pauses the run until someone answers it, andform: approvalmeans the answer isapproveorrejectplus 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.stopcancels the run when you reject. A rejection is an answer, not a cancellation, so the workflow decides what happens next.implementruns only after an approval.$approve-plan.output.textpasses on any notes you gave with the answer.testis a script node. It runs the command from thetest_commandinput, which defaults tonpm test, and printspassedand the last 4,000 characters of output as JSON.output_formaton a script certifies its stdout: output that does not match the schema fails the node.fixruns only whentestreports a failure. It loadscommands/fix-tests.md, andwith:binds the test output to the command’s$INPUTS.test_output.context: freshstarts 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.
Validate it
Section titled “Validate it”archon validate workflows build-change build-change ok
Results: 1 valid, 0 with errorsValidation 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.Dry-run it with stubs
Section titled “Dry-run it with stubs”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:
archon workflow run build-change --dry-run --stubs-init stubs.yamlEdit 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:
archon workflow run build-change --dry-run --stubs stubs.yaml "Add a --version flag"STUBBED plan (prompt)PAUSED approve-plan (wait) — durable waitSimulation outcome: pausedThe 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 stubSKIPPED stop (cancel) — when_condition_falseSTUBBED implement (prompt)STUBBED test (script)STUBBED fix (command)Simulation outcome: completedThe 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.
Keep the routes as fixtures
Section titled “Keep the routes as fixtures”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.
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 failedworkflow test never creates a run or calls a provider. Run it after every edit to the workflow, and in CI.
Run it with a real brief
Section titled “Run it with a real brief”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.
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 saywhich 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:
archon workflow attention # paused runs that need you, with their IDsarchon workflow artifacts <run-id> plan.md # print the planarchon 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.
Where to go next
Section titled “Where to go next”- Every field and node type: Workflow language.
- Repeat work until a check passes: Loop nodes.
- Write script nodes that supply facts: Script nodes.
- Start a workflow from an event, such as a new issue: Proactive workflows.
- Write better commands: Authoring commands.
- A full delivery pipeline to copy from: the bundled
sdlcpack; see SDLC workflows.
