YAML Syntax Cheat Sheet
Quick reference for the TestLab YAML test authoring format (testlab: v1-alpha).
Document Types
| Kind | Purpose | Required key |
|---|---|---|
kind: tck |
Test Collection Kit — groups tests with shared config | tests: |
kind: test |
Single test case | steps: (≥1 step) |
TCK File Structure (index.yaml)
Only TCKs declare env:. Tests inherit it via namespace: matching.
kind: tck
testlab: v1-alpha
id: my-tck
namespace: my-namespace-v1.0
metadata:
name: "My TCK"
version: "1.0.0"
description: "What this TCK certifies"
dataspace_version: saturn
standards:
- id: CX-0135
version: v3.1.0
tags: [tag1, tag2]
env:
variables:
provider_url: ""
timeout_seconds: 300
services:
- name: testlab_connector
uses: service/connector_service
with:
base_url: ${{ env.provider_url }}
management_path: /management/v3
dsp_path: /api/v1/dsp/2025-1
dataspace_version: ${{ metadata.dataspace_version }}
auth:
type: api_key
api_key: "test-api-key"
api_key_header: "X-Api-Key"
returns:
connector_service:
type: class
class: ConnectorService
schemas:
my_schema:
file: my_schema.json
testdata:
sample_body:
file: sample_body.json
tests: # ordered; entries are mappings, never bare strings
- id: test_one.yaml # the file name in tests/
name: What test one proves
- id: test_two.yaml
name: What test two proves
skippable: true # optional; the operator may omit it via skip_tests
skippable defaults to false. The player rejects the whole run when
skip_tests names a test that is unknown or not marked, so only what the author
allowed can ever be skipped.
With extensions: [labs], an entry may also say async: true: a host that holds
the run open (a TckSession) leaves that test waiting until someone runs it, and
lets them run it again. A plain testlab run runs it in order like any other.
See Extensions › labs.
Test File Structure
Tests have NO env: block — they inherit everything from their parent TCK.
kind: test
testlab: v1-alpha
id: my-test
namespace: my-namespace-v1.0
metadata:
name: "My Test"
version: "1.0"
description: "What this test validates"
setup: []
steps: []
teardown: []
Step Structure
- id: step_id
uses: category/action
name: "Human-readable label"
with:
param: value
ref_param: ${{ steps.other_step.output }}
returns:
output_name:
type: string
class: SemanticClass
validate:
- uses: validate/assert
name: "Human-readable label for this check" # optional
with: { input: output_name, operator: not_null }
if: "${{ success() }}"
timeout_s: 30.0
checkpoint: # optional: true | false | a mapping
label: "Your system accepts the push"
Field order: id → uses → name → with → returns → validate → if → timeout_s → checkpoint
checkpoint: marks a step at which the run needs someone to act (true), names
one (label, description), or switches off a step type's own (false). See
Checkpoints.
Variable References (${{ ... }})
| Pattern | Resolves to |
|---|---|
${{ env.provider_url }} |
TCK environment variable |
${{ env.services.name.output }} |
Service output (e.g., .connector_service) |
${{ env.testdata.data_name }} |
Testdata file content |
${{ env.schemas.schema_name }} |
Schema file content |
${{ steps.step_id.output_name }} |
Output from a step in steps: |
${{ setup.step_id.output_name }} |
Output from a step in setup: |
${{ metadata.dataspace_version }} |
Metadata field value |
${{ execution.id }} |
The id of this run (the job id), e.g. to name what a test leaves in a shared system |
${{ execution.resource_prefix }} |
The player namespace and run id, e.g. testlab:<run-id>:, for resource IDs embedded in payloads |
Conditionals (if:)
| Expression | Meaning |
|---|---|
${{ success() }} |
All previous steps passed (default) |
${{ failure() }} |
At least one previous step failed |
${{ always() }} |
Always executes |
${{ steps.step_id.outcome == 'success' }} |
Specific step outcome check |
${{ steps.step_id.outcome == 'failure' }} |
Specific step failed |
${{ steps.step_id.outcome == 'skipped' }} |
Specific step was skipped |
${{ vars.region == 'eu' }} |
A manifest variable or binding, read by its id (!= too) |
${{ vars.execution.step_id.field == 'RECEIVED' }} |
A field an earlier step lists under returns: (vars.setup.… for a setup step) |
${{ vars.execution.step_id.field }} |
That field is truthy |
One expression per if:: there is no and, or or not — combine conditions with a flow/if step.
steps.<id> reads an earlier step of the same phase. Teardown ignores if:, and a step nested in a flow
step never reads its own, so the compiler refuses both, and any expression outside this table.
Returns Block
returns:
output_name:
type: string # string | integer | object | array | boolean
class: AssetId # Semantic class (for type filtering)
Common classes: AssetId, PolicyId, AgreementId, AuthToken, StatusCode, ResponseBody, ResponseHeaders, Uuid, Url, Bpn, ConnectorService, DataplaneUrl, MockInstance, Policy, String
Validate Block
Assertions reference the step's own output names directly (not ${{ }} for local outputs):
validate:
- uses: validate/assert
name: "the request is accepted" # optional; the report calls the check this
with:
input: status_code
operator: equals
value: 200
- uses: validate/assert
with:
input: "${{ steps.other_step.output }}" # cross-step refs use ${{ }}
operator: not_null
Operators:
| Operator | Description |
|---|---|
equals |
Exact match |
not_equals |
Not equal |
not_null |
Value is not null |
not_empty |
Value is not empty |
contains |
Contains substring/element |
not_contains |
Does not contain |
regex |
Matches regular expression |
status_code |
HTTP status code match |
greater_than |
Numeric > |
less_than |
Numeric < |
greater_or_equal |
Numeric ≥ |
less_or_equal |
Numeric ≤ |
between |
Numeric range (inclusive) |
JSON Schema Validation (validate/schema)
validate/schema validates a value against a full JSON Schema
document — distinct from the scalar operators above, which compare single values.
The schema is normally a reference to a file declared in the TCK env.schemas
block; the step fails (marking the step FAILED) when the payload does not conform,
reporting the offending field paths.
- id: validate_twin
uses: validate/schema
with:
input: "${{ steps.query_dt.response_body }}" # dict, list, or JSON string
schema: "${{ env.schemas.shell_descriptor_schema }}"
An inline schema object is also accepted — useful for a one-off existence check,
e.g. asserting an array contains an element with a given field:
- id: has_submodel_value_endpoint
uses: validate/schema
with:
input: "${{ steps.query_dt.response_body }}"
schema:
type: object
required: [submodelDescriptors]
properties:
submodelDescriptors:
type: array
contains:
type: object
properties:
endpoints:
type: array
contains:
properties:
interface: { const: SUBMODEL-VALUE-3.1 }
Extracting Values (json_path_extract)
json_path_extract reads a value out of a dict/list using a dot-separated path,
storing it in a variable for later steps.
- id: get_asset_id
uses: json_path_extract
with:
source: response_body # variable NAME, or a ${{ }} expression
path: "datasets.0.id" # dot path; numeric segments index lists
store_in_variable: asset_id # optional; step output is the value either way
source— either the name of a context variable (response_body), or a${{ }}expression that resolves to the data itself ("${{ steps.query.response_body }}"). Both forms work.path— dot-separated. Numeric segments index into lists (endpoints.0.href).
Predicate filters — select the first list element whose field matches a value, instead of relying on a positional index:
| Path | Selects |
|---|---|
items[id=abc].value |
first items element with id == abc |
endpoints[interface='SUBMODEL-VALUE-3.1'] |
quote values that contain ., ;, #, etc. |
descriptors.endpoints[interface='…'] |
steps over an intermediate array — no index needed |
descriptors[endpoints.interface='…'].id |
select by a nested field (dotted predicate key) |
Quote a predicate value ('…') when it contains dots or other separators —
interface names and semantic IDs always need quoting. A predicate that matches
nothing raises a clear error rather than returning a wrong value.
Utility Steps
util/parse_kv — parse a delimited key=value string (e.g. an EDC
subprotocolBody) into a dict, or select one key. Each pair is split on the
first = only, so a value may itself contain = (a URL query string, base64).
- id: get_edc_asset_id
uses: util/parse_kv
with:
input: "${{ subprotocol_body }}" # "dspEndpoint=https://…;id=urn:uuid:1234"
select: id # omit to return the whole dict
store_in_variable: edc_asset_id
# pair_separator: ";" (default)
# kv_separator: "=" (default)
util/base64 — encode or decode a string with base64 / base64url. The
AAS DTR requires an aas_identifier to be base64url-encoded before it goes
in a request path, so url_safe: true is the common case. Decoding restores
padding automatically, so an unpadded value round-trips without extra =.
- id: encode_aas_id
uses: util/base64
with:
input: "${{ env.twin_id }}" # "urn:uuid:1234" or a URL
mode: encode # encode (default) | decode
url_safe: true # -/_ instead of +//; needed for DTR
strip_padding: false # drop trailing '=' when encoding
store_in_variable: aas_identifier_b64
util/log — echo a resolved value to stdout and the run log while authoring.
Asserts nothing and always passes; remove once a test is finalised.
- id: show_href
uses: util/log
with:
message: SUBMODEL-VALUE-3.1 href
value: "${{ submodel_value_href }}"
Complex Variables (TCK only)
Typed variables in env.variables cover everything a test needs before its steps
run: runtime inputs and reusable config objects such as policies and assets.
Declare each one once and reference it by its id — ${{ env.<id> }}. Every
variable publishes one value under value, whatever its type, and its uses:
verb decides that type: the compiler refuses a returns: that says otherwise.
env:
variables:
# Runtime input collected from the SUT operator
- id: sut_dsp_url
uses: variable/type/string
name: SUT DSP Endpoint URL
with:
source: input
scope: sut
placeholder: "https://connector.example.com/api/dsp"
returns:
value:
type: string
# A secret the SUT operator supplies: usable like any input, shown as *** in every record
- id: sut_api_token
uses: variable/type/string
name: Token the SUT's API expects
secret: true
with:
source: input
scope: sut
returns:
value:
type: string
# Constant value with a default
- id: sut_response_timeout
uses: variable/type/integer
with:
value: 300
returns:
value:
type: integer
# Reusable access policy, referenced wherever a step needs it
- id: usage_policy
uses: config/connector/policy
name: Required usage policy
with:
value:
permissions:
- action: use
constraints:
and:
- left_operand: UsagePurpose
operator: isAnyOf
right_operand: "cx.ccm.base:1"
returns:
value:
type: object
class: Policy
# Asset definition the provider steps provision
- id: api_asset
uses: config/connector/asset
name: API asset
with:
value:
name: CCMAPI Notification Asset
base_url: "https://backend.example.com/ccm"
properties:
dct:type:
"@id": "https://w3id.org/catenax/taxonomy#CCMAPI"
cx-common:version: "3.0"
returns:
value:
type: object
class: Asset
Reference in tests: ${{ env.sut_dsp_url }}, ${{ env.usage_policy }},
${{ env.api_asset }} — the id alone, for every type.
secret: true marks a variable whose value is a secret. It is read like any other — ${{ env.sut_api_token }}
resolves to the string — and every record of the run (trace, live events, console transcript, job API) shows it
as ***. An embedder reads the flag off the declaration (Tck.all_variables()[id].secret), in an uncompiled
manifest and in a compiled package alike. An input whose id looks like a credential (…secret, …password,
…token, …api_key, …credential, …authorization) is masked even without the flag. A secret input is read
without surrounding whitespace (a trailing newline from a key file is dropped), and a number or an object is
masked too — every string inside the object.
Handed to the SUT operator (with.source: register): the TCK carries the
document and the operator registers it in their own system before the run —
a policy their connector offers, a twin their registry holds, a payload their
application serves. with.instructions says what to do with it; the engine
shows both before the run starts. A test reads the same document back, so it
knows exactly what the SUT should answer with:
- id: iso9001_certificate
uses: variable/type/object
name: ISO9001 test certificate
with:
source: register
instructions: >
Register this certificate in your certificate management and share it
in your EDC as a CompanyCertificate asset. The pull test retrieves it.
value:
businessPartnerNumber: "${{ env.provider_bpn }}"
type:
certificateType: ISO9001
returns:
value:
type: object
Failure Handling
There is no per-step failure policy. A failed validation (hard assertion) fails its step; a failed step fails the test — execution stops, the remaining steps are skipped, and teardown runs. Teardown steps keep executing even when one of them fails.
Common uses: Handlers
| Prefix | Examples |
|---|---|
mock/ |
mock/api, mock/wait/http_request |
connector/ |
connector/pull_data_filtered, connector/create_asset, connector/health_check |
util/ |
util/generate_uuid, util/log, util/parse_kv, util/base64 |
validate/ |
validate/assert, validate/field, validate/schema |
variable/ |
variable/type/string, variable/type/integer, variable/type/boolean |
config/ |
config/connector/policy, config/connector/asset |
service/ |
service/connector_service |
| (no prefix) | json_path_extract |
See Extracting Values and Utility Steps below for the extraction and helper handlers.
Dataspace Versions
| Version | EDC Compatibility | Protocol |
|---|---|---|
saturn |
EDC v0.11+ | DSP 2025-1 |
jupiter |
EDC v0.8–0.10 | Legacy DSP |
Set in metadata.dataspace_version or per-service via with.dataspace_version.
Variable Resolution Priority
- Runtime variables — CLI
--varflags or API input - TCK environment —
env.variablesin the TCK file - Step outputs — resolved at execution time