Thanks to visit codestin.com
Credit goes to github.com

Skip to content

CaboLabs/criterion-spec

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

14 Commits
 
 
 
 
 
 

Repository files navigation

Criterion Rule Specification

Criterion is a Generic Rule Engine. This is the public specification of the rule format.

Introduction

The Criterion Rule Format is expressed in JSON. A rule has two top-level sections: input and logic.

  • input — the runtime contract. Declares the values the caller must supply when evaluating the rule. A missing required input is an error before execution begins.
  • logic — a sequence of Blocks that defines what the rule does.

A rule may also declare internal variables inside logic blocks and fetch data from external sources via Data Source Blocks. These three mechanisms — input contract, internal variables, and data sources — are distinct and serve different purposes.

{
  "name": "Sample Rule",
  "input": [
    {
      "var": "input1",
      "type": "string"
    },
    {
      "var": "input2",
      "type": "integer"
    }
  ],
  "logic": [
    ...
  ]
}

Input Declarations

Input declarations define the rule's public runtime contract. They appear in the top-level input array and are resolved before execution begins — not during logic execution.

Field Required Description
var yes Variable name. Available as $name throughout the rule.
type yes Data type. See Criterion Data Types.
default no Value used when the caller does not supply this input. Makes the input optional. Must match the declared type.

Required Input

An input with no default is required. If the caller does not provide it, the engine must reject the evaluation before execution begins and return a structured error. It is a compile-time error to reference an undeclared variable.

{ "var": "patient_id", "type": "string" }

Optional Input

An input with a default is optional. When the caller omits it, the engine substitutes the default value before execution starts.

{ "var": "max_results", "type": "integer", "default": 10 }

The default value must be a literal compatible with the declared type. Variable references and function calls are not valid as defaults.

Error: Missing Required Input

When a required input is not provided, the engine returns a structured error and does not execute the rule:

{
  "error": {
    "code": "MISSING_REQUIRED_INPUT",
    "message": "Required input 'patient_id' was not provided",
    "input": "patient_id"
  }
}

Input Declarations vs Variable Declarations

Input declarations (top-level input array) and variable declarations inside logic blocks use the same var/type syntax but serve different roles:

Input declaration Variable declaration
Location top-level input array logic block
Populated by the caller at evaluation time the rule's logic (via assignment)
Missing value error or default unassigned — error if used before set
Visible to caller yes — public contract no — internal to the rule

Example showing both in one rule. score and threshold are caller-supplied. adjusted is internal — the caller never sees it.

{
  "name": "Score Check",
  "input": [
    { "var": "score",     "type": "integer" },
    { "var": "threshold", "type": "integer", "default": 50 }
  ],
  "output": { "type": "boolean" },
  "logic": [
    { "var": "adjusted", "type": "integer", "=": { "+": ["$score", 5] } },
    { "return": { ">=": ["$adjusted", "$threshold"] } }
  ]
}

Basic

Criterion Rules have three basic operations: variable declaration, value assignment and the combination of both in the same operation, which we call declare-assign or dec-assign.

Variable Declaration

Variable declarations inside logic blocks create internal working variables. They are not part of the rule's public interface and are never supplied by the caller. For caller-supplied values, see Input Declarations.

A variable declaration has a var name and a type. The variable exists from the point of declaration to the end of its enclosing block. Using a variable before it has been assigned a value is a compile-time error.

{
  "var": "myVar",
  "type": "integer"
}

Refer to the Criterion Data Types section to see the possible types a variable can have.

Arguments

The values that can be assigned or used as function parameters are called Argument. Arguments can have four types:

  1. Literal: represents a simple constant value of a certain type like the integer 2 or the string "hellow world".
  2. Value Reference: represents the reference to an existing variable by its name, and always starts with $, for instance $myVar.
  3. Return Literal: represents the value returned by a function, and which can be assigned to a variable.
  4. Data Source Literal: represents a value that is extracted and retrieved from a Data Source (JSON, XML, etc).

Value Assignment

As mentioned in the previous section, the Criterion Rule Format defines four types of values (called Arguments) that can be assigned to variables: Literal, Value Reference, Return Literal and Data Source Literal. The following examples show how to assign each type of Argument.

Literal Value Assignment

Literals are constant values given "inline", like numbers and strings. In this example, 2 is a Literal assigned to variable myVar. Note the $ prefix indicates that is a reference to an existing (declared) variable.

{
  "$myVar": 2
}

Value Reference Assignment

A Value Reference is a reference to an existing variable and represents the access to that variable's value. In this example we are assigning the value of variable anotherVar to the variable myVar.

{
  "$myVar": "$anotherVar"
}

Return Literal Assignment

A Return Literal represents a value that is returned by calling a function. In this example the function is +, the normal arithmetic addition, that adds two values: the value of the variable anotherVar and the literal 2.

