Creating Custom Presets
A preset is a self-contained loop definition that lives in a single directory. All shipped presets follow the same structure — there is nothing special about the built-in auto* family that a custom preset cannot do.
Directory structure
A preset directory contains four kinds of files:
my-preset/
├── autoloops.toml # Loop configuration (required)
├── topology.toml # Role deck and handoff graph (required for multi-role loops)
├── harness.md # Shared instructions loaded every iteration (required)
├── README.md # Human-facing description (optional)
└── roles/ # Role prompt files referenced by topology.toml
├── first-role.md
├── second-role.md
└── ...The directory can live anywhere. Built-in presets live under presets/<name>/; custom presets just need a path that autoloop run can resolve.
Step 1: Define the topology
topology.toml declares roles, their allowed events, and how events route between roles.
name = "my-preset"
completion = "task.complete"
[[role]]
id = "analyst"
emits = ["analysis.done", "task.complete"]
prompt_file = "roles/analyst.md"
[[role]]
id = "implementer"
emits = ["impl.ready", "impl.blocked"]
prompt_file = "roles/implementer.md"
[[role]]
id = "verifier"
emits = ["verified", "rejected"]
prompt_file = "roles/verifier.md"
[handoff]
"loop.start" = ["analyst"]
"analysis.done" = ["implementer"]
"impl.ready" = ["verifier"]
"impl.blocked" = ["analyst"]
"verified" = ["analyst"]
"rejected" = ["implementer"]Key rules:
- Every role needs an
id, anemitslist, and eitherprompt_fileorprompt(inline string). prompt_filepaths are relative to the preset directory.- The
[handoff]section maps events to the roles that should handle them. An event not listed in the handoff map causes all roles to be suggested (no routing preference). completionsets the topology-level completion event. It can also be set inautoloops.tomlviaevent_loop.completion_event— the topology value takes precedence, with the config value used as a fallback."loop.start"is the synthetic event emitted at iteration 1. Use it to define which role kicks off the loop.
See docs/topology.md for the full reference.
Step 2: Write role prompts
Each role gets a markdown file in roles/. A role prompt should:
- Open with identity — "You are the analyst." This anchors the model.
- State what the role does NOT do — "Do not implement. Do not verify." Boundary-setting prevents role drift.
- Define the job — Numbered steps for what the role does on every activation.
- Specify when to emit each event — Be explicit about the conditions for each event in the role's
emitslist. - List rules — Constraints, defaults, and fail-closed behaviors.
Example (roles/analyst.md):
You are the analyst.
Do not implement. Do not verify.
Your job:
1. Read the objective and current state.
2. Break the problem into a prioritized list of tasks.
3. Hand the next task to the implementer.
On every activation:
- Re-read shared working files before deciding the next task.
Emit:
- `analysis.done` with the next task description.
- `task.complete` only when all tasks are done and verified.
Rules:
- One active task at a time.
- Be specific enough that the implementer can act without guessing.Inline prompts are also supported — set prompt = "You are the analyst." directly in topology.toml instead of using prompt_file. This works for simple roles but markdown files are better for anything non-trivial.
Step 3: Write the harness instructions
harness.md contains shared rules that are injected into every iteration regardless of which role is active. Use it for:
- Naming the loop's purpose.
- Declaring shared working files and their roles.
- Setting global constraints (one task at a time, fresh context every iteration, etc.).
- Requiring use of the event tool for handoffs.
- Listing state file contracts.
Example:
This is a custom analysis-and-implementation loop.
Global rules:
- Shared working files are the source of truth: `{{STATE_DIR}}/tasks.md`, `{{STATE_DIR}}/progress.md`.
- One task at a time. Do not start the next task before the current one is verified.
- Use the event tool instead of prose-only handoffs.
- Fresh context every iteration: re-read shared working files before acting.
- Use `{{TOOL_PATH}} memory add learning ...` for durable learnings.
- Do not invent extra phases. Stay inside analyst → implementer → verifier.
State files:
- `{{STATE_DIR}}/tasks.md` — task list with priorities and status.
- `{{STATE_DIR}}/progress.md` — current task, verification results, what the next role should do.The harness.instructions_file key in autoloops.toml points to this file. It defaults to harness.md.
Template placeholders
Use and in harness instructions, role prompts, and metareview prompts instead of hardcoding .autoloop/ paths. The harness expands these placeholders at load time before the prompt reaches the model.
| Placeholder | Expands to | Example |
|---|---|---|
| The loop's state directory (e.g. .autoloop) | /progress.md |
| The full event tool path (e.g. ./.autoloop/autoloops) | emit review.ready "done" |
Why placeholders? The concrete state directory can vary — worktrees, chains, and nested loops all change the path. Placeholders let the harness inject the correct path at runtime so presets stay portable.
Important: Raw .autoloop/ paths in prompt text are not supported and will cause a load error. Always use and placeholders.
Step 4: Configure the loop
autoloops.toml sets iteration limits, backend, completion conditions, and memory/review settings.
event_loop.max_iterations = 100
event_loop.completion_event = "task.complete"
event_loop.completion_promise = "LOOP_COMPLETE"
event_loop.required_events = ["verified"]
backend.kind = "pi"
backend.command = "pi"
backend.timeout_ms = 3000000
review.enabled = true
review.timeout_ms = 300000
memory.prompt_budget_chars = 8000
harness.instructions_file = "harness.md"
core.state_dir = ".autoloop"
core.journal_file = ".autoloop/journal.jsonl"
core.memory_file = ".autoloop/memory.jsonl"Key settings:
event_loop.required_events— events that must have been emitted at least once beforetask.completeis accepted. Use this to enforce quality gates (e.g., require a review pass before completion).event_loop.completion_promise— fallback string the model can output as plain text to signal completion when the event tool is unavailable.review.enabled— enables the metareview review loop. Seedocs/metareview.md.backend.kind—"pi"for the Pi adapter (production),"command"for custom/mock backends.
See docs/configuration.md for the full key reference.
Step 5: Run the preset
# From the repo root
node bin/autoloop run path/to/my-preset "Your objective here"
# Built-in presets can use their bundled name
autoloop run autocode "Your objective here"
# Explicit flag form for built-in names or custom dirs
autoloop run --preset autocode "Your objective here"
autoloop run --preset path/to/my-preset "Your objective here"
# Override backend for a one-off run
autoloop run -b claude --preset autocode "Your objective here"run loads autoloops.toml, topology.toml, and harness.md from the selected preset directory. Built-in presets resolve by name through presets/<name>/; custom presets still use a directory path.
Registering a preset in chains
To use a custom preset in chain compositions, the chain step name must resolve to the preset directory. Built-in presets resolve via presets/<name>/. For custom presets, use the directory path as the step name:
# chains.toml
[[chain]]
name = "my-pipeline"
steps = ["autocode", "path/to/my-preset", "autotest"]Or compose ad hoc on the command line:
autoloop run . --chain autocode,path/to/my-preset,autotestDesign patterns
Linear pipeline
Roles flow in one direction. Each role hands off to the next, and the last role can either complete or cycle back to the first.
analyst → implementer → verifier → analyst (cycle) or task.completeThis is the most common pattern — used by autocode, autodoc, autosec, and most presets.
Rejection loop
A verifier or critic can reject work back to the producer, creating a tighten-until-correct cycle.
[handoff]
"impl.ready" = ["verifier"]
"rejected" = ["implementer"] # bounces back
"verified" = ["analyst"] # advancesBlocked escalation
When a role cannot proceed, it emits a .blocked event that routes to a role that can replan or unblock.
[handoff]
"impl.blocked" = ["analyst"] # analyst replans around the blockerFan-back
Multiple events route to the same role, making it a convergence point. Reporters and summarizers often use this pattern.
[handoff]
"finding.confirmed" = ["hardener"]
"finding.dismissed" = ["reporter"]
"fix.applied" = ["reporter"]
"fix.blocked" = ["reporter"]Naming convention
The built-in family uses auto + single lowercase word (autocode, autofix, autosec). Custom presets are not required to follow this convention, but if you are contributing a preset to the project, use the auto prefix and a single word that answers "what does this loop do?"
Checklist
Before running a new preset:
- [ ] Every event in every role's
emitslist appears in the[handoff]map (or you are OK with fallback-to-all routing). - [ ]
"loop.start"is mapped in the handoff to the role that kicks off the loop. - [ ]
event_loop.completion_eventin config matches the completion event in at least one role'semits. - [ ] If
event_loop.required_eventsis set, the required events are reachable in the handoff graph. - [ ] Role prompt files exist at the paths declared in
prompt_file. - [ ]
harness.mdexists (orharness.instructions_filepoints to the correct file). - [ ] Shared working file names are consistent between
harness.mdand role prompts. - [ ] Use
andplaceholders instead of hardcoded.autoloop/paths in harness and role prompts.