Skip to Content
ReferenceYAML Configuration

YAML Configuration Reference

This page documents every key Probe reads from a workflow file, the expression context each field is evaluated in, and the validation rules that apply.

Workflow Structure

The skeleton below shows every property a workflow file can carry and where each one belongs.

name: string # Required: workflow name description: string # Optional: what the workflow does vars: # Optional: workflow variables key: value secrets: # Optional: environment variables to hide in output - NAME defaults: # Optional: default `with` values per action, for every job http: url: string actions: # Optional: names for external actions name: string jobs: # Required: a list of jobs - name: string # Required: job name id: string # Optional: job id, used by needs needs: [job-id, ...] # Optional: job dependencies always: boolean # Optional: run even when a needed job failed skipif: expression # Optional: skip the job when true fail_fast: boolean # Optional: skip the steps after one that failed defaults: # Optional: default `with` values per action http: url: string repeat: # Optional: run the job repeatedly count: integer interval: duration async: boolean warmup: integer test: expression # Optional: assertion over the runs baseline_test: expression # Optional: assertion against an earlier run steps: # Required: a list of steps - name: string # Optional: step name id: string # Optional: step id, to read its outputs through uses: string # Required: action name, or a name given under actions with: # Optional: action parameters key: value test: expression # Optional: assertion echo: string # Optional: text added to the report vars: # Optional: step variables key: value outputs: # Optional: values published for later steps key: expression skipif: expression # Optional: skip the step when true wait: duration # Optional: wait before running the step timeout: duration # Optional: step timeout iteration: # Optional: run the step once per entry; - key: value # a list, or a template that comes to one retry: # Optional: retry on failure max_attempts: integer interval: duration initial_delay: duration

jobs is a list, not a mapping. A workflow that writes jobs: as a mapping of job ids fails to load.

There is no top-level env key. Environment variables are read through vars.

Top-Level Properties

Six properties sit at the root of a workflow file alongside jobs: what to call it, what it does, the values it starts with, which of those values must not be shown, the with values its steps share, and the names it gives to external actions.

name

Type: String (required)
Description: Name of the workflow, shown at the top of the report.

name: "API Health Check"

description

Type: String (optional)
Description: Longer explanation of what the workflow does.

description: | Checks the production API: - endpoint availability - response times

vars

Type: Object (optional)
Description: Variables available to every job and step as vars.<name>.

vars is the only place where environment variables are visible, and they are referenced by their bare name. Values are evaluated once, before the first job starts.

vars: # Read an environment variable api_url: "{{API_URL}}" # With a fallback timeout: "{{REQUEST_TIMEOUT ?? '30s'}}" # Computed at load time run_id: "{{random_str(8)}}" # Nested values are supported auth: user: "{{API_USER}}"

A var can be given when the workflow is run, as probe --var api_url=http://localhost:8080 workflow.yml: the value is used as it is in place of what is written here, and the vars that read it see it. See --var, and --env-file for reading the variables the environment lacks from a file.

Expressions inside a step cannot read environment variables directly - there is no env in the expression context. Put the variable in vars and read vars.<name>.

A var can read another var as vars.<name>, wherever the other one is written in the block. Each var is evaluated after the vars it reads, so a value computed once, such as a random password, is the same in every var built from it:

vars: http_port: "{{HTTP_PORT ?? '18080'}}" base: "http://localhost:{{vars.http_port}}" password: "{{PASSWORD ?? random_str(24)}}" basic_auth: "Basic {{encode_base64('alice:' + vars.password)}}"

A var that reads itself, directly or through other vars, stops the workflow before the first job starts with an error naming the chain, such as vars: circular reference: a -> b -> a. A name that is not a var reads as nil, so vars.<name> ?? 'default' gives the default. A var that reads vars by a key known only when it runs, as vars[KEY], is evaluated after all the others. A var that calls template, such as one built from a template file, is evaluated after all the vars that do not, since what the template reads is known only when it runs. When the template reads another var that calls template and has not been evaluated yet, that var is evaluated first, and a cycle between them stops the workflow as one between other vars does.

The templates in a list are evaluated as those in a map are, at any depth, so ports: ["{{vars.http_port}}"] holds the port.

