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 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_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