Transfer template¶
A transfer template chains multiple Rail firings into one
business-meaningful Transfer. The classic example: an
"ACH origination cycle" template fires three legs — debit the
customer DDA, credit the suspense holding GL, then debit suspense and
credit the Federal Reserve master account — and the template
guarantees those three legs happen together, with a deterministic
expected_net close-out.
A template has:
name— unique identifier referenced fromTransaction.template_nameso any leg of a templated bundle can be traced back to its parent template.leg_rails— ordered list of Rail names declaring which rails fire as part of the template. Each leg of a firing posts via the named Rail.expected_net— the L1 Conservation invariant for this template: the sum of every non-Failed leg's signedamount_moneyMUST equal this value. Most templates use0(debit + credit pair nets to zero); a few use a non-zero value when an external system contributes the offsetting side (e.g. anExternalForcePostedleg lands a credit the institution's books don't include).leg_rail_xor_groups(optional, AB.3) — declares mutually-exclusive subsets ofleg_rails. For each group, exactly one member fires per Transfer (the alternative variants share a closure slot but the institution's runtime picks one per cycle). The classic case is a multi-mode settlement:[SettlementAuto, SettlementStandard, SettlementSlow]— same closure, three possible cadences picked per-merchant. Each group needs ≥2 members, all members must be inleg_rails, all must be Variable-direction SingleLegRails (validator C1a-d). The "exactly-one-fires" runtime check surfaces on the_xor_group_violationmatview: 0 firings ⇒ missed (the template didn't close); ≥2 firings ⇒ overlap (the runtime double-posted).
Templates are how the L1 dashboard knows a multi-leg cycle is "open"
— a leg has fired but the close-out leg hasn't yet, so the running
sum doesn't equal expected_net. Stuck templates surface in the
Pending Aging + Unbundled Aging matviews depending on which leg's
late.
Multi-mode templates (AB.3): one closure, several variants¶
Real-world banking is rarely uniform. The same merchant
settlement might be authorized at three speeds — auto (intraday
sweep), standard (T+1), slow (weekly batch) — driven by the
merchant's contract tier. The institution treats them as the
same closure event (the expected_net is the same; the
transfer_key is the same merchant cycle), but exactly ONE
variant fires per cycle.
leg_rail_xor_groups is how you express this. Declare each
variant as its own Variable-direction SingleLegRail
(SettlementAuto / SettlementStandard / SettlementSlow),
list them all in leg_rails, then group the ones that compete:
leg_rail_xor_groups: [[SettlementAuto, SettlementStandard,
SettlementSlow]]. The runtime picks one per cycle; the
_xor_group_violation matview flags the bug cases (no variant
fired, or ≥2 fired) without you having to write the check
manually.
You can declare multiple groups on one template — e.g. one group for settlement timing + one group for fraud-review depth. Each group is independent; the constraint is "exactly one member of each group fires", and any rail can be in at most one group (validator C1c).
The L1 Pending Aging + Unbundled Aging bar charts stack their
counts by rail_name so each variant becomes a distinct color
band — "the slow variant is dragging" reads off the chart
directly. The topology diagram renders the group as a nested
sub-cluster inside the template cluster, labeled "XOR group N
(exactly 1 fires)", so the mutual-exclusion contract is visible
on the diagram pane without leaving the topology.
Don't reach for a Transfer Template just because two rails can fire together. Use it when the rails MUST fire together as one business event — the template's
expected_netis the binding close-out invariant. If two rails are independent, model them as two rails with a chain edge instead.