Docs

Rule DSL reference

Grammar, condition operators, built-ins, actions, priority bands, conflict order, and complete WB-1 rule examples.

Rules are versioned data evaluated once per 1 kHz engine cycle. A candidate revision is parsed and validated before activation; an unknown key fails the revision and leaves the active set untouched.

Grammar

rule        := "rule:" id newline priority when then
priority    := "priority:" integer[1..100]
when        := "when:" ("all:" condition+ | "any:" condition+)
condition   := input comparator value | builtin comparator value
then        := "then:" action+
action      := output set value | emit event | hold_until condition

Canonical YAML shape:

rule: descriptive-kebab-id
priority: 50
when:
  all:
    - input: domain.zone.signal
      gte: 1
then:
  - output: domain.zone.actuator
    set: value
  - emit: audit.descriptive_event

Condition operators

OperatorValue typesMeaning
equals, not_equalsscalar, enum, booleanexact normalized value
gt, gte, lt, ltenumber, durationordered comparison
betweennumber, timeinclusive two-value range
in, not_inscalar + listmembership
changedchannel pathvalue changed this cycle
stale_forchannel path + durationno accepted sample within duration
matchesstring + bounded regexRE2 expression, 128-character limit

Units are normalized before comparison. Comparing 4 mA with a channel declared in volts fails validation instead of guessing a conversion.

Built-ins

Built-inReturnsExample
time_of_day()local timebetween: ["06:00", "09:00"]
zone()current zone IDequals: court.3
rolling_avg(path, window)numberrolling_avg(vibration.rms, 5s)
rate(path, window)number / secondrate(door.edge, 30s)
age(path)durationage(sensor.temp) > 2s
ml.*model outputml.anomaly_score > 0.8

ml.* values are ordinary normalized inputs. They do not bypass priorities or safety rules.

Action paths

Outputs use stable domain.zone.id paths. A single segment may be *; a bounded range uses 4..8.

- output: light.court.*.scorebar
  set: freeze
- output: audio.court.4..8.horn
  pulse: 800ms
- output: hvac.zone.2.setpoint
  set: 68F
- emit: audit.period_end

Wildcards resolve to the current device map at activation time and are written to the audit record as concrete paths.

Priority bands

BandConventionExamples
90–100safety and hard interlocksE-stop hold, egress lighting, over-temp cutoff
50–89orchestration and operator intentgame cues, room plans, changeover recipes
1–49ambient and conveniencesoft-open lighting, comfort setpoints

Conflicts resolve by priority, then freshest input, then the rule with more matched conditions. The audit entry records both contenders and the deciding key. See the engine for the timing path.

Example — sports period end

rule: tournament-period-end
priority: 86
when:
  all:
    - zone: court.*
    - event: scoreboard.period_end
    - confidence_gte: 0.98
then:
  - light.court.*.scorebar: freeze
  - audio.court.*.horn: pulse_800ms
  - emit: audit.tournament_period_end

Example — gym soft-open

rule: gym-entrance-soft-open
priority: 40
when:
  all:
    - input: sensor.door.reed
      equals: closed
    - time_of_day:
        between: ["06:00", "09:00"]
then:
  - output: light.zone.2
    fade_to: {value: 30%, over: 5s}
  - output: hvac.zone.2.setpoint
    set: 68F

Example — industrial E-stop hold

rule: cell4-estop-hold
priority: 100
when:
  any:
    - input: cell.4.estop_a
      equals: open
    - input: cell.4.estop_b
      equals: open
then:
  - output: cell.4.drive_enable
    set: false
  - output: cell.4.air_dump
    set: true
  - hold_until: safety.cell4.manual_reset
  - emit: audit.safety_hold

A priority-100 rule can still be replaced only by a reviewed revision; it cannot be suppressed by an ambient rule. Validate with POST /api/v1/rules:validate before fleet activation.

Audit fields

Every firing and conflict record includes event_id, rule_id, revision, matched_inputs, resolved_outputs, priority, deciding_key, device_id, zone, cycle, ts, previous_hash, and hash. Export is hash-chained NDJSON. The API overview documents paging and verification.

copied!