A var whose template cannot be evaluated, such as {{nothing.here}}, which reads a field of nil, stops the workflow before the first job starts with exit status 2. The error names the var and the template, as vars.a: {{nothing.here}}: cannot fetch here from <nil>. A var that reads one that failed is not evaluated, so its error is not repeated.

secrets

Type: Array of strings (optional)
Description: Names of environment variables whose values must not appear in anything Probe prints or writes.

secrets: - API_TOKEN - DB_PASSWORD vars: api_token: "{{API_TOKEN}}" db_password: "{{DB_PASSWORD}}"

Wherever the value of a listed variable would appear, Probe writes <secret:NAME> instead. That covers the report on the terminal, step names, echo, the request and response shown for a failed step, --verbose output, the log records of actions, and the files written by --report. The value is also hidden in the escaped forms it takes when it is quoted in an error message or encoded as JSON. Steps, tests and outputs still see the real value; only what is shown changes.

A name that is not set, or set to an empty string, has nothing to hide and is skipped. A value shorter than 4 characters, such as 1, ok or dev, is masked only where it is a whole value, such as a header or a field of a request or a response that holds nothing else, and not inside other text: replacing it wherever it appears would cut it out of unrelated words, which makes the output unreadable and gives the value away by the letters missing from them. Probe prints a warning to stderr once for each such secret, naming it but not showing its value, as [WARN] the secret PIN is shorter than 4 characters, so it is masked only where it is a whole value; use a value of at least 4 characters. Give such a credential a longer value if it must stay out of the output.

Independently of secrets, the values of the Authorization, Proxy-Authorization, Cookie and Set-Cookie headers are always shown as <redacted>. A token obtained while the workflow runs, such as one returned by a login step and sent in a later request, is never listed in secrets, but it travels in one of these headers. Probe learns these values as an action is about to send or has received them, and hides them from then on, including in the action’s own log records.

The same goes for credentials passed to an action: the value of any password, key_passphrase, secret_access_key or session_token field, at any depth of with, is shown as <redacted>, and so is the password in a dsn or a url, such as redis://user:password@host, while the rest of the URL stays visible. A password written straight into a step therefore does not appear in --verbose output or an action’s log records, even though it is not listed in secrets. A credential learned this way that is shorter than 4 characters is, for the same reason as a short secret, not masked inside other text, such as a log record, but it is still shown as <redacted> under its header or field name, as the password of a URL, and where it is a whole value, whatever its length. When the template of such a value cannot be evaluated, the error names the value, as with.password, but does not say why, since the template and the error may each quote the credential. A url is treated so only when the template is in its user info, as in redis://app:{{vars.pw}}@host; the error of a template elsewhere in a url is shown.

The password of any URL in what Probe shows or writes is shown as <redacted> as well, whatever its length and wherever the URL comes from: postgres://app:pw@db/main in a driver’s error, or https://x-access-token:abc@github.com/o/r in a response body, comes out as postgres://app:<redacted>@db/main and https://x-access-token:<redacted>@github.com/o/r. The password is what follows the first colon of the user info, up to the last @ before the host, so that a password holding an @ is hidden whole. A password holding an unescaped /, ? or # is not recognized, since the host is taken to end there: write it percent-encoded, as %2F, %3F and %23, as a URL requires anyway. The values of the parameters password, passwd, pass, pwd, secret, client_secret, token, access_token, refresh_token, id_token, api_key, apikey, X-Amz-Security-Token, X-Amz-Signature, X-Goog-Signature and sig, and of those named like the credential headers and fields above, are hidden the same way, whatever the case of their names, in the query of a URL and in its fragment, where the OAuth implicit flow returns #access_token=…: jdbc:postgresql://db/app?user=app&password=pw comes out as jdbc:postgresql://db/app?user=app&password=<redacted>. Parameters are separated by &, also as JSON writes it, \u0026, and as HTML does, &amp;. A value ends at the next of them, at a #, or at the space or quote after the URL. Parameters separated by ;, as in jdbc:sqlserver://host;password=pw, are not recognized. The user name, the host, the other parameters and the rest of the URL stay visible, and a value listed in secrets that stands in a URL is still shown as <secret:NAME>. A URL whose :// or @ is percent-encoded, as in a form body, is not recognized as one.

defaults

