A universal, deterministic, and explainable decision engine for business-critical systems.
Criterion helps you encode business decisions as pure, testable functions with built-in validation and explainability.
Instead of scattering if/else statements across your codebase, you define decisions declaratively:
- "Should this transaction be flagged as high-risk?"
- "Is this user eligible for a premium tier?"
- "What discount applies to this order?"
Every decision returns not just a result, but a complete explanation of why that result was reached — perfect for audits, debugging, and compliance.
| Package | Description |
|---|---|
| @criterionx/core | Pure decision engine — no I/O, no side effects |
| @criterionx/server | HTTP server with auto-generated docs, rate limiting |
| Package | Description |
|---|---|
| @criterionx/react | React hooks (useDecision, CriterionProvider) |
| @criterionx/express | Express & Fastify middleware |
| @criterionx/trpc | tRPC procedures with full type safety |
| Package | Description |
|---|---|
| @criterionx/opentelemetry | Tracing & metrics instrumentation |
| @criterionx/generators | Generate decisions from declarative specs |
| @criterionx/testing | Test utilities and mocks |
| @criterionx/devtools | Debug and inspect decisions |
| @criterionx/cli | Command-line interface |
| @criterionx/mcp | Model Context Protocol server for LLMs |
# Core engine only
npm install @criterionx/core zod
# With HTTP server
npm install @criterionx/server zod
# React integration
npm install @criterionx/react @criterionx/core
# Express/Fastify middleware
npm install @criterionx/express @criterionx/core
# tRPC integration
npm install @criterionx/trpc @criterionx/core @trpc/serverimport { Engine, defineDecision } from "@criterionx/core";
import { z } from "zod";
const riskDecision = defineDecision({
id: "transaction-risk",
version: "1.0.0",
inputSchema: z.object({ amount: z.number() }),
outputSchema: z.object({ risk: z.enum(["HIGH", "MEDIUM", "LOW"]) }),
profileSchema: z.object({ threshold: z.number() }),
rules: [
{
id: "high-risk",
when: (input, profile) => input.amount > profile.threshold,
emit: () => ({ risk: "HIGH" }),
explain: (input, profile) => `Amount ${input.amount} > ${profile.threshold}`,
},
{
id: "low-risk",
when: () => true,
emit: () => ({ risk: "LOW" }),
explain: () => "Amount within acceptable range",
},
],
});
const engine = new Engine();
const result = engine.run(
riskDecision,
{ amount: 15000 },
{ profile: { threshold: 10000 } }
);
console.log(result.data); // { risk: "HIGH" }
console.log(engine.explain(result));
// Decision: transaction-risk v1.0.0
// Status: OK
// Matched: high-risk
// Reason: Amount 15000 > 10000import { CriterionProvider, useDecision } from "@criterionx/react";
function App() {
return (
<CriterionProvider
decisions={[riskDecision]}
profiles={{ "transaction-risk": { threshold: 10000 } }}
>
<RiskChecker />
</CriterionProvider>
);
}
function RiskChecker() {
const { result, evaluate } = useDecision("transaction-risk");
return (
<button onClick={() => evaluate({ amount: 15000 })}>
Check Risk: {result?.data?.risk ?? "—"}
</button>
);
}import express from "express";
import { createDecisionRouter } from "@criterionx/express";
const app = express();
app.use(express.json());
app.use("/api/decisions", createDecisionRouter({
decisions: [riskDecision],
profiles: { "transaction-risk": { threshold: 10000 } },
}));
app.listen(3000);
// POST /api/decisions/transaction-riskimport { createServer } from "@criterionx/server";
const server = createServer({
decisions: [riskDecision],
profiles: { "transaction-risk": { threshold: 10000 } },
});
server.listen(3000);
// Server running at http://localhost:3000
// Docs at http://localhost:3000/docs- Pure & Deterministic — Same input always produces the same output
- Fully Explainable — Every decision includes a complete audit trail
- Contract-First — Zod schemas validate inputs, outputs, and profiles
- Profile-Driven — Parameterize decisions by region, tier, or environment
- Zero Side Effects — No I/O, no database, no external calls
- Testable by Design — Pure functions are trivial to test
- Framework Agnostic — Works with React, Express, Fastify, tRPC, and more
- Observable — OpenTelemetry integration for tracing and metrics
Full documentation available at tomymaritano.github.io/criterionx
A decision is a unit of business logic with:
- Input schema — What data is required
- Output schema — What the decision returns
- Profile schema — What can be parameterized
- Rules — The evaluation logic
Rules are evaluated in order. First match wins:
rules: [
{ id: "rule-1", when: (i) => i.x > 100, emit: ..., explain: ... },
{ id: "rule-2", when: (i) => i.x > 50, emit: ..., explain: ... },
{ id: "default", when: () => true, emit: ..., explain: ... },
]Profiles parameterize decisions without changing logic:
// Same decision, different thresholds
engine.run(decision, input, { profile: usProfile });
engine.run(decision, input, { profile: euProfile });- A workflow/BPMN engine
- A machine learning framework
- A data pipeline
- A plugin marketplace
Criterion is a micro engine: small core, strict boundaries.
Tomas Maritano — @tomymaritano
