Decision Node Editor
The Decision Node Editor enables admins to compose boolean logic expressions using a visual query builder. Admins define conditions (e.g., "user age is more than 18 AND country equals USA"), save them as jsonLogic, and test them in a preview mode before deploying to workflows. Built on React Query Builder (Material UI skin) with client-side evaluation via json-logic-js.
Overview
When building a workflow, admins can add a Decision node that branches based on a true/false condition. The Decision Node Editor is the modal interface where they compose that logic. It provides a natural-language query builder with type-aware operators, a catalog of available variables, and a preview mode to test the logic with sample data.
Defining branching logic in a workflow Decision node
JsonLogic expression with a required title
Build rules, preview with test data, save or cancel
Interactive Preview
Click the Decision node to open the editor. Use the mock variables below to build a rule, then preview the result.
Decision
New Decision
Variables available in this preview: user.name, user.email, user.age, user.dob, user.is_active, user.countries, application.id, application.created_at, application.status, profile.is_verified
Setup
The editor depends on four packages and requires two CSS imports.
react-querybuilder
Core query builder@react-querybuilder/material
Material UI skinreact-querybuilder/parseJsonLogic
Load existing logic into builder statejson-logic-js
Client-side rule evaluation in preview modeBoth imports are required. The base stylesheet comes from the library; the override sheet applies layout and token fixes.
import "react-querybuilder/dist/query-builder.css"; import "@/styles/queryBuilder.css";
Operator Reference
Operators are presented in natural language and are constrained by the selected variable's inferred type. Below are the operator sets available for each type. Operators are mapped internally to jsonLogic.
equals
===mapped to jsonLogicExact numeric matchmore than
>mapped to jsonLogicGreater thanat least
>=mapped to jsonLogicGreater than or equalless than
<mapped to jsonLogicLess thanat most
<=mapped to jsonLogicLess than or equaldoesn't equal
!==mapped to jsonLogicNot equalNote:Operators are deliberately natural-language (e.g., "more than" not ">") to reduce ambiguity and improve clarity for non-technical admins. Regex, startsWith, and endsWith are intentionally excluded from the initial set; they may be added later if needed.
All Types operators (is present / is missing) are defined in fixture data but are not wired into the component's OPERATORS map. They are not available in the current build.
Variable Catalog
Variables are provided by the Variables Manager as a flat list (no nested paths). They are displayed with their key and an optional tooltip showing the sample value. Type is inferred from the value when present.
user.name
"John Smith"user.email
"john@email.com"user.age
28user.dob
"1996-05-15"user.is_active
trueSource
Variables Managerflat list, no nested pathsDisplay
key label, optional value tooltipGrouping
optional: by type or alphabeticalEmpty state
Clear message encouraging use of constantsType inference
automatic from value if presentType Handling
Type is inferred automatically from a variable's sample value. The inferred type determines which operators are available. Below are the inference rules.
value is an array
arrayinferred type["USA", "Canada"]value is a number
numberinferred type28value is true or false
booleaninferred typetruevalue parses as ISO 8601 or epoch
datetimeinferred type"2024-01-15T10:30:00Z"otherwise
stringinferred type"John Smith"no value provided
string (default)inferred typeundefined → stringDatetime
Text input (no date picker yet). Values should be epoch ms integers for consistent evaluation. ISO 8601 strings are inferred as datetime but stored as-is.
Numbers
Standard numeric input. Operators update to numeric set (more than, at least, etc.).
Strings
Text input. Operators limited to equals, doesn't equal, contains.
Arrays
Admin supplies a single value to check; operators become 'includes the value' / 'doesn't include the value'.
Booleans
Radio group or toggle. Operators: is true / is false.
Query Builder UI
The core interface for composing rules. React Query Builder (Material UI skin) provides drag-and-drop rule composition, automatic type constraints, and grouping with AND/OR/NOT.
Title field
Required. Admin names the decision logic.
Rule composition
Drag variables, select operators, enter constants.
Grouping
AND/OR to combine rules; NOT to invert group result. Enables complex nested logic.
Type constraints
UI hides unsupported operators per type.
Constants editor
Type-aware; respects datetime picker, number input, etc.
Save button
Disabled until dirty; no structural validationDirty state
Set on title change or query change; reset on openCancel action
Calls onClose() directly — no built-in confirmationOperator filtering
OPERATORS map per type; unknown falls back to equals / doesn't equalInput type
number for numeric variables; text for all othersDecision Node Editor Modal
When an admin selects a Decision node on the Workflow Editor canvas, the full Decision Node Editor modal opens instead of a properties panel. This dedicated modal provides the rule builder interface in a focused, full-width dialog optimized for complex rule configuration.
Width
860pxallows two-column preview layoutBorder radius
10pxShadow
tokens.shadow.modalTrigger
Click Decision node on canvasClose behavior
Cancel or × calls onClose() — no built-in dirty guardSave
Disabled until dirty; calls onSave({ title, jsonLogic })Header (build)
Icon (36px, primaryLight bg) + title + close ×Header (preview)
← Back button + divider + "Preview: {title}" + close ×Decision title
Required TextField (no custom focus styling)Rule builder card
"Rules" overline, QueryBuilder, Preview button belowFooter (build)
Delete node (left) | Cancel, Save (right)Footer (preview)
Close preview (right only)Rule Builder Card
The Card-style container holds rules and combinator controls:
- Combinator row: AND/OR Select + Not Switch + "+ Rule" (contained primary) + "+ Group" (outlined)
- Not switch: Inverts the result of the group. When enabled, negates the entire group's boolean output. Example: if a group evaluates to
true, NOT makes itfalse. In jsonLogic, this wraps the group in anotoperator. - Rule rows: Field Select (minWidth 130px) + Operator Select (minWidth 120px, flex 0.3) + Value TextField (flex 1) + Delete ×. Rows have a full 1px border, not a left-only accent.
- Preview button: Outlined button below the rule builder card; opens preview mode without closing the modal.
Modal interaction
- Modal opens when Decision node is clicked
- Save button disabled until form is dirty
- Preview button (below rule builder) switches to preview mode — header changes to "← Back | Preview: {title}", footer shows "Close preview" only
- Back button or "Close preview" returns to build mode without losing changes
- Cancel closes without saving; the component calls onClose() directly — any dirty confirmation must be handled by the parent
- Delete node removes the node from the workflow
Component API
DecisionNodeEditor is a self-contained dialog. All internal state resets when the dialog opens (keyed on node.id).
open
booleanrequiredControls dialog visibilityonClose
() => voidrequiredCalled by Cancel and × — no dirty guard built inonSave
({ title, jsonLogic }) => voidrequiredCalled on Save; title is a string, jsonLogic is the formatted outputonDelete
() => voidoptionalWhen omitted the Delete node button is not renderednode
{ id, type, label, jsonLogic }requiredCurrent node data. jsonLogic is parsed back to builder state via parseJsonLogic() on openvariables
Array<{ key, value, type }>requiredFlat list. type must be one of: string | number | boolean | datetime | array | unknown<DecisionNodeEditor
open={selectedNode?.type === "Decision"}
onClose={() => setSelectedNodeId(null)}
onSave={({ title, jsonLogic }) => {
updateNode(selectedNode.id, { label: title, jsonLogic });
setSelectedNodeId(null);
}}
onDelete={() => {
removeNode(selectedNode.id);
setSelectedNodeId(null);
}}
node={selectedNode}
variables={variables}
/>JsonLogic & Validation
The builder exports logic as jsonLogic format. On save, the editor validates that the expression is boolean-producing. Existing jsonLogic can be imported back into the builder via parseJsonLogic().
Flat-to-nested conversion: Variables use dot-notation keys (e.g., user.age). Before evaluation, the component converts these to nested objects — { user: { age: 25 } } — via an internal flatToNested() helper. Any integration that evaluates jsonLogic outside the component must replicate this step, otherwise variable lookups will return undefined.
Internal builder state
↓
jsonLogic export (on save)
↓
Persisted to Decision node
↓
On load: parseJsonLogic() → builder state
Boolean output
Expression must evaluate to true or falseSupported operators
Only jsonLogic-mapped operators allowedGroups
AND, OR, NOT must produce booleanDatetime format
Must parse and normalize to epoch msEmpty builder
Save disabled; shows helper textPreview Mode
Admins can toggle to Preview mode to test their logic with sample data. Input fields are auto-generated from variables and pre-filled with their sample values. Evaluation is live and responsive.
Type coercion: All test inputs are stored as strings (text fields). Before evaluation, inputs are coerced to their variable type: numbers via parseFloat(), booleans by checking === "true", arrays by splitting on comma and trimming whitespace. Datetime variables are not coerced — pass epoch ms as a string and the jsonLogic comparison will handle it numerically.
Left column
Test values — one editable field per variable used in the current query (not all variables); pre-filled from variable.value as a stringRight column
Rule summary — field and operator as plain text, test value as Chip; hover on a Chip or a test value field cross-highlights bothResult
Below rule summary; True (green) or False (red) indicator with colored dot and labelNo rules in query
"Add rules to test values." in left columnEvaluation fails
"Unable to evaluate — add rules and test values." — shown when jsonLogic.apply() returns non-booleanTrue
Logic evaluates to true
False
Logic evaluates to false
Error States
Target error states — these are not yet implemented in the component. The current build only disables Save when the form is clean. These states represent what validation should enforce when rule structure checking is added.
Non-boolean expression
Decision must evaluate to true/false. Adjust your rules.
Unsupported operator
Operator not supported. Choose a different one.
Invalid datetime
Enter a valid date/time.
Empty builder
Add at least one rule before saving.
Non-boolean
Inline error + Save disabledUnsupported operator
Operator hidden or disabled + Save disabledInvalid datetime
Field highlighted + helper textEmpty builder
Helper text + Save disabledConstraints & Notes
Important limitations and considerations for the Decision Node Editor.
Platform
Desktop-first, no mobile requirementAccess
Admins and super-admins onlyVariables
Flat list from Variables Manager, no nested pathsEvaluation
Client-side only via json-logic-jsType inference
Automatic from variable value; no manual overrideTimezone handling
Datetimes normalized to epoch ms; display in local timeServer parity
Future server evaluation must use same jsonLogic libraryOut of scope
DB persistence, API endpoints, versioning, autosave, draftsCode
{
"title": "Age requirement",
"jsonLogic": {
">": [
{ "var": "user.age" },
18
]
}
}{
"title": "Eligible student",
"jsonLogic": {
"and": [
{
">": [
{ "var": "user.age" },
18
]
},
{
"in": [
{ "var": "user.country" },
["USA", "Canada"]
]
}
]
}
}{
"title": "Not eligible",
"jsonLogic": {
"not": {
"and": [
{
">=": [
{ "var": "user.age" },
18
]
},
{
"in": [
{ "var": "user.country" },
["USA", "Canada"]
]
}
]
}
}
}{
"title": "Recent applicant",
"jsonLogic": {
"and": [
{
">=": [
{ "var": "application.created_at" },
1704067200000
]
},
{
"<=": [
{ "var": "application.created_at" },
1735689600000
]
}
]
}
}// The variables prop must include a type field.
// Type is inferred by the Variables Manager before passing to the component.
const variables = [
{ key: "user.name", value: "John Smith", type: "string" },
{ key: "user.age", value: 28, type: "number" },
{ key: "user.is_active", value: true, type: "boolean" },
{ key: "user.dob", value: "1996-05-15", type: "datetime" },
{ key: "user.countries", value: ["USA", "CA"], type: "array" },
{ key: "profile.status", value: null, type: "unknown" },
];import jsonLogic from 'json-logic-js';
// Variables use dot-notation keys — must convert to nested objects first
function flatToNested(flat) {
const result = {};
for (const [key, value] of Object.entries(flat)) {
const parts = key.split(".");
let obj = result;
for (let i = 0; i < parts.length - 1; i++) {
if (!obj[parts[i]]) obj[parts[i]] = {};
obj = obj[parts[i]];
}
obj[parts[parts.length - 1]] = value;
}
return result;
}
const logic = { ">": [{ "var": "user.age" }, 18] };
const flatInputs = { "user.age": 25 };
const result = jsonLogic.apply(logic, flatToNested(flatInputs));
// result: true — flatToNested converts { "user.age": 25 } to { user: { age: 25 } }