Decisions
A decision is a pure function: typed inputs in, typed outputs out,
evaluated by a graph of nodes. It has no side effects, no IO, and — given
the same inputs and the same stored model — always produces the same result.
That determinism is what makes decisions safe to version, audit, replay and
run identically in a browser (WASM), over HTTP, and inside a Lambda.
Shape
Decision
├── inputs # typed, required/optional
├── outputs # typed
├── nodes
│ ├── input
│ ├── decision_table
│ ├── expression
│ ├── switch
│ └── output
└── tests # given/expect cases, run on every change
Node types
decision_table— a set of rules; each rule pairs a condition per input column with a value per output.hit_policycontrols how multiple matching rules resolve ("first"— first match wins, evaluated top to bottom, is the common case). Cell conditions use a compact predicate syntax — ranges, comparators, membership, wildcards — documented in Expressions.expression— computes one output from an expression over the decision’s inputs (and other nodes’ outputs).switch— maps a value to one of several cases (when/thenpairs) with adefaultfallback; simpler than a table when you’re branching on one value rather than combining several.output— binds computed values to the decision’s declared outputs. Every decision ends in one.
Nodes can reference each other’s results by id, so a decision is a small DAG: a table can feed an expression, an expression can feed a switch, and so on, ending at the output node.
Tests
Every decision carries its own tests: given inputs and expected
outputs, checked on every change. A project can additionally define
test_suites — named groups of cases against a specific decision, useful for
broader regression coverage than the inline per-decision tests. Both run
through the same engine as production evaluation, so a green test is a real
guarantee, not a mock.
{
"name": "prime borrower is low risk",
"given": { "credit_score": 780, "debt_to_income": 0.2 },
"expect": { "risk_tier": "LOW" }
}
Storage and simulation
A decision is created/edited as an artifact (kind: "decision") scoped
to a project — see Projects & artifacts. Two ways to
run one without deploying anything:
- Simulate a saved decision:
POST /projects/{project}/decisions/{artifactId}/simulatewith the input object as the body. - Simulate ad hoc:
POST /engine/simulatewith{ decision, inputs }— useful while iterating on a model that isn’t saved yet.
Both paths call the exact same engine entry point a deployed workflow’s
decision_task uses (ruleflow_engine::simulate), so simulation results are
never an approximation of production behavior.
Versioning
A decision artifact is mutable while you iterate; a release freezes its current model into an immutable snapshot. Deployed workflows always run the decision model inlined at deploy time from the release that was deployed — never the live, possibly-since-edited artifact — so a running environment’s behavior never drifts out from under you.