Event subscriptions
🔵 Overview of how events reach the platform and trigger agents. The mechanisms fall into pull (the platform polls itself), push (an external system sends them), and internal (core process events).
| Mechanism | Type | What it triggers | Where it is configured |
|---|---|---|---|
| Cron | pull (timer) | an agent turn on a schedule/interval | cron: in agent.yaml / config/cron.yaml |
| Monitors | pull (source) | an agent turn on new source items | config/monitors.yaml |
| Inbound webhook | push | a single agent turn via POST /v1/inbound/:template |
server.yaml → inbound |
| Workflow webhook | push | starting a workflow via POST /v1/workflows/<name>/webhook |
triggers.webhook |
| Reactions | internal events | an action on a process event (on:) |
config/reactions.yaml |
await_event |
waiting | an agent turn waits for an event (webhook or timer) | the kind: await_event tool |
Push vs pull. Push is convenient when the external system itself knows about the event (payments, CRM, CI). Pull is for when a source needs to be polled; a monitor fires only on changes, while cron fires always on schedule.
Internal events. Reactions subscribe not to external
systems but to core process events (ProcessEvent): start/finish, tool
calls, response content, budget, etc. — and they launch an agent, a tool,
a webhook, or signal (steering of a live process).
Waiting for an event within a turn. await_event pauses the
turn and waits for an external callback POST /v1/events/callback (via event_id) or a timer;
async_action performs the full asynchronous flow
(request → show URL → callback → result).
Result delivery. The outcome of a turn/workflow is distributed to sinks (notify):
log, webhook, telegram, vk, yandex_messenger, email, messages_db,
agent, workflow, storage — see Cron.
The difference between push entry points (inbound webhook, Telegram/email channels) is covered in Widget and channels.
Outward: agent action log (agent_events)
The reverse direction — an external system learns about an agent's actions. The native
kind: agent_events hook keeps events in an in-memory ring buffer (no
disk) at selected lifecycle points, and the GET /v1/events route under
ACCESS_ADMIN exposes the log outward with long-poll and cursor drain (in batches, without
losing events). The implementation is the agent_os_events module; the config fields are in
Extending hooks.
Agent hook (hook_files: [hooks/events.yaml]):
kind: agent_events
name: agent_events
agent: acme-agent
on: [after_tool_result, after_turn] # по умолчанию только after_tool_result
ignore: [settings_get] # служебные чтения не логируем
mutating: [order_create, order_cancel]
capacity: 5000 # кольцевой буфер (в памяти)
Reading:
GET /v1/events?agent=acme-agent&mode=wait&since=N
Response contract: { ok, mode, agent, name, seq, since, count, events[], last, has_more, dropped_window }. Событие: seq, ts, agent, session, user_id, tool, kind, mutating, success, args_fp, detail (args_fp — a non-cryptographic
fingerprint of the arguments; the arguments/result themselves are not stored).
?mode= |
Behavior |
|---|---|
drain (default) |
a batch of events from seq > since, immediately |
wait |
long-poll: waits for an event (up to wait_ms), then returns a batch |
events |
the last N events |
version |
only the current seq (a cheap counter) |
The client maintains a cursor — this way the batch is not lost:
let since = 0;
for (;;) {
const r = await fetch(`/v1/events?agent=acme-agent&mode=wait&since=${since}&limit=100`,
{ headers: { Authorization: `Bearer ${adminToken}` } });
const b = await r.json();
for (const ev of b.events) handle(ev); // события по возрастанию seq
if (b.events.length) since = b.events.at(-1).seq; // сдвигаем курсор
if (b.has_more) continue; // накопилась пачка — дочитываем
if (b.dropped_window) resync(); // часть вытеснена буфером
}
Security. The GET /v1/events route is ACCESS_ADMIN only. The payload
is minimal (no arguments/results, only args_fp); since is clamped to
[0, max], limit/wait_ms are capped from above; capacity is an in-memory
ring buffer.