Event-driven agents
Agents that wake on memory changes instead of polling — the control event stream, what it emits, and when to reach for it.
An event-driven agent subscribes to a run's control event stream and acts when memory changes: a handoff lands, reflection finishes, a lesson promotes. Use this when an agent's job is to react — polling loops burn tokens and latency re-reading memory that hasn't changed, and the interesting moments (a verdict request, a new lesson) are exactly the ones the stream announces.
This page frames when to use the pattern. For the full walkthrough — wiring a consumer, filtering, rebuilding context after a wakeup — go to the event-driven agents recipe.
How it works
The control plane emits an event for every significant memory change in a run. Subscribe over SSE (GET /v2/control/events/subscribe) or gRPC (Subscribe); each SDK exposes the stream directly:
| SDK | Call | Returns |
|---|---|---|
| Python | client.advanced.subscribe({"run_id": run_id}) | iterator of events |
| JS/TS | client.advanced.subscribe({ run_id: runId }) | async-iterable stream |
| Rust | client.subscribe(payload).await? | ValueStream |
Events carry a type, the run_id, the acting agent_id, and a JSON payload. The types fall into a few families (all namespaced context.*):
| Family | Example types |
|---|---|
| Memory writes | context.ingest_completed, context.working_memory_updated, context.checkpoint_created |
| Learning | context.reflection_completed, context.lesson_promoted, context.lesson_validation_passed / _failed |
| Coordination | context.handoff_created, context.feedback_received, context.agent_registered |
| Outcomes | context.outcome_recorded, context.step_outcome_recorded |
A reactive reviewer is a few lines — Rex sleeps until Ari asks for a verdict:
for event in client.advanced.subscribe({"run_id": "nimbus:ticket-1088"}):
if event.get("type") == "context.handoff_created":
review_handoff(event) # recall traces, then feedback() a verdictWhen to use it
- Reactive review agents. Rex shouldn't poll every run for pending handoffs —
context.handoff_createdwakes him exactly when a memory-backed handoff needs a verdict, andcontext.feedback_receivedwakes the sender. - Cache invalidation. If you cache assembled context or derived state,
context.reflection_completedandcontext.working_memory_updatedtell you precisely when the cache went stale — rebuild withget_context()on wakeup rather than on a timer. - Monitoring and audit. Sana can watch
context.lesson_promotedandcontext.lesson_validation_failedacross the fleet to track what's entering shared team memory — and flag a quarantined promotion before anyone asks.
When not to use it
Don't reach for the stream when a poll would do: a batch job that runs hourly and reads memory once doesn't need a persistent connection, and handoff(..., await_verdict=True) already packages the common "block until the verdict arrives" case without any subscription. The stream is per-run — a consumer watching many runs needs a subscription per run, which is a real cost at fleet scale.
The core data plane has its own pub-sub with semantic subscriptions — fire when a written node crosses a similarity threshold to a query embedding. It's a separate, lower-level surface, available only where the instance operator has enabled it; see Pub-sub & semantic subscriptions if you have access.
Related
- Event-driven agents recipe — the full implementation walkthrough
- Pub-sub & semantic subscriptions — both event surfaces, decision model, and lifecycle
- Memory-backed handoff — the coordination artifacts these events announce
- The learning loop — what reflection and promotion events mean
- Control gRPC reference — the
Subscribestreaming contract