{
  "$myVar": {
    "+": ["$anotherVar", 2]
  }
}

Data Source Literal Assignment

The Data Source Literal represents a value that is extracted from a Data Source, like a JSON or XML document.

In this example, we reference the "patient" Data Source, and extract an exact data point using JSON Path, the patient's gender, and assign that to the patient_sex` variable:

{
  "$patient_sex": {
    "source": "@patient",
    "extract": {
      "jsonpath": [
        "$.gender"
      ]
    },
    "aggregate": "first",
    "transform": "noop"
  }
}

Declare Assignment

This operation is the combination of Variable Declaration and Value Assignment. The values that can be assigned are Arguments as mentioned on the previous sections.

Dec-Assign Literal

Note for the assignment, the key = is used. Then the other two keys are exactly the same to a Variable Declaration (var and type).

{
  "var": "myVar",
  "type": "integer",
   "=": 2
}

Dec-Assign Value Reference

This example declares the variable myVar and assigns the value of anotherVar to it.

{
  "var": "myVar",
  "type": "integer",
  "=": "$anotherVar"
}

Dec-Assign Return Literal

This example declares the variable myVar and assigns the value returned by the + function to it.

{
  "var": "myVar",
  "type": "integer",
  "=": {
    "+": ["$anotherVar", 2]
  }
}

Dec-Assign Data Source Literal

This example extracts the value form the patient Data Source, and assigns it to the variable sex.

{
  "var": "sex",
  "type": "string",
  "=": {
    "source": "@patient",
    "extract": {
      "jsonpath": [
        "$.gender"
      ]
    },
    "aggregate": "first",
    "transform": "noop"
  }
}

Logic Blocks

Return Block

The Return Block is used to return a value from the rule, and stop the rule execution. It's like the return keyword from most programming languages. The types of values that can be returned are: literal, value reference, return literal and data source literal.

Return Block Literal

In this example, the rule would return a boolean value true.

{
  "return": true
}

Return Block Value Reference

In this example the rule would return the current value of anotherVar.

{
  "return": "$anotherVar"
}

Return Block Return Literal

{
  "return": {
    "<": ["$value1", 1000]
  }
}

Return Block Data Source Literal

This example returns a value extracted from the patient Data Source.

{
  "return": {
    "source": "@patient",
    "extract": {
      "jsonpath": [
        "$.gender"
      ]
    },
    "aggregate": "first",
    "transform": "noop"
  }
}

Block List

Defines a list of blocks that can be used by the rule or by other blocks. For instance the rule's logic section is a Block List. Another example is the If-Else's then and else blocks are Block Lists. In this example we have a Block List with two blocks: a Dec-Assign and a Return block.

[
  {
    "var": "a",
    "type": "integer",
    "=": 1
  },
  {
    "return": "$a"
  }
]

If-Else Block

This is a flow-control block that checks a boolean condition. When the condition evaluates to true, the blocks in the then Block List are executed; otherwise the blocks in the else Block List are executed. The condition must resolve to boolean — passing any other type is a compile-time error.

{
  "if": "$is_pending",
  "then": [
    {
      "return": 555
    }
  ],
  "else":[
    {
      "return": 890
    }
  ]
}

Data Source Block

A Data Source Block is an inbound, read-only construct. It retrieves data from an external resource, caches it under a named identifier, and makes it available for extraction via Data Source Literals. The data is fetched once and reused for all literals that reference it within the same rule execution.

The external access is configured via the access field, which specifies the transport type (http, db, file, etc.) and the parameters needed to fetch the data. See External Access Types for details.

A Data Source Block is strictly read-only. It must not modify any external state. Outbound operations (sending data, triggering hooks, writing to a database) are handled by Action Blocks, which are a separate construct that uses the same underlying access types in write mode. See Action Blocks in the Candidate Features section.

In this example, we retrieve a JSON document via an HTTP GET request and name it "patient". Check the Data Source Literal Assignment section to see how to extract values from it. Multiple Data Source Literals can reference the same source — for instance, one to extract the patient's gender and another for their date of birth — without triggering additional requests.

{
  "source": "patient",
  "type": "JSON",
  "access": {
    "type": "http",
    "url": "https://my-fhir-server/Patient/1234",
    "method": "GET"
  }
}

External Access Types

Access types define the transport mechanism used by both Data Source Blocks (read) and Action Blocks (write). They are not tied to either concept — the same http access type that performs a GET for a Data Source can perform a POST in an Action Block.

Defined access types:

Type Description
http HTTP/HTTPS request. Method determines direction: GET for data sources, POST/PUT/DELETE for actions.
db Database access. Query type determines direction: SELECT for data sources, INSERT/UPDATE/DELETE for actions.
file File system access. Read for data sources, write for actions.

HTTP Access Type

The http access type supports authentication, custom headers, query parameters, in-URL variable substitution, and request bodies.

