One file says what an experiment needs. cosmos-run reads it, works out whether the testbed can supply it, and carries it out. This page is the reference for the file format.
apiVersion: cosmos/v1 # required - which format this is
name: my-experiment # required - identifier, lowercase
description: One line # optional but write it
parameters: {...} # what the user may override
resources: {...} # required - what hardware is needed
subdomains: {...} # optional - per-sandbox overrides
services: {...} # optional - shared equipment to configure
steps: [...] # what to do
collect: [...] # what to keep
teardown: [...] # extra steps that always run
Only three keys are required: apiVersion, name, resources. A description with nothing else is valid and does nothing — useful for asking --resolve-only whether hardware exists.
apiVersionCurrently cosmos/v1, and nothing else is accepted. A description naming a version the console does not know is refused rather than guessed at — the point of the field is to fail loudly when a console's framework is older than the experiment it was handed, instead of silently ignoring a key it does not understand.
nameLowercase, starting with a letter, hyphens not underscores: ^[a-z][a-z0-9-]*$, at most 64 characters. It names the run directory and appears in the manifest, so make it recognisable six months later.
Note the difference from the identifiers inside the file — resource and parameter names use ^[a-z][a-z0-9_]*$, with underscores. The validator will tell you which one you got wrong:
experiment description 'needs_impossible' is not valid (1 problem):
name: 'needs_impossible' does not match '^[a-z][a-z0-9-]*$'
parametersWhat a user may change without editing the file. Each entry declares a type and usually a default:
parameters:
sub: {type: string, default: bpsk, choices: [bpsk, qpsk, 8psk, 16qam, 64qam]}
amplitude: {type: float, default: 0.15, min: 0.001, max: 1.0}
seconds: {type: int, default: 10, description: Measurement duration}
verbose: {type: bool, default: false}
amplitudes: {type: list, default: [0.02, 0.04, 0.08]}
| Key | Meaning |
|---|---|
type |
Required. string, int, float, bool, list, dict |
default |
Value when the user does not override |
choices |
Permitted values; an override outside them is refused by name |
min, max |
Numeric bounds |
required |
If true, the run refuses without a value |
description |
Shown to the reader — worth writing |
Parameters are referenced elsewhere with {{ }} and overridden on the command line:
cosmos-run my-experiment.yml -e sub=qpsk -e amplitude=0.06
A default that never works is a trap. Ship the values that actually run. Measured on 2026-08-03: an sb5 description whose default transmit gain could not close the link decoded nothing across a 73-point sweep, and every published command for it silently overrode the default — which was the signal that the default was wrong, not that the commands were careful.
resourcesRequired, and covered in full on resources and feasibility. The short version — each named resource is either pinned to specific hardware or described by what it must be able to do:
resources:
tx:
pin: [node1-1.sb5.cosmos-lab.org] # this node or nothing
rx:
requires: {sdr: {usrp: {}}} # any node with a USRP
count: 1
image: baseline-sdr.ndz
stepsAn ordered list. Each step carries exactly one kind plus optional modifiers — see step kinds:
steps:
- name: Start the receiver
run:
cmd: /root/rx.py --freq 2400000000
background: true
unit: cosmos-rx
target: rx
- name: Measure
role: cosmos_lab.orchestration.gnuradio_ber
target: tx
vars:
cosmos_ber_mods: [bpsk, qpsk]
target: aims a step at one declared resource. Without it a step runs on every node in the experiment.
collectWhat to bring back to the console. Paths on the nodes:
collect: [/run/cosmos/fr3-ofdm]
Everything lands in ~/cosmos-artifacts/<name>-<timestamp>/ beside the run manifest — see artifacts.
teardownSteps that run whether or not the experiment succeeded. Anything that puts shared equipment into a non-default state belongs here, because the next user's experiment depends on it:
teardown:
- name: Return the attenuator to maximum
service: {rfmatrix: {atten_db: 95}}
Roles that configure equipment already restore it themselves; teardown is for what your description did directly.
subdomains and servicessubdomains overrides values per sandbox, for the cases where one description genuinely must differ between them. services names shared equipment — attenuator matrices, positioners — that the run should configure. Both are optional and neither is needed for a two-node experiment.
A description is validated twice before anything is touched.
Shape comes first: which keys exist, of what type, in what combination. That is JSON Schema, and it catches a misspelled key or a string where a number belongs.
Reference comes second, and it is where the interesting mistakes are: a step aimed at a resource nobody declared, an override that is not among the declared choices, a service the description never asked for. Shape-checking cannot catch those — it can only say "does not match schema", which tells you nothing. The semantic pass names the wrong identifier and lists the ones that would have worked:
step 2 references undeclared resource 'reciever' - declared: tx, rx
That is the class of error users actually hit, so it gets the better message.
cosmos-run my-experiment.yml --resolve-only
Reads the description, validates both layers, queries the read-only inventory, and reports which nodes it would use. Touches no hardware and needs no reservation — so it is the right way to develop a description before booking time.
Last verified: 2026-08-03 on console.grid.cosmos-lab.org.