Skip to main content
Trinity
Guides/Agent Loops

Sequential Agent Loops

Run the same task against one agent repeatedly, in order, with a bounded run count — for example “process the next backlog item” × 20. Start a loop once, get back a loop_id, and disconnect: the loop runs server-side, dispatching each iteration through the standard execution pipeline.

Build Autonomous Loops for Your AI Agents

Jun 2026

Loops are the sequential counterpart to Fan-Out (parallel batch) and a single chat turn (one-shot).

Concepts

Loop — A server-managed sequence of up to max_runs task executions against one agent. Iterations run strictly one at a time, in order.
Message Template — The task message, re-rendered each iteration. Supports two placeholders: {{run}} (the 1-indexed run number) and {{previous_response}} (the trailing 2,000 characters of the previous iteration's response; empty on run 1).
Fixed mode — No stop signal set: the loop runs exactly max_runs iterations.
Until mode — A stop_signal is set: after each iteration, Trinity checks whether the agent's response contains the stop signal as a plain substring. On match, the loop ends early with reason stop signal matched. A recommended sentinel is [[DONE]] — tell the agent to emit it when the work is finished.
Cooperative stop — Stopping a loop never kills the in-flight iteration. The current run finishes, then the loop exits with reason stopped by user.

How It Works

1

Open an agent's detail page and select the Loops tab.

2

Click Run Loop (the agent must be running — the button is disabled otherwise).

3

Fill in the form: Message template (required, e.g. Process item {{run}}. Previous result: {{previous_response}}), Max runs (required, 1–100), Stop signal (optional), Delay between runs (0–3600 seconds), Timeout per run (10–7200 seconds; defaults to the agent's execution timeout), Model (per-loop override), and Allowed tools (restrict tools for every iteration, or leave All Tools / Unrestricted).

4

Click Start Loop. The loop appears in the list below with a live status badge and a Run N / M progress counter.

5

Click a loop row to expand it: a per-run table shows each iteration's status, cost, duration, and a response preview, and the Last response is rendered as markdown below the table.

6

Click Stop on an active loop to request a graceful stop. The badge shows the current iteration finishing, then the loop ends with reason stopped by user.

The panel updates in real time via WebSocket events as each run completes, with a periodic backstop refresh while a loop is active.

Loop Lifecycle

StatusMeaning
queued / runningLoop active; iterations dispatching
completedReached max runs, or the stop signal matched
stoppedStopped by a user; the in-flight iteration finished first
failedAn iteration failed (task error or exception) — the loop aborts at that iteration
interruptedThe backend restarted mid-loop; loops do not auto-resume

Observability: every iteration is a normal execution — it creates its own row with triggered_by: "loop" and the parent loop_id, visible on the Executions page and per-execution detail view. In analytics timeline views, loop executions appear as their own Loopscategory with a distinct color — they are not folded into Scheduled.

Common Patterns

These shapes cover most loop use cases. Pick by what ends the loop: a count, a condition, or a clock.

Iterative refinement (fixed mode + chaining)

Run a fixed number of progressively better passes, feeding each result into the next:

{
  "message": "Improve the draft. Previous version: {{previous_response}}",
  "max_runs": 4
}

{{previous_response}}is lossy — only the trailing 2,000 characters carry over. When iterations build a real artifact (a draft, a migration, a report), don't rely on it: instruct the agent to keep the artifact in a file in its workspace and re-read it each run. The agent's filesystem persists across runs; the loop context doesn't.

Agentic retry (until mode)

Keep trying until it works, bounded by a safety cap:

{
  "message": "Attempt the migration. If it succeeds, paste the passing output and end your reply with [[DONE]]. Otherwise, fix the error and report what failed.",
  "max_runs": 8,
  "stop_signal": "[[DONE]]",
  "timeout_per_run": 600
}

The loop exits with stop signal matched on the first success, and max runs reached if it never succeeds within the cap.

Bounded polling (until mode + delay)

Watch something external on a cadence, without holding a connection open:

{
  "message": "Check the deploy health endpoint. If healthy, reply [[DONE]]; else report the current status.",
  "max_runs": 30,
  "stop_signal": "[[DONE]]",
  "delay_seconds": 120
}

This polls every 2 minutes for up to an hour and exits the moment the condition holds. For cadences slower than the 1-hour delay ceiling, a loop is the wrong tool — use a schedule instead.

Backlog draining (until mode + a work skill)

If the agent has a work-queue skill (for example the abilities work-loop pattern: pick one issue, do it, close it, exit), a loop turns it into a bounded autonomous session:

{
  "message": "Run /work-loop. Process exactly one backlog item. If the backlog is empty, reply [[DONE]].",
  "max_runs": 20,
  "stop_signal": "[[DONE]]"
}

Each iteration is one unit of work with its own execution row, cost, and timeout — far more observable than one giant “do everything” task.

Writing Good Until-Mode Loops

Until mode is only as reliable as the stop condition you write. Three rules:

1

Tie the sentinel to verifiable evidence, not self-assessment. Models skew positive when grading their own work — “reply [[DONE]] when the report is good” exits on the first pass. Anchor the sentinel to something checkable: passing test output, an HTTP 200, zero remaining TODO markers. If the condition is inherently subjective, keep max_runs low instead.

2

Always set the cap. stop_signal is best-effort — the agent has to actually emit it. max_runs is guaranteed. The cap is the safety net, not the expected exit.

3

Set timeout_per_run. A hung iteration silently stalls the whole sequence. Size it to the task (e.g. 600s for a test suite) rather than inheriting a long agent default.

And once a loop is running, watch for stalls: if consecutive responses are near-identical (same failure, same output, no state change), the loop is burning budget without progress. Stop it rather than letting it run to the cap.

For Agents

Agents (and scripts) start loops via MCP tools or REST. The permission model matches chat_with_agent: owner, admin, or shared access on the target agent; agent-scoped MCP keys need an explicit permission grant.

From Claude Code: /trinity:loop

If you work in Claude Code with the abilities trinity plugin installed, /trinity:loop is the fastest way to start a loop — it parses a natural-language request (/trinity:loop @ci-agent run the test suite until it passes, max 10), picks the mode, writes the sentinel instruction into the message for you, fires run_agent_loop, and watches progress locally.

MCP Tools

ToolDescription
run_agent_loopStart a loop; returns loop_id immediately. agent_name is required for user-scoped keys; agent-scoped keys default to the bound agent
get_loop_statusLoop status plus per-run summaries (run number, execution id, status, response preview, cost, duration) and the last full response
stop_loopGraceful stop; returns stopping (runner signaled) or already_done (loop already terminal)
mcp__trinity__run_agent_loop({
  agent_name: "my-agent",
  message: "Process the next backlog item ({{run}} of 20). Reply [[DONE]] if the backlog is empty.",
  max_runs: 20,
  stop_signal: "[[DONE]]",
  delay_seconds: 30
})
// → { success: true, loop_id: "loop_...", status: "queued", ... }

REST API

EndpointMethodDescription
/api/agents/{name}/loopsPOSTStart a loop; returns 202 with {loop_id, status, agent_name, max_runs}
/api/agents/{name}/loopsGETList the agent's loops, most recent first (?status=, ?limit= 1–200, default 50)
/api/loops/{loop_id}GETStatus, per-run summaries, last full response (404 unknown; 403 if the caller is neither the initiator nor has agent access)
/api/loops/{loop_id}/stopPOSTGraceful stop → {status: "stopping" | "already_done"}
curl -X POST http://localhost:8000/api/agents/my-agent/loops \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "message": "Process item {{run}}. Previous result: {{previous_response}}",
    "max_runs": 10,
    "stop_signal": "[[DONE]]",
    "delay_seconds": 5
  }'

Parameters

ParameterDefaultRangeDescription
message— (required)1–100,000 charsTemplate; supports {{run}} and {{previous_response}}
max_runs— (required)1–100Hard ceiling on iterations
stop_signalnone (fixed mode)≤200 charsSubstring that ends the loop early; whitespace-stripped, blank = fixed mode
delay_seconds00–3600Pause between iterations
timeout_per_runagent's execution timeout10–7200Per-iteration timeout in seconds
modelagent defaultModel override applied to every iteration
allowed_toolsunrestrictedTool restrictions applied to every iteration

Limitations

One agent, sequential — A loop targets a single agent and never runs iterations in parallel. For parallel batches, use Fan-Out.
Shared capacity — Each iteration goes through the agent's normal capacity admission and counts against its max_parallel_tasks budget alongside chat, schedules, and other traffic.
Fail-fast — Any failed iteration ends the loop with status failed; remaining runs are skipped.
No resume after restart — A backend restart marks in-flight loops interrupted. They do not auto-resume; start a new loop.
Stop is not instant — The in-flight iteration always finishes; only subsequent iterations are skipped.
Bounded inputsmax_runs is capped at 100, delay at 1 hour, and per-run timeout at 2 hours.