> ## Documentation Index
> Fetch the complete documentation index at: https://docs.gorules.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Rust Rules Engine

> Integrate GoRules into your Rust application for maximum performance.

The ZEN Engine is written in Rust, giving you direct access to the core engine with zero FFI overhead.

## Installation

Add to your `Cargo.toml`:

```toml theme={null}
[dependencies]
zen-engine = "2"
zen-expression = "2"
serde_json = "1.0"
tokio = { version = "1", features = ["full"] }
```

<Note>
  **Upgrading from 0.x?** `arbitrary_precision` is no longer a default feature. If you rely on arbitrary-precision number handling, enable it explicitly: `zen-engine = { version = "2", features = ["arbitrary_precision"] }`. Language bindings are unaffected.
</Note>

## Basic usage

```rust theme={null}
use zen_engine::DecisionEngine;
use zen_engine::model::DecisionContent;
use serde_json::json;

#[tokio::main]
async fn main() {
    let decision_content: DecisionContent =
        serde_json::from_str(include_str!("./pricing-rules.json")).unwrap();

    let engine = DecisionEngine::default();
    let decision = engine.create_decision(decision_content.into()).unwrap();

    let response = decision.evaluate(json!({
        "customer": { "tier": "gold", "yearsActive": 3 },
        "order": { "subtotal": 150, "items": 5 }
    }).into()).await.unwrap();

    println!("{}", response.result);
    // => {"discount":0.15,"freeShipping":true}
}
```

## Loader

Attach a loader to serve decisions by key. Build one declaratively from `LoaderConfig`, or construct the loader structs in `zen_engine::loader` directly.

### Static

Serve decisions from an in-memory map. `LoaderConfig::Static` builds a `MemoryLoader` under the hood:

```rust theme={null}
use std::collections::HashMap;
use zen_engine::DecisionEngine;
use zen_engine::loader::LoaderConfig;
use zen_engine::model::DecisionContent;
use serde_json::json;

#[tokio::main]
async fn main() {
    let pricing: DecisionContent =
        serde_json::from_str(&std::fs::read_to_string("./rules/pricing.json").unwrap()).unwrap();

    let mut content = HashMap::new();
    content.insert("pricing.json".to_string(), pricing);

    let loader = LoaderConfig::Static { content }.into_loader().unwrap();
    let engine = DecisionEngine::default().with_loader(loader);

    let response = engine.evaluate("pricing.json", json!({ "amount": 100 }).into()).await.unwrap();
    println!("{}", response.result);
}
```

To add or remove decisions at runtime, use `MemoryLoader` directly - it exposes `add`, `get`, and `remove`.

### File system

Load decisions from files under a root directory. Keys resolve to paths relative to the root:

```rust theme={null}
use zen_engine::DecisionEngine;
use zen_engine::loader::LoaderConfig;
use serde_json::json;

#[tokio::main]
async fn main() {
    let loader = LoaderConfig::Filesystem { path: "./rules".to_string() }
        .into_loader()
        .unwrap();
    let engine = DecisionEngine::default().with_loader(loader);

    let response = engine.evaluate("pricing.json", json!({ "amount": 100 }).into()).await.unwrap();
    println!("{}", response.result);
}
```

`FilesystemLoader::new(FilesystemLoaderOptions { root })` is the equivalent direct construction.

### Zip archive

Pass the bytes of a zip archive. Every `.json` entry becomes a decision keyed by its path within the archive. This pairs naturally with BRMS release ZIPs - download the release from object storage and hand the bytes to the engine:

```rust theme={null}
use zen_engine::DecisionEngine;
use zen_engine::loader::LoaderConfig;
use serde_json::json;

#[tokio::main]
async fn main() {
    // Bytes of a release ZIP, e.g. downloaded from object storage
    let bytes = std::fs::read("./decisions.zip").unwrap();

    let loader = LoaderConfig::Zip { bytes }.into_loader().unwrap();
    let engine = DecisionEngine::default().with_loader(loader);

    let response = engine.evaluate("pricing.json", json!({ "amount": 100 }).into()).await.unwrap();
    println!("{}", response.result);
}
```

### Custom loader

Define custom loading logic with an async closure:

```rust theme={null}
use zen_engine::DecisionEngine;
use zen_engine::loader::LoaderError;
use zen_engine::model::DecisionContent;
use serde_json::json;
use std::sync::Arc;

#[tokio::main]
async fn main() {
    let engine = DecisionEngine::default().with_closure_loader(|key| async move {
        // Load from any source: HTTP, S3, database, etc.
        let data = std::fs::read(format!("./rules/{key}"))
            .map_err(|_| LoaderError::NotFound(key.clone()))?;
        let content: DecisionContent = serde_json::from_slice(&data)
            .map_err(|source| LoaderError::Internal { key, source: source.into() })?;

        Ok(Arc::new(content))
    });

    let response = engine.evaluate("pricing.json", json!({ "amount": 100 }).into()).await.unwrap();
    println!("{}", response.result);
}
```

