REX is a rules engine designed to process complex conditions and actions using a structured JSON format for rule definitions. It allows for defining rules, conditions, and actions that are compiled into bytecode by the REX Compiler, then executed by the REX Engine. REX currently uses Redis as its fact store and event transport. Pub/Sub provides the default best-effort path; Redis Streams provides retained, ordered durable processing with recovery.
The default v4 execution contract evaluates each affected rule once per batch, against the same snapshot. Outputs are staged, conflicts reject the round, and committed outputs feed bounded subsequent rounds. See the M4 migration guide before recompiling v3 rulesets. M6 removed JavaScript execution from every contract; see the script migration guide before upgrading a legacy v3 deployment that used scripts.
Rulesets may opt into the v5 closed typed-fact contract with a top-level
facts declaration map; see the typed fact migration guide.
Condition leaves may opt into the v6 processing-time contract with for, which
requires the predicate to remain true for a bounded duration; see the
temporal rules guide.
Rules may opt into v7 emit: "on_change" behavior to suppress outputs already
present in persisted state; see the change-only emission guide.
Use the M5 authoring tools to explain, lint, test, replay,
and compare v4/v5/v6/v7 batch rulesets offline. rexd --dry-run --bundle ... emits simulated
results without creating Redis connections.
The planning documents have distinct roles:
- The foundation roadmap tracks the next stage of architecture, performance, reliability, and tooling work, with milestone checklists and acceptance criteria.
- The revival plan preserves the earlier revival milestones.
- The engine semantics audit is the source of truth for verified findings and their remediation status.
- The evolution reference preserves longer-term design context, open decisions, and ideas worth revisiting.
For the compiled-artifact format, compatibility contract, and upgrade guidance, see the bytecode compatibility guide. For platform, toolchain, and runtime expectations, see the compatibility matrix.
- Define rules using JSON
- Support for various data types and comparison operations
- Logical and control flow instructions
- Action execution based on rules
- Offline explain, lint, scenario-test, replay, and comparison tools
- Durable event processing, recovery, and exact partition ownership
- Safe ruleset reload and rollback with retained artifact history
- Optional typed facts, temporal conditions, and change-only emission
- Go 1.26.6 or higher
- A standalone Redis server reachable at localhost:6379 for the default configuration. Durable scripted transactions require Redis 7.0 or later; durable WATCH mode supports Redis 6.2 or later. Redis Cluster is unsupported.
Clone the repository:
git clone https://github.com/rgehrsitz/rex.gitNavigate to the project directory:
cd rexThe REX repository includes five command-line programs: rexc, rexd,
redis_setup, rex_stressor, and rule_gen. Release archives contain all five.
Purpose: The rexc executable is the compiler that translates rules defined in JSON format into bytecode instructions that the runtime engine can execute.
How to Build:
go build ./cmd/rexcHow to Run:
./rexc -rules <path_to_rules.json> [-output <path>] [-loglevel <level>] [-logoutput <output>]
./rexc validate -rules <path_to_rules.json>Command-line options:
-rules: (Required) Path to the input JSON file containing the rules.-output: (Optional) Compiled-bytecode path. Default isoutput.bytecode; ignored byvalidate.-loglevel: (Optional) Set log level. Valid values are panic, fatal, error, warn, info, debug, trace. Default is "info".-logoutput: (Optional) Set log output. Valid values are console or file. Default is "console".
Example:
./rexc validate -rules examples/rules2.ruleset.json
./rexc -rules examples/rules2.ruleset.json -output rules.bytecode -loglevel debug -logoutput filePurpose: The rexd executable is the runtime engine that processes the bytecode generated by rexc. It uses a Redis store for managing and updating facts in real-time based on the rules.
How to Build:
go build ./cmd/rexdHow to Run:
./rexd -config <path_to_config.json>Command-line options:
-config: (Optional) Path to the configuration file. If not specified, rexd will look for a file namedrex_config.jsonin the current directory,$HOME/.rex, and/etc/rex.
Configuration File (rex_config.json): The configuration file is JSON. This is a minimal Pub/Sub example; see the complete example for durable, batch-bound, and compatibility settings.
{
"bytecode_file": "output.bytecode",
"logging": {
"level": "debug",
"output": "console",
"time_format": "unixnano",
"trace_conditions": true
},
"redis": {
"address": "localhost:6379",
"username": "",
"password": "",
"database": 0,
"connect_timeout": "5s",
"health_check_interval": "1s",
"health_check_timeout": "500ms",
"event_mode": "pubsub",
"tls": {
"enabled": false,
"server_name": "",
"ca_file": ""
},
"channels": ["weather", "system", "network", "energy", "water"]
},
"engine": {
"priority_threshold": 1,
"scripts_enabled": false,
"max_actions_per_evaluation": 32,
"max_event_hops": 16
},
"observability": {
"enabled": false,
"address": "127.0.0.1:8080"
}
}Every setting can be supplied as an environment variable by uppercasing its
path, replacing dots with underscores, and adding REX_. For example,
redis.password is REX_REDIS_PASSWORD and redis.tls.ca_file is
REX_REDIS_TLS_CA_FILE. Environment variables override the configuration file,
which overrides built-in defaults. Comma-separate REX_REDIS_CHANNELS values.
See the M3 operations guide for TLS, readiness,
routing, limits, and metrics. Redis Pub/Sub remains the default event mode.
For retained delivery and restart recovery, set redis.event_mode to streams,
configure redis.durable.stream, group, and consumer, and follow the
M7 durable processing runbook.
Production durable mode uses one serial processor per daemon and a standalone
Redis commit domain. The default scripted transaction mode requires Redis 7.0
or later; set redis.durable.transaction_mode to watch for Redis 6.2 or later.
Concurrent workers sharing one Redis process and Redis Cluster are unsupported.
Optional ruleset reload validates and archives a candidate before switching at
an event boundary. Configure engine.reload and preserve its history directory
as described in the reload runbook. Managed
partitions must pass the ownership rollout
before production use.
scripts_enabled remains only as an M6 migration tripwire: false is accepted,
while true fails startup. See the M6 migration guide.
In legacy v3, engine.priority_threshold only controls the additional high-priority
diagnostic emitted after a matching rule. It does not filter candidates or
change their execution order.
Example:
./rexd -config cmd/rexd/rex_config.jsonrexd consumes JSON objects that map fact keys to their JSON values. In Pub/Sub
mode the producer publishes the object to a configured channel. In Streams mode
the producer appends it as the payload field of the configured input stream.
For example, the following event updates a numeric fact, a boolean fact, and a
string fact without type conversion:
{
"weather:temperature": 30.5,
"weather:alert": true,
"weather:status": "storm watch"
}During migration, rexd also accepts the legacy key=value form. Its value is decoded as JSON when possible, so weather:status="storm watch" is a string and weather:alert=true is a boolean. New producers should publish the JSON-object format.
rexd assigns each incoming event a trace_id. A JSON event with several facts keeps that ID for every fact update it contains. At the configured info level, the runtime emits structured records for fact_event_received, fact_event_decoded, rule_evaluation_candidates, rule_condition_evaluated, action_completed (or action_skipped / action_failed), and rule_evaluation_completed. Filter by trace_id to follow an event from Redis ingress through its rule and action outcomes.
These trace records identify facts, rules, action types, and targets, but deliberately omit arbitrary fact and action values. Other diagnostic records—including the existing high-priority rule message—may contain values, so use those logs only while investigating a trusted deployment.
Set logging.trace_conditions to false to omit the per-condition
rule_condition_evaluated records. It defaults to true for compatibility.
Candidate, action, and rule-completion summaries, warnings, and failures remain
enabled at their configured log levels. Embedded callers can use
Engine.SetConditionTracing(false) before processing events. Successful Redis
publication diagnostics now use the configured structured logger at debug;
they no longer write event payloads through the standard-library logger.
Set observability.enabled to true to expose local HTTP endpoints at observability.address (default: 127.0.0.1:8080):
/healthzconfirms that the daemon process is serving HTTP./readyzreturns200only while Redis connectivity and the configured event source are healthy. Durable mode also requires the partition ownership lease. It returns503during startup, disconnection, lease loss, and shutdown./metricsemits Prometheus text-format counters and an event-processing latency histogram. Rule and action outcome labels use fixed value sets.
The endpoint is disabled by default so an upgrade does not unexpectedly open a
port. Durable mode reports consumer-group lag, pending work, retries, and dead
letters. Redis Pub/Sub has no retained queue, producer timestamp, or broker-side
drop counter, so its queue metrics are emitted as NaN.
rexd limits each rule evaluation to engine.max_actions_per_evaluation actions (default: 32) and limits a chain of Rex-derived Redis events to engine.max_event_hops hops (default: 16). Derived updates carry an internal _rex envelope containing the existing trace ID and incremented hop; independent producers can continue sending the canonical JSON fact-object format unchanged. An event over its hop limit is rejected before rule evaluation.
These controls bound accidental feedback loops, but they do not make actions exactly-once. Rules and external action consumers should remain idempotent: use stable business keys, tolerate duplicate updates, and avoid non-idempotent side effects (such as creating a new record) without a deduplication key.
Purpose: The redis_setup executable initializes the Redis database with default values necessary for some testing of the REX system. It also provides a CLI for modifying values during debugging.
How to Build:
go build ./tools/redis_setupHow to Run:
./redis_setupThis tool doesn't have any command-line options. It connects to Redis at localhost:6379 by default.
After running, it provides an interactive CLI with the following command:
set <group:key> <value>Example:
./redis_setup
Enter command (set <group:key> <value> or exit): set weather:temperature 30.5Purpose: The rule_gen executable generates a large number of random rules in JSON format, which can be used for testing and benchmarking the REX system.
How to Build:
go build ./tools/rule_genHow to Run:
./rule_gen [-rules <number_of_rules>] [-output <output_file.json>]Command-line options:
-rules: (Optional) Number of rules to generate. Default is 1000.-output: (Optional) Output file name. Default is "generated_ruleset.json".
Example:
./rule_gen -rules 1000 -output generated_ruleset.jsonrex_stressor generates random weather fact updates for local Pub/Sub load
testing. It writes directly to Redis and is outside the durable Streams
contract.
go build ./tools/rex_stressor
./rex_stressor -redis localhost:6379 -rate 10- Define your rules in a JSON file.
- Use
rexcto compile the rules into bytecode. - Set up your Redis instance and initialize it with
redis_setupif needed. - Run
rexdwith the compiled bytecode to start the rules engine. - The engine will listen for updates from Redis, evaluate rules, and perform actions accordingly.
For a self-contained compiler -> Redis -> runtime smoke test, see the Docker Compose demo.
Pushing an annotated semantic-version tag such as v0.2.0 or
v0.2.0-alpha from a reviewed main commit publishes versioned archives for
Linux, macOS, and Windows, together with a SHA-256 manifest. Reviewed operator
notes are prepended to GitHub-generated change notes. See the
release guide for the tag and verification procedure.
cmd/rexc: Main application entry point for the compilercmd/rexd: Main application entry point for the runtime enginepkg/compiler: Contains the bytecode compiler and related functionspkg/runtime: Contains the runtime engine and related functionspkg/store: Contains the Redis store implementationpkg/logging: Contains logging utilitiestools/redis_setup: Redis setup and CLI tooltools/rex_stressor: Local Pub/Sub load generatortools/rule_gen: Random rule generation tool
Rules are defined in a JSON format. Each rule consists of conditions and actions. Here's an example:
{
"rules": [
{
"name": "rule-1",
"conditions": {
"all": [
{ "fact": "weather:temperature", "operator": "GT", "value": 30 },
{ "fact": "weather:humidity", "operator": "LT", "value": 40.01 }
]
},
"actions": [
{
"type": "updateStore",
"target": "weather:temperature_warning",
"value": "high"
}
]
}
]
}Rules are defined in a JSON object with:
rules: a required non-empty array of rule objects.facts: an optional closed map of v5 typed fact declarations.
A rule object has the following properties:
- name: a unique string identifying the rule
- priority: optional non-negative integer indicating execution priority. Lower numbers execute first; omitted priorities default to 10.
conditions: an object containing exactly one lowercasealloranyarray.actions: a non-empty array of action objects.emit: optional"on_change"behavior; omission retains repeated emission.
A condition group contains exactly one non-empty lowercase all or any array.
Each array entry is either a condition object or another condition group.
A condition object has the following properties:
fact: the fact name to evaluate.operator:EQ,NEQ,LT,LTE,GT,GTE,CONTAINS, orNOT_CONTAINS.value: the scalar value to compare against.for: an optional v6 processing-time duration.
Condition leaves and nested groups may appear in either order; the compiler canonicalizes them deterministically.
Fact names may contain colons, as shown throughout the examples. Durable mode
reserves its configured stream names, names beginning with rex:durable:, and
the private temporal-state prefix; see the ownership guide before assigning a
managed partition.
An action object has the following properties:
type: the supported action type,updateStore. Unsupported action types are rejected during compilation.target: the fact to update.value: the scalar value to write.
REX no longer accepts JavaScript definitions or {script} action values. The
compiler rejects them for every contract, and the runtime rejects legacy v3
artifacts containing script opcodes before execution. Move calculations into
producer-supplied facts or express them with declarative rules. See the
M6 migration guide.
Actions will be executed in the order they are defined in the rule.
Fact names are strings. Values can be strings surrounded by quotation marks (e.g. "fact_a"), booleans, or finite JSON numbers. V5 declarations bind each name to one of those scalar types and may explicitly permit null.
Candidate rules execute in ascending priority order, so lower numbers run first. Rules with the same priority execute in their original ruleset order. Rule evaluation is sequential for each event batch.
The current-v3 semantics safety net runs authored
scenarios and seeded differential checks against an independent AST interpreter.
Embedded tools can use store.NewMemoryStore(initial) for JSON facts without a
Redis service; snapshots and ordered publications are available for inspection.
The batch-v4 corpus independently
checks the new contract while retaining current-v3 expectations.
To run the tests:
go test ./...This project is licensed under the MIT License - see the LICENSE file for details.