Type: Object (optional)
Description: Default with values for the steps of every job, keyed by action as a job’s defaults are.

A connection that every job uses, such as a base URL or a DSN, is written once here rather than in each job:

vars: user: "{{DB_USER}}" host: "{{DB_HOST}}" api_url: "{{API_URL}}" token: "{{API_TOKEN}}" defaults: db: dsn: "postgres://{{vars.user}}@{{vars.host}}/app" http: url: "{{vars.api_url}}" headers: accept: application/json jobs: - name: Rows steps: - name: Count users uses: db with: query: SELECT count(*) AS n FROM users test: res.rows[0].n > 0 - name: API defaults: http: headers: authorization: "Bearer {{vars.token}}" steps: - name: List users # sent to the default url, with both headers uses: http with: get: /users test: res.code == 200

The closest value wins: what a step writes in with, then the defaults of its job, then those of the workflow. Objects are merged key by key, so the step above sends the accept header of the workflow and the authorization header of its job. A template in a default is evaluated when the step runs, with the step’s own vars and outputs.

The defaults of a workflow do not reach a job file run by the embedded action, which is a file of its own with its own defaults.

actions

Type: Object of strings (optional)
Description: Names for the external actions the workflow uses. A step’s uses and a key of defaults, of a job or of the workflow, can then be the name in place of the repository and commit.

actions: graphql: github.com/mozership/probe-graphql@41e4ffa222db58c63c7169117c919e6d252bdbf9 # v0.2.0 jobs: - name: Countries API defaults: graphql: url: https://countries.trevorblades.com/graphql steps: - name: Look up Japan uses: graphql with: query: '{ country(code: "JP") { capital } }' test: res.data.country.capital == "Tokyo"

The commit an action is pinned to is written once, so a newer release is a change to one line.

  • A name is letters, digits, _ and -, and starts with a letter or a digit.
  • A name cannot be that of an action of Probe, such as http or shell: what uses: http runs does not depend on the workflow. A workflow that gives such a name fails to load.
  • The value names an external action as uses does: github.com/<owner>/<repo>[/<dir>]@<commit> with a full 40-character commit SHA, or a local path starting with ./, ../ or /. It is not a template, and it cannot be another name.
  • A step can still name an action in full, and the defaults written by the name apply to it too. One defaults cannot hold both the name and the action it stands for.

Probe replaces each name by its action as it loads the workflow, so everything else sees the action in full. In particular, --allow-action takes the action, not the name: a workflow cannot choose what a name the person running Probe has allowed stands for.

A job file run by the embedded action is read by the names of the workflow that embeds it, so its steps and its defaults can use them too. See Named External Actions.

Jobs

jobs is a list of job definitions. A job groups the steps that run in sequence, and declares what it depends on.

Job Properties

A job definition takes the properties below.

PropertyTypeRequiredDescription
nameStringYesJob name, shown in the report. Supports template expressions
idStringNoIdentifier used by another job’s needs. Generated automatically when omitted
needsArrayNoIds of jobs that must finish first
alwaysBooleanNoRun once the jobs in needs are over, even when one of them failed
stepsArrayYesThe steps to run
skipifExpressionNoSkip the whole job when the expression is true
fail_fastBooleanNoSkip the steps after one that failed
defaultsObjectNoDefault with values, keyed by action name
repeatObjectNoRun the job repeatedly

There is no if, continue_on_error or timeout at the job level. A job that has to run after a failure is one with always.

needs

Jobs without dependencies start in parallel. needs refers to job ids, so a job that others depend on needs an explicit id.

jobs: - id: setup name: Setup steps: - name: Prepare uses: hello echo: "ready" - name: Test needs: [setup] steps: - name: Run uses: hello echo: "testing"

always

A job is skipped when a job it needs failed or was skipped for the same reason. With always: true, it runs once every job in needs is over, however each ended. That is what a job that removes what the others made needs: left without it, a failed run leaves its test database or its files behind, and the next run finds them.

