Skip to main content
The JDM Editor is an open-source React component that provides a full-featured visual editor for creating and editing decision models. Embed it in your application to give users the ability to build rules without leaving your product.

GitHub Repository

View source, report issues, and contribute

Installation

npm install @gorules/jdm-editor

Quick start

The DecisionGraph component requires a JdmConfigProvider wrapper:
import { useState } from 'react';
import { JdmConfigProvider, DecisionGraph } from '@gorules/jdm-editor';
import '@gorules/jdm-editor/dist/style.css';

function RuleEditor() {
  const [value, setValue] = useState({ nodes: [], edges: [] });

  return (
    <JdmConfigProvider>
      <DecisionGraph
        value={value}
        onChange={setValue}
      />
    </JdmConfigProvider>
  );
}

Loading WASM for code extensions

The editor uses WebAssembly for syntax highlighting, autocomplete, and expression validation. Load the WASM module at application startup using top-level await:
import * as ZenEngineWasm from '@gorules/zen-engine-wasm';
import wasmUrl from '@gorules/zen-engine-wasm/dist/zen_engine_wasm_bg.wasm?url';

await ZenEngineWasm.default(wasmUrl);
WASM requires these headers on your server for SharedArrayBuffer support:
Cross-Origin-Embedder-Policy: require-corp
Cross-Origin-Opener-Policy: same-origin

Features

The decision graph canvas lets you drag and drop nodes onto the canvas, connect nodes to define data flow, pan and zoom to navigate large graphs, and select and delete nodes. The spreadsheet-like decision table editor supports adding input and output columns, defining conditions with unary operators, reordering rows by drag and drop, and hit policy configuration (first, collect). The expression editor provides syntax highlighting for ZEN expressions, autocomplete for functions and fields, inline validation and error messages, and field path suggestions from input schema. The JavaScript function editor includes Monaco editor with syntax highlighting, TypeScript type checking, built-in library support (dayjs, big.js, zod), and async/await support.

Configuration

Read-only mode

Display decisions without allowing edits:
<DecisionGraph
  value={value}
  disabled
/>

Simulator integration

Add a simulator panel to test decisions directly in the editor. You can evaluate rules via your backend API or entirely in the browser using WASM.
Call your backend to evaluate the decision:
import { DecisionGraph, GraphSimulator } from '@gorules/jdm-editor';
import { PlayCircleOutlined } from '@ant-design/icons';

function RuleEditor() {
  const [graph, setGraph] = useState({ nodes: [], edges: [] });
  const [simulation, setSimulation] = useState();

  return (
    <DecisionGraph
      value={graph}
      onChange={setGraph}
      simulate={simulation}
      panels={[
        {
          id: 'simulator',
          title: 'Simulator',
          icon: <PlayCircleOutlined />,
          renderPanel: () => (
            <GraphSimulator
              onClear={() => setSimulation(undefined)}
              onRun={async ({ graph, context }) => {
                const response = await fetch('/api/evaluate', {
                  method: 'POST',
                  headers: { 'Content-Type': 'application/json' },
                  body: JSON.stringify({ context, content: graph })
                });
                const data = await response.json();
                setSimulation({ result: { ...data, snapshot: graph } });
              }}
            />
          ),
        },
      ]}
    />
  );
}

Standalone components

Use individual editor components outside of the decision graph for custom integrations.

Code editor

The CodeEditor component provides standalone expression editing with syntax highlighting and validation:
import { useState } from 'react';
import { JdmConfigProvider, CodeEditor } from '@gorules/jdm-editor';

function ExpressionEditor() {
  const [expression, setExpression] = useState('customer.age >= 18');

  return (
    <JdmConfigProvider>
      <CodeEditor
        value={expression}
        onChange={setExpression}
        type="unary"
        lint
      />
    </JdmConfigProvider>
  );
}

Editor types

TypeUse case
standardGeneral expressions that return a value
unaryBoolean conditions for decision table cells
templateString templates with {expression} interpolation
// Standard expression
<CodeEditor type="standard" value="cart.total * 0.1" />

// Unary condition
<CodeEditor type="unary" value=">= 100" />

