Top level
{
"id": "rule-bag-policy",
"name": "Bag policy · Economy LHR–DXB",
"description": "…",
"endpoint": "/v1/ancillary/bag-policy",
"method": "POST",
"status": "published",
"currentVersion": 7,
"inputSchema": { /* JSON Schema for the request */ },
"outputSchema": { /* JSON Schema for the envelope */ },
"contextSchema": { /* optional — declared ctx keys */ },
"nodes": [ /* see below */ ],
"edges": [ /* see below */ ],
"updatedAt": "2026-04-27T16:00:00.000Z",
"updatedBy": "andrew"
}
Nodes
Every node has the same outer shell:
{
"id": "n5-bag",
"type": "product", // React Flow type — engine ignores
"position": { "x": 800, "y": 170 },
"data": {
"label": "Bag · base",
"category": "product", // authoritative for the engine
"templateId": "cus-bag",
"config": { /* category-specific shape */ },
"subRuleCall": { /* optional — see sub-rules */ },
"readsContext": ["tierUplift"],
"writesContext":[]
}
}
Node categories
Eleven categories cover the runtime surface. The engine dispatches on data.category; data.type is a React Flow concern.
| Category | Purpose | Output |
|---|---|---|
input | Entry point. Engine validates exactly one per rule. | The request, untouched. |
output | Exit point. Assembles the envelope's result from upstream. | (see assembly) |
filter | String / number / date predicate. Emits a verdict. | None — verdict drives edge routing. |
logic | and / or / xor / not over upstream verdicts. | None — verdict only. |
product | Emits a structured object (e.g. a bag, a seat, a meal). | data.config.output with ${ctx.X} resolution. |
mutator | Set-property or lookup-and-replace on the upstream object. | Upstream object with one field changed. |
calc | Expression node (NCalc) — arithmetic, comparison, conditionals. | Computed scalar, or upstream object with target replaced. |
constant | Emits a literal value. | data.config.value. |
ruleRef | Wrapper around a sub-rule call. | The sub-rule's result. |
reference | Reference-set lookup (read-only). | Reserved — handled today via mutator. |
sql / api | External callouts. | Reserved — not in 0.1.0. |
Edges
{
"id": "e5",
"source": "n4-and",
"target": "n5-bag",
"branch": "pass" // pass | fail | default — see routing
}
Edge routing
Filter and logic nodes emit one of four verdicts: pass, fail, skip, error. Edges are tagged with one of three branches. Routing rule:
| Verdict | Matches edges with branch = |
|---|---|
pass | pass · default · null |
fail | fail · default · null |
skip | default · null |
error | nothing — engine halts, envelope decision: error |
Authors should never set branch: "skip" — there is no such enum.
Filter config (the most important shape)
All three filter flavors share the same outer structure. Every filter node's data.config must inline this shape — legacy flat configs ({path, operator, …}) are refused.
{
"source": {
"kind": "request", // request | context | literal
"path": "$.cabin" // JSONPath subset
},
"compare": {
"operator": "equals", // see Evaluators page for full list
"value": "Y",
"caseInsensitive": true,
"trim": true
},
"arraySelector": "first", // any | all | none | first | only
"onMissing": "fail" // fail | pass | skip
}
JSONPath subset
RuleForge implements a small, predictable subset (no full JSONPath spec):
| Token | Meaning |
|---|---|
$ | Root of the request (or context, when source.kind === 'context') |
.key | Property access |
['key'] | Quoted property access (handles dots in keys) |
[N] | Numeric array index (negative or out-of-range = nothing) |
[*] | Wildcard over array elements |
$ctx.foo | Shorthand to read parent context regardless of source.kind |
Output node assembly
The output node has its own resolution order — see the Evaluators page for the complete logic.
Required at publish
For the engine to bind an endpoint, three documents must exist in DocumentForge:
ruleversions[{id: "rv-{ruleId}-{v}"}]with the full snapshotenvironments[envName].ruleBindings[ruleId] = versionrules[ruleId]with matchingendpoint + method
Missing any of these → engine returns 404 or skips the endpoint at boot.