For full control, implement the `DecisionLoader` trait. Wrap any loader in `CachedLoader` to add an in-memory cache.

## Pre-compilation

Pre-compile decisions for improved evaluation performance:

```rust theme={null}
let mut decision = engine.create_decision(content.into()).unwrap();
decision.compile();

// Subsequent evaluations are faster
let response = decision.evaluate(input.into()).await.unwrap();
```

Compilation parses and optimizes the decision graph ahead of time, reducing overhead during evaluation. This is especially beneficial when the same decision is evaluated many times.

## Error handling

```rust theme={null}
use zen_engine::EvaluationError;

match decision.evaluate(input).await {
    Ok(response) => println!("{}", response.result),
    Err(e) => eprintln!("Evaluation failed: {:?}", e),
}
```

## Tracing

Enable tracing to inspect decision execution:

```rust theme={null}
use zen_engine::EvaluationOptions;

let response = decision.evaluate_with_opts(input, EvaluationOptions {
    trace: true,
    ..Default::default()
}).await.unwrap();

println!("{:?}", response.trace);
// Each node's input, output, and performance timing

println!("{}", response.performance);
// Total evaluation time
```

## Expression utilities

The `zen-expression` crate provides expression evaluation outside of decisions:

```rust theme={null}
use zen_expression::{evaluate_expression, evaluate_unary_expression};
use serde_json::json;

// Standard expressions
let result = evaluate_expression("a + b", json!({ "a": 5, "b": 3 }).into()).unwrap();
// => 8

let total = evaluate_expression("sum(items)", json!({ "items": [1, 2, 3, 4] }).into()).unwrap();
// => 10

// Unary expressions (comparison against $)
let is_valid = evaluate_unary_expression(">= 5", json!({ "$": 10 }).into()).unwrap();
// => true

let in_list = evaluate_unary_expression("'US', 'CA', 'MX'", json!({ "$": "US" }).into()).unwrap();
// => true
```

### High performance with Isolate

For repeated evaluations, use `Isolate` to reuse allocated memory:

```rust theme={null}
use zen_expression::Isolate;
use serde_json::json;

let context = json!({ "tax": { "percentage": 10 } });
let mut isolate = Isolate::with_environment(context.into());

// Reuses memory across evaluations
for amount in [50, 100, 150, 200] {
    let tax = isolate.run_standard(&format!("{} * tax.percentage / 100", amount)).unwrap();
    println!("Tax on {}: {}", amount, tax);
}
```

## Design notes

### Single-threaded expression engine

The expression engine is single-threaded by design for maximum performance. This avoids synchronization overhead and enables optimizations like memory reuse in `Isolate`.

### Thread-pinned futures

Although `evaluate` is async, the returned `Future` is `!Send` - it must complete on the same thread where it was started. This is intentional: sending data across threads would be costly in this scenario, and pinning enables significant performance gains. However, this can be awkward with async runtimes that expect `Send` futures.

For multi-threaded workloads, use `LocalPoolHandle` from `tokio-util` to spawn pinned tasks:

```rust theme={null}
use std::future::Future;
use std::sync::OnceLock;
use std::thread::available_parallelism;
use tokio::task::JoinHandle;
use tokio_util::task::LocalPoolHandle;

fn parallelism() -> usize {
    available_parallelism().map(Into::into).unwrap_or(1)
}

fn worker_pool() -> LocalPoolHandle {
    static LOCAL_POOL: OnceLock<LocalPoolHandle> = OnceLock::new();
    LOCAL_POOL
        .get_or_init(|| LocalPoolHandle::new(parallelism()))
        .clone()
}

fn spawn_pinned<F, Fut>(create_task: F) -> JoinHandle<Fut::Output>
where
    F: FnOnce() -> Fut + Send + 'static,
    Fut: Future + 'static,
    Fut::Output: Send + 'static,
{
    worker_pool().spawn_pinned(create_task)
}
```

Usage:

```rust theme={null}
let result = spawn_pinned(|| async {
    let engine = DecisionEngine::default();
    let decision = engine.create_decision(content.into()).unwrap();
    decision.evaluate(input.into()).await
}).await.unwrap();
```

## Best practices

**Prefer the static or zip loader in production.** Both are backed by `MemoryLoader`, so decisions are parsed once and served from memory. Wrap other loaders in `CachedLoader` to get the same effect.

**Initialize the engine once.** Create a single `DecisionEngine` instance at application startup and reuse it for all evaluations.

**Use `Isolate` for repeated expression evaluation.** It reuses allocated memory, drastically improving throughput.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.