Skip to content

DSL overview

Tales test files are written in HCL2 with a small, fixed vocabulary. This page covers the top-level structure and the execution model. Provider-specific blocks are documented under Providers.

Every .tales file accepts the following top-level blocks, all optional except scenario (you need at least one to run anything):

version = 1
config {
base_url = env("BASE_URL", "http://localhost:1337")
}
generator "<type>" "<name>" { … } // zero or more
keyword "<name>" { … } // zero or more
scenario "<name>" { … } // zero or more

version = 1 is optional and reserved for future format bumps. config { ... } is a free-form map exposed under the config.* expression namespace, put env-driven URLs, secrets, and connection definitions there.

Source order matters for scenario blocks (they run in suite order) and for step blocks inside a scenario (they run sequentially). The parser preserves textual order even when step and the alias case are interleaved.

scenario "<name>" {
tags = ["smoke", "critical"] // optional; filtered by --tag
skip_if { … } // optional, repeatable
skip_unless { … } // optional, repeatable
step "<provider>" "<name>" {
depends_on = ["earlier_step"] // optional, doc + validation only
when = can(result.previous.id) // optional, gates the step at runtime
vars { … } // optional, step-local variables
request { … } // provider-specific
expect { … } // provider-specific (alias: response)
capture { … } // optional
retry { … } // optional
skip_if { … }
skip_unless { … }
}
// ... more steps in execution order
teardown {
step "<provider>" "<name>" { … }
}
}

A case block is a backward-compatible alias for step, same decoding, same behaviour. A response block is a backward-compatible alias for expect, both names are accepted and response + expect may coexist on the same step.

  • Scenarios run in parallel with a configurable concurrency cap (--parallel, defaults to runtime.NumCPU()).
  • Steps inside a scenario run sequentially in .tales file order. A failing step halts the scenario; later steps are reported as skipped.
  • teardown always runs after the main steps, even when a step failed. Use when = can(result.foo.id) on a teardown step to skip it when the prerequisite capture is missing.
  • A step may reference (result.<step>) or depends_on only steps defined earlier in the file, forward / unknown references are rejected at load time with exit code 2.

Each step decodes its request, expect, capture, and retry blocks in source order. Variables and helpers visible to expressions are:

VariableScopeExample
configwhole fileconfig.base_url
resultdownstream steps only (after the step runs)result.create_user.id
requestinside the current step’s expect / capturerequest.body.json.email
responseinside the current step’s expect / captureresponse.json.id
inputinside a keyword’s stepsinput.email
varsinside the current step onlyvars.ts
hostanywherehost.os ("darwin", "linux", "windows")

Full reference: Expression variables.

scenario "Smoke ping" {
tags = ["smoke"]
// ...
}
Terminal window
tales test ./e2e/pass --tag smoke # only smoke
tales test ./e2e/pass --tag smoke --tag critical # smoke OR critical
tales test ./e2e/pass --scenario "Smoke ping" # exact name; overrides --tag

skip_if and skip_unless blocks gate scenarios or steps on the host OS, architecture, env vars, or any boolean expression. See Conditional execution for the full attribute matrix and cascade semantics.