Criterion is a Generic Rule Engine. This is the public specification of the rule format.
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 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. |
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" }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.
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 (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"] } }
]
}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 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.
The values that can be assigned or used as function parameters are called Argument. Arguments can have four types:
- Literal: represents a simple constant value of a certain type like the integer
2or the string"hellow world". - Value Reference: represents the reference to an existing variable by its name, and always starts with
$, for instance$myVar. - Return Literal: represents the value returned by a function, and which can be assigned to a variable.
- Data Source Literal: represents a value that is extracted and retrieved from a Data Source (JSON, XML, etc).
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.
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
}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"
}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]
}
}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"
}
}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.
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
}This example declares the variable myVar and assigns the value of anotherVar to it.
{
"var": "myVar",
"type": "integer",
"=": "$anotherVar"
}This example declares the variable myVar and assigns the value returned by the + function to it.
{
"var": "myVar",
"type": "integer",
"=": {
"+": ["$anotherVar", 2]
}
}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"
}
}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.
In this example, the rule would return a boolean value true.
{
"return": true
}In this example the rule would return the current value of anotherVar.
{
"return": "$anotherVar"
}
{
"return": {
"<": ["$value1", 1000]
}
}This example returns a value extracted from the patient Data Source.
{
"return": {
"source": "@patient",
"extract": {
"jsonpath": [
"$.gender"
]
},
"aggregate": "first",
"transform": "noop"
}
}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"
}
]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
}
]
}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"
}
}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. |
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. |
$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.
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.
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 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 functions are boolean functions (return true or false), and have boolean Arguments as input.
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]
}Returns true if at least one of the two input Arguments is true, otherwise returns false.
{
"||": [true, false]
}Returns true if the input Argument is false, and false if the input Argument is true. Takes exactly one boolean Argument.
{
"!": true
}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]
}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'
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]
}Returns true if the value of the first Argument is greater than the value of the second Argument, otherwise returns false.
{
">": [1, 2]
}Returns true if both Arguments represent the same value. Both Arguments must be of the same type.
{
"==": [1, 2]
}Returns true if the Arguments have different values or are of different types, otherwise returns false.
{
"!=": [1, 2]
}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]
}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 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
decimaltype of the host platform (e.g.BigDecimalin Java,Decimalin Python), not as IEEE 754 floating-point. This guarantees exact decimal arithmetic and avoids precision loss.
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"
}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"
}Returns the sum of two numeric Arguments.
{
"+": [1, 2]
}Returns the result of subtracting the second Argument from the first.
{
"-": [1, 2]
}Returns the result of dividing the first Argument by the second. Always returns decimal. The second Argument must not be zero.
{
"/": [1, 2]
}Returns the product of two numeric Arguments.
{
"*": [1, 2]
}Returns the remainder of dividing the first Argument by the second. Both Arguments must be integer.
{
"%": [1, 2]
}String functions operate on string Arguments. Unless otherwise noted, they return a string.
Returns a new string by joining two or more string Arguments together.
{
"concat": ["$firstName", " ", "$lastName"]
}Returns the number of characters in a string as an integer.
{
"length": "$description"
}Returns the string with leading and trailing whitespace removed.
{
"trim": "$rawInput"
}Returns the string with all characters converted to uppercase.
{
"toUpper": "$code"
}Returns the string with all characters converted to lowercase.
{
"toLower": "$email"
}Returns true if the first string Argument contains the second string Argument as a substring, otherwise returns false.
{
"contains": ["$body", "error"]
}Returns true if the first string Argument starts with the second string Argument, otherwise returns false.
{
"startsWith": ["$url", "https"]
}Returns true if the first string Argument ends with the second string Argument, otherwise returns false.
{
"endsWith": ["$filename", ".json"]
}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]
}Returns a new string with all occurrences of the second Argument replaced by the third Argument.
{
"replace": ["$template", "{name}", "$userName"]
}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.
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'
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.
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": [...]
}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] } }
]
}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" }]
}
]
}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"] } }
]
}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 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 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"
}{
"action": "db",
"operation": "insert",
"connection": "main",
"table": "audit_log",
"values": {
"rule_id": "$ruleId",
"outcome": "$result"
},
"result": "$insertedId"
}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.
Currently only HTTP GET is specified. Proposed additional access types:
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. |
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
}
}
}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). |
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"]
}
}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"
}
}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"
}
}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] } }]
}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 }
]
}Proposed rules for valid variable names:
- Pattern:
[a-zA-Z_][a-zA-Z0-9_]* - Case-sensitive:
myVarandMyVarare 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)
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.
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.
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"
}
}
}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.
Two registration strategies are common:
- Explicit registration — the engine configuration maps custom type names to implementation classes. Simple and predictable.
- 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.
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 namedfhirwould conflict. - Option B — namespaced (
"type": "ext:fhir"): unambiguous, slightly more verbose.
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.