Skip to content

File format reference

TruSpec collections are plain-text YAML. This is the complete reference for all three file types and every field they support.

Source of truth. The Zod schema in packages/core/src/format/schema.ts defines the format. A JSON Schema is generated from it into packages/core/schema/ for editors and agents — see Editor integration. When in doubt, the schema wins.

Schema version: 0.1. Files may carry tspec: "0.1"; it's optional and defaults to 0.1. Any breaking change bumps the version and ships a migration.

Strict by default. Every file type rejects unknown keys, so a typo (assertion: instead of assertions:) surfaces immediately as a validation error rather than being silently ignored.


File types and naming

FileSchemaPurpose
<name>.tspec.yamlRequestOne HTTP request.
folder.tspec.yamlFolder configConfig inherited by requests in the folder.
environments/<name>.env.yamlEnvironmentVariables and secret names for one environment.

Discovery: any file ending in .tspec.yaml (except folder.tspec.yaml) is treated as a request. Environments live in an environments/ directory at or above the collection.


Request

One request per file. The full set of fields:

yaml
tspec: "0.1"                       # schema version (optional; defaults to 0.1)
name: Get pet by id                # REQUIRED — a human-readable name
method: GET                        # GET POST PUT PATCH DELETE HEAD OPTIONS (default GET)
url: "{{baseUrl}}/pets/{{petId}}"  # REQUIRED — {{var}} resolved at run time
headers:
  Accept: application/json
query:
  expand: owner
body:
  type: json                       # none | json | text | form | graphql
  content: { name: "Rex" }
auth:                              # optional; can inherit from folder.tspec.yaml
  type: bearer                     # none | bearer | basic | apikey
  token: "{{token}}"
assertions:                        # declarative + machine-checkable
  - { type: status, equals: 200 }
  - { type: jsonpath, path: "$.id", exists: true }
  - { type: duration, ltMs: 1000 }
  - { type: schema }                 # validate body against the linked spec's response schema
capture:                           # save response values into vars for later requests
  ownerId: "$.owner.id"
order: 1                           # run order within a collection (lower first; default 0)
script:                            # advanced — see ./scripting.md
  pre: "tr.set('ts', new Date().toISOString())"
  post: "tr.expect(tr.response.status === 200, 'ok')"
docs: "Fetch a single pet by its id."
spec:                              # links request → OpenAPI operation (drift/coverage)
  operation: "GET /pets/{id}"
  operationId: getPetById

Fields

FieldTypeRequiredDefaultNotes
tspecstringno"0.1"Schema version.
namestringyesNon-empty. Shown in run output and reports.
methodenumnoGETGET POST PUT PATCH DELETE HEAD OPTIONS.
urlstring (template)yesMay contain {{vars}}. Relative URLs are joined onto the folder baseUrl.
headersmapnoString/number/boolean values; templated.
querymapnoAppended as the query string; templated.
bodyBodynoOmit entirely for no body.
authAuthnoinherits folderRequest auth overrides folder auth.
assertionsAssertion[]no[]Declarative checks.
capturemapnoSave response values into variables.
ordernumberno0Lower runs first; ties broken by file path.
script{ pre?, post? }noScripting.
docsstringnoFree-form documentation.
spec{ operation?, operationId? }noLinks to an OpenAPI operation.

Bodies

The body field is a tagged union on type. Omit body (or use type: none) for no request body. The runner sets a default Content-Type for each type unless you've already set one in headers.

json

yaml
body:
  type: json
  content:
    name: Rex
    tags: [good, boy]

content is any JSON value (object, array, string, number, …). Templated deeply — every string inside is interpolated. Sent as application/json.

text

yaml
body:
  type: text
  content: "plain text payload {{suffix}}"

Sent as text/plain.

form

yaml
body:
  type: form
  content:
    grant_type: password
    username: "{{user}}"

A map of string values, serialized as application/x-www-form-urlencoded.

graphql

yaml
body:
  type: graphql
  query: "query($id: ID!) { user(id: $id) { name } }"
  variables: { id: "{{userId}}" }

Sent as a POST with a JSON { query, variables } body (application/json). variables is optional and templated.


Auth

The auth field is a tagged union on type. Auth can be set on the request or inherited from folder config; a request's own auth wins. Secrets are referenced by name ({{token}}), never inlined.

TypeFieldsEffect
noneNo auth.
bearertokenAuthorization: Bearer <token>
basicusername, passwordAuthorization: Basic <base64(user:pass)>
apikeyname, value, inAPI key in a header (default) or query param.
yaml
auth:
  type: bearer
  token: "{{token}}"
