Why it exists
Historically inbound comms did not wake an agent. The Resend and Surge webhook handlers only fired outbound subscriber webhooks (webhook_subscriptions) — an HMAC POST to the tenant’s own SaaS — and nothing
dispatched an agent run. Cron push-through recorded no binding row, and there
were three incompatible event shapes. The router closes that gap.
The envelope
Every source normalizes into aNaiveEvent:
Subscriptions (the binding registry)
Atrigger_subscriptions row binds a source (+ optional filter) to a
target (how to build the agent run). Tenant scope is nullable: a company-wide
subscription (tenant_user_id = null) matches every operator; a scoped
subscription matches only its tenant.
filter— matched against the event.event_types/event_typematch the dotted type; any other key matches the same-named field in the payload (e.g.{ "to": "sales@acme.com" },{ "e164": "+1555…" }).target—{ prompt }or{ promptTemplate }(with{{type}},{{source}},{{payload}}substitutions) and an optionalprofileName.
Delivery
For each matched subscription the router resolves a lane:- cloud-resident agent →
sidecarFetch("/ceo/run")— the previously-missing dispatch that wakes the agent’s Hermes runtime. - box-resident agent → the device relay — the event is enqueued as a
trigger.wakeon the device’s durabledevice_eventsqueue; the box’s device-agent replays it and runs the agent locally.
delivery on the subscription is auto (resolve at delivery), sidecar, or
device. In auto, a Tower box always uses its local runtime; in cloud, a
provisioned device bound to the company routes to the box, otherwise the cloud
sidecar.
Delivery status
Every attempt writes atrigger_deliveries row and advances through
received → queued → delivered → processed | failed. An event that matched no
subscription records a single no_match row, so a silent drop (“why didn’t my
agent wake?”) is always observable. Query it at GET /v1/triggers/deliveries.
Behaviour-preserving
The router is additive. Existing outboundwebhook_subscriptions continue
to fire exactly as before — the inbound webhook handlers simply also call
routeEvent. Existing cron and webhook ingress keep their shape.
Loops
A Loop is just akind='loop' subscription whose
source is cron: the binding row is the registry, and the executor is the
box’s Hermes gateway cron (or the cloud sidecar cron), kept in sync on create.