Skip to content
Docs / dflux Runner
ContactGet started

Actions & checks

The action types a transition can run — send, check, extract, and the rest — and the check operators that gate a procedure.

Actions are what runs inside a transition. When a transition fires, its actions list executes top to bottom, then the flow advances to the transition's target state. A transition with no actions is legal — it just moves state.

This page is the conceptual tour. The exact field tables live in the flow schema reference.

A transition in three acts

The everyday transition reads the inbound message, then answers it. Order matters: check what arrived, extract anything later steps need, then send the reply.

YAML

That ordering is not a convention — the engine enforces it. A check or extract that appears after a send in the same list is rejected at flow-load, because once you have sent, the inbound message under inspection is no longer the latest frame. Read first, write last.

Action types

The first three are universal — nearly every flow uses them. The rest are specialised. cmd is off unless the process was started with -allow-cmd-actions.

TypeWhat it doesTypical position
checkAssert a field on the most recent inbound message (or on UE state)Before any send
extractRead a field off the inbound message into UE state for later useBefore any send
sendEmit a message on the wireAfter checks and extracts
parallel_sendFire N copies of one send concurrently and tally responses in-flowThe only send-type action in that transition
uplane_startArm the user-plane traffic generator once a PDU session is upBetween sends
ngap_reallocNGAP only — reallocate the RAN-UE-NGAP-ID before the next sendBetween sends
ngap_handover_swapNGAP only — move the UE binding from source to target gNBBetween sends
cmdRun a local program named by argv (no shell). Off by default.Between sends

check — assert before you send

A check evaluates a field against an operator. The field resolves from the most recent inbound message, or — when the path is prefixed ue. — from the UE's own context. A failed check aborts the transition: the flow stays in the current state and waits for a later matching event or for on_timeout. This is how negative tests assert that a peer rejected a malformed request with the right cause.

YAML

Both field and op are required on every check. Field paths are case-sensitive: wire-message fields use lowercase-with-underscores (amf_ue_ngap_id), while UE-context fields are PascalCase under the ue. prefix (ue.AmfUeNgapId).

Check operators

Per-step check actions accept these operators. Most need an expected value; the presence tests do not. Dump the live list with d3x-run vocab.

OperatorNeeds expectedPasses when
equalsyesField equals expected (compared as strings)
not_equalsyesField's string form differs from expected
not_emptynoField resolves and is non-nil, non-empty, non-zero
greater_thanyesField > expected (numeric)
less_thanyesField < expected (numeric)
greater_or_equalyesField ≥ expected (numeric)
less_or_equalyesField ≤ expected (numeric)
containsyesField's string form contains the expected substring
not_containsyesField's string form does not contain the expected substring
existsnoField resolves at all, whatever its value
regexyesField's string form matches the RE2 pattern in expected
in / oneofyesField's string form is a member of the expected set (oneof is an alias of in)
lengthyesLength of the resolved value equals expected (arrays, maps, strings)
ie_presentnoField resolves and its value is non-nil/non-empty (stricter than exists)
Note
exists and not_empty are not the same. exists only asks whether the AVP or field is present; not_empty additionally requires the value to be non-zero. Use exists when an empty or zero value is still a valid presence assertion.

The expected value can itself be a {{...}} template, so a check's right-hand side can come from a field extracted earlier in the same flow — for example expected: "{{ue.Params.pre_count}}".

extract — bridge inbound to outbound

extract reads a field off the most recent inbound message and stores it under ue.Params[<key>]. Later sends reference it through {{ue.Params.<key>}} templates. This is how a procedure correlates a request with a later one — a session ID, a transaction ID, an authentication challenge — declaratively, without writing Go.

There are two forms. The shorthand handles a single field; the list form bundles several extracts into one action.

YAML

Each entry needs a store and exactly one source: a field path to resolve, or a value template expression to evaluate. The two are mutually exclusive. A value entry lets you store a derived result — for instance, decoding a token field and keeping the result:

YAML

Like check, extract must precede sends in the actions list.

send — emit a message

A send puts a message on the wire. The message field names a registered enricher — a built-in that fills protocol-specific fields from UE state. You reference enrichers by name; you never write them. message is the only required field on a send.

YAML

Optional fields refine the send:

FieldRequiredPurpose
messageyesRegistered enricher name
message_bodynoJSON pre-filled onto the typed message struct before the enricher fills the rest
paramsnoPer-action params overlay applied after the enricher runs
protocolnoProtocol override — used in multi-protocol flows
peernoTarget peer name within the protocol

message_body is how you author negative tests: set one field to a malformed value and let the enricher fill the rest normally. Both message_body JSON values and params values accept {{...}} templates, compiled at flow-load so unknown function names and arity errors fail before the first send.

Specialised actions

These action types exist for procedures the read-check-send pattern does not cover. uplane_start, ngap_realloc, ngap_handover_swap, and cmd are allowed between sends. parallel_send cannot mix with a regular send in the same transition.

uplane_start — arm the traffic generator

uplane_start bridges signalling to user plane. It runs after a PDU session is established and arms the configured traffic generator using parameters surfaced by the inbound PduSessionResourceSetupRequest and the gNB's uplane: config block. When the run finishes, the engine fires a synthetic UplaneComplete event so the FSM can advance.

YAML

See User plane and the user-plane testing guide for the receiver setup.

ngap_realloc — renumber the UE on the same gNB

NGAP only. Reallocates the UE's RAN-UE-NGAP-ID before the next send, exercising AMF tracking when a gNB renumbers a UE on the same node.

ngap_handover_swap — move the UE between gNBs

NGAP only. After a HandoverCommand arrives, swaps the UE's gNB binding from source to target so the next send uses the target gNB's transport.

parallel_send — fire N copies of one send

Sends the same message count times, with at most concurrency in flight (0 = unbounded, capped at count). It is the only send-type action allowed in that transition — you cannot mix it with send.

cmd — run a local program

Runs argv as a list (no shell, no word-splitting) and waits for it. Off unless the process was started with -allow-cmd-actions. Use it to poke an admin CLI from inside a transition; do not turn the flag on for a daemon attached to a console you do not fully trust. Field tables live in the flow schema.

What you don't write

Notably absent: no wait, no sleep, no loop, no if/else. The state machine handles those structurally. Waiting is a state; conditional branching is multiple transitions on different events; looping is a self-targeting transition. If a flow needs an explicit retry counter, keep it as a UE param with extract and gate on it with a check.

Where to go next