// Template string
<CodeEditor type="template" value="Hello, {customer.name}!" />

Type hints

Pass variableType and expectedVariableType to enable type checking and improved autocomplete. Types use the VariableTypeJson format:
import type { VariableTypeJson } from '@gorules/jdm-editor';

const variableType: VariableTypeJson = {
  Object: {
    customer: {
      Object: {
        name: 'String',
        age: 'Number',
        tier: { Enum: [undefined, ['bronze', 'silver', 'gold']] }
      }
    },
    order: {
      Object: {
        amount: 'Number',
        items: { Array: 'Any' }
      }
    }
  }
};

<CodeEditor
  value={expression}
  onChange={setExpression}
  type="standard"
  lint
  variableType={variableType}
  expectedVariableType="Number"
/>
Available type variants:
TypeDescription
'Any'Any value
'Null'Null value
'Bool'Boolean
'String'String
'Number'Number
{ Const: string }Constant string value
{ Enum: [label, values] }Enumeration with optional label
{ Array: VariableTypeJson }Array of a specific type
{ Object: Record<string, VariableTypeJson> }Object with typed fields

Decision table

The DecisionTable component renders a standalone spreadsheet-style table editor:
import { useState } from 'react';
import { JdmConfigProvider, DecisionTable } from '@gorules/jdm-editor';
import type { DecisionTableType } from '@gorules/jdm-editor';

function TableEditor() {
  const [table, setTable] = useState<DecisionTableType>({
    hitPolicy: 'first',
    inputs: [],
    outputs: [],
    rules: [],
  });

  return (
    <JdmConfigProvider>
      <DecisionTable
        value={table}
        onChange={setTable}
        tableHeight={400}
      />
    </JdmConfigProvider>
  );
}

Schema-driven autocomplete

Pass inputsSchema and outputsSchema to provide field suggestions in the expression editor:
import type { SchemaSelectProps } from '@gorules/jdm-editor';

const inputsSchema: SchemaSelectProps[] = [
  { field: 'customer.tier', name: 'Customer Tier' },
  { field: 'customer.region', name: 'Region' },
  { field: 'order.amount', name: 'Order Amount' },
  { field: 'order.quantity', name: 'Quantity' },
];

const outputsSchema: SchemaSelectProps[] = [
  { field: 'discount', name: 'Discount' },
  { field: 'shippingFee', name: 'Shipping Fee' },
];

<DecisionTable
  value={table}
  onChange={setTable}
  tableHeight={400}
  inputsSchema={inputsSchema}
  outputsSchema={outputsSchema}
/>

Permission levels

Control editing capabilities with the permission prop:
PermissionDescription
edit:fullFull editing capabilities (default)
edit:rulesEdit rule values only, cannot add/remove columns
edit:valuesEdit cell values only, cannot modify structure
<DecisionTable
  value={table}
  onChange={setTable}
  tableHeight={400}
  permission="edit:rules"
/>

Schema validation

Validate JDM files before loading them into the editor using the exported Zod schema:
import { decisionModelSchema } from '@gorules/jdm-editor/dist/schema';

async function handleFileUpload(file: File) {
  const content = await file.text();
  const result = decisionModelSchema.safeParse(JSON.parse(content));

  if (!result.success) {
    console.error(result.error);
    alert('Invalid decision file');
    return;
  }

  setGraph(result.data);
}

TypeScript support

The editor is fully typed. Import types for your integration:
import type {
  DecisionGraphType,
  DecisionNode,
  DecisionEdge,
  DecisionTableType,
  SchemaSelectProps,
} from '@gorules/jdm-editor';

const decision: DecisionGraphType = {
  nodes: [],
  edges: [],
};

Extracting JDM output

The editor produces JDM (JSON Decision Model) format that the ZEN Engine can execute:
function SaveButton({ value }: { value: DecisionGraphType }) {
  const handleSave = async () => {
    // value is already in JDM format
    await fetch('/api/decisions', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(value),
    });
  };

  return <button onClick={handleSave}>Save Decision</button>;
}

Standalone editor

For non-React applications, use the Standalone Editor — a self-hosted Docker image with the full visual editor.