One npm package. No backend. No API calls. Your rules ship as a static file and execute right where your code runs: in the browser, on the server, at the edge.
Ask "Customer tier?" as customer_tier
Ask "Deal size?" as deal_size
discount_rate equals 0.05
If customer_tier is "enterprise"
discount_rate equals 0.15
discount equals deal_size times discount_rate
Respond with discount
devlish compile pricing.dvl --output pricing.dvlc.json
import { loadTool } from "devlish-runtime";
const tool = await loadTool({
bytecode: await fetch("/rules/pricing.dvlc.json").then(r => r.json())
});
const result = await tool.run({
customer_tier: "enterprise",
deal_size: 500000
});
console.log(result.response); // 75000
That compiled JSON file is the only artifact. Drop it in your /public
folder, fetch it at runtime, and call tool.run() with your data.
The result comes back as plain JSON.
If you have heard of WebAssembly (WASM) but are not sure what it is: it is a way to run code written in languages like Rust or C++ inside a web browser (or Node.js) at near-native speed. Think of it as a tiny, fast virtual machine that lives inside your JavaScript runtime.
Human-readable English. This is what your team writes and reviews.
Parses the English and produces a compact bytecode JSON file. Runs on your machine at build time.
A small, portable file (typically 2-20 KB). Ship it like any static asset.
A Rust virtual machine compiled to WebAssembly and bundled inside the npm package.
When you call loadTool(), it boots a tiny isolated VM, feeds it
your bytecode, and returns the result. The VM is built in Rust for speed and memory
safety; WebAssembly makes it portable to any platform.
The runtime enforces three layers of protection:
| Permission check | If a rule was written to access the network or filesystem, the runtime rejects it before it runs. Rules that only compute on data you pass in are allowed. |
| Instruction limit | Every rule has a maximum number of operations (default: 10 million). If a rule has a bug that causes an infinite loop, the runtime stops it and returns an error instead of freezing your app. |
| Memory isolation | Each rule runs in its own WebAssembly memory. It cannot read or write anything in your JavaScript application. |
By default, loadTool() runs the VM inside a
Web Worker.
That means rule execution happens in a background thread. Your React components keep
rendering, your animations keep playing, and your click handlers keep responding while
the rule is running. If you prefer synchronous execution (for server-side code where
there is no UI to block), pass mainThread: true.
loadTool(options)Load a compiled rule for repeated execution. Returns a tool you can call multiple times.
const tool = await loadTool({
bytecode: myCompiledRule, // required: the compiled JSON (object or string)
mainThread: false, // optional: skip Web Worker, run synchronously
instructionLimit: 10_000_000 // optional: max operations before timeout
});
const result = await tool.run({ key: "value" });
tool.dispose(); // clean up when done
runTool(bytecode, input)One-shot convenience. Loads, runs, and cleans up in one call. Good for infrequent use.
import { runTool } from "devlish-runtime";
const result = await runTool(myCompiledRule, { customer_tier: "premium" });
{
success: true, // did the rule complete without errors?
responded: true, // did it produce a response value?
response: 75000, // the value from "Respond with ..."
context: { ... }, // all variables after execution
results: { ... } // exported data and events
}
const result = await tool.run(input);
if (!result.success) {
console.error(result.error);
// "Instruction limit exceeded (10000000 instructions)"
// "Tool requires permissions unavailable in WASM: http_request"
// "Validation failed: amount must be at least 1000"
}
import { useState, useEffect } from "react";
import { loadTool } from "devlish-runtime";
function PricingWidget({ tier, dealSize }) {
const [discount, setDiscount] = useState(null);
const [tool, setTool] = useState(null);
useEffect(() => {
loadTool({
bytecode: fetch("/rules/pricing.dvlc.json").then(r => r.json())
}).then(setTool);
return () => tool?.dispose();
}, []);
useEffect(() => {
if (!tool) return;
tool.run({ customer_tier: tier, deal_size: dealSize })
.then(result => {
if (result.success) setDiscount(result.response);
});
}, [tool, tier, dealSize]);
if (discount === null) return <span>Calculating...</span>;
return <span>Discount: ${discount.toLocaleString()}</span>;
}