Data in experiments
Values move through a run: a parameter chooses the target, a field of one response becomes a header of the next request, a generated id goes out in a command and comes back in a check. This page covers where those values come from and how a field uses them.
| Source | Written as | Set where |
|---|---|---|
| Parameter | {{api}} or {{params.api}} | Parameters panel, a profile, Run with… |
| Variable | {{token}} or {{vars.token}} | a node during the run: Extract value, a wait, a send that waits for a reply |
| Secret | {{secret.API_TOKEN}} | the computer's credential store, or the server's environment and files |
| Built-in value | {{run.seed}}, {{now.iso}}, {{counter}} | the run itself |
| Generator | {{uuid}}, {{random_int(1, 100)}} | drawn from the run's seed |
Parameters
A parameter is a named text value that any templated field can use. Keep targets in parameters, so a change of address is one edit instead of one per node.
Adding a parameter
- Press Parameters (
{ }) in the editor's toolbar. - On the Defaults tab, press Add parameter.
- Type the Name and the Value, for example
apiandhttp://127.0.0.1:8080. - In a node's field, write
{{api}}/login.
Every change in the panel is an edit of the experiment: it is saved with it and undone with Ctrl+Z like any other.
Rules
| Rule | Limit |
|---|---|
| Name | starts with a letter or _, then letters, digits and _ |
| Reserved names | vars, params, secret, run, node, now, uuid, counter, random_int, random_float, pick |
| Parameters per experiment | 64 |
| Size of one value | 64 KiB |
| Names | unique; a variable may not have a parameter's name |
A value is plain text and is inserted as written: {{…}} inside a value is not resolved. When a field asks for a part of a parameter ({{config.ports[0]}}), the value is read as JSON; a value that is not JSON has no parts.
A parameter whose name is invalid, reserved or taken twice does not keep the experiment from saving, so you can keep typing; the experiment does not run until the name is fixed.
Profiles
A profile is a named set of parameter values — Laptop, Stage, Venue — so switching the target is a choice, not an edit of every node. A profile changes some parameters; the others keep their default.
Making a profile
- Open Parameters and press Profile. A new tab opens.
- Rename it in Profile name.
- For each parameter the profile changes, type its value. An empty field keeps the default, shown greyed in the field; Use the default value (↺) clears a value.
- Press Use for runs to run with it. The active profile's tab carries ● In use. Use for runs on the Defaults tab goes back to the defaults.
Once an experiment has profiles, a Profile list in the toolbar switches between them. The active profile is used by runs, the preview and Send now, and it is saved in the experiment, so an exported file opens with the same targets. Delete profile deletes the profile on screen.
| Rule | Limit |
|---|---|
| Profiles per experiment | 32 |
| Name | 1–64 characters, unique (spaces at the ends do not count) |
| Values | only parameters that exist; at most 64 |
Renaming or removing a parameter changes it in every profile at once.
Which value a run uses
Later wins:
- the parameter's default, on the Defaults tab;
- the active profile's value, if it sets one;
- a value typed in Run with… for this run only — see running with other values.
Run with… may only set parameters the experiment has. The run report records the profile, the values typed for the run and every value it used.
Profiles that would not run
Each time the experiment is checked, the other profiles and the defaults are checked too. One that would fail — say, a URL that is not http:// or https:// — carries ⚠ in the tabs and in the toolbar's list, and its tooltip says why. It does not stop runs with the profile in use.
Templates
Text inside {{ }} is an expression; everything else in a field is kept exactly as written.
{{api}}/users/{{user.id}}?trace={{uuid}}
Bearer {{secret.API_TOKEN}}- Spaces inside the braces do not matter:
{{ token }}is{{token}}. \{{writes a literal{{.- A
}}on its own is plain text. - A value is inserted as it is, without quotes. In a JSON body, write the quotes yourself:
"id": "{{uuid}}".
Names
| Expression | Value |
|---|---|
{{name}} | the variable name if one is set on this path, otherwise the parameter name |
{{vars.name}} | the variable only |
{{params.name}} | the parameter only |
{{secret.NAME}} | the stored secret NAME — see secrets |
{{name.field}} | a field of a JSON value |
{{name[0]}} | an element of a JSON array |
{{name["a b"]}}, {{name['a b']}} | a field whose name has other characters |
A field name after . may hold letters, digits, _ and -. Steps chain: {{reply.args[0]}}, {{order.items[2].sku}}.
How values are written
| Value | Written as |
|---|---|
| text | the text |
| number | its shortest form: 42, 0.5 |
true, false | true, false |
null | null |
| object, array | compact JSON: ["x","y"] |
Built-in values
| Expression | Value |
|---|---|
{{run.id}} | the run's job number; 0 in the preview and in Send now |
{{run.seed}} | the seed of this run |
{{node.id}} | the id of the node being executed |
{{now}} | the current time, Unix milliseconds |
{{now.iso}} | the current time in UTC, ISO 8601 with milliseconds: 2026-09-30T12:34:56.789Z |
{{counter}} | how many times this node has run in this run, this time included, from 1 |
{{counter}} counts per node: in a Loop body it is the iteration's number, in a node that repeats the send's number. run, node and now have only the fields listed; anything else is an error.
Generators
| Expression | Value |
|---|---|
{{uuid}} or {{uuid()}} | a version 4 UUID |
{{random_int(min, max)}} | a whole number from min to max, both included; whole-number arguments, min ≤ max |
{{random_float(min, max)}} | a number from min up to, but not including, max, with 3 decimals; min < max |
{{random_float(min, max, digits)}} | the same with digits decimals, 0–9 |
{{pick(a, b, c)}} | one of the arguments, at least one |
Arguments are separated by commas. One in quotes ("dark blue" or 'a, b') may hold anything but its own quote; one without quotes may hold letters, digits and _ - . : / +. An empty argument is an error.
Every generator draws from the run's seed. The values of one execution of a node depend only on the seed, the node's id and how many times the node has run, so parallel branches never change each other's values, and a run with the same seed generates the same values again. Draws within one node follow the order of its fields. {{now}} and {{run.id}} are not reproducible. See seeds.
Suggestions
Typing {{ in a templated field, or pressing Ctrl+Space, opens a list in four groups: Parameters with their values, Variables set upstream of this node with the node that sets them (a reply's fields too, such as reply.args[0]), Secrets and Generators. ↑ and ↓ choose, Enter or Tab inserts, Esc closes the list and keeps the field.
Unknown names are errors
A name without a value never becomes an empty string. Before a run, every name a field uses must be a parameter, a valid secret name, or a variable set on every path that leads to the node. The editor points at the node and the field:
| Problem | Before the run | During the run |
|---|---|---|
| A name nobody sets | name.unknown | — |
| A variable set on some paths only | name.not_on_every_path | — |
{{params.x}} without a parameter x | param.unknown | — |
| A field that a value does not have | — | template.no_field |
Unclosed {{, an empty {{}}, a malformed argument | template.*, with the position | — |
The texts of these codes are in Errors.
Which fields take templates
| Node | Templated fields |
|---|---|
| HTTP request | URL, header names and values, body, the user name and password of Basic and Digest, the Bearer token |
| OSC message | target, address, text arguments; with a reply: its address pattern and rule values |
| UDP datagram | target, payload; with a reply: its pattern |
| TCP message | host, payload |
| MQTT publish | broker host, topic, payload |
| Log marker | message |
| Response text | expected text |
| Response header | header name, expected text |
| Check value, Branch on value, a Loop's exit condition | value, expected value |
| Wait for OSC | address pattern, rule values |
| Wait for UDP, Wait for WebSocket | pattern |
| Wait for MQTT | broker and topic (parameters only), pattern |
| Wait for HTTP request | path pattern, conditions |
| Impairment | listen and target (parameters only) |
| WebSocket connect | URL, header names and values |
| WebSocket send | payload |
| WebSocket close | reason |
Numbers — ports, timeouts, delays, statuses, typed OSC numbers — and the listening addresses of waits are literal. An Emulator renders its own replies with what arrived ({{request.…}}) and the parameters; see faults.
Parameters only. Some fields are opened before the first step, when no variable exists yet: a Wait for MQTT's broker and topic, an Impairment's listen and target. They take text and parameters, nothing else (node.params_only).
Checked like literals. A field that uses only parameters is resolved before the run and checked as the text the run will send: a URL must be http:// or https://, an OSC target IP:port or host:port, a header name valid. A field with variables or generators is checked when it runs.
Preview
When the selected node has a template, its properties show what it will do with the values known now: Will be sent for a send, Will wait for for a wait, Will compare for a comparison. The engine resolves it, with the same code a run uses, so the preview never disagrees with the run.
- Parameters come from the active profile.
- Variables come from what the editor has seen in this session: the last run's steps, and Send now.
- A stored secret shows as
••••. - A name without a value yet stays as written, and the preview lists it. A secret that is not stored is listed apart.
- Generators use the experiment's pinned seed, or
0when none is pinned, as the node's first execution. With a pinned seed, the preview shows the generated values the node's first execution in a run will send.
Extracting values
Extract value reads one value of the latest HTTP response on its path and writes it into a variable.
| Field | What |
|---|---|
| Variable | the variable to write; the naming rules of parameters apply |
| Take from | where the value comes from (below) |
| JSON path, Header name or Pattern (group 1 if present) | what to read, depending on the source |
| Take from | Reads | Value |
|---|---|---|
| JSON field | the body as JSON, at a path | the JSON value: text, number, object, array |
| Header | the first header with that name, in any case | text |
| Status code | the status code | a number |
| Whole body | the whole body | text |
| Regular expression | the first match in the body | capture group 1 if the pattern has one, else the whole match |
JSON paths. $.token, $.items[0].id, $["a b"], $['a b']['c-d']; the leading $. may be left out (token, items[0].id), and $ alone is the whole body.
Regular expressions use the syntax of the Rust regex engine, which has no look-around and no back-references. The match is searched anywhere in the body; anchor it with ^ and $ when that matters.
The step fails, naming what is missing, when:
- no HTTP request ran before it on this path (
check.no_response; the editor already refuses a graph where none can,graph.needs_http); - the body is not JSON, or the path is not in it;
- the header is not there, or the pattern does not match;
- the body is over the 256 KiB a response keeps, for a JSON path or the whole body, and for a pattern that matched nothing in the part kept (
extract.truncated).
The timeline shows the value written: token = abc123.
Extract by clicking
Send now on an HTTP request shows its JSON response. Click a value in it: an Extract value node is added after the request, with the path filled in and a name taken from the key, and the value is known to the preview at once.
Variables
A variable holds a JSON value. These nodes write one:
| Node | Writes | On the output |
|---|---|---|
| Extract value | the extracted value | its output |
| Wait for OSC, Wait for UDP, Wait for MQTT, Wait for HTTP request, Wait for WebSocket | what arrived, default name reply (request for HTTP) | Matched only |
| OSC message, UDP datagram with wait for a reply | the reply, default name reply | its output |
What a wait writes is an object; later fields read its parts:
| Wait | Fields |
|---|---|
| OSC | address, args, from, ms |
| UDP | text, hex, bytes, from, ms, and match with a pattern |
| MQTT | topic, and the fields of UDP |
| WebSocket | the fields of UDP, and json when the message is JSON |
| HTTP request | method, path, query, headers, body, json, params, from, ms |
ms is the time from the branch's latest action to the arrival. The exact contents are in the nodes' reference.
Where a variable is known
A variable exists from the output that writes it onward, on the paths that pass through that output:
- After a merge of alternative paths — the Yes and No of a branch meeting again — only what every path set is known.
- After Join branches, what any branch into it set is known: all of them ran.
- After a Loop's Done or Limit, and in its exit condition, what every iteration of the body sets is known.
- A wait's variable is not known after its Timeout output.
Each parallel branch works on its own copy of the variables. A Join merges the copies in the order of its incoming wires, the later wire winning a name both set, so the result never depends on which branch finished first. See how a run moves.
Comparing values
Check value fails the run when a comparison does not hold; Branch on value leaves through Yes or No; a Loop uses the same comparison as its exit condition. Each has a Value, a Condition and an Expected value, and both texts are templates:
| Value | Condition | Expected |
|---|---|---|
{{status}} | less than | 300 |
{{reply.args[0]}} | equals | {{nonce}} |
| Condition | Holds when |
|---|---|
| equals, does not equal | the two are equal (not equal) — as numbers when both are numbers (200 equals 200.0), else as exact text, case included |
| less than, at most, greater than, at least | as numbers; a side that is not a number fails the step (compare.not_numbers) instead of a quiet no |
| contains | the value contains the expected text, case included |
| matches regex | the regular expression in the expected value matches anywhere in the value |
| is empty, is not empty | the value is empty, or not, after trimming spaces; the expected value is not used |
A number is text that reads as one after trimming spaces: 42, -1.5, 1e3. The timeline shows the comparison as made, 401 = 200, each side cut to 120 characters.
Secrets
A token or password goes into a field as {{secret.NAME}}. The experiment file keeps only the name; the value stays where it is stored and never reaches the interface.
Where secrets live
| Where Signal Lab runs | Store | From the interface |
|---|---|---|
| Desktop app on Windows | the Windows Credential Manager, under the service SignalLab, one entry per name | set, replace, remove |
| Desktop app on Linux | none: a run that needs a secret fails with secret.unsupported | — |
| Server | the environment variable SIGNALLAB_SECRET_<NAME>, else the file <NAME> in its secrets folder, /run/secrets/signallab unless set otherwise | read-only |
signallab on the command line | as a server, or the Windows Credential Manager with --secrets system | — |
The heading of the Secrets section has a tooltip that says which of these applies where you are: the Windows store, the server's environment and files, or — in the desktop app on Linux — that there is no store. There, secret.unsupported says the secrets are kept in the Windows Credential Manager, which the system does not have.
A secret belongs to the computer or the server, not to one experiment: two experiments that use {{secret.API_TOKEN}} use the same value.
On a server, the environment variable wins over the file. A file's trailing line break is not part of the value, and an empty file counts as no secret. The server's folder is set with --secrets-dir or SIGNALLAB_SECRETS_DIR; see the server. For the command line, see signallab run.
| Rule | Limit |
|---|---|
| Name | starts with a letter or _, then letters, digits and _; at most 128 characters |
| Value | not empty, at most 16 KiB |
Setting a secret
On Windows:
- Open Parameters. The Secrets section lists every secret the experiment's fields use, each stored or not set on this computer.
- Press Set… next to the name, or Secret for a name no field uses yet.
- Type the value — the field shows dots — and press Save or Enter. The field is cleared; nothing can read the value back.
Replace… stores a new value and Remove deletes it from the credential store. A name stored in this session is offered in the suggestions too.
In a browser connected to a server the section only says set on the server or not set on the server: set the value where the server runs, in one of two ways:
# in the server's environment
SIGNALLAB_SECRET_API_TOKEN='…'
# or as a file in its secrets folder
printf '%s' '…' > /run/secrets/signallab/API_TOKENA file is read each time a run starts, so a changed file counts from the next run; a changed environment variable needs the server restarted.
Before a run
Every secret the run's fields use must be stored. A missing one stops the run before any traffic, at the first node and field that use it (secret.missing). Send now checks the same for its node.
Masking
While a run or a Send now uses secrets, every occurrence of their values is replaced by •••• in everything that leaves the engine:
- step texts, errors and the variables a step wrote;
- the run report;
- Send now's result, the HTTP response it shows included;
- the Inspector's frames, captured while the run lasts — in a hex dump each byte of a value becomes
*, so offsets stay true.
Basic authentication sends name:password in base64; when either part holds a secret, that base64 text is masked too. The traffic itself carries the real value. The preview shows a stored secret as ••••. An Emulator's replies cannot use secrets.
Commands
The preview is experiment_resolve; secrets are listed, set and removed with secret_status, secret_set and secret_delete. No command returns a secret's value.