> ## Documentation Index
> Fetch the complete documentation index at: https://docs.traffical.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Decisions & attribution

> How a parameter resolution becomes a decision, how decision IDs flow through your application, and how track events get attributed to experiments.

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 object

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: 742 },
    { layerId: "layer_pricing",  policyId: "policy_discount",   allocationName: "low",       bucket: 128 }
  ]
}
```

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:

| Event              | Emitted when                            | Purpose                                                                  |
| ------------------ | --------------------------------------- | ------------------------------------------------------------------------ |
| **Decision event** | Always (unless `trackDecisions: false`) | Records that the SDK decided. Used for intent-to-treat.                  |
| **Exposure event** | When parameter values are read          | Records the user saw the variant. Used for treatment-on-treated metrics. |
| **Track events**   | When you call `track()`                 | Records outcomes. Causally linked via `decisionId`.                      |

Exposure and decision events both fire automatically. Track events are explicit.

<Accordion title="Decision vs exposure: when does it matter?">
  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.
</Accordion>

## The `decisionId`

The `decisionId` is the link between a parameter resolution and the events that follow it.

```typescript theme={null}
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:

| Mode                   | What it does                                                                               | Use 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. |
| `decision`             | Attributes 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:

```typescript theme={null}
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](/concepts/warehouse-native) for the setup.

## Next steps

<CardGroup cols={2}>
  <Card title="Events" icon="bolt" href="/concepts/events-and-metrics">
    Exposure, decision, and track events in detail.
  </Card>

  <Card title="Warehouse-native" icon="database" href="/concepts/warehouse-native">
    Use your warehouse as the source of truth for assignments and metrics.
  </Card>

  <Card title="Canonical experiments" icon="book-open" href="/guides/canonical-experiments">
    See the patterns where `decisionId` matters.
  </Card>
</CardGroup>
