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.

When to use

Defining branching logic in a workflow Decision node

Outputs

JsonLogic expression with a required title

Key interaction

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.

Dependencies
Property
Value

react-querybuilder

Core query builder

@react-querybuilder/material

Material UI skin

react-querybuilder/parseJsonLogic

Load existing logic into builder state

json-logic-js

Client-side rule evaluation in preview mode
Required CSS imports

Both 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.

Property
Value

equals

===mapped to jsonLogic
·Exact numeric match

more than

>mapped to jsonLogic
·Greater than

at least

>=mapped to jsonLogic
·Greater than or equal

less than

<mapped to jsonLogic
·Less than

at most

<=mapped to jsonLogic
·Less than or equal

doesn't equal

!==mapped to jsonLogic
·Not equal

Note: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.

Variable list example

user.name

"John Smith"

user.email

"john@email.com"

user.age

28

user.dob

"1996-05-15"

user.is_active

true
Specification
Property
Value

Source

Variables Managerflat list, no nested paths

Display

key label, optional value tooltip

Grouping

optional: by type or alphabetical

Empty state

Clear message encouraging use of constants

Type inference

automatic from value if present

Type Handling

Type is inferred automatically from a variable's sample value. The inferred type determines which operators are available. Below are the inference rules.

Inference rules
Property
Value

value is an array

arrayinferred type
·["USA", "Canada"]

value is a number

numberinferred type
·28

value is true or false

booleaninferred type
·true

value parses as ISO 8601 or epoch

datetimeinferred type
·"2024-01-15T10:30:00Z"

otherwise

stringinferred type
·"John Smith"

no value provided

string (default)inferred type
·undefined → string
Type-specific behavior

Datetime

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.

Key components

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.

Behavior
Property
Value

Save button

Disabled until dirty; no structural validation

Dirty state

Set on title change or query change; reset on open

Cancel action

Calls onClose() directly — no built-in confirmation

Operator filtering

OPERATORS map per type; unknown falls back to equals / doesn't equal

Input type

number for numeric variables; text for all others

Decision 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.

Modal specifications
Property
Value

Width

860pxallows two-column preview layout

Border radius

10px

Shadow

tokens.shadow.modal

Trigger

Click Decision node on canvas

Close behavior

Cancel or × calls onClose() — no built-in dirty guard

Save

Disabled until dirty; calls onSave({ title, jsonLogic })
Sections
Property
Value

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 below

Footer (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 it false. In jsonLogic, this wraps the group in a not operator.
  • 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).

Property
Value

open

booleanrequired
·Controls dialog visibility

onClose

() => voidrequired
·Called by Cancel and × — no dirty guard built in

onSave

({ title, jsonLogic }) => voidrequired
·Called on Save; title is a string, jsonLogic is the formatted output

onDelete

() => voidoptional
·When omitted the Delete node button is not rendered

node

{ id, type, label, jsonLogic }required
·Current node data. jsonLogic is parsed back to builder state via parseJsonLogic() on open

variables

Array<{ key, value, type }>required
·Flat list. type must be one of: string | number | boolean | datetime | array | unknown
Integration example
<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.

Conversion flow

Internal builder state

jsonLogic export (on save)

Persisted to Decision node

On load: parseJsonLogic() → builder state

Validation rules
Property
Value

Boolean output

Expression must evaluate to true or false

Supported operators

Only jsonLogic-mapped operators allowed

Groups

AND, OR, NOT must produce boolean

Datetime format

Must parse and normalize to epoch ms

Empty builder

Save disabled; shows helper text

Preview 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.

Layout
Property
Value

Left column

Test values — one editable field per variable used in the current query (not all variables); pre-filled from variable.value as a string

Right column

Rule summary — field and operator as plain text, test value as Chip; hover on a Chip or a test value field cross-highlights both

Result

Below rule summary; True (green) or False (red) indicator with colored dot and label

No rules in query

"Add rules to test values." in left column

Evaluation fails

"Unable to evaluate — add rules and test values." — shown when jsonLogic.apply() returns non-boolean
Result indicators

True

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.

Property
Value

Non-boolean

Inline error + Save disabled

Unsupported operator

Operator hidden or disabled + Save disabled

Invalid datetime

Field highlighted + helper text

Empty builder

Helper text + Save disabled

Constraints & Notes

Important limitations and considerations for the Decision Node Editor.

Property
Value

Platform

Desktop-first, no mobile requirement

Access

Admins and super-admins only

Variables

Flat list from Variables Manager, no nested paths

Evaluation

Client-side only via json-logic-js

Type inference

Automatic from variable value; no manual override

Timezone handling

Datetimes normalized to epoch ms; display in local time

Server parity

Future server evaluation must use same jsonLogic library

Out of scope

DB persistence, API endpoints, versioning, autosave, drafts

Code

Basic jsonLogic example (simple condition)
{
  "title": "Age requirement",
  "jsonLogic": {
    ">": [
      { "var": "user.age" },
      18
    ]
  }
}
Grouped rules (AND + OR)
{
  "title": "Eligible student",
  "jsonLogic": {
    "and": [
      {
        ">": [
          { "var": "user.age" },
          18
        ]
      },
      {
        "in": [
          { "var": "user.country" },
          ["USA", "Canada"]
        ]
      }
    ]
  }
}
Using NOT to invert a group
{
  "title": "Not eligible",
  "jsonLogic": {
    "not": {
      "and": [
        {
          ">=": [
            { "var": "user.age" },
            18
          ]
        },
        {
          "in": [
            { "var": "user.country" },
            ["USA", "Canada"]
          ]
        }
      ]
    }
  }
}
Datetime comparison (between)
{
  "title": "Recent applicant",
  "jsonLogic": {
    "and": [
      {
        ">=": [
          { "var": "application.created_at" },
          1704067200000
        ]
      },
      {
        "<=": [
          { "var": "application.created_at" },
          1735689600000
        ]
      }
    ]
  }
}
Variables shape (what the component expects)
// 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" },
];
Preview mode evaluation (with flat-to-nested conversion)
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 } }