Skip to main content
Version: v7 / v8

Rules engine

Live demo

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 install 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>
);
}
info

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:

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 operatorBehavior
beginsWith / doesNotBeginWithsameString prefix match.
endsWith / doesNotEndWithsameString suffix match.
contains / doesNotContaincontainsGeneric / doesNotContainGenericString substring match. (The built-in contains requires the fact to be an array, so the generic versions are used instead.)
between / notBetweensameInclusive 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' }
caution

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, and endsWith operators (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 an if/else-if/else chain. 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. (The rulepilot format 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' });
note

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: >=18
  • react-querybuilder: must match
  • json-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.