Skip to main content
The ZEN Engine compiles to WebAssembly, allowing you to evaluate decisions entirely in the browser with no backend required.

Installation

npm install @gorules/zen-engine --cpu=wasm32

Server requirements

WASM requires SharedArrayBuffer support, which needs these HTTP headers:
Cross-Origin-Embedder-Policy: require-corp
Cross-Origin-Opener-Policy: same-origin
Example Vite configuration:
vite.config.ts
import { defineConfig } from 'vite'

export default defineConfig({
  plugins: [
    {
      name: 'configure-response-headers',
      enforce: 'pre',
      configureServer: (server) => {
        server.middlewares.use((_req, res, next) => {
          res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp')
          res.setHeader('Cross-Origin-Opener-Policy', 'same-origin')
          next()
        })
      },
    },
  ],
})

Basic usage

import { ZenEngine } from '@gorules/zen-engine';

const res = await fetch('/rules/pricing.json');
const content = await res.json();

const engine = new ZenEngine();
const decision = engine.createDecision(content);

const response = await decision.evaluate({
  customer: { tier: 'gold', yearsActive: 3 },
  order: { subtotal: 150, items: 5 }
});

console.log(response.result);
// => { discount: 0.15, freeShipping: true }

engine.dispose();

Loader

The loader pattern enables dynamic decision loading from remote sources. Combined with ZenDecisionContent for pre-compilation, this provides optimal performance for multi-decision applications.
import { ZenEngine, ZenDecisionContent } from '@gorules/zen-engine';

const cache = new Map();

const engine = new ZenEngine({
  loader: async (key) => {
    if (cache.has(key)) {
      return cache.get(key);
    }

    const response = await fetch(`/rules/${key}`);
    const buffer = await response.arrayBuffer();

    const content = new ZenDecisionContent(new Uint8Array(buffer));
    cache.set(key, content);
    return content;
  }
});

const response = await engine.evaluate('pricing.json', { amount: 100 });
console.log(response.result);

HTTP handler

When decisions make HTTP requests to external APIs, use httpHandler to proxy requests through your backend. This is necessary when the frontend cannot directly access services behind a firewall or private network:
const engine = new ZenEngine({
  httpHandler: async (request) => {
    const response = await fetch('/api/proxy', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(request)
    });

    const data = await response.json();
    return {
      status: response.status,
      headers: Object.fromEntries(response.headers.entries()),
      data
    };
  }
});
Your backend proxy can forward requests to internal services, add authentication headers, or handle IAM credentials.

Error handling

Using try-catch:
try {
  const response = await decision.evaluate(input);
  console.log(response.result);
} catch (error) {
  console.error('Evaluation failed:', error.message);
}
Using safeEvaluate:
const response = await decision.safeEvaluate(input);

if (response.success) {
  console.log(response.data.result);
} else {
  console.error('Evaluation failed:', response.error);
}

Tracing

Enable tracing to inspect decision execution:
const response = await decision.evaluate(input, { trace: true });

console.log(response.trace);
// Each node's input, output, and performance timing

console.log(response.performance);
// Total evaluation time

Expression utilities

Evaluate ZEN expressions outside of a decision context:
import {
  evaluateExpression,
  evaluateUnaryExpression
} from '@gorules/zen-engine';

// Standard expressions
const sum = await evaluateExpression('a + b', { a: 5, b: 3 });
// => 8

const total = await evaluateExpression('sum(items)', { items: [1, 2, 3, 4] });
// => 10

// Unary expressions (comparison against $)
const isValid = await evaluateUnaryExpression('>= 5', { $: 10 });
// => true

const inList = await evaluateUnaryExpression('"US", "CA", "MX"', { $: 'US' });
// => true
Synchronous versions are also available:
import {
  evaluateExpressionSync,
  evaluateUnaryExpressionSync
} from '@gorules/zen-engine';

const result = evaluateExpressionSync('a * 2', { a: 10 });
// => 20

Best practices

Use ZenDecisionContent for caching. Pre-compiling decisions avoids repeated parsing overhead. Cache compiled content in a Map keyed by decision name. Initialize the engine once. Create a single ZenEngine instance at application startup and reuse it for all evaluations. Implement a loader for dynamic decisions. The loader pattern centralizes decision loading logic and enables caching at the source. Call dispose() on cleanup. Release engine resources when the application terminates to prevent memory leaks.