vars: admin_dsn: "{{ADMIN_DSN}}" test_dsn: "{{TEST_DSN}}" jobs: - id: setup name: Setup steps: - name: Create the test database uses: db with: dsn: "{{vars.admin_dsn}}" query: CREATE DATABASE probe_test test: res.code == 0 - id: tests name: Tests needs: [setup] steps: - name: Count users uses: db with: dsn: "{{vars.test_dsn}}" query: SELECT count(*) AS n FROM users test: res.rows[0].n > 0 - name: Cleanup needs: [setup, tests] always: true steps: - name: Drop the test database uses: db with: dsn: "{{vars.admin_dsn}}" query: DROP DATABASE IF EXISTS probe_test test: res.code == 0
  • The job still waits for every job in needs, a repeated one for all of its runs. List every job whose work it undoes, so that it does not run beside one of them.
  • The run fails as it would have without it: always changes which jobs run, not the exit status.
  • An output a failed or skipped job did not publish reads as nil, so write the steps to work without it, as DROP DATABASE IF EXISTS does.
  • A job that needs the one with always is held to how that one ends, as it is to any other job.
  • skipif still applies: a job with both is skipped when its expression is true.

skipif

A boolean expression. The job is skipped when it evaluates to true.

vars: environment: "{{ENVIRONMENT}}" api_url: "{{API_URL}}" jobs: - name: Production only skipif: vars.environment != "production" steps: - name: Check uses: http with: method: GET url: "{{vars.api_url}}/health" test: res.code == 200

fail_fast

The steps of a job run one after another, and a step runs whether or not the one before it failed. That suits steps that check separate things, each of which is worth its own line in the report. When the later steps work on what an earlier one made, they fail because it did, and the one failure that matters is buried under those that follow from it. With fail_fast: true, the steps after a failed one are not run, and are reported as skipped:

jobs: - name: Orders fail_fast: true defaults: db: dsn: "file:./orders.db" steps: - name: Create the table uses: db with: query: CREATE TABLE IF NOT EXISTS orders (id INTEGER PRIMARY KEY, total INTEGER) test: res.code == 0 - name: Insert an order # skipped when the table was not created uses: db with: query: INSERT INTO orders (total) VALUES (?) params: [100] test: res.rows_affected == 1
  • A step fails when its test is false, its action returns an error, or a response breaks its contract. A step without a test that nothing checks does not fail, so it stops nothing.
  • A skipped step does not wait for its wait, and its outputs are not published.
  • The remaining entries of an iteration are skipped as well, once one of them fails.
  • Each run of a job with repeat starts over: a failure skips the rest of that run only. A run in which a step was not run counts as neither a success nor a failure of it, and is told beside them, as 1/3 success (33.3%), 2 skipped.
  • Other jobs are not affected, except through needs, as by any failed job. Steps that must run after a failure, such as a cleanup, belong in a job with always.

defaults

Default parameters merged into the with of every step in the job that uses the matching action. A value set on the step wins, and a value set here wins over the defaults of the workflow, which every job shares. The key is what a step writes in uses: the name of a built-in action, an external action in full, or a name given under actions.

vars: api_url: "{{API_URL}}" token: "{{API_TOKEN}}" jobs: - name: API checks defaults: http: url: "{{vars.api_url}}" headers: authorization: "Bearer {{vars.token}}" accept: application/json steps: - name: Health uses: http with: get: /health # resolved against the default url test: res.code == 200

The http action requires a method. Either set method explicitly, or use the shorthand key get / post / put / delete / patch, whose value is a full URL or a path resolved against url.

- name: Explicit method uses: http with: url: "{{vars.api_url}}/health" method: GET

repeat

repeat runs the whole job again on an interval, which is what turns a test into a monitor.

PropertyTypeRequiredDescription
countIntegerYesNumber of runs. Capped by PROBE_MAX_REPEAT_COUNT (default 10000)
intervalDurationNoWait from the end of one run to the start of the next. With async, the time from the start of one run to the start of the next, and required
asyncBooleanNoStart each run without waiting for the one before to end
warmupIntegerNoNumber of runs before the count, which are counted in nothing. Together with count, capped by PROBE_MAX_REPEAT_COUNT
testExpressionNoChecked once every run is over, against the runs of the steps. When given, it alone decides whether the job passed. See Test over the Runs
baseline_testExpressionNoChecked once every run is over, against the runs of the steps and those of the run given by --baseline. Skipped when there is none. See Test against an Earlier Run
vars: api_url: "{{API_URL}}" jobs: - name: Poll until ready repeat: count: 10 interval: 5s steps: - name: Check uses: http with: method: GET url: "{{vars.api_url}}/status" test: res.code == 200

