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
What the engine checks at execution
Submitted parameters are checked against the workflow’s parameterSchema once the workflow version resolves and before the execution row exists, so a bag the schema refuses answers 400 InvalidParameters and consumes no execution quota. The validated bag is what gets stored, under the rules of Schema Validation and Type Conversion: a bag that does not validate as sent has its values converted to the declared type ("7" for an integer property is stored as 7), and declared defaults are materialised, so every step reading {{ parameters.<name> }} sees the same value the schema promises. Properties the schema does not declare are carried through as sent, unless the schema sets additionalProperties: false, and a workflow declaring no parameterSchema accepts any bag. A parameterSchema the validator cannot compile is logged and the bag is stored as sent.
Parameters travel in clear text
Workflow parameters are not secrets, by contract. They are stored in clear text on the execution row, are returned by the execution API to every holder of workflow-execution:read, and appear in the engine’s debug log on submit. The debug log is the one exception: a value whose parameterSchema position declares format: password (Pydantic’s SecretStr, also through $refs and anyOf/oneOf/allOf branches) is logged as ***. The stored row and the API still carry it as sent. A declared default is materialised into the stored bag like any other value, so a password property with a default carries that default into the row. Credentials belong in the function block’s own secret handling.
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.
Schema Validation and Type Conversion
Interpolated step parameters, a workflow’s execution parameters and a worker’s result data are all validated against their JSON Schema (parameterJsonSchema, parameterSchema, resultDataJsonSchema) with the same rules. They hold for any schema producer, not only the Python SDK. A schema is validated as JSON Schema 2020-12, or as draft-07 when its $schema names draft-07 or it uses the draft-07 tuple form items: [a, b].
- Defaults are filled by the engine. An omitted property with a
defaultarrives with that default, so an omittedinput: str | None = Nonereaches the worker asnull. Adefaultinside ananyOf/oneOfbranch is not filled. - A null the schema does not allow is refused, never converted to
"",0orfalse(see Null Semantics). - A document that validates as it is, ships as it is.
"",0andfalsestay what they are in a nullable field, and an explicitnullstaysnullwherever the schema allows it. - Conversion is the fallback, for the whole document. Only when the document does not validate as sent are its values converted to their declared types, e.g.
"7"from an embedded token to7for aninteger. A top-level property declaredarrayalso accepts a JSON array written as one string,"[1,2]"becoming[1,2]. - The converted document must validate without conversion, or it is refused: a failing step, or
400 InvalidParametersfor execution parameters. - In scalar unions a valid value keeps its type, whatever the branch order. An
anyOf/oneOfof scalar types (string,numberorinteger,boolean,null) is validated as onetype: [...]withnulllast when either its branches are bare{type: X}, or it has one type besidesnullwhose branch carries only that type’s own keywords, anenumor aconst– or is a$refto such a definition under$defs/definitions. That covers Pydantic’sX | None,Literal[...] | None,Enum | Noneandint | str | None. A value that needs converting is tried against the types in branch order, withnulllast:""sent for anint | Nonearrives asnull,0sent for astr | Noneas"0". This does not apply when the parent node sets one of the same keywords, or when the union mixesnumberwithinteger. - Other unions are validated as written. In a document that needs conversion, AJV converts inside every union branch it tries, so a value in such a union can be rewritten (
0,false,""↔null) if the result still validates, e.g. in a union that mixes an object or array branch with a scalar one, or that has several scalar types with constraints (int(ge=0) | str | None). - Result data is replaced by the validated copy, so downstream steps read, and the job stores, the converted data with its defaults filled.
- A
patternthat can backtrack catastrophically is refused. Patterns run on the engine’s event loop, so a quantified group that itself contains a quantifier (^(a+)+$,(\d+){2,},(a+|b)*) could let one ~35-character value block every request for minutes. Such apatternorpatternPropertieskey, or one longer than 1000 characters, makes the schema invalid: function-block registration and workflow publish answer400naming its JSON Pointer, and a schema stored before this check is no longer run: a function-block schema fails its step, and a workflowparameterSchemais treated as one that does not compile (see What the engine checks at execution). The check is the monitor’s heuristic: it can refuse a safe pattern such as^(\w+\.)+$– rewrite it without the nested quantifier, e.g.^[\w.]+$.
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.
Editor Validation (GET /schema)
GET /schema serves the workflow schema with every registered function block’s parameter schema embedded, for editors and the Monitor App. It is an authoring aid: the engine validates interpolated parameters against the function block’s own schema at execution, and that check is the authority.
- Every typed parameter also accepts a whole-string token. A
{{ ... }}string matches theJMESExpressiondefinition wherever the function block expects a number, boolean, object, array, enum or constrained string, because the runtime resolves the token to a value of any type first. JMESExpressionapproximates the runtime’s token scanner. The pattern admits one level of nested braces ({{ {id: id} }}) and does not track string literals. The runtime scanner (src/utils/jmespath/jmes-tokens.ts) nests braces to any depth and ignores braces inside'...'and`...`literals. JSON Schema patterns cannot count nesting, so/schemarefuses some tokens the runtime resolves:{{ {a: {b: c}}.a.b }}and{{ length(join('}', vals)) }}on a non-string parameter are two examples. Such a token runs as written. Only the editor reports it. The reverse also happens: a string literal that never closes, as in{{ name == 'x }}, matches the pattern, while the runtime finds no token in it and passes the string on as it is.rawParametersare checked against the same schema, without the token alternative. The engine overlays them ontoparametersand validates the merged values without interpolating them, so a{{ ... }}string is only accepted where the function block expects a string. A required parameter may sit in either bag. An undeclared key is refused in both bags, unless the function block’s schema allows extra keys.- Conditional requirements are left to the engine. A function block may require a parameter only under a condition, in a root-level
if/then/else,oneOforanyOf. Such a parameter may also sit in either bag, and/schemachecks the bags one at a time, so it cannot evaluate the condition across them. It drops those branches instead: a parameter a branch declares is accepted with that branch’s type, and a branch’srequiredis not checked. A step that misses a conditionally required parameter therefore passes the editor and fails at execution. Therequiredof the root and of an unconditionalallOfentry is still checked. - An unregistered function block is flagged, not refused. A
functionBlockthat matches no registered id, in its full,major.minorormajorform, carries adeprecationMessage, which VS Code shows as a warning. The step’s parameters are then not checked. Publishing still accepts the workflow, since a worker may register the block later. - Tuples are checked. The served document is draft-07, so a function block’s
prefixItems: [a, b]is served asitems: [a, b], and anitems: Xbeside it asadditionalItems: X. - Predicates are left as written. A leaf under
ifornotgets no token alternative.ifnever fails a document on its own, and insidenotthe alternative would refuse the token instead of accepting it.
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

