Reusable snippets
The imports: block on a spec lets you split reusable parts across files and reference them by name. A snippet can be a single step, a sequence, or even an entire spec authored to be lifted into other specs.
File layout
specs/
├── checks/
│ ├── login-admin.yml
│ └── assert-login-page.yml
├── flows/
│ ├── checkout.yml
│ └── checkout-success.yml
├── snippets/
│ ├── actions/
│ │ ├── rotate-token.yml
│ │ └── create-entity.yml
│ └── conditional-steps/
│ ├── dismiss-banner.yml
│ └── env-conditional.ymlThe directory structure is convention only — cairn walks the spec root recursively, finds every *.yml file, and indexes them by name. The name is the spec's first comment or its filename minus .yml.
Imports block
# specs/flows/checkout.yml
imports:
- actions/login-admin # refers to specs/snippets/actions/login-admin.yml
- actions/rotate-token
- conditional-steps/dismiss-banner
intent: Sign in, rotate an API token, and confirm the new token works.
outcomes:
- id: token-rotated
text: { contains: "Token rotated" }
steps:
- use: login-admin # uses the imported snippet
- use: rotate-token
- use: dismiss-banneruse: invokes the imported snippet by name within the spec's steps:. Snippets themselves can import: and use: other snippets — the import graph is a DAG, not a tree.
What gets reused
The same logic that lives in one spec is the candidate for a snippet:
- Login flows. "Sign in as an admin" is reused everywhere a flow needs an authenticated session.
- Token rotation, key rotation, secret reset. Anything where the contract is "the system shows the success toast after I do the dance."
- Conditional dismissals. Banner dismissal, modal close, and feature-flag dismissals are the same shape in 30 specs. Make them snippets.
- Form fills with a fixed shape. "Fill name + email + submit" reused across several flows.
Do not lift things that are unique to one flow. The point of a snippet is to encode the contract across many flows, not to factor code.
When you lift something to a snippet
- The contract (
intent + outcomes) should still read sensibly without steps. - The snippet's
intentbecomes "do the snippet's job" andoutcomesbecomes "snippet succeeded." The harness consuming the snippet measures success against its own outcomes, not the snippet's. - Refactor in two commits: the first introduces the snippet, the second swaps the inline steps for
use:. This makes bisect readable. - If multiple snippets need a shared precondition (e.g. "be on
/settings"), put that precondition in a top-level login snippet — don't repeat it.
Snippet vs. action file
In early adoption, the term action file and snippet are interchangeable. There is no separate "action file" syntax — a snippet file is just a spec YAML with intent + outcomes + steps. The directory structure (snippets/actions/) is a convention so consumers can grep for them; the runner doesn't care.
Conditional steps
Snippets frequently need conditional behavior. The when: predicate makes a step opt-in:
- when: { env: FEATURE_BANNER, equals: "on" }
- click: { by: { role: button, name: "Dismiss banner" } }Conditional steps are excellent snippet content because the snippet itself decides whether to run the step based on environment, harness capability, or run-time configuration. The harness consuming the snippet does not need to know.
Iteration
When a snippet's contract changes — that's rare but it happens — the contract hash changes. Re-stamp:
cairn spec verify specs/snippets/actions/rotate-token.yml --stampThen run all specs that import that snippet to confirm the contract hash drift didn't break any consumer:
cairn run specs --cold-start --format jsonPassing a directory (cairn run specs/) walks the spec tree recursively for *.yml in one shot — there is no separate --recursive flag.
Cookbook: building a snippet library
A reasonable starting library for a SaaS-ish app:
actions/login.yml— sign in flow.actions/logout.yml— sign out, defensively in case the test seeded a session.actions/rotate-token.yml,actions/revoke-token.yml— token lifecycle.actions/seed-entity.yml,actions/delete-entity.yml— entity lifecycle.conditional/dismiss-banner.yml,conditional/dismiss-cookie-banner.yml— first-paint nags.conditional/env-prod-skip.yml— short-circuit when running against prod.
That set covers 80% of the import statements in the rest of the spec tree.
See also
- Authoring — what makes a contract survive across months
- Steps and Verifiers — the typed vocabularies
- Configuration — config keys, including snippet resolution paths