Changes and delivery
A Change is the concrete intervention that implements a Hypothesis — the thing you actually ship. This page covers what a Change is, the exposure decision that determines whether it can ever support a causal claim, and how delivery behaves when networks and configs misbehave.
The Change entity
Every Change has a kind:
| Kind | What it changes |
|---|---|
page_variant |
A page or component variant served to a share of traffic |
ad_creative |
A single ad creative |
ad_campaign |
A campaign scaffold handled by Ads |
product_change |
A change shipped in your product’s own codebase |
content |
An article or other content artifact, usually from a brief |
A Change binds a Property and environment, links its Loop and Hypothesis, and carries variants or artifacts, a stable exposure tag, rollout state, readiness, health, and an audit trail. The lifecycle runs draft → ready → pending approval → live → complete or killed, with a path back to draft. The linked Loop reflects the real lifecycle — a Loop is only live when its Change actually is.
The exposure tag is a stable slug with a strict grammar, deliberately independent of the CHG-* display ID. Code references the tag, humans reference the ID, and neither breaks the other.
The exposure decision
Every Change must answer one question before it ships: how will Torno know who saw it?
A measurable Change declares its contract up front — the exposure tag and event, the variants, the goal events that count, the assignment kind, and who is eligible. A Change without that contract is still allowed, but it is explicitly badged as unmeasurable and excluded from causal claims and results. Torno will not backfill a causal story for a Change that never declared exposure.
This is the honest version of ship-and-watch: you can still ship it, and your Metrics will still move — Torno just won’t pretend the Change caused the movement. When you want a real answer, declare exposure and measure it as an Experiment.
Delivery: signed bundles, local assignment
The backend publishes signed torno-bundle/v1 configs per Property and environment: active Changes, variant weights, consent and capture rules, an optional holdout, a version and ETag, and the signing key ID. SDKs and middleware verify the signature, cache the bundle, and evaluate assignment locally — deterministic bucketing that stays sticky per Subject.
Bundles sync in the background with a target of 60 seconds or less; after that, every assignment decision is local. There is no Torno network call in your customer’s request path.
Failure follows one rule — when in doubt, serve control:
| Condition | Behavior |
|---|---|
| No bundle yet (first load, blocked fetch) | Control / pass-through |
| Bundle stale beyond tolerance | Control / pass-through |
| Signature invalid or bundle tampered | Control / pass-through |
| Torno unreachable, valid cached bundle | Local assignment continues from cache |
Client-only content swapping exists but is second-class; server and edge assignment is the first-class path.
First exposure starts measurement
Going live does not start the clock. The first accepted exposure does: it confirms the Change is actually reaching Subjects, writes the assignment through Cohorts, starts measurement, and notifies observers.
The inverse is watched too. A Change that is live but producing no exposures raises an instrumentation-drift alert instead of quietly measuring nothing. Exposure events with unknown tags are retained as unmatched evidence rather than silently attached to the nearest Change.
Rollout, ramp, kill
Operators control a sticky percentage rollout, schedule ramps, cancel ramps, and kill a Change immediately. Going live passes readiness checks and policy; kill is always available as a safety action. While an Experiment is running, arbitrary rollout edits are interlocked so the test’s allocation is not silently invalidated. Environment overrides stay isolated — staging games do not leak into production.
Working with Changes
The web app covers create, status, exposure health, rollout, and readiness. Agents drive the same lifecycle through torno change on the CLI, the API, and MCP, and Changes travel with their Loop in Loops as code.