Rules engine
Try the interactive Rules Engine demo with a musician-themed example, including live execution against sample data.
The @react-querybuilder/rules-engine package augments React Query Builder with rules engine functionality, allowing you to build executable rule sets with conditions and consequences.
What is a Rules Engine?
A rules engine is a system that evaluates conditions and executes corresponding actions (consequences) based on those evaluations. Unlike static query builders that generate query strings, a rules engine creates executable logic that can be processed at runtime.
The rules engine extension transforms react-querybuilder queries into structured rule definitions that can be executed by rules engine libraries like json-rules-engine.
Installation
- npm
- Bun
- Yarn
- pnpm
npm install react-querybuilder @react-querybuilder/rules-engine
bun add react-querybuilder @react-querybuilder/rules-engine
yarn add react-querybuilder @react-querybuilder/rules-engine
pnpm add react-querybuilder @react-querybuilder/rules-engine
Core concepts
Rules engine structure
A rules engine consists of:
- Conditions: Query logic that determines when a rule should fire (if/else if statements, aka antecedents)
- Consequences: Actions to execute when conditions are met (then statements)
- Default Consequence: Fallback action when no conditions match (else statement)
Relationship to QueryBuilder
While the standard QueryBuilder component generates queries for databases or search APIs, RulesEngineBuilder creates executable rule definitions that can be processed by rules engines to trigger actions, validations, or business logic.
Basic usage
The RulesEngineBuilder component provides a specialized interface for creating rules with conditions and consequences:
import { RulesEngineBuilder } from '@react-querybuilder/rules-engine';
function App() {
return <RulesEngineBuilder />;
}
Controlled vs uncontrolled
Using RulesEngineBuilder as an uncontrolled component by setting the defaultRulesEngine prop is the recommended approach. For a controlled component, use the rulesEngine prop.
import { useState } from 'react';
import { RulesEngineBuilder } from '@react-querybuilder/rules-engine';
import type { RulesEngine } from '@react-querybuilder/rules-engine';
function App() {
const [rulesEngine, setRulesEngine] = useState<RulesEngine>({
conditions: [],
defaultConsequent: { type: 'default-action' },
});
return (
<RulesEngineBuilder
// Uncontrolled:
defaultRulesEngine={rulesEngine}
// Controlled:
// rulesEngine={rulesEngine}
onRulesEngineChange={setRulesEngine}
consequentTypes={[
{ name: 'email-alert', label: 'Send Email' },
{ name: 'sms-alert', label: 'Send SMS' },
{ name: 'log-event', label: 'Log Event' },
{ name: 'default-action', label: 'Default Action' },
]}
/>
);
}
State management
The rules engine package uses Redux for internal state management and requires the QueryBuilderStateProvider:
import { QueryBuilderStateProvider } from 'react-querybuilder';
import { RulesEngineBuilder } from '@react-querybuilder/rules-engine';
function App() {
return (
<QueryBuilderStateProvider>
<RulesEngineBuilder />
</QueryBuilderStateProvider>
);
}
The RulesEngineBuilder component automatically wraps itself with QueryBuilderStateProvider, so explicit wrapping is only necessary if you need to share state with other components.
Accessing and updating Redux state
In event handlers, retrieve the current rules engine with props.schema.getRulesEngine() and update the entire rules engine object with props.schema.dispatchRulesEngine(newRulesEngine).
In the render phase, access rules engine state using the useRulesEngineBuilderRulesEngine hook:
import { useRulesEngineBuilderRulesEngine } from '@react-querybuilder/rules-engine';
function MyCustomComponent() {
const rulesEngine = useRulesEngineBuilderRulesEngine();
return (
<div>
<p>Number of conditions: {rulesEngine.conditions?.length ?? 0}</p>
<p>Has default consequent: {!!rulesEngine.defaultConsequent}</p>
</div>
);
}
Export
The formatRulesEngine function converts rules engine objects into formats compatible with external rules engines. Four export formats are available:
json-rules-engine— an array of rules for json-rules-engine, plus supplemental operators.native— a dependency-free, in-process evaluator function.node-rules— live node-rulesRuleobjects.rulepilot— a single rulepilotRule(single-outcome decisioning).
The desired format is passed as the second argument—either a bare string or an options object with a format property. All formats honor the evaluation mode.
json-rules-engine
import { formatRulesEngine } from '@react-querybuilder/rules-engine';
import { Engine } from 'json-rules-engine';
const rulesEngine = {
conditions: [
{
antecedent: {
combinator: 'and',
rules: [
{ field: 'temperature', operator: '>', value: 85 },
{ field: 'humidity', operator: '<', value: 40 },
],
},
consequent: { type: 'alert', message: 'High temperature detected' },
},
],
defaultConsequent: { type: 'monitor', action: 'continue-monitoring' },
};
// Convert to json-rules-engine format
const jsonRules = formatRulesEngine(rulesEngine, 'json-rules-engine');
// Create and run engine
const engine = new Engine(jsonRules);
engine.run({ temperature: 90, humidity: 35 }).then(events => {
events.forEach(event => console.log(event.type, event.params));
});
Additional operators
json-rules-engine ships with a limited set of built-in operators (equal, notEqual, lessThan, lessThanInclusive, greaterThan, greaterThanInclusive, in, notIn, contains, and doesNotContain). Several React Query Builder operators have no direct equivalent, so the export uses a set of supplemental evaluators exported as jsonRulesEngineAdditionalOperators:
| RQB operator(s) | Exported operator | Behavior |
|---|---|---|
beginsWith / doesNotBeginWith | same | String prefix match. |
endsWith / doesNotEndWith | same | String suffix match. |
contains / doesNotContain | containsGeneric / doesNotContainGeneric | String substring match. (The built-in contains requires the fact to be an array, so the generic versions are used instead.) |
between / notBetween | same | Inclusive range test. Bounds may be an array ([lo, hi]) or a comma-separated string ("lo,hi"); numeric bounds are parsed and reordered ascending, mirroring formatQuery. |
These operators must be registered on the Engine before it evaluates rules that use them. The simplest way is to pass the engine as context.engine to formatRulesEngine, which registers every additional operator on it as a side effect:
import { formatRulesEngine } from '@react-querybuilder/rules-engine';
import { Engine } from 'json-rules-engine';
const engine = new Engine();
// Passing `context: { engine }` registers `beginsWith`, `between`, etc. on the engine.
const rules = formatRulesEngine(rulesEngine, {
format: 'json-rules-engine',
context: { engine },
});
for (const rule of rules) {
engine.addRule(rule);
}
engine.run({ name: 'Dr. Strange', age: 42 }).then(({ events }) => {
events.forEach(event => console.log(event.type, event.params));
});
Alternatively, register them yourself (for example, when you need only a subset):
import { jsonRulesEngineAdditionalOperators } from '@react-querybuilder/rules-engine';
for (const [name, evaluator] of Object.entries(jsonRulesEngineAdditionalOperators)) {
engine.addOperator(name, evaluator);
}
native
The native format returns a dependency-free, in-process evaluator function with the signature (facts) => Consequent[]. Given a facts object, it returns the consequents of every condition that fires, in order, honoring the evaluation mode. This is the simplest way to run a rules engine—no external library, no async, and no operator registration.
import { formatRulesEngine } from '@react-querybuilder/rules-engine';
const evaluate = formatRulesEngine(rulesEngine, 'native');
const firedConsequents = evaluate({ temperature: 90, humidity: 35 });
// => [{ type: 'alert', message: 'High temperature detected' }]
node-rules
The node-rules format returns an array of live node-rules Rule objects—complete with condition/consequence functions—ready to pass to a RuleEngine. Antecedents are compiled to the same predicates used by the native format, so evaluation needs no operator registration. Each fired condition's consequent is pushed onto fact.events, mirroring the json-rules-engine result shape.
import { formatRulesEngine } from '@react-querybuilder/rules-engine';
import { RuleEngine } from 'node-rules';
const R = new RuleEngine(formatRulesEngine(rulesEngine, 'node-rules'));
R.execute({ temperature: 90, humidity: 35 }, ({ events }) => {
events.forEach(event => console.log(event.type, event.message));
});
rulepilot
The rulepilot format returns a single rulepilot Rule whose ordered conditions reproduce the rules engine's cascade (each condition pairs a guard with its consequent as result). Because RulePilot.evaluate returns a single result—the first matching condition's—this format models single-outcome decisioning.
import { formatRulesEngine } from '@react-querybuilder/rules-engine';
import { RulePilot } from 'rulepilot';
const rule = formatRulesEngine(rulesEngine, 'rulepilot');
// Pass `true` (trustRule) to skip pre-validation of the always-true guards.
const result = await RulePilot.evaluate(rule, { temperature: 90, humidity: 35 }, true);
// => { type: 'alert', message: 'High temperature detected' }
The rulepilot format has two limitations relative to the other targets:
- Single result. A rules engine whose nested or overlapping conditions would fire multiple consequents under the other formats yields only the first match. Cumulative evaluation mode therefore has no rulepilot representation and throws.
- No substring operators. The
contains,beginsWith, andendsWithoperators (and their negations) have no faithful rulepilot mapping and are omitted from the exported rule.
Guards built from empty antecedents (and cascade defaults) compile to an always-true { all: [] }, which rulepilot's validator rejects; evaluate such rules with RulePilot.evaluate(rule, criteria, true) to skip pre-validation.
Evaluation modes
Sibling conditions can be exported with one of two evaluation modes, stored as evaluationMode on the rules engine object (defaults to "cascade"):
"cascade"(default): conditions are evaluated in order, like anif/else-if/elsechain. A later sibling only fires when all prior siblings' antecedents failed. This is achieved by AND-ing each exported rule with the negated antecedents of its prior siblings."cumulative": every condition is evaluated independently, so any number of them may fire. (Therulepilotformat returns a single result and therefore does not support this mode—it throws if requested.)
In both modes, nested conditions are guarded by their ancestor antecedents (structural nesting). A defaultConsequent behaves differently per mode: in "cascade" it is the else branch, firing only when every sibling antecedent at its level fails; in "cumulative" it is an always-true baseline that fires on every run (still guarded by ancestor antecedents at nested levels).
The builder UI also relabels condition blocks to match the active mode: in "cascade" mode conditions read If/Else If and the default consequent reads Else; in "cumulative" mode every condition reads When and the default consequent reads Always. These labels are customizable via the blockLabelWhen and blockLabelAlways translations.
// Read from the rules engine object (defaults to "cascade")
formatRulesEngine({ ...rulesEngine, evaluationMode: 'cumulative' }, 'json-rules-engine');
// Or override per-call (takes precedence over the object's value)
formatRulesEngine(rulesEngine, { format: 'json-rules-engine', evaluationMode: 'cumulative' });
Earlier versions emitted every condition independently (equivalent to "cumulative") and always emitted the defaultConsequent as an always-true rule. The default is now "cascade": overlapping conditions short-circuit, nested conditions are emitted, and the defaultConsequent becomes the else branch. To keep the always-true defaultConsequent behavior, export with "cumulative" mode.
Custom export formats
Extend export functionality with custom processors:
import { formatRulesEngine } from '@react-querybuilder/rules-engine';
import type { RulesEngineProcessor } from '@react-querybuilder/rules-engine';
const customProcessor: RulesEngineProcessor<MyCustomFormat> = rulesEngine => ({
// Transform to your custom format
rules: rulesEngine.conditions.map(condition => ({
when: condition.antecedent,
then: condition.consequent,
})),
fallback: rulesEngine.defaultConsequent,
});
const customFormat = formatRulesEngine(rulesEngine, {
rulesEngineProcessor: customProcessor,
});
Component customization
Default components
The rules engine includes specialized components for rules engine functionality:
import { RulesEngineBuilder } from '@react-querybuilder/rules-engine';
import type { ComponentsRE } from '@react-querybuilder/rules-engine';
const customComponents: Partial<ComponentsRE> = {
consequentSelector: ({ options, value, handleOnChange }) => (
<select value={value} onChange={e => handleOnChange(e.target.value)}>
{options.map(opt => (
<option key={opt.name} value={opt.name}>
{opt.label}
</option>
))}
</select>
),
};
function App() {
return <RulesEngineBuilder components={customComponents} />;
}
QueryBuilder integration
Pass standard QueryBuilder props for condition editing:
import { RulesEngineBuilder } from '@react-querybuilder/rules-engine';
function App() {
return (
<RulesEngineBuilder
queryBuilderProps={{
fields: [
{ name: 'temperature', label: 'Temperature', datatype: 'number' },
{ name: 'humidity', label: 'Humidity', datatype: 'number' },
{ name: 'location', label: 'Location', datatype: 'text' },
],
operators: [
{ name: '>', label: 'greater than' },
{ name: '<', label: 'less than' },
{ name: '=', label: 'equals' },
],
}}
/>
);
}
Configuration
Consequent types
Define available consequence types for rules:
<RulesEngineBuilder
consequentTypes={[
{ name: 'email', label: 'Send Email' },
{ name: 'webhook', label: 'Call Webhook' },
{ name: 'database', label: 'Update Database' },
]}
autoSelectConsequentType={true}
/>
Consequent properties
Attach a properties array to any consequent type and the built-in consequent body will render an editable input for each property—no custom consequentBuilderBody component required. Property values are stored on the consequent under params[name].
<RulesEngineBuilder
consequentTypes={[
{
name: 'email',
label: 'Send Email',
properties: [
{ name: 'to', label: 'To' },
{ name: 'subject', label: 'Subject' },
{ name: 'body', label: 'Body', inputType: 'textarea' },
{
name: 'priority',
label: 'Priority',
inputType: 'select',
defaultValue: 'normal',
values: [
{ name: 'low', label: 'Low' },
{ name: 'normal', label: 'Normal' },
{ name: 'high', label: 'High' },
],
},
],
},
]}
/>
Each property's inputType may be 'text' (default), 'textarea', 'number', 'checkbox', or 'select'. A defaultValue is seeded into params[name] when the type is first selected, and 'select' inputs render the options listed in values. Selecting a different consequent type reseeds params from the new type's defaultValues, or clears the managed params when the new type defines no properties.
Nested conditions
Disallow nested condition logic:
<RulesEngineBuilder allowNestedConditions={false} />
Default consequents
Disallow "else" blocks:
<RulesEngineBuilder allowDefaultConsequents={false} />
Shifting conditions
Set showShiftActions to render up/down buttons in each condition header, letting users reorder sibling conditions. Buttons are automatically disabled at the first and last position within a sibling group.
<RulesEngineBuilder showShiftActions={true} />
The button titles and labels are customizable via the shiftActionUp and shiftActionDown translations. To intercept, cancel, or replace a shift, use the onMoveCondition handler.
Custom Translations
Customize UI labels:
import type { TranslationsRE } from '@react-querybuilder/rules-engine';
const customTranslations: Partial<TranslationsRE> = {
blockLabelIf: { label: 'When' },
blockLabelElseIf: { label: 'Otherwise when' },
blockLabelElse: { label: 'Otherwise' },
blockLabelThen: { label: 'Do' },
addCondition: { label: 'Add condition' },
addConsequent: { label: 'Add action' },
};
<RulesEngineBuilder translations={customTranslations} />;
Styling
Standard classes
RulesEngineBuilder adds specific CSS classes for styling (unless suppressStandardClassnames is set):
.rulesEngineBuilder {}
.rulesEngineBuilder-header {}
.rulesEngineBuilder-body {}
.rulesEngineBuilder-evaluationMode {}
.consequentBuilder {}
.consequentBuilder-header {}
.consequentBuilder-body {}
.consequentBuilder-standalone {}
.conditionBuilder {}
.conditionBuilder-header {}
.conditionBuilder-body {}
.shiftActions {}
.blockLabel {}
.blockLabel-if {}
.blockLabel-ifelse {}
.blockLabel-else {}
.blockLabel-then {}
.blockLabel-when {}
.blockLabel-always {}
Custom classes
import type { ClassnamesRE } from '@react-querybuilder/rules-engine';
const customClassnames: Partial<ClassnamesRE> = {
rulesEngineBuilder: 'my-rules-engine',
blockLabelIf: 'my-if-label',
blockLabelThen: 'my-then-label',
consequentBuilder: 'my-consequent-section',
};
<RulesEngineBuilder classnames={customClassnames} />;
TypeScript support
Core types
import type {
RulesEngine,
RulesEngineAny,
RECondition,
Consequent,
ConsequentTypeOption,
FormatRulesEngineOptions,
} from '@react-querybuilder/rules-engine';
// Basic rules engine structure
const rulesEngine: RulesEngine = {
conditions: [
{
antecedent: { combinator: 'and', rules: [] },
consequent: { type: 'action', data: {} },
},
],
defaultConsequent: { type: 'default' },
};
// Consequent type options for the `consequentTypes` prop
const consequentTypes: ConsequentTypeOption[] = [
{ name: 'email', label: 'Send Email', properties: [{ name: 'to', label: 'To' }] },
];
// Custom consequent type
interface MyConsequent extends Consequent {
type: 'email' | 'sms' | 'webhook';
recipient: string;
message: string;
}
Generic usage
import type { RuleType } from 'react-querybuilder';
import type { RulesEngine } from '@react-querybuilder/rules-engine';
interface CustomRule extends RuleType {
metadata?: Record<string, unknown>;
}
type CustomRulesEngine = RulesEngine<CustomRule, 'and' | 'or' | 'xor'>;
Miscellaneous features
Path utilities
Manipulate rules engine structure programmatically:
import {
regenerateREIDs,
prepareRulesEngine,
isRulesEngine,
} from '@react-querybuilder/rules-engine';
// Regenerate IDs for all elements
const withNewIds = regenerateREIDs(rulesEngine);
// Recursively add `id` properties (only if necessary)
const preparedRE = prepareRulesEngine(partialRulesEngine, {
idGenerator: () => `${Math.random()}`, // override default of `crypto.randomUUID()`
});
// Type checking
if (isRulesEngine(unknownObject)) {
// Safe to use as rules engine
}
Event handlers
<RulesEngineBuilder
onAddCondition={(condition, parentPath, rulesEngine) => {
console.log('Condition added:', condition);
return condition; // Return modified condition or true to proceed
}}
onRemoveCondition={(condition, path, rulesEngine) => {
console.log('Condition removed:', condition);
return true; // Return true to proceed with removal
}}
onMoveCondition={(condition, fromPath, direction, rulesEngine, nextRulesEngine) => {
console.log('Condition moved:', direction);
// Return true to apply the default move, false to cancel,
// or a new rules engine object to replace the next state.
return true;
}}
/>
Migration and compatibility
Version compatibility
react:>=18react-querybuilder: must matchjson-rules-engine:>=7(optional)
Future stability
As this package is in early development, expect:
- Breaking changes in minor versions
- API evolution based on community feedback
- Additional export format support
- Enhanced TypeScript definitions
Keep your implementation flexible and monitor release notes for migration guides as the package matures.
API reference
For detailed API documentation, see the TypeScript definitions and source code.