Loop hooks
The loop.* hook family — seven events, which two can block a Loop, and the payload every observer receives.
- Audience
- Operators running durable agent work
- Focus
- Loops guidance shaped for scanability, day-two clarity, and operator context.
Loops dispatch a loop.* hook family at the runtime call site, exactly like
every other hook event — typed dispatch, not an event bus. Two of the seven events are
sync-eligible and can block; the rest are observe-only. A broken hook is fail-open: the
coordinator warns and continues, and only an explicit denial from a sync-eligible event stops the
Loop.
The seven events
| Event | Fires when | Sync-eligible | A denial… |
|---|---|---|---|
loop.started | A run is created and started | no | — |
loop.generation.pre | Before the coordinator plans a generation | yes | ends the run failed |
loop.generation.post | After a generation plan is produced | no | — |
loop.gate.pre | Before a gate or terminal decision commits | yes | ends the run blocked |
loop.gate.post | After a gate decision | no | — |
loop.node.terminal | A Loop node's task run reaches a terminal state | no | — |
loop.terminal | A run reaches a terminal outcome | no | — |
loop.node.terminal is also the runtime's own progress signal — it advances last_progress_at and
wakes the coordinator. loop.terminal is where the daemon promotes a queued sibling and wakes a
parent that is awaiting a child Loop.
What a denial does
Only the two *.pre events can block, and each maps to a distinct terminal outcome:
loop.generation.predeny → the run endsfailed(reasonhook_deniedplus the denial's reason). Use this to stop a generation from ever planning — a policy or budget precondition.loop.gate.predeny → the run endsblocked. Use this to veto a gate or terminal decision — an external dependency the daemon cannot see.
Observe-only events cannot change the outcome; they receive an annotate-only patch.
Payload
Every loop.* payload carries the standard event base plus a loop context:
loop_run_id, parent_loop_run_id, workspace_id, loop_name, generation, task_id,
run_id, run_kind, node_id, resolved_network_participation, agent_name, session_id,
actor_kind, actor_id, origin_kind, and origin_ref.
resolved_network_participation is the immutable Local/Live snapshot owned by that Loop run. It
contains any resolved channel and finite bounds; hooks must not infer participation from availability
or from mutable channel metadata.
Events add their own fields on top:
loop.started/loop.terminal—status,cause,reason_code,details.loop.generation.pre/loop.generation.post—status,reason_code,details, and (on thepreevent)denied,deny_reason.loop.gate.pre/loop.gate.post—gate_id,decision,status,reason_code,details, and (on thepreevent)denied,deny_reason.loop.node.terminal—task_status,run_status,error,details.
Manage loop.* hooks with the agh__hooks_* tools and agh hooks CLI like any other event
family. See hooks for the dispatch model and authoring surfaces.
Guardrails and outcomes
The stop model — no-progress axes, the blocker-ID stall signature, budgets and ceilings, and the eleven run states a Loop can hold.
Extending Loops
How extensions add Loop definitions, actions, gate checks, and watch sources — reusing AGH's existing surfaces, with one new bridge capability.