Keywords
A keyword is a reusable flow, a named bundle of steps with declared inputs and outputs that any scenario can call. Use keywords to factor out the boilerplate that several scenarios share: a login flow, a “create tenant + admin user” bootstrap, a “verify email + claim password” handshake.
Keywords keep .tales suites DRY without dragging in a templating engine: they are part of the DSL, evaluated by the runner, with the same expression semantics as a scenario.
Defining a keyword
Section titled “Defining a keyword”keyword "authenticate" { inputs { email = string password = string }
step "http" "auth_user" { request { method = "POST" url = "${config.base_url}/auth" body { json = { email = input.email password = input.password } } }
expect { status = 200 json = { access_token = is_string() } } }
outputs { token = result.auth_user.response.json.access_token }}A keyword takes:
- An
inputs { ... }block declaring typed parameters. Types are HCL types (string,number,bool,list(...),map(...)). - One or more
stepblocks, the same syntax as inside a scenario, with their owncapture,retry,vars, etc. - An
outputs { ... }block declaring named values exposed to the caller.
Inputs are accessible via input.<name> (or input.inputs.<name>) anywhere in the keyword body. Outputs are evaluated after the keyword’s steps complete and are exposed to the caller as result.<call_name>.<output_name>.
Calling a keyword
Section titled “Calling a keyword”The keyword pseudo-provider invokes a defined keyword by name and binds its inputs:
scenario "Login then call protected endpoint" { step "keyword" "do_login" { name = "authenticate" inputs = { password = "secret123" } }
step "http" "me" { request { method = "GET" url = "${config.base_url}/me" headers = { Authorization = "Bearer ${result.do_login.token}" } }
expect { status = 200 } }}In the caller:
| Expression | Value |
|---|---|
result.do_login.token | the keyword’s declared outputs.token value |
result.do_login.<step>.id | individual step captures are not exposed |
Only outputs cross the keyword boundary. Internal step captures stay internal.
Why use keywords?
Section titled “Why use keywords?”Three concrete wins over copy-pasting the steps:
- Single source of truth. When the auth API changes, you update one keyword body and every scenario picks up the fix.
- Cleaner scenarios. A scenario reads as a business story (
do_login,create_post,delete_post) instead of HTTP plumbing. - Typed inputs. Mis-spelling a parameter or passing the wrong type fails at load time with a clear error, not in production.
Inputs typing
Section titled “Inputs typing”keyword "create_tenant" { inputs { name = string tier = string // any string seats = number // any number is_active = bool metadata = map(string) // map of string values admins = list(object({ // list of objects email = string role = string })) } // ...}HCL’s full type expression language is available. The parser rejects calls that don’t satisfy the type constraints before any step runs.
Composition
Section titled “Composition”Keywords can call other keywords. There is no recursion limit beyond runtime stack constraints; in practice keep the call graph shallow.
keyword "register_and_login" { inputs { email = string password = string }
step "keyword" "register" { name = "create_user" inputs = { email = input.email, password = input.password } }
step "keyword" "login" { name = "authenticate" inputs = { email = input.email, password = input.password } }
outputs { user_id = result.register.id token = result.login.token }}Skip rules and keywords
Section titled “Skip rules and keywords”skip_if / skip_unless on a keyword step gate the call, not the keyword definition. The keyword’s internal steps cannot have their own skip rules independent of the call site, keep complex conditional logic out of keywords and at the scenario level.
See also the Keyword pseudo-provider page for invocation semantics.