v2.0

GoRules Version 2 is here - redesigned, now with managed cloud.GoRules Version 2 is here!

Watch the launch videoWatch

Switch from json-rules-engine to GoRules.

A step-by-step guide to moving your json-rules-engine rules, fact handlers, custom operators, and listeners to GoRules: export with rule.toJSON(), convert with the AI copilot, validate in parallel, and cut over.

What you get with GoRules

Where your rules land: the parts of GoRules most relevant to this migration.

Visual editor for developers and business users

Rules are modeled as decision graphs in a web editor: decision tables, expressions, switches, and functions on one canvas. Business users and developers edit the same graph, with changes tracked per release.

Versioning, environments, and rollback built in

Semantic versioned releases, environment-scoped deployments with rollback, and branching for parallel rule development. Publish, release, and deploy are explicit human actions, each with an audit trail.

One expression language everywhere

Decision table cells, expression nodes, and switch conditions all use Zen - a single expression language with 60+ built-in functions (sum, avg, filter, map, date, duration, matches). For logic beyond expressions, functionNode runs sandboxed JavaScript with zod, big.js, dayjs, and http imports.

Native SDKs across languages

The same Rust core ships as @gorules/zen-engine for Node.js, zen-engine for Python and Rust, zen-go for Go, io.gorules:zen-engine for JVM (Java and Kotlin), GoRules.ZenEngine for .NET, plus Swift, Android, and browser or Lambda via WASM.

The migration path

A step-by-step route from your current setup to running your rules on Zen Engine.

1. Inventory the rule layer in your host application

Catalog four things in your existing json-rules-engine integration: (1) the rules themselves (use rule.toJSON() on each registered Rule to capture the declarative shape - conditions, event, priority, name); (2) every fact registered via engine.addFact(id, fnOrValue, options), noting which are constants, which are async function handlers, and what external systems each handler touches; (3) every custom operator and operator decorator registered via engine.addOperator(...)/engine.addOperatorDecorator(...); (4) every listener wired up via rule.on(...)/engine.on(...) (per-rule, engine-wide, or event-type-keyed). The first item migrates as JSON; the other three need restructuring (see steps 4 and 5).

2. Map condition trees to GoRules nodes

{ all: [...] } and { any: [...] } condition trees over leaf { fact, operator, value, path } conditions translate naturally to decisionTableNode rows: each leaf condition becomes an input column with a Zen unary test (> 50, [1..10], "US","CA", contains($, "test")), and the event.params payload becomes the output columns. Hit policy is first (one match wins) or collect (all matches as an array). Boolean trees that branch on a single fact map to a switchNode with one branch per case. { not: { ... } } becomes either positive matching in the table or an inverted switchNode default. Use decisionNode for engine.setCondition(name, conditions) shared conditions, and for any decision that you want to version independently.

3. Translate operators and decorators to Zen

Direct mappings: equal/notEqual==/!=; the four numeric comparators → </<=/>/>=; in/notIn$ in [...] / not ($ in [...]); contains/doesNotContaincontains($, value) / not contains($, value). Decorators map by transformation: everyFact:operatorall($, # operator value); someFact:operatorsome($, # operator value); everyValue:operatorall(value, $ operator #); someValue:operatorsome(value, $ operator #); the not decorator inlines as !; the swap decorator is unneeded because Zen expressions are positional. Custom operators you registered for comparisons like between, regex matches, startsWith, endsWith, temporal checks, or arithmetic map to direct Zen primitives or built-in functions (x >= 10 and x <= 20, matches(x, ".*foo.*"), startsWith(x, "pre"), etc.) - many engine.addOperator registrations become unnecessary after migration.

4. Lift fact handlers out of the engine and into the calling application

This is the largest restructure. Constant facts (engine.addFact("speed-of-light", 299792458)) become literal values in the input context. Async function facts that load data (e.g. engine.addFact("account-information", (params, almanac) => apiClient.getAccountInformation(...))) become pre-evaluation HTTP/database calls in the calling application: load the data once, then include it in the context object passed to GoRules. Cross-fact chaining patterns (almanac.factValue("account-information", { accountId }) from inside another fact handler) collapse - the calling application assembles the full input context deterministically, and values are read via Zen $.field.path accessors inside expressions. Pure-computation fact handlers can move into expressionNode content (chained named expressions evaluated top-to-bottom) or functionNode (sandboxed ES6 JavaScript). Side-effecting fact handlers stay in the host application.

5. Replace listener side effects and rule chaining with graph topology + post-evaluation actions

If your listeners (rule.on("success", fn), engine.on("success", fn), engine.on(<event.type>, fn)) post webhooks, send notifications, or write intermediate facts via almanac.addRuntimeFact(id, value) for downstream rules, none of this migrates as-is. Rewrite each pattern as follows: (a) side effects on rule success/failure become post-evaluation actions in the calling application - read the GoRules result (or specific trace entries), then trigger the side effect; (b) rule chaining via priority + onSuccess + almanac.addRuntimeFact becomes graph topology - the upstream computation lives in an expressionNode or decisionNode and produces a named field that downstream nodes reference via Zen $.field; (c) multi-step listener-driven workflows become orchestrated calls in the host (call GoRules → run side effect → call GoRules again with updated context) or a single graph that returns all decisions in one pass.

6. Swap engine.run() for the REST API or the embedded engine

For service-oriented integration: replace await engine.run(facts) with POST /projects/:projectId/evaluate/* (environment- and release-pinned variants exist). Send { "context": {...}, "trace": true } in the body, authenticate with x-access-token: <project-access-token> (or Authorization: Bearer grl_pat_... for personal access tokens). The response shape changes from { events, failureEvents, almanac, results, failureResults } to { performance, result, trace } - if you previously consumed events/failureEvents to know which rules fired, return an explicit list of fired-rule identifiers as part of result. For in-process integration in Node.js host applications: install @gorules/zen-engine and replace new Engine() with new ZenEngine({ loader }), engine.addRule(...) with engine.createDecision(graph), and engine.run(facts) with decision.safeEvaluate(context, options). The same Rust core powers both paths.

GoRules AI

Let the copilot do the translation.

Rules exported with rule.toJSON() are already JSON: the { conditions, event } shape with all/any/not trees maps almost directly to GoRules node shapes, and the copilot converts most condition trees to decisionTableNode rows or switchNode branches automatically. It handles built-in operator translation to Zen (equal==, greaterThanInclusive>=, in$ in [...], containscontains($, ...)), decorator transformation (everyFact:greaterThanall($, # > value), someFact:containssome($, contains(#, value))), and shared conditions registered via engine.setCondition becoming decisionNode references between separate decision files. What the copilot cannot see it cannot translate: fact handlers registered via engine.addFact(id, fn, options), custom operators from engine.addOperator(name, fn), decorators from engine.addOperatorDecorator(name, fn), and listener callbacks (rule.on, engine.on) are JavaScript in your host application, outside the rule JSON the copilot operates on. The restructuring in steps 4 and 5 - pre-evaluation context assembly, post-evaluation side effects, and collapsing priority + almanac.addRuntimeFact chaining into graph topology - is manual host-application work. Because the rule format is already JSON, the copilot converts what it sees quickly, leaving engineering time for that architectural change.

Leave the build step
behind.

Bring your json-rules-engine rules over, run both engines in parallel, and cut over when the outputs match.