yaml
auth:
  type: apikey
  name: X-API-Key
  value: "{{apiKey}}"
  in: header        # header (default) | query

For apikey with in: query, the key is appended to the URL's query string — and its value is masked in reported output when declared as a secret.


Assertions

Assertions are declarative and machine-checkable — they (not JS scripts) are what power CI gating and coverage. Each assertion is an object with a type and one or more conditions. An assertion must specify at least one condition; an assertion with none always fails. When an assertion lists several conditions, all of them must hold.

TypeConditionsChecks
statusequals · in: [..] · lt · gteThe HTTP status code.
headername + (equals · matches · exists)A response header (name is case-insensitive).
jsonpathpath + (equals · exists · matches)A value selected from the JSON body.
bodycontains · matchesThe raw response body text.
durationltMsWall-clock request duration (strictly less than).
schemastatus · contentType · requiredThe body against the spec's OpenAPI response schema.

status

yaml
- { type: status, equals: 200 }
- { type: status, in: [200, 201, 204] }
- { type: status, lt: 400 }            # any non-error
- { type: status, gte: 200, lt: 300 }  # combine: a 2xx
yaml
- { type: header, name: Content-Type, matches: "application/json" }
- { type: header, name: X-Request-Id, exists: true }
- { type: header, name: Cache-Control, equals: "no-store" }

matches is a JavaScript regular expression (as a string).

jsonpath

yaml
- { type: jsonpath, path: "$.id", exists: true }
- { type: jsonpath, path: "$.status", equals: "active" }
- { type: jsonpath, path: "$.items[0].sku", matches: "^SKU-" }
  • exists checks whether the path selects any value.
  • equals uses structural equality, so it works for objects and arrays too.
  • matches tests the stringified value against a regex.
  • The body must parse as JSON; if it doesn't, jsonpath assertions don't match.

See JSONPath support for the supported subset.

body

yaml
- { type: body, contains: "ok" }
- { type: body, matches: "\"status\"\\s*:\\s*\"active\"" }

Runs against the raw response text — useful for non-JSON responses.

duration

yaml
- { type: duration, ltMs: 1000 }   # fail if the request took ≥ 1s

schema

yaml
- { type: schema }                          # validate against the linked operation's response schema
- { type: schema, status: 200 }             # pin a specific status's schema
- { type: schema, contentType: application/json }
- { type: schema, required: true }          # fail if the spec declares no schema for this status

Validates the response body against the OpenAPI response schema for the operation the request is linked to — catching behavioral drift the structural drift check can't see. It needs a spec supplied to the run (truspec run --spec <openapi>, or the dedicated truspec contract gate); without a spec it's a passing skip, so a collection stays runnable spec-free. By default it checks the schema for the response's actual status and application/json; an undocumented status is skipped unless required: true. With truspec run --spec, every spec-linked request is validated automatically — no explicit schema assertion needed. See Spec sync → Response validation.

Invalid regexes fail gracefully. A bad matches pattern fails that assertion with an assertion error: … message rather than aborting the whole run.


Chaining with capture

capture saves values out of a response into variables that later requests in the same run can use. Combined with order, this expresses login-then-call flows with no scripting.

yaml
# auth/01-login.tspec.yaml
name: Log in
method: POST
url: "{{baseUrl}}/login"
order: 1
body:
  type: json
  content: { username: "{{user}}", password: "{{password}}" }
capture:
  token: "$.access_token"     # jsonpath shorthand
yaml
# users/02-me.tspec.yaml
name: Get current user
method: GET
url: "{{baseUrl}}/me"
order: 2
auth:
  type: bearer
  token: "{{token}}"          # the value captured above

A capture source can be:

FormExampleCaptures
jsonpath string (shorthand)token: "$.access_token"A value from the JSON body.
{ jsonpath }id: { jsonpath: "$.data.id" }Same, explicit.
{ header }loc: { header: "Location" }A response header value.
{ status: true }code: { status: true }The numeric status code.

Notes:

  • Requests run in order (ascending), then by file path — so lower-order requests can feed higher ones.
  • A jsonpath that selects an object/array is captured as its JSON string.
  • A capture whose source resolves to nothing is simply skipped (the variable stays unset).
  • Captures flow forward only within a single run invocation; they are not persisted.

The spec block ties a request to an OpenAPI operation so drift and coverage can reason about it.

yaml
spec:
  operation: "GET /pets/{id}"   # "${METHOD} ${path}" — matches the spec's path template
  operationId: getPetById       # preferred when both the spec and request have it

