Steps
The graphql step
A graphql step is an http step with the request written as a query. vero sends it as a POST
whose JSON body is {"query", "variables", "operationName"}, and everything about an
http step holds for it: the names in scope, the timing phases, the event log,
the 8 MiB body cap. What the step adds is a parse of the query at load, and a check that the
server did not answer with errors.
| Key | Type | Meaning |
|---|---|---|
url |
string | templated |
query |
string | the query, a literal parsed at load |
queryFile |
string | the query in a file relative to the plan, instead of query |
variables |
any | sent as variables; strings are templated, numbers are sent as written |
operationName |
string | sent as operationName; templated |
headers |
map of strings | templated values |
The query is a literal. A {{ inside it is refused, because a value belongs in variables,
where it is templated like any JSON string. queryFile is read when the plan loads, with the
same rules as an http bodyFile: a literal path relative to the plan, at most 8 MiB, and the
bytes that were there at load are the bytes sent. The body that goes out has query always,
variables when the step sets it and operationName when the step sets it.
A plan
examples/graphql.yaml runs against the fixture's /fixtures/graphql,
which echoes the query it got. The first task reads the echo back:
- name: order-query
steps:
- graphql:
url: "{{ env.baseUrl }}/fixtures/graphql"
query: |
query GetOrder($id: ID!) {
order(id: $id) { id status }
}
operationName: GetOrder
variables: { id: "ord-{{ run.id }}" }
assert:
- status == 200
- body.data.echo.operationName == "GetOrder"
- body.data.echo.variables.id startsWith "ord-"
results:
variables: body.data.echo.variables
vero plan --explain order-not-found examples/graphql.yaml describes the step as
graphql POST {{ env.baseUrl }}/fixtures/graphql: 1 request, and the plan sends two requests in
all.
Errors are a failure unless the plan reads them
A GraphQL server answers a failed query with 200 and an errors array, so status == 200
passes on it. A graphql step therefore carries one assertion of its own,
body.errors == nil or len(body.errors) == 0, which runs after the step's written assertions
and is reported at the step's graphql: line. It is left out when one of the step's own
assertions reads body.errors. That is how a plan says it expects an error, as the example's
second task does:
- name: order-not-found
steps:
- graphql:
url: "{{ env.baseUrl }}/fixtures/graphql"
query: "{ order(id: \"missing\") { id } }"
variables: { fail: "order not found" }
assert:
- status == 200
- body.data == nil
- body.errors[0].message == "order not found"
- body.errors[0].path == ["order"]
Send the same query with only status == 200 and the step fails. The failure block is an http
failure block, with the body vero sent, and the added assertion comes with a note that says what
to do:
FAIL order-query › step 1 › assert #2 fail-graphql.yaml:9
POST http://127.0.0.1:18080/fixtures/graphql HTTP/1.1 200 OK 4.5ms
> accept-encoding: gzip
> content-type: application/json
> user-agent: vero
> {"query":"{ order(id: \"missing\") { id } }","variables":{"fail":"order not found"}}
< content-length: 72
< content-type: application/json
< date: Sat, 03 Oct 2026 02:19:04 GMT
< {"data":null,"errors":[{"message":"order not found","path":["order"]}]}
assert body.errors == nil or len(body.errors) == 0 failed
body.errors = [{"message":"order not found","path":["order"]}]
a graphql step fails when the response carries errors; a plan that expects an error says so with an assertion that reads body.errors
fail-graphql.yaml:9
timing dns n/a connect 0.4ms tls n/a ttfb 2.1ms total 4.5ms (new connection)
0 passed 1 failed 0 errored 0 timed out 0 skipped 0 blocked 0 not run (1 tasks, 1 requests, 0.0s)
assert #2 is the added assertion: the plan wrote one, and the errors check is the second.
Names in scope
The same as an http step: request (method, url), status, proto, headers, body,
duration with the phases dns, connect, tls, ttfb and total, redirects, and env,
tasks, run. body is the decoded response, so body.data and body.errors are the paths a
plan reads. repeat and retry are not keys of a graphql step; a query that has to be sent
several times is an http step with a json body.
Load errors
| Plan | Load error |
|---|---|
neither query nor queryFile |
a graphql step needs query, or queryFile naming a file relative to the plan |
| both | a graphql step takes query or queryFile, not both |
a {{ in the query |
a graphql query is a literal; pass values in variables |
| a query that does not parse | the query does not parse at its line 1, column 9: Expected Name, found <EOF>, with the line and column inside the query |
no url |
a graphql step needs a url |
A load error names the step by path and the plan line, and nothing is sent:
load-errors.yaml:13: tasks.templated-query.steps[0].graphql.query: a graphql query is a literal; pass values in variables
load-errors.yaml: 1 load error(s); nothing ran
This page is docs/content/steps/graphql.md in the repository.