Chapter 02

Rule Schema

A rule is a directed acyclic graph of nodes connected by branch-tagged edges. The engine reads a rule from JSON, validates it, and walks it once per request.

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.

CategoryPurposeOutput
inputEntry point. Engine validates exactly one per rule.The request, untouched.
outputExit point. Assembles the envelope's result from upstream.(see assembly)
filterString / number / date predicate. Emits a verdict.None — verdict drives edge routing.
logicand / or / xor / not over upstream verdicts.None — verdict only.
productEmits a structured object (e.g. a bag, a seat, a meal).data.config.output with ${ctx.X} resolution.
mutatorSet-property or lookup-and-replace on the upstream object.Upstream object with one field changed.
calcExpression node (NCalc) — arithmetic, comparison, conditionals.Computed scalar, or upstream object with target replaced.
constantEmits a literal value.data.config.value.
ruleRefWrapper around a sub-rule call.The sub-rule's result.
referenceReference-set lookup (read-only).Reserved — handled today via mutator.
sql / apiExternal 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:

VerdictMatches edges with branch =
passpass · default · null
failfail · default · null
skipdefault · null
errornothing — 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):

TokenMeaning
$Root of the request (or context, when source.kind === 'context')
.keyProperty access
['key']Quoted property access (handles dots in keys)
[N]Numeric array index (negative or out-of-range = nothing)
[*]Wildcard over array elements
$ctx.fooShorthand 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:

  1. ruleversions[{id: "rv-{ruleId}-{v}"}] with the full snapshot
  2. environments[envName].ruleBindings[ruleId] = version
  3. rules[ruleId] with matching endpoint + method

Missing any of these → engine returns 404 or skips the endpoint at boot.