Field Required Description
url yes Target URL. May contain $variable references resolved at execution time.
method yes HTTP method: GET, POST, PUT, PATCH, DELETE.
auth no Authentication configuration. See HTTP Authentication below.
headers no Map of header names to values. Values may be literal strings or $variable references.
params no Map of URL query parameter names to values. Values may be literal strings or $variable references. Appended as ?key=value&... to the URL.
body no Request body. Valid for POST, PUT, PATCH. May be a literal object or a $variable reference.
In-URL Variable Substitution

$variable references embedded in the url string are resolved to their current rule variable values before the request is sent. This enables dynamic endpoint paths without string concatenation.

{
  "source": "patient",
  "type": "JSON",
  "access": {
    "type": "http",
    "url": "https://my-fhir-server/Patient/$patient_id",
    "method": "GET"
  }
}

If $patient_id is "12345" at execution time, the resolved URL is https://my-fhir-server/Patient/12345. A $variable reference in the URL that does not match a declared rule variable is a compile-time error.

HTTP Authentication

The auth field selects an authentication strategy. Three strategies are defined:

basic — sends Base64-encoded username:password as an Authorization: Basic ... header:

"auth": {
  "type": "basic",
  "username": "$apiUser",
  "password": "$apiPass"
}

bearer — sends a token as Authorization: Bearer <token>:

"auth": {
  "type": "bearer",
  "token": "$authToken"
}

api_key — injects a key/value pair into a header or query parameter:

"auth": {
  "type": "api_key",
  "name": "X-API-Key",
  "value": "$apiKey",
  "in": "header"
}
"auth": {
  "type": "api_key",
  "name": "api_key",
  "value": "$apiKey",
  "in": "query"
}

The in field accepts "header" or "query". All credential fields (username, password, token, value) accept $variable references.

HTTP Examples

GET with bearer auth, custom headers, and in-URL variable:

{
  "source": "patient",
  "type": "JSON",
  "access": {
    "type": "http",
    "url": "https://my-fhir-server/Patient/$patient_id",
    "method": "GET",
    "auth": {
      "type": "bearer",
      "token": "$authToken"
    },
    "headers": {
      "Accept": "application/fhir+json",
      "X-Tenant-Id": "$tenantId"
    }
  }
}

GET with query parameters:

{
  "source": "encounters",
  "type": "JSON",
  "access": {
    "type": "http",
    "url": "https://my-fhir-server/Encounter",
    "method": "GET",
    "auth": {
      "type": "bearer",
      "token": "$authToken"
    },
    "params": {
      "patient": "$patient_id",
      "date": "$encounter_date",
      "_count": "100"
    }
  }
}

POST with body and bearer auth:

{
  "source": "analysis_result",
  "type": "JSON",
  "access": {
    "type": "http",
    "url": "https://api.example.com/analyze",
    "method": "POST",
    "auth": {
      "type": "bearer",
      "token": "$serviceToken"
    },
    "headers": {
      "Content-Type": "application/json"
    },
    "body": {
      "patient_id": "$patientId",
      "criteria": "$criteria"
    }
  }
}

GET with basic auth:

{
  "source": "lab_results",
  "type": "JSON",
  "access": {
    "type": "http",
    "url": "https://lab-api.example.com/results/$patient_id",
    "method": "GET",
    "auth": {
      "type": "basic",
      "username": "$labUser",
      "password": "$labPass"
    },
    "headers": {
      "Accept": "application/json"
    }
  }
}

GET with API key in query:

{
  "source": "drug_interactions",
  "type": "JSON",
  "access": {
    "type": "http",
    "url": "https://drug-api.example.com/interactions",
    "method": "GET",
    "auth": {
      "type": "api_key",
      "name": "api_key",
      "value": "$drugApiKey",
      "in": "query"
    },
    "params": {
      "drug1": "$medication_a",
      "drug2": "$medication_b"
    }
  }
}

Functions

Functions can receive 0..N input parameters, which will be a type of Argument and will return a Literal Argument. In general, the result of a Function will be assigned to a Variable or used as input for another Function.

Logic

Logic functions are boolean functions (return true or false), and have boolean Arguments as input.

And

The result is true if both input Arguments are true, otherwise the result will be false. In this example, we show the and function being called using boolean Literal Arguments true and false, though any of the inputs can be any type of Argument (Literal, Variable Reference, Return Literal or Data Source Literal).

{
  "&&": [true, false]
}

Or

Returns true if at least one of the two input Arguments is true, otherwise returns false.

{
  "||": [true, false]
}

Not

Returns true if the input Argument is false, and false if the input Argument is true. Takes exactly one boolean Argument.

{
  "!": true
}

Xor

Returns true if exactly one of the two input Arguments is true. If both are true or both are false, returns false.

{
  "xor": [true, false]
}