The warmup runs come first, and are run as any other, interval and async included, but are left out of the result: a run in which a step failed does not fail the job, though a job that cannot start, such as for a name that cannot be evaluated, fails in a warmup run as in any other, and the counts and the response times are those of the count runs after them. They give the service the time to fill its caches and pools before the runs that are measured.

Each run’s steps read the outputs that the earlier steps of the same run published, also when async runs them at the same time. The jobs after it read each output as the latest run that published it left it, so a step the last run skipped keeps the value of the run before.

Test over the Runs

Without repeat.test, a run in which a step fails fails the job. repeat.test is checked once every run is over, against what the runs of each step came to, and decides alone whether the job passed:

vars: api_url: "{{API_URL}}" jobs: - name: Search repeat: count: 100 interval: 100ms warmup: 5 test: steps.search.success_rate >= 0.99 && steps.search.rt.p95 < 0.3 steps: - name: Search by keyword id: search uses: http with: url: "{{vars.api_url}}" get: /search?q=probe test: res.code == 200

The expression reads vars, the workflow variables, and steps, the runs of each step of the job by its id, or by step_0 and so on for a step without one:

FieldTypeDescription
runsIntegerThe runs counted: success, failure and skipped together
successIntegerThe runs in which the step passed, or was skipped by its own skipif
failureIntegerThe runs in which its test failed or its action returned an error
skippedIntegerThe runs in which it was not run, after a step that failed in a fail_fast job, or for an iteration without an entry
success_rateFloatsuccess divided by runs, from 0 to 1
rtObjectThe times its action took in the runs in which it returned one, in seconds, as rt.sec is: min, mean, p50, p90, p95, p99 and max. nil when it returned none

The percentiles are by nearest rank, as the response times of the report are. The runs of warmup are counted in none of them. A step with an iteration is counted over all of its entries.

With repeat.test, the runs are counted and fail nothing by themselves:

  • A run in which a step fails does not fail the job, and the step is reported as passed, with its failed runs counted in its repeat.
  • The job passes when the test is true, and fails when it is false, cannot be evaluated or is not a boolean. A false test tells which condition is false, and what it compared, as a step’s test does.
  • The exit status is that of the test: 1 when it failed, whatever the runs failed for.
  • A job whose skipif skipped every run counted is skipped, and the test is not checked. When it skipped only some, the test is checked against the runs that ran.
Test against an Earlier Run

repeat.baseline_test compares the runs to those of an earlier run, the report given by --baseline. It is checked once every run is over, after repeat.test, and reads what that test reads and, under baseline.steps, the steps of the same job in the earlier run, by their id:

vars: api_url: "{{API_URL}}" jobs: - name: Search repeat: count: 100 warmup: 5 test: steps.search.success_rate >= 0.99 baseline_test: steps.search.rt.p95 < baseline.steps.search.rt.p95 * 1.2 steps: - name: Search by keyword id: search uses: http with: url: "{{vars.api_url}}" get: /search?q=probe test: res.code == 200

A step under baseline.steps has fewer fields than one under steps, as the report records fewer:

FieldTypeDescription
runsIntegerThe runs counted
successIntegerThe runs in which the step passed
success_rateFloatsuccess divided by runs, from 0 to 1
rtObjectThe times of the step in seconds, with the same fields as under steps, from the response times of the report. nil when it has none

A job matches the job of the same id, and a step the step of the same id. A step with an iteration is several steps of one id in the report: runs and success are those of them all, but their times cannot be put together from the percentiles, so rt is nil.

The test is skipped, and the job passes by it, when no --baseline was given, when the earlier run has none of a step that the test reads under baseline.steps, or none of the times that it reads under rt. The workflow therefore runs as it is with and without --baseline. A skipped test tells why, as skipped: the baseline has no step search. Whether the earlier run has what the test reads is known before it runs only when the step and the field are written in it, so a step or a field read by a key known only when it runs, as baseline.steps[vars.id], is refused as the workflow is loaded, with exit status 2.

