Parameters & Interpolation
Workflows use JMESPath expressions enclosed in double curly braces ({{ ... }}) to make steps dynamic. Parameters can reference the execution context, workflow parameters, and results from previous steps.
Workflow Parameters
Define what inputs your workflow accepts using parameterSchema (standard JSON Schema):
parameterSchema:
type: object
properties:
subnet:
type: string
description: "Subnet to scan (CIDR notation)"
community:
type: string
description: "SNMP community string"
required:
- subnet
When executing the workflow, pass parameter values:
{
"workflow": "wf.example.neops.io/device_discovery:1.0.0",
"executeOnParameters": {},
"parameters": { "subnet": "10.0.1.0/24", "community": "public" }
}
Access them in steps via {{ parameters.<name> }}:
- type: functionBlock
label: get_version
functionBlock: "fb.examples.neops.io/show_version:1.0.0"
runOn: device
Interpolation Sources
Every {{ expression }} is evaluated as a JMESPath query against the execution context. The context contains:
| Path | Description | Example |
|---|---|---|
parameters.* |
User-provided execution parameters | {{ parameters.timeout }} |
context.device.* |
Current device data | {{ context.device.hostname }} |
context.device.facts.* |
Device facts from CMS | {{ context.device.facts.version }} |
context.device.platform.* |
Platform info | {{ context.device.platform.shortName }} |
context.interface.* |
Current interface data | {{ context.interface.name }} |
context.device_group.* |
Current group data | {{ context.device_group.name }} |
<step_label>.result.* |
Result from a previous step | {{ ping.result.data.reachable }} |
<step_label>.result.success |
Whether a step succeeded | {{ ping.result.success }} |
<step_label>.results |
Per-entity result map of a previous entity-scoped step | {{ ping.results }} |
steps.<step_label>.result.* |
Explicit path to a previous step’s result | {{ steps.ping.result.success }} |
<workflow_label>.* |
The whole context, aliased under the workflow’s label |
{{ my_workflow.parameters.timeout }} |
Accessing Previous Step Results
Each step’s result is stored under its label. If a step labeled check_reachability returns { "data": { "reachable": true, "latency": 5 } }:
parameters:
is_reachable: "{{ check_reachability.result.data.reachable }}"
latency_ms: "{{ check_reachability.result.data.latency }}"
A previous step’s result is exposed in two forms:
<step_label>.result(singular) – the one result relevant to the current step: for a previous step with the same scope (runOn), the result produced for the entity the current step is running on; for a previousrunOn: globalstep, its single result.<step_label>.results(per-entity map) – for entity-scoped previous steps, a map from entity id to that entity’s result, covering the entities in the current step’s context.
The same data is available under steps.<step_label>.result, and – absolute form – under <workflow_label>.steps.<step_label>.result (the workflow’s label aliases the entire context, which is useful inside embedded workflows).
Global step results are visible to steps of every scope
A runOn: global step produces exactly one result. That result is exposed via <step_label>.result to all later steps regardless of their scope – a runOn: device step can read {{ render_config.result.data.template }} from a previous global render_config step. For global steps <step_label>.results is an empty map: use .result.
Whole-String vs Embedded Tokens
How a token’s value lands in the parameter depends on whether the token is the entire parameter string:
- Whole-string token – a parameter that is exactly one
{{ ... }}token yields the raw JMES value, whatever its type: numbers stay numbers, booleans stay booleans, objects and arrays stay structured values, andnullstaysnull. Nothing is coerced to a string. - Embedded token(s) – tokens inside a larger string are stringified and concatenated in place: strings pass through, numbers and booleans are converted via
String(), objects and arrays are JSON-encoded.
parameters:
timeout: "{{ ping.result.data.nextMaxRtt }}" # raw number, e.g. 110
summary: "rtt={{ ping.result.data.nextMaxRtt }} meta={{ ping.result.data }}"
# -> 'rtt=110 meta={"nextMaxRtt":110}'
Null Semantics
null is a value, not an error – but only where a value of any type is allowed:
- A whole-string token that resolves to
null(a literal null, or an unresolvable path such as a mistyped step label) shipsnullas the parameter value. The function block’s parameter schema then decides: if the schema does not allow null, the workflow fails during parameter validation with adoes not allow nullreason – the step’s execute job is never created, so no coerced or empty value reaches a worker. - A token embedded in a larger string that resolves to
nullis an evaluation error: null cannot be meaningfully embedded into a string. The workflow fails with a reason naming the expression (cannot embed null into a string for expression '...') instead of shipping corrupted text likehost=null.
In both cases the terminal state follows the usual failure classification: FAILED_SAFE if only pure steps executed before the failure, FAILED_UNSAFE otherwise.
to_string() and Object Encoding
to_string() JSON-encodes its argument, which is handy for passing structured data as a string:
parameters:
resultJson: "{{ to_string(ping.result.data) }}" # '{"nextMaxRtt":110}'
rttString: "{{ to_string(ping.result.data.nextMaxRtt) }}" # '110'
Key order of multi-key objects is not deterministic
Step results pass through PostgreSQL jsonb storage, which rewrites object key order. The JSON encoding produced by to_string() (or by embedding an object into a string) is only byte-for-byte predictable for objects with a single key. Do not build logic on the exact string encoding of multi-key objects.
Error Handling
Evaluation Errors Halt the Workflow
A JMES expression that cannot be evaluated – a malformed expression (parse error) or an embedded token resolving to null – fails the workflow, and the offending step’s execute job is never created: broken values (error markers, clobbered strings, coerced empties) are never shipped to a worker. This applies to expressions in parameters, in acquire clauses, and in context-acquire filter expressions. The failure reason names the offending expression, and the terminal state follows the failure classification rules: FAILED_SAFE when only pure steps executed before the failure, FAILED_UNSAFE otherwise.
Conditions are the deliberate exception: an erroring condition simply evaluates to false and skips its step – see Conditions & Assertions.
The !! Error Fallback
An expression can declare a fallback value with the !! operator:
The fallback applies on evaluation errors only – a parse error, or an embedded token resolving to null. It does not apply when a whole-string expression resolves to null: null is a legitimate value, so the parameter becomes null and schema validation decides (see Null Semantics). A !! inside a JMES string literal (e.g. contains(note, '!!')) is not treated as the fallback operator.
JMESPath Functions
JMESPath supports built-in functions for filtering, transforming, and testing data:
condition:
type: jmes
# Check if any interface is in DOWN state
jmes: "{{ length(context.interfaces[?state == 'DOWN']) > `0` }}"
Common functions: length(), contains(), starts_with(), type(), keys(), values(), sort(), join(). See the JMESPath specification for the full list.
Raw Parameters
Use rawParameters when you need literal strings that contain {{ }} (e.g., Jinja templates):
parameters:
hostname: "{{ context.device.hostname }}" # interpolated at runtime
rawParameters:
jinja_template: "hostname {{ hostname }}" # literal, not interpolated
rawParameters are merged with parameters. If a key exists in both, rawParameters takes precedence.
This is essential when your function block receives a Jinja template or JMESPath expression as a parameter value – you want the curly braces passed through as-is, not evaluated by the engine.
Worker-side Jinja templates belong in rawParameters
Worker-side Jinja2 templating shares the {{ }} delimiters with the engine’s JMES
interpolation. A Jinja template placed in parameters is treated as a set of JMES
expressions by the engine: a Jinja token like {{ interface.name }} references a
loop variable that does not exist in the engine’s context, resolves to null embedded
in the template string, and halts the workflow (see
Null Semantics). The engine will not silently ship a clobbered
template. Fields in rawParameters bypass interpolation entirely and reach the
worker byte-for-byte.
See also:
- Context – What data is available in the execution context
- Conditions & Assertions – JMESPath in conditions and assertions
- Schema Reference – Full schema for step parameters