Comparison

All comparison functions return boolean. Comparison operators are overloaded: they work on any type that has an order relationship, as long as both operands are compatible. Compatibility is checked at rule compilation time — a type mismatch is a compile-time error, not a runtime error.

Comparable type pairs:

Left type Right type Notes
integer integer exact match
decimal decimal exact match
integer decimal numeric widening — allowed
string string lexicographic order
date date chronological order
datetime datetime chronological order

Any other combination (e.g. string vs integer, date vs decimal) is a compile-time type mismatch error.

When integer and decimal are compared, the integer is widened to decimal for the comparison. The original variable is not mutated.

Examples of valid and invalid usage:

{ "var": "score",  "type": "integer" }
{ "var": "cutoff", "type": "decimal" }
{ "var": "label",  "type": "string"  }
{ "<": ["$score", 50] }

Valid — integer compared to integer literal.

{ "<": ["$score", "$cutoff"] }

Valid — integer vs decimal, numeric widening applies.

{ "<": ["$score", "$label"] }

ERROR: type mismatch — '<' cannot compare 'integer' and 'string'

Lower Than

This function will return true if the value represented by the first Argument is lower than the value represented by the second Argument, otherwise it will return false.

{
  "<": [1, 2]
}

Greater Than

Returns true if the value of the first Argument is greater than the value of the second Argument, otherwise returns false.

{
  ">": [1, 2]
}

Equals To

Returns true if both Arguments represent the same value. Both Arguments must be of the same type.

{
  "==": [1, 2]
}

Not Equals To / Different Than

Returns true if the Arguments have different values or are of different types, otherwise returns false.

{
  "!=": [1, 2]
}

Lower Or Equals

Returns true if the value of the first Argument is lower than or equal to the value of the second Argument, otherwise returns false.

{
  "<=": [1, 2]
}

Greater Or Equals

Returns true if the value of the first Argument is greater than or equal to the value of the second Argument, otherwise returns false.

{
  ">=": [1, 2]
}

Arithmetic

Arithmetic functions operate on numeric Arguments (integer and decimal). When both inputs are integer the result is integer, except for Division which always returns decimal. If either input is decimal, the result is decimal.

Precision note: Numbers with a decimal point are represented as the native decimal type of the host platform (e.g. BigDecimal in Java, Decimal in Python), not as IEEE 754 floating-point. This guarantees exact decimal arithmetic and avoids precision loss.

Increment

Increments the value "in place" and returns it. If the input Argument is a Variable Reference, it will increment the value of that variable.

{
  "++": "$myNumber"
}

Decrement

Decrements the value "in place" and returns it. If the input Argument is a Variable Reference, it will decrement the value of that variable.

{
  "--": "$myNumber"
}

Addition

Returns the sum of two numeric Arguments.

{
  "+": [1, 2]
}

Subtraction

Returns the result of subtracting the second Argument from the first.

{
  "-": [1, 2]
}

Division

Returns the result of dividing the first Argument by the second. Always returns decimal. The second Argument must not be zero.

{
  "/": [1, 2]
}

Multiplication

Returns the product of two numeric Arguments.

{
  "*": [1, 2]
}

Modulus

Returns the remainder of dividing the first Argument by the second. Both Arguments must be integer.

{
  "%": [1, 2]
}

Strings

String functions operate on string Arguments. Unless otherwise noted, they return a string.

Concat

Returns a new string by joining two or more string Arguments together.

{
  "concat": ["$firstName", " ", "$lastName"]
}

Length

Returns the number of characters in a string as an integer.

{
  "length": "$description"
}

Trim

Returns the string with leading and trailing whitespace removed.

{
  "trim": "$rawInput"
}

ToUpper

Returns the string with all characters converted to uppercase.

{
  "toUpper": "$code"
}

ToLower

Returns the string with all characters converted to lowercase.

{
  "toLower": "$email"
}

Contains

Returns true if the first string Argument contains the second string Argument as a substring, otherwise returns false.

{
  "contains": ["$body", "error"]
}

StartsWith

Returns true if the first string Argument starts with the second string Argument, otherwise returns false.

{
  "startsWith": ["$url", "https"]
}

EndsWith

Returns true if the first string Argument ends with the second string Argument, otherwise returns false.

{
  "endsWith": ["$filename", ".json"]
}

Substring

Returns a portion of the string starting at the index given by the second Argument and ending before the index given by the third Argument. Indices are zero-based.

{
  "substring": ["$text", 0, 5]
}

Replace

Returns a new string with all occurrences of the second Argument replaced by the third Argument.

{
  "replace": ["$template", "{name}", "$userName"]
}

Criterion Data Types

Every variable and function result in Criterion has one of the following types. The type determines what values a variable can hold and what operations are valid on it.

