Run a Loop
A loop is an autonomous agent task that runs iteratively until a defined goal is met — or until a safety rail (iteration, token, or time budget) stops it. Unlike a one-shot prompt or a scheduled job, a loop self-monitors: each pass does a focused unit of work, then a goal evaluator decides whether to stop or keep going, steering the next iteration with what it learned.
Use a loop to optimize every page until it loads under a latency target, add tests until coverage reaches a threshold, repair until CI is green, or refactor until a quality bar is met.
Loops build on the same machinery as team orchestration and sub-agents: each iteration spawns a coordinator agent (which can itself fan out sub-agents), and the run is tracked end-to-end with live events streamed to the UI.
Concepts
Section titled “Concepts”Trigger types
Section titled “Trigger types”How a loop run gets started.
| Type | What starts it |
|---|---|
manual | You start it in chat (/loops run <name>) or via POST /api/loops/{loop_id}/run. The run is bound to the conversation that started it. |
schedule | A cron expression. The run starts headless (no conversation) when the scheduler fires, via a dedicated loop delivery method. |
action | An external event. An HMAC-signed POST /api/loops/events/{event_type} whose event_type matches the loop’s action_event (e.g. pr.opened) fires the loop. |
Goal types
Section titled “Goal types”How a loop decides it is done.
| Type | Exit condition |
|---|---|
verifiable | A shell command runs after each iteration; the goal is met when it exits 0. When it fails, a cheap LLM summarizer reads the command output to produce a useful next focus for the following iteration — it never gates the exit, it only steers. |
llm_judge | A model judges done-ness against your llm_judge_prompt and returns a structured verdict (met, reasoning, next_focus). |
hybrid | Both must pass. The verifiable command is the hard gate; the LLM judge confirms and supplies the richer steering. Best when the loop chases a moving target (perf, coverage ramps). |
Safety rails
Section titled “Safety rails”Checked at the start of each iteration, before any agent work begins. Any rail can stop a run even if the goal isn’t met.
| Rail | Default | Behavior when exceeded |
|---|---|---|
max_iterations | 25 | Hard stop — run marked completed, limit reached. |
max_tokens | 500,000 | Hard stop. |
max_duration_minutes | 60 | Hard stop. |
warning_threshold_pct | 80 | Emits a budget-warning event + notification; the loop continues. |
| Consecutive errors | 3 | The loop pauses and waits for you. |
| No progress | 3 | The same next focus three iterations running ⇒ pause and alert (“loop may be stuck”). |
max_iterations and max_tokens are sized to be mutually reachable (≈ 500k ÷ 25 ≈ 20k tokens/iteration). Each iteration’s coordinator gets a per-iteration token cap of remaining_tokens ÷ remaining_iterations (capped at 100k), so neither rail silently dominates the other.
Creating a loop
Section titled “Creating a loop”In chat (natural language)
Section titled “In chat (natural language)”The fastest path. Describe the goal in chat; Snippbot extracts a loop definition, auto-generates a short slug from the goal, and confirms the fields with you before saving.
“Set up a loop that optimizes our app until every page loads under 50ms.”
You confirm or edit the proposed name, trigger, goal type, work prompt, and budget, then save.
In the Loop drawer
Section titled “In the Loop drawer”Open the drawer with /loops create (or New Loop on the Loops panel), then fill in the fields top to bottom:
| Field | Required | What it is |
|---|---|---|
| Name | Yes | A short slug for the loop. Pre-filled from the goal when created via natural language. |
| Description | No | A one-line summary of what the loop does. |
| Goal | Yes | The goal type — Verifiable, LLM Judge, or Hybrid. Decides which goal fields appear below. |
| Goal Statement | Yes | A plain-English statement of the end state. |
| Verifiable command | If Verifiable / Hybrid | The shell command whose exit 0 means “done.” |
| Judge Prompt | If LLM Judge / Hybrid | The question the LLM judge answers to decide done-ness. |
| Work Prompt | Yes | The instruction the coordinator runs each iteration. |
| Model | No | The model the coordinator runs each iteration. |
| Trigger | Yes | Manual, Schedule (cron), or Event (action event). |
| Budget | Yes | Iterations, Tokens, and Minutes rails (default 25 / 500k / 60). |
| Advanced | No | Multi-agent, sandbox, budget-warning %, and completion notifications. |
How to fill in each field
Section titled “How to fill in each field”- Goal Statement — one unambiguous, checkable sentence describing the end state you’re converging on, e.g. “Every page loads under 50ms” or “Test coverage is at least 90%.” It’s shown to the goal evaluator on every iteration, so keep it concrete.
- Verifiable command (Verifiable / Hybrid) — the shell command whose exit
0means the goal is met; it runs after each iteration. Keep it fast and deterministic, e.g.pytest tests/perf/ -qorpytest --cov --cov-fail-under=90. On a non-zero exit its output is summarized into the next iteration’s focus. - Judge Prompt (LLM Judge / Hybrid) — the yes/no quality question the model judge answers, e.g. “Have all pages been optimized to load under 50ms, with no regressions?” The judge returns a structured verdict plus a steering hint for the next pass.
- Work Prompt — the instruction the coordinator agent runs every iteration (the “do work” half; the goal is the “are we done?” half). Phrase it as a repeatable unit of progress, not a one-shot, e.g. “Profile the slowest remaining page, make one focused improvement, then re-measure every page under the same test.”
- Model — the model the coordinator runs each iteration (defaults to Claude Sonnet, the same picker as the Default Internal Model setting). Pick a stronger model for hard reasoning loops, a cheaper one for mechanical sweeps.
- Trigger — choose how runs start, then fill the detail field it reveals:
- Manual — no detail field; you start runs yourself (
/loops run <name>or the Run button). - Schedule — reveals a cron field; enter a 5-field expression such as
0 2 * * *(nightly 2am). The loop then runs headless on that schedule. - Event — reveals an action event field; enter an event name such as
pr.opened. The drawer shows the receiver URL — the loop fires on an HMAC-signedPOST /api/loops/events/<event>(sign with the loop event secret).
- Manual — no detail field; you start runs yourself (
- Budget — the three rails that stop a run if the goal isn’t reached first: Iterations (25), Tokens (500,000), Minutes (60).
- Advanced (click to expand) —
- Multi-agent — let each iteration fan out sub-agents; costs 5–10× more per iteration (the token rail binds first).
- Sandbox — how the verifiable command runs:
none,allowlist, orstrict. Choosing allowlist reveals an Allowed commands box (one command per line). - Budget warning at % — emit a warning notification when usage crosses this percent of budget (default 80); the loop keeps going.
- Notify on complete — pick push and/or slack for the completion notification (defaults to both if you select neither).
Then Save & Run (creates the loop and starts it immediately — the status bar appears above the chat input) or Save Only (saves it to run later with /loops run <name>).
Built-in templates
Section titled “Built-in templates”Seven ready-made loops ship with Snippbot. List them with GET /api/loops/templates; run one with POST /api/loops/templates/{name}/run. Templates declare parameters (a coverage threshold, a latency target) that you’re prompted for at run time.
| Template | Goal type | Default trigger | What it does |
|---|---|---|---|
perf-optimize | hybrid | manual | Optimize until every page loads under a configurable latency target (latency_target_ms, default 50). |
test-coverage | verifiable | manual | Add tests until coverage reaches coverage_threshold (default 80). |
arch-refactor | llm_judge | manual | Refactor a target path until it meets a quality bar (DRY, simple, clean). |
logging-coverage | llm_judge | manual | Add logging until all important paths produce useful, tested logs. |
docs-sweep | llm_judge | schedule (nightly 2am) | Review the codebase, update docs to match, open a PR. |
error-sweep | verifiable | schedule (nightly 3am) | Review prod logs, fix actionable errors, open a PR, notify via Slack. |
seo-audit | hybrid | schedule (weekly, Sun 1am) | Audit crawlability, indexation, and structured data; fix critical issues. |
Running, watching, and controlling a loop
Section titled “Running, watching, and controlling a loop”While a loop runs, a status bar sits above the chat input showing the current iteration, elapsed time, token usage against budget, and the last action — with Pause and Stop controls. Each completed iteration drops a card into the chat stream summarizing what happened and the goal verdict, and a final card reports the outcome.
Progress also streams live to the Loops panel (a tab in the right pane) and to the global Loops page in the left navigation, which lists running and historical runs across all conversations — including headless scheduled/action runs that have no conversation.
You can drive a running loop three ways:
- Buttons — Pause / Resume / Stop on the status bar and in the Loops panel.
- Slash commands — see below.
- Natural language — “pause the loop”, “stop the loop” in the chat input.
/loops slash commands
Section titled “/loops slash commands”| Command | Action |
|---|---|
/loops | Open the Loops panel. |
/loops create | Open the Loop creation drawer. |
/loops edit <name> | Open the drawer pre-filled with a saved loop. |
/loops run <name> | Start a saved loop immediately. |
/loops status | Show running loops (opens the panel). |
/loops pause <name> | Pause a running loop after its current iteration. |
/loops stop <name> | Terminate a running loop immediately. |
Progress log
Section titled “Progress log”Every run keeps a human-readable Markdown log at:
~/.snippbot/loops/<loop-name>/<run_id>/progress.mdOne block per iteration records the work done, tokens used, the goal verdict, and the next focus. On long runs, older blocks are LLM-compacted into a summary so the context injected into each coordinator prompt stays bounded. The most recent blocks (3 by default — configurable in Settings → Loops) are fed into the next iteration’s prompt for continuity.
# Loop: perf-50ms-optimize (Run: <run_id>)Started: 2026-06-23T02:00:00ZTrigger: schedule (cron: "0 2 * * *")Goal: Every page loads in under 50ms
## Iteration 7 — 2026-06-23T02:46:00Z | 11,240 tokens- Measured /admin: 92ms → 43ms ✅ (query restructured)- Measured /billing/enterprise: 88ms — deferredGoal check: NOT MET — "2 pages remain above threshold"Next focus: /billing/enterprise (88ms) and /settings/advanced (67ms)Chaining and notifications
Section titled “Chaining and notifications”A loop’s optional on_complete array runs when the goal is met:
start_loop:<name>— start a follow-on loop (chains are depth-1 only; a chained loop’s ownon_completeis ignored to prevent cascades, and circular chains are rejected at save time).notify:<channel>— send a notification.
The notifications block fires through a unified notifier over push and Slack backends:
| Field | Fires on |
|---|---|
on_complete | Goal met or a rail stopped the run. |
on_error | An unexpected failure. |
on_budget_warning | The warning_threshold_pct is crossed. |
Each is a channel list such as ["push", "slack"]. Headless (scheduled/action) runs have no conversation to stream into, so notifications are how you hear about them. Set the target Slack channel with the SNIPPBOT_LOOP_NOTIFY_SLACK_CHANNEL environment variable.
Settings
Section titled “Settings”Two instance-wide loop settings live under Settings → Loops (GET/PUT /api/loops/settings):
| Setting | Default | Range |
|---|---|---|
progress_context_window | 3 | 1–20 recent iteration blocks injected into each coordinator prompt. |
default_execution_sandbox | none | none · allowlist · strict — the sandbox a new loop gets when it doesn’t specify one. |
REST API
Section titled “REST API”Loops are server-side; closing the browser does not stop a run. All endpoints are under /api/loops and require a bearer API key.
| Method | Path | Description |
|---|---|---|
GET | /api/loops | List loop definitions. |
POST | /api/loops | Create a loop. |
GET | /api/loops/{loop_id} | Get a loop. |
PATCH | /api/loops/{loop_id} | Update a loop. |
DELETE | /api/loops/{loop_id} | Delete a loop. |
POST | /api/loops/{loop_id}/run | Start a manual run (optional conversation_id). |
GET | /api/loops/{loop_id}/runs | List run history for a loop. |
GET | /api/loops/runs/{run_id} | Get run status + iterations. |
POST | /api/loops/runs/{run_id}/pause | Pause after the current iteration. |
POST | /api/loops/runs/{run_id}/resume | Resume a paused run. |
POST | /api/loops/runs/{run_id}/stop | Terminate immediately. |
GET | /api/loops/templates | List built-in templates. |
POST | /api/loops/templates/{name}/run | Run a template (optional params). |
POST | /api/loops/events/{event_type} | HMAC-signed action-trigger receiver. |
GET / PUT | /api/loops/settings | Get / update instance loop settings. |
A minimal create body:
{ "name": "perf-50ms", "description": "Optimize every page under 50ms", "goal": { "type": "verifiable", "description": "All pages load under 50ms", "verifiable_command": "pytest tests/perf/ -q" }, "work_prompt": "Optimize the slowest page, then measure every page the same way.", "trigger": { "type": "manual" }, "budget": { "max_iterations": 25, "max_tokens": 500000, "max_duration_minutes": 60 }}Optional top-level fields include model — the model the coordinator agent runs each iteration, as a provider:model string (e.g. claude-native:claude-sonnet-4.5) or a bare CLI alias (defaults to sonnet) — plus multi_agent_enabled, execution_sandbox / allowed_commands, on_complete, and notifications.
The /loop skill
Section titled “The /loop skill”The snippbot/loop marketplace skill teaches an agent to drive these endpoints — when to reach for a loop instead of a scheduled job, how to shape a goal, and how to read back a run’s progress log. It’s a thin wrapper over the same REST API documented above.
Admin visibility
Section titled “Admin visibility”Each loop iteration emits a routed intent, so loop activity appears in the Intent Dispatcher decision log. Filter the /admin/dispatcher table by source=loop to see every loop decision.
Out of scope
Section titled “Out of scope”Loops are for converging on a checkable condition, not open-ended creation. Don’t use a loop to build a feature from scratch — the goal definition is too brittle to be a reliable exit condition.
Related
Section titled “Related”- Schedule a Job — single-pass recurring work (and how to tell it apart from a loop)
- Use Sub-Agents — the multi-agent fan-out a loop coordinator can use
- Execution — the team-orchestration pattern loops generalize
- Intent Dispatcher — where loop triggers route and decisions are logged
- Proactivity — Snippbot’s other autonomous behaviors