Skip to main content
A decision is the SDK’s record of resolving parameters for a user. Every time you call getParams (or decide), the SDK produces a decision: which parameters were resolved, which allocations the user fell into, and a decisionId that links downstream events back to this point in time. Decisions are the backbone of attribution — they’re how a purchase track event ends up tied to the treatment variant of the pricing_test policy that the user saw fifteen minutes earlier.

The decision record

Conceptually:
Decision {
  decisionId:    "dec_abc123"
  unitKey:       "user_789"
  timestamp:     "2026-05-21T10:30:00Z"
  assignments:   { "checkout.button.color": "#22C55E", ... }
  layers: [
    { layerId: "layer_checkout", policyId: "policy_color_test", allocationName: "treatment", bucket: 7420 },
    { layerId: "layer_pricing",  policyId: "policy_discount",   allocationName: "low",       bucket: 1280 }
  ]
}
The SDK returns the decision from decide(). getParams() returns just the resolved assignments map, but the decision is still produced and tracked internally.

What gets emitted

A single resolution can emit three things:
EventEmitted whenPurpose
Decision eventAlways (unless trackDecisions: false)Records that the SDK decided. Used for intent-to-treat.
Exposure eventWhen parameter values are readRecords the user saw the variant. Used for treatment-on-treated metrics.
Track eventsWhen you call track()Records outcomes. Causally linked via decisionId.
Exposure and decision events both fire automatically. Track events are explicit.
Suppose a user lands on a page where the SDK resolves a layout parameter — the SDK records a decision. But the user closes the tab before the layout renders.
  • A decision-event-based analysis counts this user in the variant they were assigned to. This is intent-to-treat: the experiment offered them the variant, regardless of whether they saw it.
  • An exposure-event-based analysis only counts users whose code paths actually read the variant. This is treatment-on-treated: only people who really saw the change.
Most experiments are analysed on exposures, because that’s what the user actually experienced. Decisions matter when you want to detect issues that prevent the variant from rendering (e.g. a JS error in the new variant) — a treatment-on-treated analysis would miss those.

The decisionId

The decisionId is the link between a parameter resolution and the events that follow it.
const decision = traffical.decide({
  context: { userId: "user_789" },
  defaults: { "pricing.discount_pct": 0 },
});

applyDiscount(decision.assignments["pricing.discount_pct"]);

// later — explicitly tied to the same decision
traffical.track("purchase", {
  unitKey: "user_789",
  decisionId: decision.decisionId,
  properties: { order_total: 49.99 },
});
For ordinary in-process usage, the SDK threads decisionId automatically — track() calls inherit the most recent decision for that unit key. You don’t usually need to pass it explicitly. The places where you do need to pass decisionId explicitly are:
  • Cross-process — backend resolves, frontend tracks. Send decisionId in your API response and pass it to track() on the client.
  • Batch / offline — your batch job decides, the outcome event comes from an email provider or webhook days later. Thread decisionId through the external system.

Attribution modes

The SDK supports two attribution modes:
ModeWhat it doesUse when
cumulative (default)Attributes a track event to all the layers the user was exposed to during the session.The outcome plausibly depends on more than one experiment — e.g. catalog → PDP → checkout funnels.
decisionAttributes only to the specific decision named in the track event.You need clean single-decision attribution, often for warehouse-native analyses.
Set the mode on the client:
const traffical = await createTrafficalClient({
  orgId, projectId, env, apiKey,
  attributionMode: "decision",   // or "cumulative" (default)
});

How attribution actually runs

There are two layers of attribution worth understanding: 1. In the SDK (per track event). When you call track(), the SDK builds an attribution array and embeds it on the event payload. In cumulative mode (default), that array contains every layer/policy/allocation the user has been exposed to in this session — deduplicated, last-write-wins per layer. In decision mode, it contains only the layers from the decision named by decisionId. This array is the SDK’s contribution to attribution. 2. In the pipeline (per metric). When the warehouse-native pipeline computes a metric, it joins track events to assignments on unit_key with a temporal constraint: a track event counts toward an allocation if it happened after the user’s first exposure to that allocation. The join key is unit_key, not decisionId. The temporal ordering replaces what a “time window” would do — but it’s open-ended, not capped. The practical implications:
  • For events sent through the SDK (the normal case), the SDK’s attribution array drives dashboard breakdowns, and the pipeline’s temporal join drives metric numerators. Both are populated automatically — you don’t have to think about it.
  • For events sent from outside the SDK (a webhook hitting /v1/events directly), the pipeline’s temporal join still works as long as the event carries the unit_key of an already-exposed user. No decisionId required.
  • Pass decisionId explicitly when you need it: in decision mode (where attribution is restricted to a single named decision), or across processes where the cumulative session cache lives on a different machine. The SDK’s per-event attribution array picks it up.

External assignments

Decisions don’t have to come from a Traffical SDK at all. If your assignments live in your data warehouse — say, a experiment_assignments table maintained by another tool — you can use those as the source of truth and have Traffical compute metrics on top. This is the warehouse-native mode. The assignment table replaces the decision event stream; everything else (track events, metrics, significance) works the same way. See warehouse-native for the setup.

Next steps

Events

Exposure, decision, and track events in detail.

Warehouse-native

Use your warehouse as the source of truth for assignments and metrics.

Canonical experiments

See the patterns where decisionId matters.