Multi-protocol flows
A multi-protocol flow drives more than one wire protocol from a single FSM on a single UE. Use it to model end-to-end procedures that span the radio plane and the policy plane simultaneously — for example, attaching a UE on NGAP while an MME concurrently fetches subscription data over S6a Diameter. This guide covers when to reach for multi-protocol flows, how to wire them up in YAML, and how to configure the environment so each protocol's transport is reachable.
When to use one
Most flows are single-protocol: NGAP gNB-side, Diameter S6a MME-side, SBI client/server. Reach for a multi-protocol flow when the test really needs cross-protocol semantics on one UE — when correctness depends on the order of arrivals across protocols, or when one UE's state evolves under inputs from multiple protocols.
The shipped reference is multinf/ngap_plus_diameter.yaml (flow name multinf_ngap_plus_diameter) — the gNB starts NGAP registration and an MME-side S6a ULR in the same transition, then waits for either response to land first and continues only when both have echoed back. Read the full, un-elided flow in the templates repository.
The engine resolves -flow by name, not by path, so invoke the reference with its flow name:
Anatomy of a multi-protocol transition
The action executor groups frames by protocol and posts each group to <protocol>.tx.enrich on the bus, so both sends fan out to their respective transports in parallel.
The flow's top-level protocol: and nf: declare the primary protocol — the one the FSM uses for inbound demux, and the NF the UE attaches to. Cross-protocol sends override per-action via protocol:. UE state (subscriber, ContextID) is shared — both protocol pipelines see the same ue.* fields.
Routing inbound events
Each protocol's RX pipeline emits events into the bus tagged with the protocol. The FSM's event: clause can match either bare names (ULA) or protocol-prefixed names (diameter.ULA); the matcher strips one leading prefix on either side. The recognized prefixes are ngap., diameter., and sbi. — any other prefix is left intact and matched literally. Use the prefixed form when the same suffix is ambiguous across protocols:
Sharing UE state
Both pipelines see one UE per execution. Examples of cross-protocol state sharing already in the shipped flows:
- The Diameter S6a User-Name AVP populates from the UE's IMSI, which the NGAP NAS pipeline also consumes.
- A
checkonResult-Codein a Diameter-side transition reads off the most recent inbound Diameter message; the NGAP pipeline doesn't see it. - An
extractaction stores intoue.Params[<key>], which any latersendfrom any protocol can reference via{{ue.Params.<key>}}.
Configuring the environment
A multi-protocol flow needs the environment to declare every transport it touches. Use config/lab-multinf.yaml as the starting point — the excerpt below shows the two NFs this flow exercises:
Two NFs (gnb-1, mme-1) on two transports — one NGAP client, one Diameter S6a peer. The flow's primary nf: gnb matches gnb-1; the per-action protocol: diameter send routes through whichever NF in the environment has a Diameter transport. The full config/lab-multinf.yaml also declares a second gNB (gnb-2, sharing ngap-out) and a SBI UDM (udm-1); this flow ignores both.
If your environment declares more than one NF per protocol (e.g. two MMEs on two different Diameter transports), use peer: on the action to disambiguate:
Ordering and timing
The action executor dispatches sends concurrently — both protocol pipelines start enriching at the same time. The order in which messages hit the wire depends on each protocol's own latency. The FSM tolerates either response landing first by branching at wait_first into wait_ngap_only or wait_diameter_only.
If your test requires strict ordering, drive sends sequentially across multiple transitions instead of bundling them into one.
Troubleshooting
Send dispatched on the wrong transport — check the action's protocol: field. Without it, the dispatcher uses the enricher's registered protocol (every shipped enricher is registered to exactly one protocol).
event %q didn't match any transition for a Diameter message — the matcher strips one prefix on either side. event: diameter.ULA matches a bus event of either diameter.ULA or ULA; mismatched casing or a typo in the event name won't match.
Cross-protocol race makes the test flaky — split the burst into separate transitions so each protocol's response lands before the next send fires. Acceptable when you don't actually need true concurrency — most cross-protocol tests don't.
One protocol's transport fails to start — environment validation catches missing transports: blocks, but a bad peer address fails at first dial. Run d3x-run check -c <env> to surface every transport's reachability before launching the flow.
Where to go next
- Flows — how a single-protocol FSM is structured, before you span two.
- Configuring environments — full NF and transport reference for the YAML above.
- Flow schema — every action and transition field, including
protocol:andpeer:.