When it is checked, the job passes only when it holds, and the exit status is that of the test, as for repeat.test. It does not change how a failed run is taken: without repeat.test, a run in which a step fails still fails the job. baseline can be read only by this test, and probe check reports a baseline_test that reads a step or a field that baseline.steps does not have.

Steps

A step is one action invocation plus what surrounds it: the condition that decides whether it runs, the test that decides whether it passed, and the outputs it leaves for later steps.

Step Properties

A step definition takes the properties below.

PropertyTypeRequiredDescription
usesStringYesAction to run: a built-in such as http or shell, or an external action
nameStringNoStep name, shown in the report
idStringNoIdentifier that namespaces this step’s outputs
withObjectNoAction parameters
testExpressionNoAssertion. The step fails when it is false
echoStringNoText added to the report
varsObjectNoVariables local to the step
outputsObjectNoValues published for later steps and jobs
skipifExpressionNoSkip the step when true
waitDurationNoWait before running the step
timeoutDurationNoStep timeout (default 5m). An action still running then is stopped, and the step fails
iterationArray or templateNoRun the step once per entry, exposed as vars
retryObjectNoRetry the step on failure

The key is uses, not action, and the conditional key is skipif, not if.

outputs

Values published for use by later steps and jobs. Each is read by its name, as outputs.token, and, when the step has an id, through it as well, as outputs.auth.token. Give the step an id when another step publishes the same name, so that each value can be read.

Each value is an expression, not a template, so it is written without {{ }}.

steps: - name: Log in id: auth uses: http with: url: "{{vars.api_url}}/login" method: POST test: res.code == 200 outputs: token: res.body.access_token user_id: res.body.user.id

Later steps read them either namespaced by step id or by the output name alone:

headers: Authorization: "Bearer {{outputs.auth.token}}" X-User: "{{outputs.user_id}}"

An id containing a hyphen is not a valid identifier in an expression, so it has to be read with brackets:

echo: "{{outputs['create-user'].user_id}}"

retry

retry repeats a single step until its test passes or the attempts run out.

PropertyTypeRequiredDescription
max_attemptsIntegerYesTotal attempts, at least 1. Capped by PROBE_MAX_ATTEMPTS (default 10000)
intervalDurationNoWait between attempts
initial_delayDurationNoWait before the first attempt
steps: - name: Flaky endpoint uses: http with: method: GET url: "{{vars.api_url}}/slow" test: res.code == 200 retry: max_attempts: 3 interval: 2s

iteration

Runs the step once per entry. Each entry’s keys are exposed through vars.

steps: - name: Check {{vars.path}} uses: http iteration: - path: /health - path: /metrics - path: /version with: method: GET url: "{{vars.api_url}}{{vars.path}}" test: res.code == 200

In place of a list written in the workflow, iteration takes one template that comes to a list of objects. It is evaluated when the step is reached, with the workflow’s and the step’s vars and the outputs published so far, so the entries can be cases kept in a file or what an earlier step found:

steps: # one run for each case of the file, which can grow without the workflow - name: "{{vars.name}} can sign in" uses: http iteration: "{{ parse_json(file('testdata/users.json')) }}" with: post: /login body: user: "{{vars.name}}" password: "{{vars.password}}" test: res.code == 200 - name: List orders id: orders uses: db with: query: SELECT id, total FROM orders WHERE status = 'open' test: res.code == 0 outputs: rows: res.rows # one run for each row the step above read - name: "Order {{vars.id}} has a total" uses: http iteration: "{{ outputs.orders.rows }}" with: get: "/orders/{{vars.id}}" test: res.body.total == vars.total
  • The value is one {{ }} template and nothing else; text around it, or two templates, would come to text, and the workflow fails to load.
  • Each entry is an object, whose keys the step reads as vars. A list of anything else, such as strings, fails the step; turn it into objects with map, as "{{ map(vars.paths, ({path: #})) }}".
  • A template that cannot be evaluated, or that does not come to a list, fails the step without running it.
  • A list without entries skips the step, which is reported as skipped: there is nothing to run it with. An iteration written as an empty list, [], runs the step once, as a step without iteration does.
  • The entries are taken as they are: a {{ }} in a value of an entry is not evaluated. The step’s own vars are evaluated for the template, and again for each entry, as they are for each entry of a list written in the workflow; a var that calls random_str is another value each time.
  • An iteration takes up to 1,048,576 entries; a longer list fails the step.
  • In a job with repeat, the template is evaluated in each run and may come to another number of entries in each. Each entry is counted by its place in the list, and the steps after it as themselves. A run in which the list has no entry counts as neither a success nor a failure of the step, as a run a fail_fast job did not run it in does.

