Skip to main content
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:

Basic usage

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

Custom loader

Define custom loading logic with an async closure:
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:
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

Tracing

Enable tracing to inspect decision execution:

Expression utilities

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

High performance with Isolate

For repeated evaluations, use Isolate to reuse allocated memory:

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:
Usage:

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.