Nodora
Language

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 ruleset. 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_enabled

An 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 { ... }) or OnSignalNamed("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.WaitGroup to run listeners concurrently.

On this page