Running and observing
Fill the run form, dry-run a plan, watch generations stream live, handle approval gates, and track every run across the workspace.
Running a Loop is the arrive-and-use moment: supply its declared inputs, optionally dry-run the plan, then watch the run stream live until it reaches one named outcome.
The run form
The run form is generated from the Loop's declared inputs
— a typed field per input, each with a type badge, a required marker, and inline validation. An
agent or ref input renders the matching workspace catalog, a secret ref lists redacted Vault
metadata, a runtime input offers provider models, Reasoning, normal|fast speed, and other advertised
ACP options while accepting exact custom model IDs, and a boolean renders a switch. An authored enum
always replaces catalog choices. If a saved reference
is no longer listed, the field keeps the exact value visible so the daemon can return the field-level
error instead of silently replacing it. file remains a text field; the form does not browse or
check the path. A sticky preview beside the form shows
"what will run" — the contract, the verification rows, the seven terminal chips, and a lifecycle line
— and recomputes on every change. After a successful dry-run, the preview switches to the returned
materialized_contract, so input-backed goal text is exactly what the runtime will use.
Two actions submit the form:
- Start run — start the run. Disabled until required inputs are filled.
- Dry run — validate the inputs and render the first generation's plan without starting a run. It spends no budget and creates no run row. See authoring.
Close lives in the top bar and leaves the form without submitting it. A folded Limits · this
run only section exposes six per-run limit overrides — iteration cap, token budget, wall-clock
budget, no-progress window, fan-out window, and gate max revisions — plus the budget-exceeded
policy. Each numeric field shows the per-Loop default and its runtime bound. The fan-out window has
no fixed daemon cap; logical width remains bounded by each node's positive max_fan_out. The
summary changes from "loop defaults" to "overrides set" only when a value actually differs.
The same typed controls appear wherever Loop inputs are edited: scheduled automation, event-trigger
mappings, fork and amend flows, and human-request answers annotated with x-compozy-kind. Every
surface submits the exact stored identifier; display labels are never treated as references.
From the CLI, repeat --input key=value. Number, boolean, and JSON values are decoded normally;
runtime inputs also accept provider/model@reasoning:speed=normal|fast, with - leaving provider
or model unset and -/-:speed=fast selecting speed alone. Use a JSON runtime object with
acp_options: [{"id":"thinking","bool_value":true}] when a run needs another advertised control. In
an interactive terminal, compozy loop run prompts only for supported required inputs that remain
missing after defaults. Use --no-prompt to fail instead. Structured output and non-interactive
input never prompt, so scripts keep deterministic behavior.
If validation fails, HTTP/UDS, CLI, native tools, and the web form receive the stable
input_validation payload: { loop, field, kind?, value?, origin, reason }. The web form attaches
the reason to that field. No run row, task, or external action is created.
The live run page
The run page is the truthful, real-time view of one execution. It streams over SSE and never renders a control the runtime does not support.
A sticky contract header carries the live status pill (pulsing only while running or
watching), the generation and attempt count, the run's start metadata, the goal, and the valid
Pause, Resume, or Cancel run action. Cancel is the destructive escape and appears only
while the run is live. The Progress story reads the run's
materialized_contract; Inspect keeps the raw executed_definition available for authoring
and audit questions.
Five meters track spend against limits — attempts, tokens, wall clock, cost, and breadth (fan-out). Bars warn-tint only near a ceiling. The cost meter is derived (tokens × price) and never a cap.
A generation timeline shows each generation as a collapsible card on a flat node spine. A
fan-out renders as batch branches; a gate renders as a flat verdict card (pass tinted success,
fail tinted danger) with its reason and route (revise or next_generation). When a Loop drives
a channel conversation, the converse exchange is embedded
inline. A right rail streams the live event feed and the run facts.
Each generation also shows its durable origin and parent_generation. Most generations point to
N-1; a ratchet_restore points to the older best generation it was seeded from. Scored verdicts
show score, and the accepted baseline carries the Best marker. The run header keeps
best_generation and best_score separate from the current generation, so a later regression or
an exhausted run does not make the last attempt look like the winner.
Structured status output also carries generations[].route_causes. Each entry identifies the route
node or gate, selected forward node, lane index, decision cause, and time. A route condition includes
matched_when; a default selection sets default: true. These facts are workspace-scoped and come
from the durable route_taken event, so HTTP, CLI, native-tool status, and SSE replay agree.
Human requests
An ask node parks the run until someone answers; a review block parks an action node until
someone decides on the arguments it is about to run with. Both surface in the run page's Needs
you region, and both are answerable from the CLI and the API too.
Pending requests present one question at a time. The question is the daemon's persisted prompt, with its lane and deadline beside it; when several requests wait, a "Question 1 of N" header with previous and next controls steps through them instead of stacking forms. The rest of what the daemon persisted — the bounded redacted context preview, a fetch for the full redacted context, and the node and generation that asked — sits behind a closed Details disclosure under the answer.
The answer form renders only what the daemon authorized. An ask generates its fields from the
expect schema — enum values become selectable choices, booleans answer Yes or No — and a review
shows the proposed arguments with the node's own decision allowlist; a decision the node did not
authorize is absent, never a disabled button. On edit, the fields come pre-filled with the
proposed arguments. Submitting disables the form and waits for the daemon: an answer that fails
the shape comes back with the failure on the field that caused it and the request stays open, and
a request that someone else already answered — or whose run has since ended — shows the recorded
outcome instead of a form.
Fan-out lanes each carry their own request, named by lane and answerable independently. The waits rail counts pending requests alongside timers and events.
Fan-out strategy and partial completion
When a fan-out declares a completion strategy, the progress panel names it and its threshold, and
separates four things a single "failed" count would blur: lanes that succeeded, lanes that failed,
lanes the strategy canceled, and lanes that never materialized because the window did not
open them. A best_effort join that met its threshold with a gap reports partial with its
coverage numbers, and because partiality is a run-level fact (completion_state), it reads
partial everywhere the outcome appears — the outcome card, the run lists, and the diff. A wide
fan-out reports aggregate counts rather than one row per lane.
Approval gates
A human gate parks the run in needs-approval — a live pause, never a terminal outcome. The
page shows the decision context (for a merge gate: the branch, diff, test result, and verifier) and
three actions:
- Approve → the run resumes.
- Request changes → the run revises and continues into the next generation.
- Reject → the run halts on a
blockedterminal outcome.
Pause, resume, and cancel
- Pause is a generation-boundary operation, not a mid-node cancellation. The daemon records a pause
intent; the in-flight generation's already-claimed nodes finish; the status flips
running → pausedonly at the boundary. Pause is hidden or disabled outsiderunning. - Resume continues a paused (or pause-requested) run.
- Cancel run immediately fences new work and commits
canceledwith causeoperator_cancel. After that commit, the daemon concurrently stops every session owned by the run. A stop that does not complete is kept in a durable cleanup queue and retried after transient failures or a daemon restart. A session borrowed from the run's origin is never stopped.
Cancel is destructive and idempotent: retrying the same accepted request returns the committed result instead of starting a second path. A canceled run cannot Resume. Use Rerun from here to start a new generation with new work and sessions.
Node controls and inventories
The run page offers only the verbs valid for a node's current state: Pause, Resume,
Cancel, Requeue, Amend output, and Rerun from here. Pause supports drain
(the default) or cancel; Resume supports plain, reset_attempts, or immediate, and can carry
a JSON payload for a manual wait. Requeue is available only for quarantined work and creates a
bounded successor generation.
Amend output corrects a settled output on a parked or paused node. It appears only when the node declares an output shape to validate against, shows the recorded original read-only beside the corrected value, and takes a reason. Amendments are append-only: the recorded output is never rewritten, the corrected value becomes what resume and downstream reads see, and both stay visible in history and in a diff. Amending does not re-run consumers — pair it with Rerun from here.
Rerun from here opens a new generation from a settled node. Its dialog previews the rerun set — the node plus everything that depends on it — and the count carried forward untouched. The verb is absent while the node is still parked and while a generation is in flight.
Use the workspace inventories to find durable work before acting:
| State | What it finds |
|---|---|
waiting | Durable waits and approvals that are parked. |
quarantined | Attempts removed from dispatch until an explicit requeue. |
attention | Silence, exhausted resume, or quarantined-dependency attention. |
retrying | Attempts with a durable next_attempt_at. |
compozy loop nodes reads in two modes, and --run selects which one:
| Mode | Command | --state accepts | Page size |
|---|---|---|---|
| Workspace inventory | compozy loop nodes --state <state> | waiting, quarantined, attention, retrying | 50 default, 200 max |
| Run roster | compozy loop nodes --run <run id> [--all] | all, running, queued, waiting, retrying, paused, quarantined, succeeded, failed, canceled, not_taken | 50 default, 500 max |
The inventory is workspace-scoped and requires --state, because it tracks exception states only —
--state running is valid only in roster mode. Narrow the inventory with --loop or --run-id.
The roster covers one run's nodes across generations. --generation narrows it to one round, and
--all pages the whole roster in a single call, which is why --all and --cursor are mutually
exclusive. Both --all and --generation require --run.
Fan-out workers keep the source item indexes that created them. Filtering or batching can leave gaps
such as 2, 5; missing indexes are not pending workers. Roster progress and fanout_rollups count
only the worker rows that exist.
Structured rows in either mode carry the exact run, generation, node, and item identity needed by the node verbs. See Failure handling for the command forms.
Runs across the workspace
The Runs view is a first-class, workspace-wide list of every execution across every Loop. The
window trail is Loops › Runs, so the catalog stays one click away. A KPI strip surfaces "Active now,"
"Awaiting you" (the needs-approval queue), "Done today," and "Needs a look." Toolbar filter chips narrow the list by outcome — the full status vocabulary stays
selectable, so filtering for an absent status reaches an honest empty state — by origin (catalog or
session), or by an exact origin session id, and a Runs/Nodes switch flips the same view to the node
inventory. Active and Past runs list in tables with an outcome pill, the Loop and run id, the goal,
the generation count, timing, and a budget mini-bar.
Status detail versus run summaries
Use the detail surface when you need provenance or gate decisions. List surfaces stay deliberately small.
| Surface | Shape |
|---|---|
compozy loop status / compozy__loop_status | The detail includes raw executed_definition, input-resolved materialized_contract, and the snapshot-pinned effective_config with per-field sources; run includes optional completed_at, best_generation, and best_score; generations[] includes parent_generation, origin, verdicts[], and outputs. |
compozy loop runs / compozy__loop_runs | Each run summary includes optional best_generation/best_score; it never embeds generation history. |
| Loop catalog list | last_run includes optional best_generation/best_score; it never embeds generation history. |
The best fields are omitted until an approved finite score establishes a baseline. A detailed JSON response can therefore look like this:
{
"run": {
"generation": 3,
"best_generation": 1,
"best_score": 0.82
},
"generations": [
{
"generation": 3,
"parent_generation": 1,
"origin": "ratchet_restore",
"verdicts": [{ "gate_id": "quality", "outcome": "rejected", "score": 0.61 }]
}
]
}completed_at is the exact time a run entered a terminal state. It remains absent for live runs and
for migrated history that has no durable terminal-status event. Terminal duration is
completed_at - started_at; live duration continues to use the latest progress time. An explicit
rerun clears the old completion time before the run becomes live again.
Run detail always returns the effective config persisted with that run. Its sources object maps
JSON Pointer paths to logical sources such as definition, loops.defaults.delivery,
loop_config, or per_run. Changing current defaults does not change a historical status response.
Origins use the closed runtime vocabulary: initial, stop_when, reattempt, gate_revise,
gate_next_generation, dod_retry, ratchet_restore, requeue, operator_rerun, and fork_seed. See
Ratchets for how a scored best changes succession.
Every surface, not just the UI
| Action | CLI | HTTP | Native tool |
|---|---|---|---|
| Start / dry-run | compozy loop run [--dry-run] | POST /loops/:name/run[?dry=true] | compozy__loop_run |
| Run status | compozy loop status | GET /loop-runs/:id | compozy__loop_status |
| List runs | compozy loop runs | GET /loop-runs | compozy__loop_runs |
| Pause / resume | compozy loop pause · compozy loop resume | POST /loop-runs/:id/pause · /resume | compozy__loop_pause · compozy__loop_resume |
| Approve | compozy loop approve --gate-id … --decision … | POST /loop-runs/:id/approve | compozy__loop_approve |
| Cancel run | compozy loop cancel | POST /loop-runs/:id/cancel | compozy__loop_cancel |
| List node state | compozy loop nodes --state … | GET /loop-nodes?state=… | compozy__loop_nodes |
| Pause node | compozy loop node pause | POST /loop-runs/:id/nodes/:node/pause | compozy__loop_node_pause |
| Resume node | compozy loop node resume | POST /loop-runs/:id/nodes/:node/resume | compozy__loop_node_resume |
| Cancel node | compozy loop node cancel | POST /loop-runs/:id/nodes/:node/cancel | compozy__loop_node_cancel |
| Amend node | compozy loop node amend | POST /loop-runs/:id/nodes/:node/amend | compozy__loop_node_amend |
| Requeue node | compozy loop node requeue | POST /loop-runs/:id/nodes/:node/requeue | compozy__loop_node_requeue |
| List requests | compozy loop requests | GET /loop-requests | compozy__loop_requests |
| Request detail | compozy loop request | GET /loop-runs/:id/nodes/:node/request | compozy__loop_request |
| Answer request | compozy loop respond | POST /loop-runs/:id/nodes/:node/respond | compozy__loop_respond |
| Diff history | compozy loop diff | GET /loop-runs/:id/diff | compozy__loop_diff |
| Rerun from node | compozy loop rerun | POST /loop-runs/:id/rerun | compozy__loop_rerun |
| Fork run | compozy loop fork | POST /loop-runs/:id/fork | compozy__loop_fork |
Pass --item or item_index to pause, resume, or cancel one fan-out cell without changing
its siblings. Amendments apply only to a settled output while its run, node, or cell is parked.
They append provenance and an effective-output overlay; the recorded generation output stays
unchanged. Resume or downstream execution reads the newest amendment. Run detail returns
amendments[] with bounded, redacted values or a byte-size and content-hash summary for large data.
There is no API that reads an amendment's private output reference directly.
Run history also exposes two-way fork lineage: a child carries forked_from, and the source lists
forks. See Time travel for diff, rerun, fork, and idempotency rules.
Live events stream from GET /loop-runs/:id/events (SSE). HTTP routes are workspace-scoped under
/api/workspaces/{workspace_id}/…; the Loops API reference lists
the exact paths and payloads. See also the compozy loop CLI.