Business Rules Engine
A framework-agnostic, plugin-based rules engine. The AST is the single source of truth: the builder edits it, the executor walks it, the renderer describes it, the debugger explains it. No paid APIs, no cloud services, no third-party engine underneath.
- Packages
- 11
- Operators
- 27
- Functions
- 23
- Rule kinds
- 7
Imports from ADYSRE AST, jsonLogic, json-rules-engine, MongoDB query filters.
What it does
A JSON tree
Rules are plain JSON. Storable, diffable, versionable, and readable by anything.
Everything is a plugin
Operators, functions, actions, fields, storage, renderers. Add one without touching the engine.
Runs anywhere
Synchronous, pure, zero dependencies. The same rule evaluates on a server, in a browser or in a test.
Says what it means
Every rule renders as a sentence, generated from the tree and never parsed back into it.
Shows its reasoning
Which row decided the verdict, what the operator received, and what short-circuiting hid.
Edits itself
A visual builder with undo, per-row validation and a live preview, driven entirely by the plugin metadata.
Takes what you have
Converts other rule formats, warning where an equivalent is near rather than exact.
Keeps its history
Versioning, restore and querying behind one contract every adapter is checked against.
Try it
Edit any condition. The sentence, the verdict and the explanation follow as you type.
A nested group, a date function, and two outcomes on one rule.
When
Then
Otherwise
In words
MatchedLarge orders from new customers need approval
Why this answer
Why this answer
MatchedTook 1msevery condition contributed, so no single one decided
1 branches never ran
- Matchedall of these are true0.85ms
- Matched
greaterThan0.49msThe operator received: 2400, 1000
- Matchedany of these are true0.20ms
- Matched
equals0.12msThe operator received: "new", "new"
- Never ran
before
The rule, as JSON
Using the Business Rules Engine
Everything you need to add rules to a project: install, write one, run it, explain it, extend it, and let people edit it. Every snippet below runs as written.
- 01
Install
Take only what you use. The engine and its vocabulary have zero runtime dependencies; the builder is a separate package because a server that only evaluates rules should not ship a React editor.
terminal# the engine and its vocabulary pnpm add @adysre/rules-core @adysre/rules-types # the visual builder, when you want one pnpm add @adysre/rules-ui @adysre/rules-react @adysre/rules-theme
- 02
Write a rule
A rule is plain JSON. The builders handle ids and timestamps, but nothing stops you writing the object by hand, storing it, diffing it in review or generating it from somewhere else.
rules/large-orders.tsimport { all, condition, field, literal, rule } from '@adysre/rules-core'; // A rule is plain JSON. The builders exist so ids and timestamps are handled // for you, but nothing stops you writing the object by hand. const largeOrders = rule({ name: 'Large orders from new customers need approval', kind: 'validation', when: all([ condition({ left: field('order.total'), operator: 'greaterThan', args: [literal(1000)], }), ]), then: [{ id: 'a_hold', type: 'requireApproval' }], }); - 03
Run it
Synchronous and pure. The registry starts empty because what rules may do is your choice; pass the built-ins for the usual set. The result carries the verdict, the actions and the trace.
rules/run.tsimport { builtinPlugins, createContext, createRegistry, evaluateRule } from '@adysre/rules-core'; // The registry starts EMPTY: what rules may do is your choice. Pass the // built-ins for the usual twenty-seven operators and twenty-three functions. const registry = createRegistry(builtinPlugins); const outcome = evaluateRule(registry, largeOrders, createContext(order)); outcome.verdict; // 'matched' | 'unmatched' | 'skipped' | 'errored' outcome.actions; // what to do — the engine never does it for you outcome.trace; // every node that ran, and what it concluded - 04
Read it back
Every rule renders as a sentence, generated from the tree and never parsed back into it. Structure comes first, so a UI can highlight the field being edited rather than reformatting prose.
rules/explain.tsimport { describeRule } from '@adysre/rules-renderer'; describeRule(largeOrders, { plugins: registry }).text; // When order total is greater than 1,000 // Then require approval // Structure first, text second: `.lines` gives typed segments, so a UI can // highlight the field being edited or colour the condition that decided it. - 05
Add your own operator
Everything the engine can do is a plugin. Register one and the builder offers it, the debugger explains it, and your rules stay plain JSON. An operator that cannot answer throws rather than returning false.
rules/registry.tsimport { createRegistry, builtinPlugins, RuleError } from '@adysre/rules-core'; import type { OperatorPlugin } from '@adysre/rules-types'; // Your own operator. The builder picks it up, the debugger explains it, and // your rules stay plain JSON — no change to the engine. const withinBusinessHours: OperatorPlugin = { id: 'withinBusinessHours', labelKey: 'operators.withinBusinessHours', arity: 0, accepts: ['date'], evaluate: (left) => { if (typeof left !== 'string') { // Not `false`: a broken comparison must not look like a failed one. throw new RuleError('type_mismatch', 'withinBusinessHours needs a date.'); } const hour = new Date(left).getHours(); return hour >= 9 && hour < 17; }, }; const registry = createRegistry(builtinPlugins, { operators: [withinBusinessHours] }); - 06
Let people edit it
The visual builder is driven entirely by the plugin metadata: arity decides how many value boxes appear, accepts decides which operators a field is offered. Pass sample data and it runs the rule as you type.
app/editor.tsx'use client'; import { RuleBuilder } from '@adysre/rules-ui'; export function Editor({ rule, onChange }) { return ( <RuleBuilder rule={rule} registry={registry} sample={sampleOrder} // runs the rule as you type onChange={onChange} /> ); } - 07
Find out why
The debugger names the row that decided the verdict, and runs the rule a second time with short-circuiting off to surface faults the fast path stepped over.
rules/debug.tsimport { debugRule } from '@adysre/rules-devtools'; const session = debugRule(registry, largeOrders, createContext(order)); session.decision; // which row decided, or that several did session.comparison.hiddenErrors; // faults a short circuit stepped over - 08
Keep it
One storage contract with versioning and restore. A restore writes a new version rather than rewinding, and any adapter you write can be checked against the same suite the built-in ones pass.
rules/storage.tsimport { createMemoryStorage, runStorageConformance } from '@adysre/rules-storage'; const storage = createMemoryStorage(); await storage.save(largeOrders); // version 1 await storage.restore(largeOrders.id, 1); // a NEW version holding version 1 // Writing your own adapter? Check it against the same contract: const results = await runStorageConformance(() => createMyAdapter());
The packages
Each one lands with the phase that gives it something to do. Install only what you use.
- The vocabulary
@adysre/rules-typesType vocabulary for the ADYSRE Business Rules Engine: the rule AST, execution contracts and plugin interfaces.
v0.1.0 - The engine
@adysre/rules-coreFramework-agnostic core of the ADYSRE Business Rules Engine: AST builders, traversal and validation. No runtime dependencies.
v0.1.0 - Importers
@adysre/rules-parserImports rules into the ADYSRE AST from JSON, jsonLogic, json-rules-engine and MongoDB-style queries.
v0.1.0 - Natural language
@adysre/rules-rendererTurns an ADYSRE rule AST into readable language. Generated from the tree, never parsed back into it.
v0.1.0 - Persistence
@adysre/rules-storageWhere rules live: the storage contract, versioning, querying, and a reference adapter every other one has to match.
v0.1.0 - Builder state
@adysre/rules-reactHeadless state for a rule builder: an immutable store with history, plus the React bindings.
v0.1.0 - The visual builder
@adysre/rules-uiThe visual rule builder: React components over @adysre/rules-react, styled with design tokens.
v0.1.0 - The debugger
@adysre/rules-devtoolsThe execution debugger: why a rule answered what it answered, and what short-circuiting hid.
v0.1.0 - Over HTTP
@adysre/rules-nextRules over HTTP: route handlers and server actions on the Web standard, so they run in Next.js and anywhere else.
v0.1.0 - Design tokens
@adysre/rules-themeDesign tokens for the rule builder, and a contrast audit that proves a theme is readable.
v0.1.0 - Sandbox and examples
@adysre/rules-playgroundA runnable sandbox and the worked examples, each verified against the engine that runs it.
v0.1.0
Decisions worth knowing
The AST is the only source of truth
Everything else is a projection of it. Prose is generated from the tree and never parsed back, so the two can never disagree.
An error is not a false
A condition that cannot be evaluated is errored, and so is the rule. A broken comparison never looks like a failed one.
Actions are intent, never performed
The engine reports what should happen and the host decides. That is what lets a rule be run twice, in a preview, or in a test.
The registry cannot be mutated
Extending it returns a new one, so two tenants can hold different capabilities without one leaking into the other.
Every answer carries its reasoning
Each node records what it saw and what it concluded, so somebody can be shown the condition that decided it.
No language to learn
There is no text format to hand-author, so there is no second system to keep in step with the tree.