Your first factory
Three agents, one loop, one way in. This is the whole of examples/planner.toml, and it is parsed
and validated by the test suite, so it cannot quietly stop working.
# The minimal factory shape documented in docs/architecture.md.
#
# Start here: three agents, one loop, one manual pipeline. For the reference scenario — scheduled
# triggers, conditional prompts and a rendezvous on both ends — see workitem-factory/.
#
# This file is parsed and validated by crates/layover-core/tests/examples.rs, so the documented
# configuration cannot quietly stop being loadable.
[layover]
work_dir = "workspace"
logbook = ".layover/logbook.md"
prompt_dir = "prompts"
[defaults]
runner = "claude"
# Only the *name* goes here; the Tower reads the value from its own environment at spawn time, so
# this file stays committable. Name whichever your runner wants.
env_from = ["ANTHROPIC_API_KEY"]
max_hops = 8
fuel_usd = 5.00
max_runs = 64
timeout_sec = 900
# ── How to invoke each supported CLI ───────────────────────────────
# The composed instructions go to the process on **stdin**, never on the command line: Windows
# caps one at 32,767 characters and real prompts run to tens of kilobytes. A `{prompt}` placeholder
# would be a *path* to that text, for CLIs that take a file — none of these three do.
[runners.claude]
command = ["claude", "-p", "--model", "{model}", "--output-format", "stream-json"]
mcp = { flag = "--mcp-config", format = "claude_json" }
# A runner may also fix a value itself rather than carry an agent's: every agent on this one reasons
# at `high`, whatever it declares. workitem-factory/ shows the other way, with `{effort}`.
[runners.copilot]
command = ["copilot", "--model", "{model}", "--reasoning-effort", "high", "--allow-all-tools",
"--output-format", "json"]
mcp = { flag = "--additional-mcp-config", format = "claude_json", prefix = "@" }
[runners.codex]
command = ["codex", "exec", "--model", "{model}", "-"]
mcp = { flag = "-c", format = "codex_toml" }
# ── Agents ─────────────────────────────────────────────────────────
[agents.planner]
description = "Breaks incoming goals into concrete tasks and dispatches them"
purpose = """
Route here when a goal still needs decomposing. The planner is also where rejected work comes
back to, so it decides whether to retry, re-scope or stop.
"""
runner = "claude"
model = "claude-opus-4"
resident = false
prompt = """
You break incoming goals into concrete tasks and dispatch them.
Record durable conclusions with layover_memory_write.
"""
[agents.coder]
description = "Implements the task described in the incoming flight"
runner = "copilot"
prompt = "You implement the task described in the incoming flight."
[agents.reviewer]
description = "Approves work or returns concrete defects"
runner = "codex"
access = "read-only"
prompt = "You review work and either approve it or return concrete defects."
# ── Pipelines: how work enters the mesh ────────────────────────────
[pipelines.build]
description = "Turn a goal into reviewed work"
entry = "planner"
trigger = "manual"
# ── The route map: directed edges ──────────────────────────────────
[[routes]]
from = "planner"
to = "coder"
[[routes]]
from = "coder"
to = "reviewer"
[[routes]]
from = "reviewer"
to = "planner"
What each part does
[layover] says where things live. prompt_dir is resolved relative to the configuration
file, so a factory can be run from anywhere.
[defaults] sets the safety rails. max_hops bounds how deep a chain of flights can go;
fuel_usd and max_runs bound how wide it can spread. They are not interchangeable — see
Pipelines and triggers.
[runners.*] says how to invoke each CLI. The composed instructions reach the process on
stdin, not on the command line — see Configuration. A {prompt}
placeholder, where a runner needs one, is a path to that text rather than the text itself.
[agents.*] declares an agent. The table key is its name. description is what peers see
when they ask Layover who they can reach, so write it for another agent to read.
[pipelines.*] is how work gets in. This one is manual: a human starts it.
[[routes]] is the route map. planner → coder does not imply coder → planner; both
directions are written out. An edge that is not listed means the flight is refused.
Try it
layover validate --config examples/planner.toml --strict
layover explain --config examples/planner.toml
layover prompt planner --config examples/planner.toml
layover serve --config examples/planner.toml # runs the factory and its dashboard; open the address it prints
layover run --config examples/planner.toml --dry-run # what is queued, without starting it
layover serveruns the factory. Trigger a workflow from the dashboard and the Tower authorises the flight against the route map and the rails, spawns the agent, watches it, and records what happened. When the agent hands work on withlayover_send, the next agent runs in the same chain, on the same budget.layover rundoes the same once, for whatever is queued, and then exits. See Status.
What it does not say
Notice what is missing: any statement of what happens after the coder finishes. The route map says the coder may send to the reviewer, not that it will. Agents decide that at runtime.
This is the central design choice. Layover is a permission mesh, not a pipeline engine. It is what makes the reference factory's review loop possible without Layover knowing anything about reviews.