The Model
A trigger occurrence flows through one pipeline regardless of origin (a UI signal, an inbound webhook, a scheduled timer tick): declare → emit → match → deliver → wake. Only the final wake is session-scoped and turn-ordered; everything before it is a runtime-level, session-agnostic ingress.
+ configured source constructors (cron.Schedule)"] Reg["triggers.register(source, target process, inputs)
durable subscription, keyed by source_type + source_key"] Emit["source owner emits occurrence
TriggerStore.record_occurrence (session-agnostic)"] Match["reserve_matching_deliveries
match by source_type + source_key"] Start["start target process (idempotent effect)
with stored registrant + env_ref"] Wake["process wake → queued work
EarliestSafeBoundary"] Turn["turn runs → commit to session graph"] Decl --> Reg Reg -.subscription.-> Match Emit --> Match --> Start --> Wake --> Turn
Declare And Subscribe
A host declares the events it can emit and the trigger sources it exposes; programs subscribe by registering triggers against those sources.
- Catalog.
TriggerEventCatalogholds typedTriggerEventdeclarations (resource_type,alias,event, and a named payload type). Identity is thesource_type=alias.event; duplicate source types are rejected. Emitted payloads are validated against the declared type. - Sources. A host exposes configured pure source constructors such as
cron.Schedule(...); zero-config sources such asui.button.pressed({})are derived from declaredTriggerEvents. In both cases the session catalog records protocol metadata that the constructed value satisfiesTriggerSource<Event>. Declarativetriggersyntax does not exist; programs pass a source value plus a process definition and explicitinputstotriggers.register/triggers.list/triggers.cancel. - Subscriptions.
triggers.registerwrites a durable subscription into the runtime trigger store, keyed for matching bysource_typeandsource_key. Each subscription carries a registrant (HostorSession), a captured execution environment reference (env_ref), an optional wake target, the target process definition identity (module ref, host requirements ref, process ref, and process name), and an input template;trigger.eventis the whole-event placeholder. Subscription handles are unique per registrant scope (registrant_scope_id), and session-side listing and cancellation filter by the registrant session. Deleting a session deletes that session's subscriptions; host-registered subscriptions are untouched. Session identity is optional metadata on those edges, not the substrate of trigger matching.
Emit, Match, Deliver
A source owner emits an occurrence; the router records it and fans out to every matching subscription. The event itself names no session; the match supplies the targets.
| Step | What happens |
|---|---|
| record | TriggerStore::record_occurrence stores the occurrence with a deterministic id derived from source_type, source_key, and idempotency key. The occurrence is session-agnostic. |
| match | reserve_matching_deliveries looks up subscriptions by the occurrence's source_type and source_key, then reserves one delivery per match. A single occurrence may fan out to 0..N subscribers, including host-registered subscriptions with no session edges. |
| deliver | Each reservation starts the subscription's target process through the effect controller with the subscription's registrant as provenance, its captured env_ref, and its wake target; a wake target also earns that session a handle grant on the started process. The delivery process id is deterministic (occurrence_id + subscription_id). Delivery is therefore idempotent / exactly-once and replay-safe; the started process carries CausalRef::TriggerOccurrence so causality is referenced, not copied. |
A matched trigger occurrence does not write the conversation graph and is not a session command. It references the started process as its consequence; the event log and the conversation graph stay separate.
Only The Wake Is Session-Ordered
Starting a process is safe at any time. It is independent background work. The one place a trigger occurrence touches a session's turn state is an optional wake the matched process emits.
A trigger-started process is durable background work — it declares the Rerunnable recovery disposition, so it is lease-protected and re-executed on crash through DurableProcessWorker (ADR 0019); see durability. When it has a wake target and wakes an agent, the wake is written to that session's queued work and delivered at EarliestSafeBoundary: immediately when the session is idle, or at the next safe boundary while a turn runs. With no live wake target, the wake remains a process event and no delivery is materialized. Triggers are therefore a separate runtime-level ingress and never appear as queued user input themselves; only the resulting wake enters queued work. How queued work and pending turn input are persisted and claimed is detailed under persistence.
The Store Is A Backend Seam
Subscriptions, occurrences, and delivery reservations live behind a TriggerStore trait, parallel to RuntimePersistence, and separate from it.
The default implementation is in-memory. Local durable hosts can use the first-party SqliteTriggerStore; shared-worker deployments can use PostgresTriggerStore; hosts can also provide another TriggerStore implementation when it preserves occurrence idempotency, matching, reservation, and delivery semantics. The distributed Restate/Postgres/MinIO E2E is one exercised deployment, not the interface contract. The trigger store is durable intent for delivery, not conversation history; durable session state remains in RuntimePersistence.
Timers And Cron
Timers and recurring jobs are host-owned source policies, not Lashlang syntax and not queued work.
A source owner keeps its own schedules and, when a schedule fires, emits a declared trigger occurrence with a stored source key. From there it is an ordinary occurrence: matched by source_type plus source_key and delivered to subscribed processes through the same durable worker path. A cron firing is thus the same shape as a button press; there is no separate by-handle activation path.