Signals
A signal is a typed domain event. Signals are emitted by actions and are the authoritative source for cache invalidation, flow triggers, and timeline projection state.
Defining a signal
Every signal has a version, an optional description, and a typed payload.
domain:
signals:
custom:
message.created:
description: "A message was successfully posted"
version: 1
payload:
id: id!
text: text!
user_id: id!
created_at: timestamp!
Signal IDs use dot notation (message.created). The payload is a typed field map using the same type syntax as collections.
Who emits signals
Only actions emit signals. An action declares its emitted signals in the emits block. The runtime rejects any signal emission from an action that did not declare it in effects.
domain:
actions:
guestbook.submit:
effects: [emit, write]
emits:
- signal: message.created
payload:
id: { $expr: { var: generated_ids.ids.0 } }
text: { $expr: { var: input.text } }
user_id: { $expr: { var: principal.subject } }
created_at: { $expr: { var: ctx.now } }
Who consumes signals
Three things consume signals:
- Flows — A flow's
onfield lists the signal IDs that trigger it. - Timeline projections — A timeline projection's
signalsfield lists the signals whose history it accumulates. - Cache invalidation — The runtime invalidates query results when a signal listed in the query's invalidation set is emitted.
Versioned payloads
Signal payloads are versioned. When the shape of a payload changes, bump version and add a migrate block mapping each older version to the current schema.
domain:
signals:
custom:
message.created:
version: 3
payload:
id: id!
text: text!
user_id: id!
channel: text!
created_at: timestamp!
migrate:
- from_version: 1
payload:
id: { $expr: { var: payload.id } }
text: { $expr: { var: payload.text } }
user_id: { $expr: { var: payload.user_id } }
channel: direct
created_at: { $expr: { var: payload.created_at } }
- from_version: 2
payload:
id: { $expr: { var: payload.id } }
text: { $expr: { var: payload.text } }
user_id: { $expr: { var: payload.user_id } }
channel: { $expr: { var: payload.channel } }
created_at: { $expr: { var: payload.created_at } }
Each migrate entry maps from a specific older version to the current schema. The runtime applies migrations in ascending version order when replaying historical signals — this is required for timeline projections to remain consistent as payload schemas evolve.
The compiler emits a warning when a signal has migrate blocks spanning more than 3 versions without a compaction. When migration chains grow long, flatten them into a single migration from the oldest still-live version to the current one.
Signal guards
Signals can declare guards on their payloads. A guard on a signal prevents the signal from being emitted if the payload fails the declared rules.
domain:
signals:
custom:
message.rejected:
version: 1
payload:
id: id!
user_id: id!
reason: text!
guards:
on_fail: abort
pre:
structural:
- description: "Rejection reason must be actionable."
rule:
contains:
- [spam, profanity, off_topic]
- var: payload.reason
Signal guards use the same $expr and guard model as action guards.
What's next
- Actions: how to emit signals
- Flows: how flows react to signals
- Timeline projections: how signal history builds read model state