A programmable rule engine and policy evaluator for Node.js. Define rules with conditions and actions, group them into policies, and evaluate them against JSON fact objects.
- Rule DSL: TypeScript-native rule definitions with operators, conditions, priority, and chaining
- Policy Compiler: Compiles rules into an optimized evaluation plan with condition reordering
- Execution Engine: Evaluates facts against compiled policies with short-circuit logic
- Conflict Detector: Finds contradictory or redundant rules before deployment
- Coverage Tracker: Reports which rules fired/didn't across a batch of facts
- Tracer: Records full evaluation traces for debugging
- Diff Engine: Compares two policy versions to understand what changed
- CLI: Validate, run, and diff policy files from the command line
- Plugin System: Register custom operators for domain-specific logic
- Cache: LRU cache for compiled policies keyed by content hash
npm install ruleflowimport { PolicyCompiler } from 'ruleflow/compiler';
import { evaluate } from 'ruleflow/engine';
import type { Policy } from 'ruleflow/types';
const policy: Policy = {
meta: {
name: 'User Access Policy',
version: '1.0.0',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
rules: [
{
id: 'rule-adult-premium',
name: 'Adult Premium Access',
priority: 10,
conditions: {
all: [
{ field: 'user.age', operator: 'gte', value: 18 },
{ field: 'user.subscription', operator: 'eq', value: 'premium' },
],
},
actions: [{ type: 'flag', target: 'access', payload: { level: 'full' } }],
},
],
};
const compiler = new PolicyCompiler();
const compiled = compiler.compile(policy);
const result = evaluate(compiled, { user: { age: 25, subscription: 'premium' } });
console.log(result.matchedRules); // [{ ruleId: 'rule-adult-premium', ... }]# Validate a policy file
ruleflow validate policy.json
# Run a policy against facts
ruleflow run policy.json facts.json
# Run with trace output
ruleflow run policy.json facts.json --trace
# Diff two policy versions
ruleflow diff policy-v1.json policy-v2.json
# Coverage report
ruleflow coverage policy.json facts.jsonConditions use dot-path field access and a rich set of operators:
const conditions: ConditionGroup = {
all: [
{ field: 'user.age', operator: 'gte', value: 18 },
{
any: [
{ field: 'user.role', operator: 'in', value: ['admin', 'moderator'] },
{ field: 'user.verified', operator: 'eq', value: true },
],
},
{ field: 'user.banned', operator: 'eq', value: false, negate: false },
],
};| Operator | Description | Example |
|---|---|---|
eq |
Strict equality | { field: 'status', operator: 'eq', value: 'active' } |
neq |
Not equal | { field: 'status', operator: 'neq', value: 'banned' } |
gt |
Greater than | { field: 'age', operator: 'gt', value: 18 } |
gte |
Greater than or equal | { field: 'score', operator: 'gte', value: 100 } |
lt |
Less than | { field: 'price', operator: 'lt', value: 50 } |
lte |
Less than or equal | { field: 'qty', operator: 'lte', value: 10 } |
in |
Value in array | { field: 'role', operator: 'in', value: ['a', 'b'] } |
nin |
Value not in array | { field: 'tag', operator: 'nin', value: ['spam'] } |
contains |
String/array contains | { field: 'name', operator: 'contains', value: 'foo' } |
startsWith |
String starts with | { field: 'email', operator: 'startsWith', value: 'admin' } |
endsWith |
String ends with | { field: 'email', operator: 'endsWith', value: '.com' } |
matches |
Regex match | { field: 'id', operator: 'matches', value: '^[A-Z]{3}' } |
between |
Inclusive range | { field: 'age', operator: 'between', value: [18, 65] } |
exists |
Field exists | { field: 'user.profile', operator: 'exists', value: null } |
isNull |
Field is null | { field: 'deletedAt', operator: 'isNull', value: null } |
import { globalRegistry } from 'ruleflow/operators';
globalRegistry.register('divisibleBy', (actual, expected) => {
if (typeof actual !== 'number' || typeof expected !== 'number') return false;
return actual % expected === 0;
});{
"meta": {
"name": "My Policy",
"version": "2.0.0",
"author": "Yash Bavadiya",
"createdAt": "2026-01-15T00:00:00.000Z",
"updatedAt": "2026-05-20T00:00:00.000Z"
},
"rules": [
{
"id": "rule-001",
"name": "High Value Order",
"priority": 100,
"conditions": {
"all": [
{ "field": "order.total", "operator": "gte", "value": 1000 },
{ "field": "order.currency", "operator": "eq", "value": "USD" }
]
},
"actions": [
{ "type": "flag", "target": "review", "payload": { "reason": "high_value" } }
],
"tags": ["finance", "review"],
"enabled": true
}
]
}import { detectConflicts } from 'ruleflow/conflict';
const conflicts = detectConflicts(policy);
// [
// { type: 'contradiction', ruleA: 'rule-001', ruleB: 'rule-002', severity: 'error', ... },
// { type: 'redundancy', ruleA: 'rule-003', ruleB: 'rule-004', severity: 'warning', ... },
// ]import { runBatch } from 'ruleflow/engine';
import { computeCoverage } from 'ruleflow/coverage';
const batchResult = runBatch(compiled, facts);
const coverage = computeCoverage(compiled, batchResult);
// { totalRules: 5, firedRules: 3, coveragePercent: 60, neverFiredRules: ['rule-d', 'rule-e'] }import { diffPolicies } from 'ruleflow/diff';
const diff = diffPolicies(policyV1, policyV2);
// { addedRules: [...], removedRules: [...], changedRules: [...], metaChanges: [...] }Policy JSON
│
▼
policyParser ──► Policy (validated)
│
▼
PolicyCompiler
├── conditionCompiler (builds CompiledConditionGroup)
├── ruleCompiler (validates + compiles each Rule)
└── optimizer (reorders by cost, prunes disabled)
│
▼
CompiledPolicy (cached by SHA-256)
│
▼
evaluator.evaluate(fact)
├── shortCircuit (AND/OR/NONE with early exit)
└── tracer (optional trace recording)
│
▼
EvalResult
MIT © Yash Bavadiya