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
- Resolve. Engine asks
IRuleSource.GetByIdAsync(ruleId, pinnedVersion)."latest"readscurrentVersionfrom the rule header. - Build the sub-request. For each
{ subInputKey: parentJsonPath }entry, resolve the path against the parent's request (or$ctx.Xagainst parent context). Each resolved value becomes a top-level field of a fresh JSON object — that's the sub-rule's request. - 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. - Map the result. For each
{ parentTarget: subRulePath }entry, resolvesubRulePathagainst the sub-rule's full envelope (soresult.Xreads the sub-rule'sresult.X). Targets prefixed withctx.write parent context; everything else accumulates onto the host node's output object. - Run the host node. Whatever logic the host node has runs after the sub-rule, with
subRuleResultavailable as the host's output forruleRefnodes. - Trace. The host node's trace entry carries
subRuleRunId, plusctxWrittenshowing what the mapping put into context.
onError semantics
The sub-rule's envelope decision drives the next step:
| Sub-rule decision | Behavior |
|---|---|
apply | Map result.* through outputMapping. Host node continues normally. |
skip or error | Routes 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. |
onError | What the engine does |
|---|---|
skip | No writes. Host node continues with no subRuleResult. |
fail | Host node trace gets outcome: error; envelope decision: error; engine halts. |
default | defaultValue 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
- Mappings are uni-directional:
inputMappingreads parent → builds sub request;outputMappingreads sub envelope → writes parent. Don't try to use one for the other direction. - Forward parent ctx explicitly. If the sub-rule needs
ctx.tier, route it viainputMapping: { tierFromParent: "$ctx.tier" }. Sub-rules never inherit context. - Use
defaultValuefor graceful degradation. Pair withonError: defaultwhen the parent should still produce a sensible result without the sub-rule's contribution. - Pin versions at publish.
pinnedVersion: 1immortalizes the sub-rule snapshot for that parent version."latest"follows the sub-rule forever — use sparingly. - Validate at publish time. The engine refuses cycles at run time, but the admin app should also catch
A → calls B → calls Aat publish.