The Expression Context

Inside a step, an expression sees exactly these names:

NameTypeDescription
varsObjectWorkflow variables merged with the step’s own vars
resObjectThe action’s response
reqObjectThe request as it was sent
rtObjectResponse time: rt.duration (string) and rt.sec (float seconds)
statusIntegerAction exit status, 0 on success
outputsObjectOutputs published by earlier steps
repeat_indexIntegerCurrent index when the job repeats, from 0, the warmup runs included

There is no env, jobs or steps in this context.

The res Object for http

Each action defines its own res. For http it holds the following.

FieldTypeDescription
res.codeIntegerStatus code, such as 200
res.statusStringStatus line, such as "200 OK"
res.headersObjectResponse headers, keyed by canonical name such as Content-Type
res.bodyAnyResponse body. Parsed into an object or array when the response is JSON, otherwise the raw string
res.rawbodyStringThe unparsed body, present when the body was parsed as JSON
test: | res.code == 200 && res.headers["Content-Type"] contains "application/json" && res.body.status == "ok" && rt.sec < 1

Other actions publish their own res fields; see the Actions Reference.

Data Types

Two kinds of value in a workflow file have their own syntax: durations, and the expressions and templates that read the context.

Duration

A duration is either a Go duration string or a number of seconds, which may have a fraction. A number may be written bare or quoted, and is read the same way in a step’s timeout and wait, a repeat’s interval, and a retry’s interval and initial_delay.

timeout: "30s" timeout: "5m" interval: 10 # 10 seconds interval: 1.5 # 1.5 seconds wait: "0.5" # 500 milliseconds

A duration cannot be negative, longer than about 292 years, or above zero and shorter than a nanosecond; such a value is an error, as is one that is not a duration at all. Zero is a duration: it waits for nothing, and leaves a step’s timeout at its default. An action’s timeout takes the same forms, but must be above zero unless the action’s reference says otherwise.

Expressions and Templates

A template expression appears inside a string and is replaced by its value:

url: "{{vars.api_url}}/users/{{outputs.auth.user_id}}"

A boolean expression is written bare, without braces:

test: res.code == 200 && rt.sec < 2 skipif: vars.environment == "local"

outputs values are expressions too, so they take no braces.

A template that cannot be evaluated is an error, never text in the value. A name that is not defined reads as nil and is not an error, but reading a field of nil is, as is an expression that does not parse:

Where the template isWhat happens
A step’s with, vars or nameThe step fails with the kind template, and the action does not run. The error names the value, as with.headers.authorization
A workflow’s varsThe workflow stops before the first job, with exit status 2
A job’s nameThe job fails, with exit status 2
A step’s echoThe error is shown in place of the text, and the step’s result does not change
# Fails the step when no earlier step published outputs.login headers: authorization: "Bearer {{outputs.login.token}}" # Falls back instead headers: authorization: "Bearer {{outputs.login?.token ?? 'none'}}"

See Built-in Functions for what can be called inside an expression.

Validation Rules

  • name is required at the workflow level, and jobs must be a non-empty list.
  • Every job needs a name and at least one step.
  • Every step needs a uses, and it must name an action: one of Probe, an external action, or a name given under actions. One that names none stops the run before its first job, with exit status 2, even in a step that would be skipped.
  • A name under actions must not be that of an action of Probe, and must stand for an external action pinned to a commit or at a local path.
  • A job’s needs must refer to ids that exist, and the dependency graph must be acyclic.
  • repeat.count and repeat.warmup must be zero or greater, and retry.max_attempts at least 1.

probe check reports these without running the workflow, together with keys a workflow does not take, expressions that do not parse and outputs read before they are published. See the CLI Reference.

File Merging

Several files can be combined by passing them as separate arguments or comma-separated. They are concatenated in order, and a top-level key defined more than once takes the value from the last file:

probe base.yml production.yml probe base.yml,production.yml

See File Merging for the merge rules.

See Also

Updated at