Skip to content
Hoody.com

Agent loops are scheduled, self-prompting recurrences attached to an agent session. Each loop fires a prompt on a configured interval with optional cost and wall-clock ceilings, and runs off a dedicated loop lane rather than the regular command channel. Use the endpoints on this page to list, create, immediately fire, update, or delete loops scoped to a single session. Per-request realm headers are rejected on these routes because loops are session-frozen; open a realm-scoped session instead.


Lists the recurring loops scheduled for a session (loops.list). The response is the standard kit list envelope { items, meta: { total } }, with optional page / limit pagination applied gateway-side (the underlying snapshot is session-scoped, so the page window is applied after the daemon returns).

NameInTypeRequiredDescription
idpathstringYesSession identifier.
pagequeryintegerNo1-based page number for pagination.
limitqueryintegerNoMaximum items per page (0 = no pagination).
X-Hoody-CwdheaderstringNoPer-request working-directory scope for the .hoody project layer / record cwd / tool+workflow cwd.
X-Hoody-Config-DirheaderstringNoPer-request --config-dir override selecting which on-disk .hoody install a stateless read/write resolves.
X-Hoody-ContainerheaderstringNoPer-request bound remote container (omit for local). Rejected (400) on routes with no container dimension.
X-Hoody-RealmheaderstringNoPer-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes.
realmquerystringNoQuery-string alias of the X-Hoody-Realm header (read only when the header is absent). Same accepted values and rejection rules.

This endpoint takes no request body.

{
"items": [
{
"loopId": "lp_8f3a1c9d",
"prompt": "Summarize open pull requests",
"interval": "30m",
"maxRuns": 0,
"paused": false,
"maxCostUsd": 5,
"maxWallMs": 0,
"runCount": 14,
"nextRunAt": "2026-03-04T14:30:00Z"
},
{
"loopId": "lp_2b91e7f4",
"prompt": "Review the changelog for upstream releases",
"interval": "6h",
"maxRuns": 0,
"paused": true,
"maxCostUsd": 0,
"maxWallMs": 0,
"runCount": 3,
"nextRunAt": null
}
],
"meta": {
"total": 2,
"page": 1,
"limit": 20
}
}
// List loops on a session, paginated.
await client.agent.loops.listLoopsIterator(sessionId, { page: 1, limit: 20 });
// Or simply list all of them (limit=0 → no pagination).
await client.agent.loops.listLoopsIterator(sessionId);

Creates a recurring loop on a session (loops.create). Configure the interval, an optional stop_when predicate, a run cap (max_runs), and optional cost / wall-clock ceilings. Loop fires use the dedicated loop lane, not the command channel.

NameInTypeRequiredDescription
idpathstringYesSession identifier.
X-Hoody-CwdheaderstringNoPer-request working-directory scope for the .hoody project layer / record cwd / tool+workflow cwd.
X-Hoody-Config-DirheaderstringNoPer-request --config-dir override selecting which on-disk .hoody install a stateless read/write resolves.
X-Hoody-ContainerheaderstringNoPer-request bound remote container (omit for local). Rejected (400) on routes with no container dimension.
X-Hoody-RealmheaderstringNoPer-request realm selector: "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes.
realmquerystringNoQuery-string alias of X-Hoody-Realm (read only when the header is absent). Same accepted values and rejection rules.
NameTypeRequiredDescription
promptstringYesThe prompt fired each loop run.
intervalstringNoRun interval as a duration token, e.g. "30m".
max_runsintegerNoStop after this many runs (0 = unlimited).
stop_whenstringNoOptional stop predicate evaluated each run.
max_cost_usdnumberNoCost ceiling (0 = unlimited).
max_wall_msintegerNoWall-clock ceiling in ms (0 = unlimited).
{
"prompt": "Summarize the last 24 hours of GitHub issues and post a digest",
"interval": "30m",
"max_runs": 0,
"stop_when": "all_issues_acknowledged",
"max_cost_usd": 5,
"max_wall_ms": 120000
}
{
"loopId": "lp_8f3a1c9d",
"sessionId": "sess_4d8e2a1b",
"prompt": "Summarize the last 24 hours of GitHub issues and post a digest",
"interval": "30m",
"maxRuns": 0,
"stopWhen": "all_issues_acknowledged",
"maxCostUsd": 5,
"maxWallMs": 120000,
"paused": false,
"runCount": 0,
"nextRunAt": "2026-03-04T14:00:00Z"
}
await client.agent.loops.createLoop(sessionId, {
prompt: "Summarize the last 24 hours of GitHub issues and post a digest",
interval: "30m",
max_runs: 0,
stop_when: "all_issues_acknowledged",
max_cost_usd: 5,
max_wall_ms: 120000
});

POST /api/v1/agent/sessions/{id}/loops/{loopId}/run-now

Section titled “POST /api/v1/agent/sessions/{id}/loops/{loopId}/run-now”

Fires a loop’s next run immediately (loops.run_now) on the dedicated loop lane. The session id and loop id come from the path; the body is optional and forwarded to the daemon as-is (reserved _-prefixed keys are ignored, and the per-request cwd / config-dir scope is applied automatically).

