Loops as code
Growth configuration can live in your repository. A torno.config.ts or YAML file represents Metrics, Loops, Changes, tag slugs, and the event dictionary, so changes to the growth system travel through code review like everything else — without bypassing policy.
What lives in the config
The schema covers Property context and Environments, plus the syncable objects: Metrics with their leading-indicator pairings and guardrails, Loops, Changes with their exposure contracts, and event, goal, and trait dictionary entries.
# torno.config.yaml
property: demo-app
environments: [dev, staging, prod]
metrics:
- key: activation_rate
kind: ratio
guardrails: [support_ticket_rate]
loops:
- name: onboarding-activation
metric: activation_rate
changes:
- name: shorter-signup-form
kind: page_variant
exposure:
goals: [signup_completed]
# `torno config pull` writes the server-allocated tag slug here
events:
- name: signup_completed
properties:
plan: string
torno config validate checks the file locally and accumulates exact field errors instead of stopping at the first one. TypeScript configs are evaluated in a restricted process with no ambient credentials. YAML edits preserve your comments where possible, and computed TypeScript values that cannot be patched are reported honestly rather than silently rewritten.
Pull, diff, and push
torno config pullcompares server state with local ownership and writes server-allocated values, such as tag slugs, back into the file. Adopting objects that already exist on the server is explicit, via--adoptor--adopt-all.torno config diffis a three-way comparison against the last sync base. Every difference is attributed: a local edit, drift from the UI, a genuine conflict, a pending approval, a create, an update, or an archive.torno config pushplans the blast radius first, refuses unresolved conflicts, and requires explicit flags for overwriting, destroying, or taking over ownership. It records the source commit and branch, and it applies through the same policy seams as every other surface — a push that needs approval waits for one. With--dry-run, it plans and never applies.
Ownership without surprises
One repository is pinned as the syncing owner per Property. Removing an object from the config does not silently delete it on the server; release and archive are explicit operations. Objects created in the UI stay UI-owned until you adopt them, so a teammate’s work cannot be steamrolled by a stale branch.
Static checks for CI
Two checkers run against your source before anything ships:
torno tag checkscans for Change exposure tags at framework, data-attribute, and server call sites, with file and line evidence. It fails on unknown Change slugs, skips generated code, and flags dynamic values it cannot resolve rather than guessing.torno events checkcompares literal track, goal, and trait call sites against the event dictionary in both directions and validates payload shapes. It can append inferred entries to your local config for review, but an unregistered runtime event is never silently promoted into the tracking plan.
Both emit text, JSON, GitHub annotations, or SARIF, so a broken contract fails the pull request instead of the Experiment.
Runtime violations and drift
The contract keeps working after merge. Runtime dictionary violations can be merged into checker output, and a live Change with no observed exposure and no statically found tag raises a drift alert: the config says measurable, the deployment disagrees, and someone should know. See Changes for the exposure contract itself and Event collection for dictionary enforcement at ingest.
All of this runs through the CLI with --json output, so agents can operate the sync loop too — subject to the same policy gates as everyone else.