Type Description JSON literal example
boolean A truth value true or false
integer A whole number, positive or negative 42, -7
decimal A number with a decimal part, represented with exact precision 3.14, -0.5
string A sequence of characters "hello"
date A calendar date, encoded as an ISO 8601 string "2024-01-15"
datetime A date and time with timezone, encoded as ISO 8601 "2024-01-15T10:30:00Z"
array An ordered collection of values of the same type [1, 2, 3]
object An untyped key-value map {"key": "value"}

Declaration examples:

{ "var": "active",     "type": "boolean"  }
{ "var": "age",        "type": "integer"  }
{ "var": "ratio",      "type": "decimal"  }
{ "var": "name",       "type": "string"   }
{ "var": "dob",        "type": "date"     }
{ "var": "created_at", "type": "datetime" }
{ "var": "scores",     "type": "array",   "items": "integer" }
{ "var": "payload",    "type": "object"   }

Arrays require an items field that specifies the type of each element. The object type is schema-less and can hold any key-value structure.

Type System

Criterion is strongly typed. Every variable, every function input, and every block condition has a declared or inferred type. All type checking is performed at rule compilation time. A rule that contains a type mismatch anywhere in its logic must be rejected before execution begins — it must never be partially executed and then fail at runtime due to a type error.

The type requirements for each construct are:

Construct Type requirement
if condition boolean
while condition boolean
&&, ||, !, xor inputs boolean
<, >, ==, !=, <=, >= inputs same type, or numeric-compatible (integer and decimal)
+, -, *, /, %, ++, -- inputs integer or decimal
%, ++, -- inputs integer only
String function inputs string
forEach target array; as binding inherits the items type
return value must match the output type declared on the rule, if present

When a variable reference is used as input to any of these constructs, the compiler resolves its declared type and checks it against the requirement. A mismatch is reported as a compile-time error identifying the construct, the expected type, and the actual type.

Example — if condition must be boolean:

{ "var": "score", "type": "integer" }
{
  "if": "$score",
  "then": [{ "return": true }]
}

ERROR: type mismatch — 'if' condition must be 'boolean', got 'integer'

Example — logical operator inputs must be boolean:

{ "var": "age",    "type": "integer" }
{ "var": "active", "type": "boolean" }
{ "&&": ["$age", "$active"] }

ERROR: type mismatch — '&&' expects 'boolean' operands, got 'integer' for first operand

Example — arithmetic inputs must be numeric:

{ "var": "name", "type": "string" }
{ "+": ["$name", 1] }

ERROR: type mismatch — '+' expects numeric operands, got 'string'


Candidate Features for v1

The following features are proposed for inclusion in v1 of the specification. Each item includes a description and example. Items marked with an open decision need a choice before the spec can be finalised.


Rule Metadata

Add id, description, and version fields to the top-level rule object to support tooling, registries, and versioning.

Open decision: Which fields are required vs optional? Recommendation: name required, others optional.

{
  "id": "eligibility-check-us-v1",
  "name": "US Eligibility Check",
  "description": "Returns true if the person is 18 or older and located in the US",
  "version": "1.0.0",
  "input": [
    { "var": "age", "type": "integer" },
    { "var": "country", "type": "string" }
  ],
  "logic": [...]
}

Output Declaration

Add an output field to the top-level rule to declare the return type. Gives consumers and implementors a type contract for what the rule returns.

Open decision: Required or optional?

{
  "name": "Is Eligible",
  "input": [
    { "var": "age", "type": "integer" }
  ],
  "output": { "type": "boolean" },
  "logic": [
    { "return": { ">=": ["$age", 18] } }
  ]
}

Else-If / Chained Conditions

Two options for handling chained conditions without deeply nested if-else blocks.

Open decision: Add explicit elseif key (Option A), or document nested if inside else as the canonical pattern (Option B, no new syntax)?

Option A — elseif key:

{
  "if": { "<": ["$score", 50] },
  "then": [{ "return": "fail" }],
  "elseif": [
    {
      "condition": { "<": ["$score", 70] },
      "then": [{ "return": "pass" }]
    }
  ],
  "else": [{ "return": "distinction" }]
}

Option B — nested if inside else (no new syntax):

{
  "if": { "<": ["$score", 50] },
  "then": [{ "return": "fail" }],
  "else": [
    {
      "if": { "<": ["$score", 70] },
      "then": [{ "return": "pass" }],
      "else": [{ "return": "distinction" }]
    }
  ]
}

Iteration Blocks

forEach

Iterates over each element of an array variable, binding the current element to a named variable for use inside the do block.

The variable named in as is implicitly typed: its type is taken from the items declaration of the array variable referenced in forEach. Operations inside do that are incompatible with that type must produce a type mismatch error at rule compilation time, not at runtime.

For example, if $items is declared as array of date, the rule below must fail to compile because + does not accept date operands:

