tensteps.yml

One file at the root of your repo describes what to test. Tensteps also accepts tensteps.yaml (not both). Check it any time with npx tensteps@beta validate.

Add this first line for autocomplete and checking in your editor:

# yaml-language-server: $schema=https://tensteps.ai/schema/v1.json

A complete example

version: 1

defaults:
  max_steps: 30

studies:
  - name: checkout
    objective: "Buy any pair of shoes under $50 and get it shipped to your home address"
    success:
      url_matches: "/order/confirmation"
    sessions: 8
    guardrails:
      payment: test_card
      forbid_actions: [delete]
    matrix:
      device: [desktop_chrome, android_low_end]
      network: [wifi, slow_3g]

  - name: signup
    objective: "Create a new account"
    start_path: /signup
    success:
      text_visible: "Welcome aboard"
    population: low_tech_comfort

follow_up:
  - key: frustration
    type: rating
    scale: 5
    prompt: "How frustrating was that? (1 = not at all, 5 = very)"

triggers:
  pull_request: { sessions: 6, branches: [main] }

Top level

| Key | Required | What it is | | --- | --- | --- | | version | yes | Always 1. | | studies | yes | 1 to 25 studies. Each is one goal tested by its own personas. | | defaults | no | Values every study uses unless it sets its own. Any study field except name, objective and success. | | follow_up | no | Questions every persona answers after their session. | | triggers | no | How many personas a pull request gets, and which pull requests run. |

Studies

| Key | Default | What it is | | --- | --- | --- | | name | required | Short id: lowercase letters, numbers and underscores. | | objective | required | The goal in plain words, as you'd say it to a person. | | success | required | How Tensteps knows a persona made it. See below. | | start_path | / | Where personas start, relative to the site's URL. | | sessions | 50 | Personas per run, 1 to 1,000. A trigger's sessions overrides it. | | max_steps | 40 | Steps a persona may take before the session ends, up to 200. | | timeout | 5m | Real-person time a persona will spend, like 90s or 5m (up to 30 minutes). | | population | general_us | Who the personas are. See below. | | auth | none | How personas log in. See below. | | guardrails | none | What personas must not do. See below. | | matrix | desktop Chrome, Wi-Fi, en-US | Devices, networks and browser languages to spread personas across. | | fail_if | success_rate_drop: "10%" | Thresholds for comparing against a baseline. Saved now; used once baselines ship. |

success

Exactly one of:

| Rule | Passes when | | --- | --- | | url_matches: "/order/confirmation" | The page URL matches this regular expression. | | text_visible: "Welcome aboard" | This text is visible on the page. | | element_visible: "#receipt" | An element matching this CSS selector is visible. | | all: [ ... ] | Every rule in the list passes. | | any: [ ... ] | At least one rule in the list passes. |

population

| Preset | Who | | --- | --- | | general_us | US adults 18+, approximate census mix of age, gender and region. | | low_tech_comfort | Adults who avoid unfamiliar interfaces; skews older. | | low_patience | Adults who abandon quickly at friction, errors or slow pages. | | screen_reader | Blind or low-vision adults navigating with a screen reader. | | non_native_english | Adults reading English as a second language. |

Preview any population with npx tensteps@beta personas -p screen_reader. The same seed always gives the same persona, so runs are repeatable.

auth

secret:NAME values are read from the environment variable NAME. Never put a password in the file.

guardrails

| Key | What it does | | --- | --- | | payment: test_card | Personas pay with the test card 4242 4242 4242 4242, so use payments in test mode. | | forbid_paths: ["/admin/**"] | Pages personas can't open. * matches within a path segment, ** across segments. | | forbid_actions: [delete, purchase, send] | Buttons and links personas can't use, by what they say ("Delete account", "Place order", "Send"). |

matrix

Personas are spread evenly across every combination.

| Key | Values | | --- | --- | | device | desktop_chrome, desktop_safari, iphone_15, android_mid, android_low_end, tablet | | network | wifi, 4g, slow_3g, offline_flaky | | locale | Browser languages, like en-US or es-MX |

follow_up

Each question has a key (snake_case), a prompt, and a type:

| Type | Needs | | --- | --- | | rating | scale: 2 to 10 | | single_select | options: at least 2, each { key, label } | | text | nothing more |

triggers

pull_request controls runs on pull requests:

| Key | What it does | | --- | --- | | sessions | Personas per study on a pull request. | | branches, branches_ignore | Only PRs into (or not into) these branches. | | head_branches, head_branches_ignore | Only PRs from (or not from) these branches. | | labels | Only PRs with at least one of these labels. | | paths, paths_ignore | Only PRs that change (or don't only change) matching files. |

Without a triggers block, a pull request gets 30 personas per study. If the block exists but has no pull_request, pull requests aren't tested. push_main and nightly are accepted and saved for scheduled runs, which aren't automated yet.