Installation
Add to yourCargo.toml:
Basic usage
Loader
Attach a loader to serve decisions by key. Build one declaratively fromLoaderConfig, 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:
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:DecisionLoader trait. Wrap any loader in CachedLoader to add an in-memory cache.
Pre-compilation
Pre-compile decisions for improved evaluation performance:Error handling
Tracing
Enable tracing to inspect decision execution:Expression utilities
Thezen-expression crate provides expression evaluation outside of decisions:
High performance with Isolate
For repeated evaluations, useIsolate 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 inIsolate.
Thread-pinned futures
Althoughevaluate 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:
Best practices
Prefer the static or zip loader in production. Both are backed byMemoryLoader, 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.