{ "var": "items", "type": "array", "items": "date" }
{
  "forEach": "$items",
  "as": "item",
  "do": [
    { "$total": { "+": ["$total", "$item"] } }
  ]
}

+ ERROR: type mismatch — '+' expects numeric operands, 'item' is 'date'

A valid usage where $items is declared as array of integer:

{ "var": "items", "type": "array", "items": "integer" }
{ "var": "total", "type": "integer", "=": 0 }
{
  "forEach": "$items",
  "as": "item",
  "do": [
    { "$total": { "+": ["$total", "$item"] } }
  ]
}

while

Repeats the do block while the condition is true. The condition must resolve to boolean — passing any other type is a compile-time error.

{
  "while": { "<": ["$count", 10] },
  "do": [
    { "$count": { "++": "$count" } }
  ]
}

Action Blocks

Action Blocks are the outbound, write counterpart to Data Source Blocks. They use the same External Access Types (http, db, file) but in write mode: sending notifications, invoking webhooks, creating or modifying remote resources, writing audit records, etc.

The key distinction from Data Source Blocks:

  • Data Source Block — reads external data into the rule (inbound). Must not modify state.
  • Action Block — pushes data out from the rule (outbound). Does not return extractable data, only an optional status result.

Open decision: Scope v1 to HTTP only, or include DB and FILE?

HTTP Action

HTTP Actions use methods that modify state (POST, PUT, PATCH, DELETE). Use cases include: invoking a webhook, sending a notification, creating or updating a remote resource, triggering an integration.

{
  "action": "http",
  "method": "POST",
  "url": "https://api.example.com/notifications",
  "headers": {
    "Content-Type": "application/json",
    "Authorization": "$authToken"
  },
  "body": "$payload",
  "result": "$httpResponse"
}
{
  "action": "http",
  "method": "DELETE",
  "url": "https://api.example.com/sessions/$sessionId",
  "result": "$httpResponse"
}

DB Action

{
  "action": "db",
  "operation": "insert",
  "connection": "main",
  "table": "audit_log",
  "values": {
    "rule_id": "$ruleId",
    "outcome": "$result"
  },
  "result": "$insertedId"
}

Variable Scope Rules

Variables declared inside then, else, or do blocks are local to that block. Variables declared in the top-level logic block are accessible from all nested blocks.

{
  "if": "$isAdmin",
  "then": [
    { "var": "level", "type": "integer", "=": 10 }
  ],
  "else": [
    { "var": "level", "type": "integer", "=": 1 }
  ]
}

level in then and level in else are separate variables in separate scopes.


Additional Data Source Access Types

Currently only HTTP GET is specified. Proposed additional access types:

FILE Access Type

The file access type reads data from a local file system path or a remote file server (FTP/SFTP). Supported formats: JSON, CSV, XLSX. The source may be a single file or all files in a directory that match a configurable glob filter.

Field Required Description
path yes (local) Absolute path to a file or directory. Mutually exclusive with remote.
remote yes (FTP/SFTP) Remote connection object. Mutually exclusive with path.
filter no Glob pattern applied to file names when targeting a directory (e.g. "*.csv"). Ignored for single-file paths.
format no Format hint: json, csv, xlsx. Inferred from file extension if omitted.
csv_options no CSV-specific options object. Supported fields: delimiter (string), has_header (boolean).

Remote connection object fields (FTP/SFTP):

Field Required Description
protocol yes ftp or sftp.
host yes Remote host. Accepts $variable.
port no Port. Defaults: FTP = 21, SFTP = 22.
path yes Path to a file or directory on the remote host. Accepts $variable and in-path variable substitution.
credentials no Object with username and password, both accepting $variable.
filter no Glob pattern for directory listings (e.g. "feed_*.csv").

Supported formats:

Format Notes
json Single document or array of documents.
csv Each row becomes an object. Named fields require has_header: true.
xlsx First sheet is read by default.
FILE Examples

Single local JSON file:

{
  "source": "config",
  "type": "JSON",
  "access": {
    "type": "file",
    "path": "/etc/rules/config.json"
  }
}

All CSV files in a local directory:

{
  "source": "monthly_reports",
  "type": "array",
  "access": {
    "type": "file",
    "path": "/data/reports/",
    "filter": "*.csv",
    "format": "csv",
    "csv_options": {
      "delimiter": ",",
      "has_header": true
    }
  }
}

Single XLSX file:

{
  "source": "reference_table",
  "type": "array",
  "access": {
    "type": "file",
    "path": "/data/reference/codes.xlsx",
    "format": "xlsx"
  }
}

Remote SFTP — single file with in-path variable:

{
  "source": "patient_export",
  "type": "JSON",
  "access": {
    "type": "file",
    "remote": {
      "protocol": "sftp",
      "host": "$sftpHost",
      "port": 22,
      "path": "/exports/patients/$patientId.json",
      "credentials": {
        "username": "$sftpUser",
        "password": "$sftpPass"
      }
    }
  }
}

