Core concepts

Step sets and the .saffron dialect

The two pains this solves

If you've run a Cucumber suite for a few years, you know both of these:

  1. The same step exists under two wordings. One author writes "I navigate to the home page", another writes "I visit the home page". Same behavior, two entries in the suite's vocabulary. In Saffron this is worse than cosmetic: exact step text is the cache identity, so the second wording is a cache miss, the agent re-records it at real token cost. Wording drift has a price tag.

  2. The same group of steps is pasted into every scenario. Every scenario that tests the checkout page first has to get through the guest information page: the same five or six steps, copied into every scenario in the file. Gherkin's Background: only covers steps at the very start of every scenario in one file; it can't help mid-scenario.

Saffron attacks the second pain with step sets, and the first with a step vocabulary that is surfaced everywhere you write, in the CLI, in your IDE, and to AI assistants.

Step sets: write the flow once

A step set is a named, reusable sequence of steps. It lives in a .saffron file: Saffron's own file type, which is a superset of Gherkin: everything a .feature file can contain, plus the StepSet: keyword. (Think TypeScript and JavaScript: .ts is .js plus keywords. Renaming a .feature file to .saffron changes nothing, it just unlocks the extra syntax.)

Feature: Checkout Flow

StepSet: Complete guest information
    Given I am on guest information page
    When I enter guest name "Chathuranga Jayasinghe"
    And I click continue button
    Then I am not on guest information page

Scenario: Verify checkout happy path
    Given I am on calendar page
    When I select an available date
    And I click "search" button
    Then I am on package selection page
    When I select an available package
    And I click choose button
    StepSet Complete guest information
    Then I am on checkout page
    When I click continue without payment
    Then I see booking success page

The syntax rule is the same one Gherkin already uses: block keywords carry a colon, step keywords don't.

  • StepSet: <name> defines a set (like Scenario: defines a scenario)
  • StepSet <name>: invokes a set inside a scenario (like Given starts a step)

If you accidentally type the colon at an invocation site, the project-wide unique-name rule catches it: the accidental "definition" duplicates an existing set name and fails parsing with a "remove the colon to invoke" hint. Invoking a set that doesn't exist is also a parse error: silence is never an option.

What actually happens: expansion

Saffron resolves a StepSet invocation at parse time, exactly the way it already merges Background: steps, the set's steps are inlined into the scenario before anything runs:

checkout.saffron: what you write StepSet: Complete guest information Given I am on guest information page When I enter guest name "Chathuranga" And I click continue button Then I am not on guest information page Scenario: Verify checkout happy path Given I am on calendar page ... (search, choose package) And I click choose button StepSet Complete guest information Then I am on checkout page EXPANDS TO what Saffron runs Scenario: Verify checkout happy path Given I am on calendar page ... (search, choose package) And I click choose button Given I am on guest information page When I enter guest name "Chathuranga" And I click continue button Then I am not on guest information page Then I am on checkout page The sage lines came from the set. Sub-steps keep their exact text, so caching, seeding, healing and propagation work unchanged. The set's first step is a guard and its last step an exit assertion, so every invocation checks itself. WHEN YOU EDIT A STEP SET Edit the set once In one place. Callers go stale Honest, never silently wrong. Re-record, seeded Only changed steps cost AI. Green at zero tokens Back to free replay.
Step set expansion and the edit lifecycle

This one design choice is what makes step sets more than sugar:

  • The expanded steps keep their exact text. To the cache, the seeding engine, healing, and propagation, they are ordinary steps, recorded once, seeded into every scenario that invokes the set. No new machinery, no new failure modes.
  • Editing a set is honest. Change a step inside the set and every invoking scenario's cache is stale: Saffron re-records them, but every unchanged step seeds from the index, so you pay AI cost for the changed steps only.
  • Assertions inside sets follow the assertion policy by position. A set's Then steps become regular assertions of the calling scenario: mid-scenario they are checkpoints; if a set is invoked as the last thing in a scenario, its trailing Thens join the final assertion block: strict under every policy, like any other final assertion.

The recommended set shape (visible in the example): make the first step a guard ("Given I am on guest information page") and the last step an exit assertion ("Then I am not on guest information page"). Every invocation then verifies its own precondition and its own success, wherever it's dropped into a flow.

A few boundaries in v1: set names are unique across the project (same-file definitions win lookup), sets can contain tables, doc strings and <param> placeholders, sets cannot invoke other sets, and a set never runs on its own: only through invocations.

Global step sets: the shared library file

Step sets are project-wide by design. The registry is built from every .saffron file under your features directory before anything parses, so a set defined in one file is invocable from all of them, and running a single file still resolves sets defined elsewhere. There is no import statement and no separate file format: where a set lives is an organizational choice, not a semantic one.

That gives you a natural home for steps common to the whole application: cookie banners, login, navigation. The convention (TypeScript's .d.ts move: a naming convention, not a new grammar): a sets-only library file with a .steps.saffron suffix:

# features/shared.steps.saffron
Feature: Shared step sets

StepSet: Accept cookies if prompted
    When I accept the cookie banner if it is shown

StepSet: Complete guest information
    Given I am on guest information page
    When I enter guest name "Chathuranga Jayasinghe"
    And I click continue button
    Then I am not on guest information page

Any scenario in any .saffron file can now write StepSet Complete guest information. A library file yields zero runnable scenarios, so the runner simply skips it at execution time while its sets feed the registry.

Two things to know:

  • The Feature: header is required even in a sets-only file, the underlying Gherkin parser needs a Feature root. Name it whatever reads well (Feature: Shared step sets).
  • Keep local sets local: a set used by one flow belongs in that flow's file, next to its scenarios. Promote a set to the library file when a second feature starts invoking it, the project-wide unique-name rule guarantees the move is safe (a forgotten duplicate fails the parse instead of silently shadowing).