Skip to content

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.


How a loop run gets started.

TypeWhat starts it
manualYou 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.
scheduleA cron expression. The run starts headless (no conversation) when the scheduler fires, via a dedicated loop delivery method.
actionAn 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.

How a loop decides it is done.

TypeExit condition
verifiableA 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_judgeA model judges done-ness against your llm_judge_prompt and returns a structured verdict (met, reasoning, next_focus).
hybridBoth 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).

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.

RailDefaultBehavior when exceeded
max_iterations25Hard stop — run marked completed, limit reached.
max_tokens500,000Hard stop.
max_duration_minutes60Hard stop.
warning_threshold_pct80Emits a budget-warning event + notification; the loop continues.
Consecutive errors3The loop pauses and waits for you.
No progress3The 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.


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.

Open the drawer with /loops create (or New Loop on the Loops panel), then fill in the fields top to bottom:

FieldRequiredWhat it is
NameYesA short slug for the loop. Pre-filled from the goal when created via natural language.
DescriptionNoA one-line summary of what the loop does.
GoalYesThe goal type — Verifiable, LLM Judge, or Hybrid. Decides which goal fields appear below.
Goal StatementYesA plain-English statement of the end state.
Verifiable commandIf Verifiable / HybridThe shell command whose exit 0 means “done.”
Judge PromptIf LLM Judge / HybridThe question the LLM judge answers to decide done-ness.
Work PromptYesThe instruction the coordinator runs each iteration.
ModelNoThe model the coordinator runs each iteration.
TriggerYesManual, Schedule (cron), or Event (action event).
BudgetYesIterations, Tokens, and Minutes rails (default 25 / 500k / 60).
AdvancedNoMulti-agent, sandbox, budget-warning %, and completion notifications.
  • 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 0 means the goal is met; it runs after each iteration. Keep it fast and deterministic, e.g. pytest tests/perf/ -q or pytest --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-signed POST /api/loops/events/<event> (sign with the loop event secret).
  • 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, or strict. 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>).


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.

TemplateGoal typeDefault triggerWhat it does
perf-optimizehybridmanualOptimize until every page loads under a configurable latency target (latency_target_ms, default 50).
test-coverageverifiablemanualAdd tests until coverage reaches coverage_threshold (default 80).
arch-refactorllm_judgemanualRefactor a target path until it meets a quality bar (DRY, simple, clean).
logging-coveragellm_judgemanualAdd logging until all important paths produce useful, tested logs.
docs-sweepllm_judgeschedule (nightly 2am)Review the codebase, update docs to match, open a PR.
error-sweepverifiableschedule (nightly 3am)Review prod logs, fix actionable errors, open a PR, notify via Slack.
seo-audithybridschedule (weekly, Sun 1am)Audit crawlability, indexation, and structured data; fix critical issues.

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.
CommandAction
/loopsOpen the Loops panel.
/loops createOpen 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 statusShow running loops (opens the panel).
/loops pause <name>Pause a running loop after its current iteration.
/loops stop <name>Terminate a running loop immediately.

Every run keeps a human-readable Markdown log at:

~/.snippbot/loops/<loop-name>/<run_id>/progress.md

One 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:00Z
Trigger: 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 — deferred
Goal check: NOT MET — "2 pages remain above threshold"
Next focus: /billing/enterprise (88ms) and /settings/advanced (67ms)

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 own on_complete is 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:

FieldFires on
on_completeGoal met or a rail stopped the run.
on_errorAn unexpected failure.
on_budget_warningThe 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.


Two instance-wide loop settings live under Settings → Loops (GET/PUT /api/loops/settings):

SettingDefaultRange
progress_context_window31–20 recent iteration blocks injected into each coordinator prompt.
default_execution_sandboxnonenone · allowlist · strict — the sandbox a new loop gets when it doesn’t specify one.

Loops are server-side; closing the browser does not stop a run. All endpoints are under /api/loops and require a bearer API key.

MethodPathDescription
GET/api/loopsList loop definitions.
POST/api/loopsCreate 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}/runStart a manual run (optional conversation_id).
GET/api/loops/{loop_id}/runsList run history for a loop.
GET/api/loops/runs/{run_id}Get run status + iterations.
POST/api/loops/runs/{run_id}/pausePause after the current iteration.
POST/api/loops/runs/{run_id}/resumeResume a paused run.
POST/api/loops/runs/{run_id}/stopTerminate immediately.
GET/api/loops/templatesList built-in templates.
POST/api/loops/templates/{name}/runRun a template (optional params).
POST/api/loops/events/{event_type}HMAC-signed action-trigger receiver.
GET / PUT/api/loops/settingsGet / 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 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.


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.


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.


  • 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