- Purpose — what the field controls.
- Engine behavior — exactly what the engine does when this value changes.
- Verification — how to prove the field had an effect (test script, doc cross-link, or live API call).
Phase 1 — Narrow
Inventory node
Match Creatives node
Enrich node
Qualify node
Contact Policy node
Implicit Contact Policy (#169 — engine-injected)
If a flow has no explicitcontact_policy node and the flow’s skipContactPolicy flag is not true, the engine injects an implicit contact_policy node with mode: "all" at the end of Phase 1. This is a safety rail — frequency caps, DNC, and cooldowns must apply by default. The implicit injection is centralized in injectImplicitContactPolicy() and unit-tested at src/lib/__tests__/implicit-contact-policy.test.ts.
Filter node
Conditional (split) node
Phase 2 — Score & Rank
Score node — the most-configurable node in the flow
See Scoring Strategies for the operator-facing decision guide. Reference of every field:Engine-wide settings that affect Score behavior
These live onTenant.settings and apply to every flow’s Score node:
Algorithm models (referenced from modelKey)
Optimize node (deprecated — folded into Score’s strategyProfileId)
The Optimize node is kept for back-compat and now acts as a passthrough. UseScore.strategyProfileId instead.
Rank node
At cold-start (no interaction data yet),
propensity/formula scoring returns a flat 0.5 for every candidate. The rank node breaks these ties by descending Offer.priority, so results still order by business priority rather than arbitrarily. See How Scoring Works.Group node
Rank and Group are mutually exclusive. A flow may contain a
rank node or a group node, not both — the pipeline validator rejects a flow with both (RANK_AND_GROUP_CONFLICT, pipeline-validator.ts). Use rank for single-placement top-N; use group for multi-placement allocation across zones. (A rank node upstream of group throttles the candidate pool and starves placements.)Channel coupling — the supported atomicity lever
Within-channel atomic coupling replaces the deprecatedallowPartial. After the Group node allocates candidates to placements, applyChannelCoupling() (pipeline-runner.ts:503) runs a post-allocation pass:
Cross-channel coupling is intentionally not supported — different channels are independent attention surfaces. Cross-channel suppression comes only from
do_not_contact (unconditional), the consent stage, and cross_channel_cap (by volume).
Phase 3 — Output
Compute node — runs in any phase
The compute executor doesn’t care about phase. What changes is which candidate set the formulas evaluate against. See Computed Values for formula syntax.
Placement choice (T19):
- Phase 1 (after Enrich) → applies to every candidate that survived Qualify + Contact Policy. Use this when a downstream node (Score, Filter) needs to read the computed value.
- Phase 3 (after Rank/Group) → applies only to the final top-K. Use this for render-only personalization (greetings, computed credit limits, conditional CTA text).
Set Properties node
Response node
Cross-phase nodes
Call Flow node
A
call_flow node must sit in Phase 1 or Phase 2 — the validator rejects it in Phase 3 (CALL_FLOW_WRONG_PHASE). Sub-flow nesting is capped at depth 2 and circular references are rejected (CALL_FLOW_MAX_DEPTH / CALL_FLOW_CIRCULAR, validateCallFlowDepth).Where that is enforced: on POST and PUT to /api/v1/decision-flows, which return 422 naming the offending path (A → B → A). The check is transitive — a flow is refused when a cycle is reachable through anything it calls, not only when it closes the loop itself. Since a target must exist before another flow can point at it, a cycle is nearly always closed by an edit, so PUT is the path that matters most.Extension Point node
Verification index
Each test below is reproducible from a clean checkout. Scripts run viajiti so they don’t need the Next.js dev server.
Coverage gaps tagged “UI-tested” above are exercised through the studio recommendation preview + decision-traces UI but don’t yet have committed scripts. Adding them is the operator-friendly way to close the audit — each row gets a script + a doc cross-link.