Matching rules:

  • If both the request and the spec operation have an operationId, they match on that.
  • Otherwise the operation string (METHOD path) is normalized and matched against the spec's METHOD path key.

Use the path template exactly as it appears in the spec (/pets/{id}), not a concrete URL.


Variables and interpolation

Any string field may contain {{name}} placeholders. They're resolved at run time from the active environment, folder config, secrets, and values captured earlier in the run (see Core concepts → Variables).

  • Placeholder names may contain letters, digits, ., -, and _: {{baseUrl}}, {{api.key}}, {{user-id}}.
  • Surrounding whitespace is ignored: {{ token }}{{token}}.
  • Interpolation descends into objects and arrays (e.g. every string in a JSON body).
  • Unresolved variables fail the request before it is sent, and the run reports exactly which names were missing — nothing is silently sent with an empty value baked in.

Folder config

folder.tspec.yaml holds configuration inherited by every request in its folder and all subfolders. It's how you avoid repeating a base URL or auth on every request.

yaml
tspec: "0.1"
name: Blog                 # optional label
baseUrl: "{{baseUrl}}"     # prepended to relative request URLs
headers:
  Accept: application/json
auth:
  type: bearer
  token: "{{token}}"
FieldTypeNotes
tspecstringDefaults to 0.1.
namestringOptional label.
baseUrlstring (template)Joined onto a request url that isn't already absolute.
headersmapMerged into each request's headers (request headers win).
authAuthUsed when a request has no auth of its own.

Composition (root → leaf, deeper wins):

  • baseUrl, auth, name — the deepest value replaces shallower ones.
  • headers — merged key by key across the chain, then merged with the request's own headers (the request wins on conflicts).

A request url that begins with http:// or https:// is treated as absolute and the baseUrl is not applied.


Environment files

Environments live in environments/<name>.env.yaml and are selected with --env <name>.

yaml
tspec: "0.1"
name: local                # REQUIRED
variables:
  baseUrl: "http://localhost:4000"
  petId: "1"
secrets:                   # NAMES only — values come from the OS env or a .env file
  - token
FieldTypeNotes
tspecstringDefaults to 0.1.
namestringRequired.
variablesmapString/number/boolean values exposed as {{name}}. Default {}.
secretsstring[]Names of OS/.env variables surfaced as {{name}}. Default [].

Secrets are never stored here — only their names. At run time each name is looked up in:

  1. a .env file at the workspace root (KEY=VALUE lines, # comments, optional quotes), then
  2. real OS environment variables, which win over the .env file.

If a declared secret can't be resolved, truspec run prints a warning naming it. Resolved secret values (6+ characters) are masked with *** everywhere they could surface in reported output — URLs, bodies, headers, captured values, and error messages — including their percent-encoded form in query strings.


JSONPath support

jsonpath assertions and captures use a small, dependency-free subset of JSONPath, enough for typical response shapes. A path must start with $.

SyntaxExampleSelects
Root$The whole body.
Member access$.user.name, $['user']['name']An object property.
Array index$.items[0]An element by index.
Negative index$.items[-1]An element counted from the end.
Wildcard$.items[*], $.items.*All array elements / object values.

Not supported in v0: recursive descent (..) and filter expressions ([?(…)]). For exact behavior, see packages/core/src/runner/jsonpath.ts.

When a path matches multiple values, equals/matches pass if any match satisfies the condition. If a path selects nothing, exists: false passes and exists: true fails. A capture of a multi-match path takes the first value.


Editor integration

A JSON Schema is generated from the Zod source into packages/core/schema/:

FileValidates
request.schema.json*.tspec.yaml request files
folder.schema.jsonfolder.tspec.yaml
environment.schema.jsonenvironments/*.env.yaml

Point your editor's YAML language server at them for autocomplete and inline validation. With the VS Code YAML extension:

jsonc
// .vscode/settings.json
{
  "yaml.schemas": {
    "./node_modules/@truspec/core/schema/request.schema.json": "*.tspec.yaml",
    "./node_modules/@truspec/core/schema/environment.schema.json": "environments/*.env.yaml"
  }
}

You can also validate programmatically — see Programmatic API → format. To regenerate the schema after a format change, run pnpm gen:schema.


See also

  • CLI — run and validate these files.
  • Spec sync — drift and coverage off the spec link.
  • Scripting — the script.pre / script.post escape hatch.
  • Core concepts — the workspace, inheritance, and variable model.

Released under the MIT License.