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.

verodocs