Defining Tours
A tour definition is one JSON document: the ordered steps, the branching
rules, the A/B variant allocation, and the reset policy. This page covers
the file snug tour create --file takes;
{
"name": "docs4_new_user_setup",
"title": "New User Setup",
"steps": [
{
"id": "step_welcome",
"index": 0,
"title": "Welcome tour",
"semantic_target": { "kind": "route", "name": "/home", "description": "Landing page walkthrough" },
"completion_flag": "saw_welcome",
"mandatory": true
},
{
"id": "step_profile",
"index": 1,
"title": "Complete your profile",
"semantic_target": { "kind": "ui_element", "name": "profile-form", "test_hint": "data-testid=profile-form" },
"completion_flag": "profile_completed",
"prerequisites": ["saw_welcome"],
"mandatory": true
},
{
"id": "step_advanced",
"index": 2,
"title": "Advanced settings",
"semantic_target": { "kind": "route", "name": "/settings/advanced" },
"completion_flag": "saw_advanced",
"prerequisites": ["saw_welcome"]
}
],
"branching": [
{ "when": "$variant == 'power' && $flags.profile_completed", "next_step": "step_advanced" }
],
"variant": { "experiment_id": "docs4_exp", "allocation": { "control": 50, "power": 50 } },
"reset_policy": { "on_version_change": "reset_incomplete_steps", "preserve_flags": ["saw_welcome"] }
}Steps
Each step requires id, index, title, semantic_target, and
completion_flag; mandatory, prerequisites, and reward_hook are
optional. The step's completion_flag is the bit that records its
completion - the set of all completion flags is the tour's flag
vocabulary, and each version freezes a flag-to-bit-offset map. A
prerequisites list names flags, not step ids, and every entry must be
some step's completion flag - creating a definition with a dangling
prerequisite fails with 400 invalid_definition; reproduced:
Invalid tour definition: prerequisite flag 'nonexistent_flag' is not produced by any stepDuplicate step ids and unknown branch targets are rejected the same way.
The semantic_target says where in the product the step happens:
kind is ui_element, route, or action, plus a name and optional
description and test_hint. Nothing server-side interprets it during
progress tracking - it exists so frontends, test harnesses, and AI agents
can locate and verify each step from the definition alone. reward_hook
(kind, target, enabled) can point a step's completion at another
service; see the API reference for the
schema.
Branching rules
Branching decides the current step. On every status read and event, rules
run in order against the user's state; the first rule whose predicate is
true and whose next_step is currently available wins. If none match, the
current step is the first available step in index order. With the definition above, a user bucketed into power was steered to
step_advanced the moment profile_completed was set, skipping ahead of
lower-index incomplete steps.
when is a predicate over:
$variant- the user's assigned variant name (also$user.variant)$flags.<flag>- whether a completion flag is set$completed.<step_id>- whether a step is completed$completed_count- number of set flags
with comparisons == != < <= > >= against 'strings', numbers,
true/false/null; combined with &&/||/! (or the words
and/or/not) and parentheses. A bare path is a truthiness test:
$flags.profile_completed alone means "that flag is set". Predicates are
parsed at definition time, so a syntax error fails the create or update
with 400 invalid_definition rather than surfacing later.
Variants
variant holds an experiment_id and an allocation map of bucket names
to relative weights. Assignment hashes experiment, user, and version
(SHA-256), so it needs no storage: the same user always lands in the same
bucket for a given version, with no assignment call. Two consequences:
- Assignment exists before the user ever touches the tour - a status read
of a
not_starteduser already reports the variant. - A version bump re-rolls assignment: the same user was
poweron version 1 andcontrolon version 2.
Per-variant funnel counts come back in the analytics endpoint.
Updating and versioning
PUT /tours/{tour_id} replaces whole sections (title, status,
steps, branching, audience, reset_policy) - arrays are replaced,
not merged. Changing status (draft, active, paused, deprecated)
does not bump the version; replacing steps bumps it by one and
immediately applies the reset policy to every participant of the previous
version.
Reset policies
on_version_change takes four modes. What they actually do:
no_op- the version bump clears nothing.reset_all- every participant's progress and flags are cleared.reset_incomplete_stepsandpreserve_selected_flags- progress is cleared, then any flag listed inpreserve_flagsthat the user held is set again on the new version.
Note that reset_incomplete_steps does not carry over completed
steps' flags automatically - it behaves identically to
preserve_selected_flags, keeping only the preserve_flags list.
A user with saw_welcome and profile_completed set came
out of a version bump with only saw_welcome (the preserved flag),
completed_steps empty, and state back to not_started. List every
flag you want to survive explicitly.
An explicit POST /tours/{tour_id}/reset with scope of user, version
(one version's participants), or tour (all versions) does the same
clearing on demand; the wide scopes are admin-only.