An event is a typed domain signal whose deliveries are allowed to make the
program do something. It is the third member of Sema’s telemetry-and-reaction family,
and the one that closes the loop:
collectorrecords telemetry that can never drive control flow.monitoranswers “has this stream’s distribution shifted?”eventis the construct whose deliveries do work — the missing counterpart that corpus code used to improvise as ambientalert(...)calls, watcher tasks with manualcancel, and approval-record polling.
Why events are a language construct
Section titled “Why events are a language construct”Callback and listener APIs are invisible to the type system: they don’t appear in effect rows, they escape policies, and they hide the delivery graph. Fire-and-forget delivery with unbounded queues loses signals silently. Sema replaces all of that with a construct where the payload is a contract, delivery is journaled and bounded, handlers run under their own policy, and the compiler sees the complete emit/subscribe graph at load time.
Syntax
Section titled “Syntax”An event declares a nominal payload record; a function emits it; a subscriber
handles it:
event IncidentQuarantined: sem "An ingested item was quarantined by a semantic guard" incident: Incident evidence: SemanticsViolation key incident.region # optional per-key ordering/partition
def quarantine(i: Incident, v: SemanticsViolation) -> None !{event.emit, fs.write}: audit_store(i, v) emit IncidentQuarantined(incident=i, evidence=v)
subscriber quarantine_review on IncidentQuarantined: sem "Queue quarantined incidents for analyst review" where event.incident.severity >= Severity.high # deterministic, effect-free filter queue ring(4096), on_full=block handle event !{db.write, event.emit}: review_queue.push(event.incident, event.evidence)The verified crisis-logistics corpus uses exactly this shape to quarantine a public
briefing that fails a semantic guard, so the block becomes a persisted record for
review instead of a silent drop:
event IncidentQuarantined: sem "A public briefing was blocked by a semantic guard before publication" briefing: PublicBriefing sem "The withheld briefing, redacted but unpublished" evidence: SemanticsViolation sem "Guard verdict with predicate, judge, and excerpts"
# ... inside publish_public_briefing, under an except SemanticsViolation branch: emit IncidentQuarantined(briefing=safe, evidence=violation)
subscriber quarantine_review on IncidentQuarantined: sem "Persist quarantined briefings for analyst review" queue ring(256), on_full=block handle event !{fs.write}: write_quarantine_record("state/quarantine/briefings.jsonl", event.briefing, event.evidence)The payload is a boundary contract at the emit site
Section titled “The payload is a boundary contract at the emit site”An event declaration is a nominal payload record: its fields carry sem
descriptors, where refinements, and coerce by normalizers exactly as struct fields
do. The whole payload is a full boundary contract at the emit site — a payload
that fails its contract never enters the stream. The failure is a typed
ContractViolation blamed on the emitter, not on some downstream handler. emit adds
event.emit to the caller’s effect row, and policies confine
it per event type:
forbid event.emit except event.emit(IncidentQuarantined)Subscribers: static, bounded, policy-confined
Section titled “Subscribers: static, bounded, policy-confined”A subscriber is a static declaration, parallel in shape to a monitor.
Registration happens at container/module load, so the compiler sees the complete
delivery graph — it warns on events with no subscriber (a dead signal) and on
statically detectable emit cycles. Three parts matter:
where <filter>— an effect-free deterministic filter. It cannot perform I/O or call a model; it only decides whether this delivery is relevant.queue ring(n), on_full=<mode>— a bounded queue.ring(n)is required; an unbounded queue is a compile error (same rule as collectors).on_fullis one ofblock(default — backpressure to the emitter),drop_oldest, orfail. Drops are journaled asEventDroppedrecords — never silent.handle event !{...}— the handler, with its own effect row. It runs under the subscriber’s policy envelope, never the emitter’s.
The robotics-cell corpus brings the cell to a deterministic safe stop when a
monitor emits a fault event — the monitor detects drift, the
event carries it, the subscriber reacts:
subscriber safe_stop on CellFaultDetected: sem "Bring the cell to a deterministic safe stop when telemetry drifts" queue ring(64), on_full=block handle event !{ffi.call}: hardware_safe_stop() log.info("cell safe-stopped after telemetry drift", order=event.order_id)Delivery semantics: journaled, ordered, exactly-once
Section titled “Delivery semantics: journaled, ordered, exactly-once”Emission appends an EventEmitted record to the same hash-chained journal as
model calls and contract verdicts — the event bus is not a side channel, and replay
reproduces delivery order and handler effect traces exactly. Delivery is:
- Asynchronous, with per-subscriber FIFO order per emitter (per
keyvalue when declared; cross-key deliveries are concurrent). - Exactly-once per subscriber within a run.
- Structured — handlers run as structured children of the scope that owns the
subscriber (the module’s container scope by default, or the enclosing
supervisewhen declared inside one). There are no orphan handler tasks. A handler failure is a typedSubscriberFailurerouted to the owning supervision scope; the emitter is never affected.
Shutdown drains queues under the container deadline and journals undelivered events as
EventUndelivered. Handler-emitted events are depth-budgeted (default 16) against
cycles; exceeding it is a typed EventCycleBudgetExceeded on the emitting handler.
Runtime lifecycle events are ordinary subscribers
Section titled “Runtime lifecycle events are ordinary subscribers”The prelude declares runtime lifecycle events on this same construct — Alert (the
target of the alert(...) sugar), MonitorVerdictChanged, HealEvent,
ContainerStarted, PolicyDenied, RepairExhausted. So operational reactions like
“page someone when a heal escalates” are ordinary subscribers, not special runtime
hooks:
subscriber page_oncall on RepairExhausted: queue ring(64), on_full=block handle event !{net.connect}: page(event.supervisor, event.outcome)Failure modes
Section titled “Failure modes”- Emit under a policy without
event.emit→ typed denial. - A handler effect row exceeding the subscriber’s policy → compile error.
- A contract-failing payload → emitter-blamed
ContractViolation; nothing is delivered. - Queue overflow under
on_full=fail→ typedEventBackpressureat the emit site. - An event with no subscriber → dead-signal warning at compile time.
- A calibrated
semantics()guard inside awherefilter → a decision site like any other; monitor coverage applies.
How it’s checked
Section titled “How it’s checked”sema check <project>builds the emit/subscribe graph, warns on dead signals and static emit cycles, verifies handler effect rows against subscriber policies, and rejects unbounded queues.- Emission and delivery are journaled (
EventEmitted,EventDropped,EventUndelivered), so replay reproduces the exact delivery sequence. - An event stream is a valid monitor target
(
monitor X on IncidentQuarantined:).
See also
Section titled “See also”- Monitors & Drift — the statistical channel that emits events on drift.
- Collectors & Taps — the observation-only channel that cannot react.
- Supervise & Heal — the scope that owns subscriber tasks
and receives
SubscriberFailure. - Policies — confining
event.emitper event type.