NameInTypeRequiredDescription
idpathstringYesSession identifier.
loopIdpathstringYesLoop identifier.
X-Hoody-CwdheaderstringNoPer-request working-directory scope for the .hoody project layer / record cwd / tool+workflow cwd.
X-Hoody-Config-DirheaderstringNoPer-request --config-dir override selecting which on-disk .hoody install a stateless read/write resolves.
X-Hoody-ContainerheaderstringNoPer-request bound remote container (omit for local). Rejected (400) on routes with no container dimension.
X-Hoody-RealmheaderstringNoPer-request realm selector: "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes.
realmquerystringNoQuery-string alias of X-Hoody-Realm (read only when the header is absent). Same accepted values and rejection rules.

This endpoint accepts an optional JSON object. No specific fields are defined — any non-reserved keys are forwarded to the daemon loops.run_now RPC as pass-through.

{
"loopId": "lp_8f3a1c9d",
"sessionId": "sess_4d8e2a1b",
"fired": true,
"runId": "run_0192c1f4",
"scheduledAt": "2026-03-04T13:47:22Z"
}
// Fire the loop immediately with no extra body fields.
await client.agent.loops.runLoopNow(sessionId, loopId);
// Or pass a body if the daemon accepts one.
await client.agent.loops.runLoopNow(sessionId, loopId, { /* extra body */ });

PATCH /api/v1/agent/sessions/{id}/loops/{loopId}

Section titled “PATCH /api/v1/agent/sessions/{id}/loops/{loopId}”

Updates a loop by body shape. Set exactly one intent per request — the server dispatches to one of three underlying RPCs and rejects requests that mix intents with 400:

  • Run state (paused) — dispatches loops.set_state (true pauses, false resumes).
  • Expiry (expires_in) — dispatches loops.set_expiry (duration-from-now token, e.g. "2h"; "" or "never" clears).
  • Budget (max_cost_usd and/or max_wall_ms) — dispatches loops.update to set / revive the cost and wall-clock ceilings only. interval and prompt are not changeable in v1.
NameInTypeRequiredDescription
idpathstringYesSession identifier.
loopIdpathstringYesLoop identifier.
X-Hoody-CwdheaderstringNoPer-request working-directory scope for the .hoody project layer / record cwd / tool+workflow cwd.
X-Hoody-Config-DirheaderstringNoPer-request --config-dir override selecting which on-disk .hoody install a stateless read/write resolves.
X-Hoody-ContainerheaderstringNoPer-request bound remote container (omit for local). Rejected (400) on routes with no container dimension.
X-Hoody-RealmheaderstringNoPer-request realm selector: "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes.
realmquerystringNoQuery-string alias of X-Hoody-Realm (read only when the header is absent). Same accepted values and rejection rules.
NameTypeRequiredDescription
pausedbooleanNoRun-state intent (loops.set_state): true pauses, false resumes.
expires_instringNoExpiry intent (loops.set_expiry): duration-from-now token, e.g. "2h". "" or "never" clears.
max_cost_usdnumberNoBudget intent (loops.update): cost ceiling (0 = unlimited).
max_wall_msintegerNoBudget intent (loops.update): wall-clock ceiling in ms, or a duration token (0 = unlimited).
{
"paused": true
}
{
"loopId": "lp_8f3a1c9d",
"sessionId": "sess_4d8e2a1b",
"paused": true,
"expiresIn": "30m",
"maxCostUsd": 5,
"maxWallMs": 120000,
"intent": "set_state"
}
// Pause the loop (run-state intent).
await client.agent.loops.updateLoop(sessionId, loopId, { paused: true });
// Set / clear an expiry (expiry intent).
await client.agent.loops.updateLoop(sessionId, loopId, { expires_in: "2h" });
// Revive / change the cost and wall-clock budgets (budget intent).
await client.agent.loops.updateLoop(sessionId, loopId, {
max_cost_usd: 10,
max_wall_ms: 300000
});

DELETE /api/v1/agent/sessions/{id}/loops/{loopId}

Section titled “DELETE /api/v1/agent/sessions/{id}/loops/{loopId}”

Deletes a recurring loop from a session (loops.delete). The session id and loop id come from the path; the body is optional and forwarded to the daemon as-is (reserved _-prefixed keys are ignored, and the per-request cwd / config-dir scope is applied automatically).

NameInTypeRequiredDescription
idpathstringYesSession identifier.
loopIdpathstringYesLoop identifier.
X-Hoody-CwdheaderstringNoPer-request working-directory scope for the .hoody project layer / record cwd / tool+workflow cwd.
X-Hoody-Config-DirheaderstringNoPer-request --config-dir override selecting which on-disk .hoody install a stateless read/write resolves.
X-Hoody-ContainerheaderstringNoPer-request bound remote container (omit for local). Rejected (400) on routes with no container dimension.
X-Hoody-RealmheaderstringNoPer-request realm selector: "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes.
realmquerystringNoQuery-string alias of X-Hoody-Realm (read only when the header is absent). Same accepted values and rejection rules.

This endpoint accepts an optional JSON object. No specific fields are defined — any non-reserved keys are forwarded to the daemon loops.delete RPC as pass-through.

{
"loopId": "lp_8f3a1c9d",
"sessionId": "sess_4d8e2a1b",
"deleted": true
}
// Delete with no extra body fields.
await client.agent.loops.deleteLoop(sessionId, loopId);
// Or pass a body if the daemon accepts one.
await client.agent.loops.deleteLoop(sessionId, loopId, { /* extra body */ });