Getting started: running your first experiment
An experiment in dif is one Markdown file in your repo. The CLI reads the active files and compiles them into a typed client your app imports at render time, so picking a variant never leaves the page. This guide goes from an empty repo to a live variant on screen.
Install the CLI
The CLI ships as a single binary, so there is no separate runtime to install. On macOS or Linux:
Prefer a package manager? Use brew install dif-sh/tap/dif, or npm install -g @dif.sh/cli on any platform with Node 18 or newer.
Scaffold the workspace
Run dif init in your repo. The --surface flag names the first
place you will run experiments. Everything lands in one dif/ folder, checked
into git:
$ dif init --surface checkout created dif/config.yaml created dif/surfaces/checkout.md created dif/experiments/active/ and concluded/ created dif/audiences/locale.ts, device_type.ts added dif guidance to CLAUDE.md, AGENTS.md
Those last guidance files teach a coding agent the workflow. That is the next guide. Skip them for now with dif init --no-agent-files if you would rather add them later.
Create the experiment
dif new writes the draft, you fill it in, then dif validate and dif build check and compile it. The owner comes from your git config user.email:
$ dif new checkout-cta-v2 --surface checkout # fill in the hypothesis and brief, then set status: active $ dif validate $ dif build client → dif/generated/client.ts context → dif/context.json
The draft starts with status: draft, today's date, and two variants
(control and variant_a) split 50/50. If the surface has past
results, dif new pastes the last three into the brief as a comment, so you
draft from what you already learned.
Fill it in
Open the file and write the parts that need a human: the hypothesis, the brief, the
rationale. Then change status: draft to status: active.
# dif/experiments/active/checkout-cta-v2.md --- id: checkout-cta-v2 status: active owner: ada@acme.dev surface: checkout hypothesis: > A more urgent checkout CTA will lift completed-checkout among returning visitors. audience: include: - returning_visitor: true variants: - id: control weight: 50 - id: variant_a weight: 50 summary: "Get it today" metrics: primary: completed_checkout guardrails: [refund_rate] created: 2026-06-17 --- ## Brief Returning visitors drop off between cart and payment. Test whether more urgent copy closes the gap. Copy only, no layout change. ## Decision <!-- drafted by dif conclude -->
A few rules the validator enforces: weights total 100, control stays first, and
anything you name in audience (here returning_visitor) is declared
in dif/config.yaml. dif validate reports every problem in one pass
and points at the line.
dif build only emits experiments with status: active. A draft stays
out of the client until you flip it, so you can commit work in progress without shipping it.Render it
Install the SDK:
Import the generated client once at boot, then call dif() where you want the
variant. The branch keys match the variant ids in your file:
// import the generated client once, at boot import "./dif/generated/client"; import { dif } from "@dif.sh/sdk"; dif.init({ userId: () => currentUser?.id ?? null }); const cta = dif("checkout-cta-v2", { control: () => "Place order", variant_a: () => "Get it today", }); button.textContent = cta();
dif() returns a function you call at the render site. It buckets the user,
returns their variant, and fires one exposure event. There are adapters for React and Svelte if you would rather use a hook or a store.
dif qa --force checkout-cta-v2=variant_a --preview-url https://staging.example.com/checkout prints a link with that variant forced. Forced views do not fire exposures.That is the loop: write a file, build, render. When the test has run its course, dif conclude records the decision and moves the file to concluded/.
To hand the same workflow to a coding agent, read on.