Remote FTP — all CSV files in a folder:

{
  "source": "daily_feeds",
  "type": "array",
  "access": {
    "type": "file",
    "remote": {
      "protocol": "ftp",
      "host": "$ftpHost",
      "path": "/incoming/feeds/",
      "credentials": {
        "username": "$ftpUser",
        "password": "$ftpPass"
      },
      "filter": "feed_*.csv"
    },
    "format": "csv",
    "csv_options": {
      "delimiter": ";",
      "has_header": true
    }
  }
}

DB Access Type

The db access type executes a SQL query against a relational database. The connection can be referenced by a pre-configured name (from the engine's connection registry) or specified inline with full credentials.

Field Required Description
connection yes Named connection reference (string) or inline connection object.
query yes SQL query. Positional placeholders ($1, $2, ...) are replaced by values in params in order.
params no Ordered list of $variable references or literals bound to query placeholders.

Inline connection object fields:

Field Required Description
driver yes Database driver: postgresql, mysql, mssql, oracle, sqlite.
host yes Database host. Accepts $variable.
port no Database port. Defaults to driver default if omitted.
database yes Database or schema name. Accepts $variable.
credentials no Object with username and password, both accepting $variable.
options no Driver-specific key/value options (e.g. encrypt, connection_timeout).
DB Examples

Named connection reference:

{
  "source": "patient_record",
  "type": "object",
  "access": {
    "type": "db",
    "connection": "main",
    "query": "SELECT id, name, birth_date FROM patients WHERE id = $1",
    "params": ["$patientId"]
  }
}

Inline connection with credentials:

{
  "source": "patient_record",
  "type": "object",
  "access": {
    "type": "db",
    "connection": {
      "driver": "postgresql",
      "host": "$dbHost",
      "port": 5432,
      "database": "patients",
      "credentials": {
        "username": "$dbUser",
        "password": "$dbPass"
      }
    },
    "query": "SELECT id, name, birth_date FROM patients WHERE id = $1",
    "params": ["$patientId"]
  }
}

Multi-parameter query:

{
  "source": "recent_encounters",
  "type": "array",
  "access": {
    "type": "db",
    "connection": "clinical_db",
    "query": "SELECT * FROM encounters WHERE patient_id = $1 AND date >= $2 ORDER BY date DESC LIMIT 10",
    "params": ["$patientId", "$cutoffDate"]
  }
}

Inline connection with driver options:

{
  "source": "audit_summary",
  "type": "object",
  "access": {
    "type": "db",
    "connection": {
      "driver": "mssql",
      "host": "$reportingHost",
      "database": "audit",
      "credentials": {
        "username": "$auditUser",
        "password": "$auditPass"
      },
      "options": {
        "encrypt": true,
        "connection_timeout": 30
      }
    },
    "query": "SELECT COUNT(*) AS total FROM events WHERE rule_id = $1",
    "params": ["$ruleId"]
  }
}

Data Source Literal — aggregate and transform Values

The valid values for aggregate and transform are not yet enumerated.

aggregate — controls how to reduce multiple extraction results:

Value Meaning
first Use the first match
last Use the last match
all Return all matches as an array
count Return the number of matches as integer
sum Sum all numeric matches
min Minimum of numeric matches
max Maximum of numeric matches

transform — post-extraction value conversion:

Value Meaning
noop No transformation, use value as-is
toString Convert to string
toInt Parse as integer
toDecimal Parse as decimal
toBoolean Parse as boolean
trim Remove leading/trailing whitespace
toLower Convert to lowercase
toUpper Convert to uppercase

Example using non-default options:

{
  "$totalScore": {
    "source": "@results",
    "extract": { "jsonpath": ["$.scores[*]"] },
    "aggregate": "sum",
    "transform": "noop"
  }
}

jsonpath — Single vs Multi-Path

Currently jsonpath is always an array, but the multi-path semantics (try each path in order, use first non-null) are not defined.

Open decision: Support multi-path array (try each path as a fallback), or simplify to a single string?

Multi-path (try each in order, use first non-null result):

{
  "$sex": {
    "source": "@patient",
    "extract": {
      "jsonpath": ["$.gender", "$.sex", "$.administrativeGender"]
    },
    "aggregate": "first",
    "transform": "noop"
  }
}

Single-path (plain string, simpler):

{
  "$sex": {
    "source": "@patient",
    "extract": {
      "jsonpath": "$.gender"
    },
    "aggregate": "first",
    "transform": "noop"
  }
}

Null / Missing Value Handling

Define behaviour when a variable is unassigned or an extraction returns no match.

Proposal: failed extraction returns null. Operations on null produce a runtime error unless a default is specified.

default field on Data Source Literal:

{
  "$age": {
    "source": "@patient",
    "extract": { "jsonpath": ["$.age"] },
    "aggregate": "first",
    "transform": "toInt",
    "default": 0
  }
}

isNull function:

{
  "if": { "isNull": "$age" },
  "then": [{ "return": false }],
  "else": [{ "return": { ">=": ["$age", 18] } }]
}

Error Handling

Open decision: Rule-level error propagation only (simpler, recommended for v1), or explicit try/catch blocks?

Option A — error propagation (no new syntax): Rule execution stops and returns a structured error object to the caller. Implementors handle it at the host level.

Error object shape:

{
  "error": {
    "code": "DATA_SOURCE_UNAVAILABLE",
    "message": "HTTP GET to patient endpoint returned 503",
    "source": "patient"
  }
}

Option B — try/catch block:

{
  "try": [
    {
      "source": "patient",
      "type": "JSON",
      "access": { "type": "http", "url": "https://my-fhir-server/Patient/1234", "method": "GET" }
    },
    { "$sex": { "source": "@patient", "extract": { "jsonpath": ["$.gender"] }, "aggregate": "first", "transform": "noop" } }
  ],
  "catch": [
    { "return": false }
  ]
}

Variable Naming Rules

Proposed rules for valid variable names:

  • Pattern: [a-zA-Z_][a-zA-Z0-9_]*
  • Case-sensitive: myVar and MyVar are distinct variables
  • Maximum length: 64 characters
  • The $ prefix is a reference marker, not part of the name itself

Valid: age, patient_id, isActive, _temp, x1

Invalid: 1count (starts with digit), my-var (hyphen not allowed), my var (space not allowed)


Type Coercion Rules

Open decision: Strict typing (no implicit coercion) or permissive (automatic widening)?

Recommendation for v1: strict typing. No implicit coercion. Assigning a decimal to an integer variable is a runtime error. Use explicit transform in Data Source Literals or string conversion functions for type changes.

{ "var": "age", "type": "integer", "=": 3.14 }

The above must produce a runtime error, not silently truncate to 3.


Custom / Extensible Access Types

Status: Discussion only. Not a hard requirement for v1. The intent is to explore whether the access type system should be open to user-defined extensions and what the implementation contract would look like.

Criterion's built-in access types (http, db, file) cover common integration patterns. Real-world deployments often need domain-specific transports: FHIR-specific clients with built-in capability negotiation and SMART on FHIR auth, proprietary EHR connectors, or other specialized protocols. Rather than extending the core spec for every integration, the access type system could be made extensible: users define a custom type name in the rule, then provide a corresponding implementation in code.

Note that some custom types are thin wrappers over a built-in type — for example, a fhir access type is essentially HTTP with FHIR-specific defaults and SMART on FHIR authentication baked in. Extensibility is still useful in these cases because it encapsulates the specialization and keeps rules concise.

Concept

A custom access type is identified by a user-defined name. The rule engine resolves that name to a registered implementation at startup. The access object is passed as-is to the implementation — the engine does not interpret its fields beyond variable resolution.

{
  "source": "patient",
  "type": "JSON",
  "access": {
    "type": "fhir",
    "server": "$fhirBaseUrl",
    "resource": "Patient",
    "id": "$patient_id",
    "auth": {
      "type": "smart",
      "client_id": "$clientId",
      "client_secret": "$clientSecret",
      "token_url": "$tokenUrl"
    }
  }
}

Implementation Contract

An implementor extends a base class or implements an interface provided by the engine SDK. The contract covers two operations, one per direction:

Method Direction Used by
fetch(accessConfig, resolvedVars) inbound Data Source Block
send(accessConfig, resolvedVars, payload) outbound Action Block

The engine handles variable resolution (substituting $variable references in accessConfig before calling the implementation), result caching, and error propagation. The implementation handles only the transport and returns a raw value or throws a typed error.

Discovery and Registration

Two registration strategies are common:

  1. Explicit registration — the engine configuration maps custom type names to implementation classes. Simple and predictable.
  2. Service discovery — the engine scans a plugin directory or classpath for implementations that declare their type name via metadata (e.g. Java SPI, Python package entry points). Enables drop-in deployment without modifying engine configuration.

Type Naming

Open question: Should custom type names live in a reserved namespace to avoid collisions with future built-in types, or use plain strings registered at startup?

  • Option A — plain name ("type": "fhir"): simpler to write, but a future built-in named fhir would conflict.
  • Option B — namespaced ("type": "ext:fhir"): unambiguous, slightly more verbose.

Compile-Time Validation

If the engine has access to a schema for the custom type's access object at compile time (e.g. a JSON Schema), it can validate the rule at compile time, matching the behaviour of built-in types. Without a schema, validation is deferred to execution time and errors surface as runtime failures.

About

Criterion is a Generic Rule Engine. This is the public specification of the rule format.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors