Writing features
Plain Gherkin: no step definitions, no glue code. The steps are natural language; the agent figures out how to perform them:
Feature: Login
Background:
Given I open the application
And I accept cookies if prompted
@smoke
Scenario: Successful login
When I enter "standard_user" in the username field
And I enter "secret" in the password field
And I click the "Log in" button
Then I should see the products page
Scenario Outline: Failed login shows an error
When I enter "<username>" in the username field
And I enter "<password>" in the password field
And I click the "Log in" button
Then I should see the error message "<message>"
Examples:
| username | password | message |
| locked | secret | This user has been locked. |
| standard | wrong | Wrong username or password |
Tips for cache-friendly features
Given/Whensteps are actions: the agent may adapt them when the UI drifts.Thensteps are assertions: under the default policy they can never be adapted, so make them state exactly what must be true. (Opt-inadaptable-midrelaxes mid-scenario checkpoints only; the finalThenblock is always strict.)Dynamic values work: "I record the displayed price as "X"" becomes a
captureText, and ""X" should differ from "Y"" becomes a live comparison at replay time.Data tables work: prefer the 2-column key/value shape:
When I enter the following credentials | username | premium_user | | password | secret2 |Recorded actions reference
<table:username>etc., so editing the table values later replays at zero tokens; only changing the keys triggers a re-record.Multi-row record tables work too: header row + one row per record:
When I add the following guests | firstName | email | city | | Alice | alice@test.com | Oslo | | Bob | bob@test.com | Bergen |Cells record as
<table:1:firstName>,<table:2:email>, … so editing any cell value replays free. Adding/removing a row or renaming a header column is a structural change and honestly re-records. (A 2-column table is always treated as key/value, never as records.)Doc strings (
"""blocks) work: the content records as<docstring>, so rewording a note, message, or payload replays at zero tokens. If a later assertion checks that same content ("the saved note should be shown"), it follows your edits too.Secrets never go in files: write
{env:VAR}and export the variable (or put it in a git-ignored.env, real env wins):When I enter "{env:ADMIN_USER}" in the username field And I enter "{env:ADMIN_PASSWORD}" in the password fieldCaches, proposals, reports and history only ever contain the token; values resolve at replay, and a missing variable fails fast by name. One honest note: during the first recording the agent types the real value (it transits the AI provider once): replays never involve it. Record with rotatable staging credentials.
Waiting on the backend works: say it in the step and the recording carries a network matcher instead of a brittle sleep:
When I place the order and wait for the order API to return 201 And I wait until the job status API reports "READY" Then the order request should have returned 201The wait blocks replay until a matching response (URL pattern, method, status, optional body regex) is observed; the
Thenform is an API contract assertion and is never healed. Waits match responses from the triggering step onward, so "click and wait" in one step never races.Relative dates work: "select a check-in date 1 day from today" records as a
{date+1}template and stays valid every day.Conditional steps ("accept cookies if prompted") are fine, the agent records them as no-ops when nothing appears.
Dialogs, uploads, drag-and-drop, and iframes all record live and replay: confirm() dialogs record as an armed accept/dismiss placed before the trigger, uploads take a file path (relative paths resolve from the working directory), drag records source and destination targets, and elements inside iframes are scoped automatically, an assertion that misses an iframe boundary still replays, because positive lookups fall back to searching child frames. Hand-writing a cache entry (
recordedBy: "manual") remains a supported path; the zero-AI replay verifies it like any recording.Tabs are plain prose too. A click that opens a new tab is followed automatically; to work across tabs, say so: "When I switch back to the first tab", "And I close the preview tab", "When I open a new tab at the admin page". The agent's tab operations record as tab actions and replay in the same order. Name the tab in the step so the recording is unambiguous.
Repeated step groups? Rename the file to
.saffron(a superset of Gherkin: nothing else changes) and define a step set once:StepSet: Add the standard guests When I add the following guests | firstName | email | city | | Alice | alice@test.com | Oslo | | Bob | bob@test.com | Bergen | Then the guest count should be 2 Scenario: Note after the standard guests Given I am on the guestbook page StepSet Add the standard guests When I leave a note ...Colon defines, no colon invokes. The set's steps are inlined at parse time, so they cache and seed like ordinary steps; editing the set makes every invoking scenario re-record (mostly seeded), while table-value edits inside a set replay free as usual. Tip: start a set with a guard step and end it with an exit assertion so every invocation checks itself.
Sets are project-wide: define once, invoke from any
.saffronfile. Put application-wide flows (login, cookie banner) in a sets-only library file, by conventionfeatures/shared.steps.saffron(it still needs aFeature:header; it yields no runnable scenarios). Keep sets used by one flow in that flow's own file, and promote them to the library when a second feature needs them.For the full
.saffronexperience install the Saffron VS Code extension (sibling reposaffron-vscode): highlighting, step completion with recorded/divergent badges, StepSet go-to-definition, and near-duplicate wording warnings. Without it,npx saffron steps --snippetsgives native snippet completion and{ "files.associations": { "*.saffron": "feature" } }gives highlighting. Before writing new steps, check what already exists:npx saffron steps <word>: reusing an exact wording replays at zero tokens. If an AI assistant writes your feature files, serve it the vocabulary:claude mcp add saffron -- npx saffron mcp. JetBrains or Neovim?npx saffron lspserves the same completion, navigation and diagnostics to any LSP-capable editor: JetBrains setup (incl. Community editions, via the free LSP4IJ plugin) is in the guide. And to draft a whole file from prose:npx saffron author notes.txt. Full guide: Step Sets & Vocabulary.