Skip to content

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 previous runOn: global step, 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, and null stays null. 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) ships null as 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 a does not allow null reason – 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 null is 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 like host=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:

parameters:
  summary: "mode={{ parameters.mode !! unknown }}"   # embedded null -> "mode=unknown"

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: