Skip to content
Hoody.com

Change tokens are opaque per-topic hints: a short, opaque string per work list that moves whenever that list could have changed. Use them to avoid polling. The recommended client loop reads the tokens, lists the work lists, then later re-reads the tokens and re-lists only the topics whose token moved, and every topic when scope changes. The lists are authoritative; tokens are just hints.

The first endpoint below returns one snapshot as JSON. The second opens a Server-Sent Events stream that emits a snapshot frame at start, changed frames for moved topics, and a fresh snapshot when scope changes. The SSE stream sends no id: lines, so after a reconnect, start again from the new snapshot and re-list the topics that differ from what you hold.

Returns one change token per work list topic: sessions, todos, workflow_runs, loops, tasks, gates, agents, and rules.

The X-Hoody-Realm header (or ?realm= query alias) selects the realm: "global" (not tied to a realm) or a 24-hex realm id; omitted, the agent’s current realm. A realm this login does not serve is 404 not_found. An agent pinned to one realm acts only in that realm: any other value is 400 realm_scope_unsupported. A container header is unconditionally 400 realm_scope_unsupported. The X-Hoody-Cwd and X-Hoody-Config-Dir headers scope sessions and todos as they scope sessions.list and todos.list, and with them tasks, gates, and rules, which are read over those visible sessions. rules also covers the files hooks.getRules reads for the same headers.

One read is shared for 1 s by the requests with the same realm and scope headers, so an answer can be up to 1 s old. After the active realm or account switches, the new scope arrives within that second.

NameInTypeRequiredDescription
X-Hoody-CwdheaderstringNoPer-request working-directory scope: the .hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; todos.create also accepts a body cwd).
X-Hoody-Config-DirheaderstringNoPer-request --config-dir override selecting which on-disk .hoody install a stateless read/write resolves against.
X-Hoody-RealmheaderstringNoPer-request realm selector: "global" (not tied to a realm) or a 24-hex realm id (also accepted as ?realm=). On a session route it names the realm the session is looked up in: a session in another realm, or in a realm this login does not serve, is 404 not_found. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes, and for any other realm on an agent pinned to one realm.
realmquerystringNoPer-request realm selector, the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" (not tied to a realm) or a 24-hex realm id. On a session route it names the realm the session is looked up in: a session in another realm, or in a realm this login does not serve, is 404 not_found. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes, and for any other realm on an agent pinned to one realm.
{
"scope": "active",
"tokens": {
"sessions": "01HQZ3K4N5P6R7S8T9V0W1X2Y3",
"todos": "01HQZ3K4N5P6R7S8T9V0W1X2Y4",
"workflow_runs": "01HQZ3K4N5P6R7S8T9V0W1X2Y5",
"loops": "01HQZ3K4N5P6R7S8T9V0W1X2Y6",
"tasks": "01HQZ3K4N5P6R7S8T9V0W1X2Y7",
"gates": "01HQZ3K4N5P6R7S8T9V0W1X2Y8",
"agents": "01HQZ3K4N5P6R7S8T9V0W1X2Y9",
"rules": "01HQZ3K4N5P6R7S8T9V0W1X2ZA"
}
}

Fields:

  • scope (string, required): names the realm and account the tokens belong to (it never carries the account). When it changes, re-list every topic.
  • tokens (object, required): one opaque token per topic. Compare a token with the one you last saw; when it differs, re-list that topic. Never parse a token.
    • tokens.sessions (string): moves when a visible session is created, closed, renamed or re-persisted (model, agent, cost), when it attaches or detaches, and when a turn starts, parks on a gate or ends. Re-list: sessions.list.
    • tokens.todos (string): moves on any change to a visible todo. Re-list: todos.list (and getTodo for an open one).
    • tokens.workflow_runs (string): moves when a visible run starts, steps, finishes, is cancelled or is evicted. Re-list: workflows.listRuns (workflows.getRun for one).
    • tokens.loops (string): moves when a visible loop is created, deleted, paused, resumed, fires, finishes or spends. Countdowns (next_in_ms, expires_in_ms) do not move it. Re-list: loops.list.
    • tokens.tasks (string): moves when a helper task of a visible live session starts, makes progress or ends. Re-list: tasks.list for the sessions you show.
    • tokens.gates (string): moves when a gate of a visible live session parks or resolves: the session’s own confirm or question, and on a helper_gates session each helper’s. Re-list: gates.list (or sessions.getSnapshot for one session: pending_gate, pending_helper_gates).
    • tokens.agents (string): moves when the realm’s agent list changes: a create, delete, rename, model, tools or turns edit, or a source edit. Re-list: definitions.list.
    • tokens.rules (string): moves when a rules layer of a visible live session changes (hooks.setRules), or a settings file hooks.getRules reads for the same scope headers, with or without a live session. Re-list: hooks.getRules / hooks.list / sessions.listApplicableRules.
  • unavailable (object): present only when a topic could not be read; maps topic to reason. That topic has no token in this answer.
import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
const result = await client.agent.changes.get();

With the optional X-Hoody-Cwd header:

const result = await client.agent.changes.get({
XHoodyCwd: '/home/user/work'
});

Server-Sent Events stream over changes.get. The first frame is a snapshot carrying {scope, tokens}. After it, a changed frame with {topic, token} for each topic whose token moved, and a new snapshot frame when scope changes. Tokens are re-read every 2 s (one read per scope per second is shared by every stream). There are no id: lines, so after a reconnect, start again from the new snapshot and re-list the topics that differ from what you hold. Heartbeat comments arrive every 25 s. The stream ends with an end frame carrying {reason} when the tokens cannot be read.

The stream counts toward the per-IP stream cap (--http-max-streams-per-ip, default 16): a Frame tab holds this stream, the focused session’s /stream, and a prompt:stream while sending.

The same scoping rules apply as for changes.get: the X-Hoody-Realm header (or ?realm= query alias) selects the realm, with a 400 realm_scope_unsupported only when an agent is pinned to a different realm, and a container header is unconditionally 400 realm_scope_unsupported. X-Hoody-Cwd and X-Hoody-Config-Dir scope sessions and todos, and through them tasks, gates, and rules. rules also covers the files hooks.getRules reads for the same headers.

NameInTypeRequiredDescription
X-Hoody-CwdheaderstringNoPer-request working-directory scope: the .hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; todos.create also accepts a body cwd).
X-Hoody-Config-DirheaderstringNoPer-request --config-dir override selecting which on-disk .hoody install a stateless read/write resolves against.
X-Hoody-RealmheaderstringNoPer-request realm selector: "global" (not tied to a realm) or a 24-hex realm id (also accepted as ?realm=). On a session route it names the realm the session is looked up in: a session in another realm, or in a realm this login does not serve, is 404 not_found. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes, and for any other realm on an agent pinned to one realm.
realmquerystringNoPer-request realm selector, the in:query alias of the X-Hoody-Realm header (read only when the header is absent): "global" (not tied to a realm) or a 24-hex realm id. On a session route it names the realm the session is looked up in: a session in another realm, or in a realm this login does not serve, is 404 not_found. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes, and for any other realm on an agent pinned to one realm.

The body is an SSE event stream (text/event-stream). Each frame is one or more event: and data: lines followed by a blank line. Heartbeats are SSE comment lines (: heartbeat) every 25 s.

event: snapshot
data: {"scope":"active","tokens":{"sessions":"01HQZ3K4N5P6R7S8T9V0W1X2Y3","todos":"01HQZ3K4N5P6R7S8T9V0W1X2Y4","workflow_runs":"01HQZ3K4N5P6R7S8T9V0W1X2Y5","loops":"01HQZ3K4N5P6R7S8T9V0W1X2Y6","tasks":"01HQZ3K4N5P6R7S8T9V0W1X2Y7","gates":"01HQZ3K4N5P6R7S8T9V0W1X2Y8","agents":"01HQZ3K4N5P6R7S8T9V0W1X2Y9","rules":"01HQZ3K4N5P6R7S8T9V0W1X2ZA"}}
event: changed
data: {"topic":"sessions","token":"01HQZ3K4N5P6R7S8T9V0W1X2Y4"}
: heartbeat
event: end
data: {"reason":"tokens_unreadable"}

Frame shapes:

  • snapshot: carries {scope, tokens} shaped like the changes.get 200 body. Sent at start and again whenever scope changes.
  • changed: carries {topic, token} for one moved topic.
  • end: carries {reason} when the tokens cannot be read.
  • Heartbeat: an SSE comment line : heartbeat every 25 s.
import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
const stream = await client.agent.changes.stream();

With the optional X-Hoody-Cwd header:

const stream = await client.agent.changes.stream({
XHoodyCwd: '/home/user/work'
});