Chapter 04

Sub-Rules

Any node can carry a subRuleCall. The engine resolves the sub-rule, builds an isolated input from inputMapping, runs it, and routes the result back through outputMapping.

Anatomy of a call

{
  "id": "n5-tier",
  "type": "ruleRef",
  "position": { "x": 800, "y": 80 },
  "data": {
    "label":    "Tier uplift",
    "category": "ruleRef",
    "subRuleCall": {
      "ruleId":        "rule-tier-bonus",
      "pinnedVersion": 1,                            // or "latest"
      "inputMapping":  { "pax": "$.pax" },
      "outputMapping": { "ctx.tierUplift": "result.bonusPieces" },
      "onError":       "default",                      // skip | fail | default
      "defaultValue":  { "bonusPieces": 0, "bonusKg": 0 }
    },
    "writesContext": ["tierUplift"]
  }
}

What happens when the call fires

  1. Resolve. Engine asks IRuleSource.GetByIdAsync(ruleId, pinnedVersion). "latest" reads currentVersion from the rule header.
  2. Build the sub-request. For each { subInputKey: parentJsonPath } entry, resolve the path against the parent's request (or $ctx.X against parent context). Each resolved value becomes a top-level field of a fresh JSON object — that's the sub-rule's request.
  3. Run. The sub-rule walks its own DAG with a fresh, empty execution context. It cannot see the parent's ctx unless the parent explicitly forwarded it via inputMapping.
  4. Map the result. For each { parentTarget: subRulePath } entry, resolve subRulePath against the sub-rule's full envelope (so result.X reads the sub-rule's result.X). Targets prefixed with ctx. write parent context; everything else accumulates onto the host node's output object.
  5. Run the host node. Whatever logic the host node has runs after the sub-rule, with subRuleResult available as the host's output for ruleRef nodes.
  6. Trace. The host node's trace entry carries subRuleRunId, plus ctxWritten showing what the mapping put into context.

onError semantics

The sub-rule's envelope decision drives the next step:

Sub-rule decisionBehavior
applyMap result.* through outputMapping. Host node continues normally.
skip or errorRoutes through onError — see below. Note: the brief implies onError fires only on errors; the engine treats skip the same so authors get a single knob.
onErrorWhat the engine does
skipNo writes. Host node continues with no subRuleResult.
failHost node trace gets outcome: error; envelope decision: error; engine halts.
defaultdefaultValue is mapped through outputMapping as if it were the sub-rule's result. Host node continues.

Worked example

Parent: bag policy. Sub-rule: tier bonus. Goal: bag pieces = 2 base + bonus from tier.

The sub-rule (rule-tier-bonus@1)

{
  "id": "rule-tier-bonus",
  "endpoint": "/v1/ancillary/tier-bonus",
  "method": "POST",
  "currentVersion": 1,
  "nodes": [
    { "id": "in",    "data": { "category": "input" } },
    { "id": "tier",  "data": {
        "category": "filter", "templateId": "sys-filter-str",
        "config": {
          "source":  { "kind": "request", "path": "$.pax[*].tier" },
          "compare": { "operator": "in", "values": ["GOLD", "PLAT", "IO"] },
          "arraySelector": "any", "onMissing": "fail"
        }
      }
    },
    { "id": "bonus", "data": {
        "category": "constant",
        "config": { "value": { "bonusPieces": 1, "bonusKg": 5 } }
      }
    },
    { "id": "out",   "data": { "category": "output" } }
  ],
  "edges": [
    { "source": "in",    "target": "tier",  "branch": "default" },
    { "source": "tier",  "target": "bonus", "branch": "pass"    },
    { "source": "bonus", "target": "out",   "branch": "default" }
  ]
}

Parent invocation (excerpt)

// In the parent rule's nodes:
{
  "id": "n5-tier", "type": "ruleRef",
  "data": {
    "category": "ruleRef",
    "subRuleCall": {
      "ruleId": "rule-tier-bonus", "pinnedVersion": 1,
      "inputMapping":  { "pax": "$.pax" },
      "outputMapping": { "ctx.tierUplift": "result.bonusPieces" },
      "onError": "default",
      "defaultValue": { "bonusPieces": 0 }
    }
  }
}

Trace, GOLD pax

{
  "nodeId": "n5-tier",
  "outcome": "pass",
  "output": { "bonusPieces": 1, "bonusKg": 5 },
  "ctxWritten": { "tierUplift": 1 },
  "subRuleRunId": "srr-rule-tier-bonus-237a468842ed4ffbb1bd6b54fb924599"
}

Trace, BLUE pax (sub-rule skips → default applies)

{
  "nodeId": "n5-tier",
  "outcome": "pass",
  "ctxWritten": { "tierUplift": 0 },
  "subRuleRunId": "srr-rule-tier-bonus-…"
}

Authoring tips