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) — that flexibility applies to directory presets. A single-file preset supports inlinepromptonly. 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" |
| Absolute path to the preset/config dir (holds roles/, scripts/, hooks/) | sh /scripts/okf-init.sh |
lets a role or hook reference preset-bundled files by path — e.g. a bootstrap script the loop runs before any scripts have been copied into the work dir. The same placeholders are also expanded in the hooks.* commands (pre_run, pre_iteration, post_iteration, post_run), so a hook can invoke a preset-bundled script (e.g. a deterministic pre_run bootstrap) without hardcoding paths.
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.
Single-file presets
Everything above describes a directory preset: autoloops.toml + topology.toml + harness.md + roles/*.md, spread across several files. For anything ad hoc, one-off, or machine-generated, autoloop also supports a single-file preset — one merged TOML document that carries both the config tables and the topology tables, with role prompts inlined.
When to use which:
- Directory preset — a preset you'll reuse, iterate on, and check into a repo. Long role prompts read and diff better as separate markdown files;
harness.md/README.mdgive it a home. - Single-file preset — a preset generated on the fly (e.g. by the
autoarchitectworkflow, which writes one for every run), sketched quickly at the command line, or handed to someone as a single file to copy-paste. No directory structure to scaffold, noroles/to keep in sync.
Format
A single-file preset is one .toml file containing:
- The usual config tables:
[event_loop],[backend],[memory],[core],[hooks],[review], etc. — same keys asautoloops.toml. - The topology tables in the same document: top-level
nameandcompletion,[[role]]entries,[handoff], and optional[[gate]]entries.
name = "autoreview"
completion = "task.complete"
[event_loop]
max_iterations = 25
completion_event = "task.complete"
[backend]
kind = "command"
command = "claude"
timeout_ms = 600000
[memory]
prompt_budget_chars = 4000
[[role]]
id = "planner"
prompt = "You are the planner. Break the objective into review targets."
emits = ["targets.ready"]
[[role]]
id = "reviewer"
prompt = "You are the reviewer. Review each target and emit findings."
emits = ["review.ready", "review.blocked"]
[[role]]
id = "finalizer"
prompt = "You are the finalizer. Confirm the review is complete."
emits = ["task.complete"]
[handoff]
"loop.start" = ["planner"]
"targets.ready" = ["reviewer"]
"review.ready" = ["finalizer"]
"review.blocked" = ["planner"]prompt_file is not supported in single-file mode — only inline prompt = "..." strings. topology.validateTopology(topology, { singleFile: true }) flags any prompt_file reference in a single-file preset as a prompt-file-in-single-file warning, and both autoloop preset promote and the architect auto-chain refuse to run a preset with validation warnings.
Creating one
autoloop init --single-file preset.tomlScaffolds a builder → critic single-file preset at preset.toml with inline prompts, ready to run or edit.
Running one
# Explicit, discoverable flag — hard-fails on a missing or non-.toml path
autoloop run --preset-file ./preset.toml "objective"
# -p/--preset also recognizes an explicit .toml path
autoloop run -p ./preset.toml "objective"
# The bare positional form works too
autoloop run ./preset.toml "objective"--preset-file and -p/--preset differ slightly: -p is polymorphic — it accepts a bundled preset name, a preset directory, or a .toml file path, and falls through to bundled-preset resolution if the path doesn't otherwise resolve. --preset-file is a stricter, single-purpose contract — it always expects an existing .toml file and fails immediately with a clear error if the path isn't one, rather than silently trying other resolutions. Prefer --preset-file when scripting against a known file path.
Promoting to a permanent preset
A single-file preset — hand-written, init-generated, or produced by the architect workflow — can graduate into a permanent named preset:
autoloop preset promote ./preset.toml myreviewThis validates the source with singleFile: true and copies it into the user presets directory, after which autoloop run myreview "..." resolves it by name like any bundled preset.