Signals
Declaring signals and emitting them from rules
Signals are how a rule communicates external effects to the host application. A rule's outputs describe what is true about the input; its signals describe what should happen next.
Declaring a signal
Signal declarations live at the top level of a file, outside any rule:
signal BlockAccount(user_id)
signal NotifyUser(user_id, message)
signal Audit()A declaration names the signal and lists its positional parameters. The parameter list documents the expected arity but does not constrain types — emitted arguments are passed through to listeners exactly as the rule produces them.
A signal can be declared once per program. Re-declaring is a compile-time error.
Emitting
Inside a rule, use emit Name(args...) to queue an emission:
rule AccountApproval {
is_adult = input.age >= 18
allowed_country = input.country == "us" || input.country == "ca"
eligible = is_adult && allowed_country
emit BlockAccount(input.user_id) when !eligible
emit SendVerificationEmail(input.user_id) when eligible && !input.email_verified
}The number of arguments must match the signal declaration. The compiler checks this.
when
emit ... when <expr> makes the emission conditional. The expression
must be bool. If the condition is false or undefined, the signal
is not emitted.
emit Audit(input.user_id) when input.audit_enabledAn emit without when always fires:
emit Audit(input.user_id)How signals reach your code
After evaluation, the engine returns the list of emitted signals in
order in emitted_signals:
{
"outputs": { "approved": false },
"emitted_signals": [
{ "name": "BlockAccount", "args": ["u123"] }
]
}You can also register listeners up front:
- Go evaluator:
Evaluator.OnSignal("BlockAccount", func(args []any) error { ... })orOnSignalNamed("BlockAccount", func(args map[string]any) error { ... })which uses parameter names from the declaration. - JS evaluator:
evaluator.on("BlockAccount", (user_id) => { ... }). - CLI:
--exec "BlockAccount=./block.sh {1}"runs a shell command per emission, substituting{1},{2}, ... with the arguments.
Notes
- Signals are queued during a rule run and dispatched after all ops
complete — emission order matches source order, filtered by
when. - Signals are local to a single evaluation. There is no persistent signal bus.
- Listeners are invoked synchronously by default. The Go evaluator
accepts a
*sync.WaitGroupto run listeners concurrently.