Skip to main content

Workflow Variables

Workflow variables store and pass data between tasks. They can be defined in the variables section of a workflow or task, and assigned in inputs, variables, and outputs sections.

Types of Variables​

System Variables​

Automatically created and accessed using double curly braces:

  • {{ workflowId }}: ID of the workflow
  • {{ workflowName }}: Name of the workflow
  • {{ organizationId }}: ID of the organization
  • {{ currentUserId }}: ID of the current user
  • {{ currentEmployeeId }}: ID of the current employee

User-Defined Variables​

Created and used by workflow designers.

Accessing Variables​

Basic Syntax​

Use double curly braces: {{ variableName }}

Nested Variables​

Use dot notation: {{ order.orderId }}

Optional Chaining​

Use ?. to avoid errors with undefined properties:

inputs:
to: "{{order?.customer?.email?}}"

Optional Variable Names​

Append ? to a variable name to make it return null instead of throwing an error when the variable is not defined in the task inputs. This is useful for task inputs that may or may not be provided depending on the workflow path.

inputs:
headers: "{{ headers? }}"
columnMappings: "{{ columnMappings? }}"

Without the ? suffix, referencing an undefined variable throws a runtime error. With ?, the variable resolves to null, allowing the task to handle the missing value gracefully (e.g., fall back to a default).

tip

The ? suffix on variable names works independently of the ?. optional chaining operator on property paths. You can combine both: {{ optionalVar?.nestedProp }}.

Working with List Variables​

List variables can be accessed using square brackets.

For example, to access the first item in the commodities array variable, use {{ commodities[0] }}.

Accessing List Item​

List variables can be accessed using square brackets.

For example, to access the first item in the commodities array variable, use {{ commodities[0] }}.

Filtering List​

Nested list can be filtered using square brackets. For example, to filter commodities by type, use {{ commodities[type="box"] }}.

This will return all commodities with a type of box.

Accessing Item in Filtered List​

Nested arrays can be accessed using square brackets. For example, to access the first item in the commodities array variable, use {{ commodities[type="box"].[0] }}.

This will return the first commodity with a type of box.

Flatten List​

To flat the list of orders in the jobs variable, use {{ jobs[*].orders }}.

This will return a list of orders.

Recursive Flatten​

To recursively flatten a self-referencing structure (like nested commodities), use [**].

For example, if commodities can contain other commodities:

inputs:
allCommodities: "{{ order.commodities[**] }}"

This will return all commodities at all nesting levels in a single flat list.

Example structure:

Box A (level 1)
β”œβ”€β”€ Package 1 (level 2)
β”‚ β”œβ”€β”€ Item X (level 3)
β”‚ └── Item Y (level 3)
└── Package 2 (level 2)
└── Item Z (level 3)

Using containerCommodities[**] returns: [Box A, Package 1, Item X, Item Y, Package 2, Item Z]

Depth Index​

Use [-N] to filter items by depth from the end of a self-referencing structure:

  • [-1] - Returns leaf nodes (items with no children)
  • [-2] - Returns parents of leaf nodes (second-to-last level)
  • [-3] - Returns grandparents of leaf nodes (third-to-last level)

Examples:

inputs:
# Get all leaf items (deepest level)
leafItems: "{{ order.commodities[**][-1] }}"

# Get all packages (parents of leaf items)
packages: "{{ order.commodities[**][-2] }}"

# Get all boxes (grandparents of leaf items)
boxes: "{{ order.commodities[**][-3] }}"

Example with variable depths:

If you have commodities with varying nesting depths (some 3 levels, some 4 levels), [-2] will always return the parents of the deepest items in each branch:

Box A β†’ Package 1 β†’ Item X (3 levels)
Box B β†’ Package 2 β†’ Subpackage β†’ Item Y (4 levels)

Using [**][-2] returns: [Package 1, Subpackage] (parents of leaves in each branch)

inputs:
leafItems: "{{ order.commodities.containerCommodities[**][-1] }}"
packages: "{{ order.commodities.containerCommodities[**][-2] }}"
boxes: "{{ order.commodities.containerCommodities[**][-3] }}"

Field Selection​

To select a specific field from a list of orders, use {{ jobs[*].orders.[*].{ id customerId parentJobId:_.jobId } }}.

  • To access parent object fields, use _.fieldName.
  • To use aliases, use alias:fieldName.

This will return a list of orders with the following fields: id, customerId and parentJobId.

[
{
"id": "1",
"customerId": "1",
"parentJobId": "1"
},
{
"id": "2",
"customerId": "2",
"parentJobId": "1"
}
]

Sub Expressions​

Sub Expressions can be used to access nested variables. Subexpressions are surrounded by parentheses. For example {{order.items.[status=(selected_status)].[0].name}}.

Sub Expressions are useful when you need to map specific reference data based on the value of another variable.

Sub Expression Example​

The following example shows how to map a state code based on the state name.

inputs:
address:
stateCode: "{{states.[name=(address.stateName)].code}}"

Converting Variables​

Variables can be converted to a specific type using the following functions:

  • string - converts a variable to a string
  • int - converts a string to an integer
  • decimal - converts a string to a decimal
  • bool - converts a string to a boolean
  • datetime - converts a string to a datetime
  • luceneString - converts a string to a lucene string
  • emptyIfNull - returns empty string if the variable is null
  • nullIfEmpty - returns null if the variable is empty
  • transliterate - converts a cyrillic string to a latin string
  • parseJson - parses a json string
  • toJson - converts a variable to a json string
inputs:
orderId: "{{ string orderId }}"

Transliterate​

Transliterate is used to convert a cyrillic string to a latin string.

Example of Transliterate:

inputs:
name: "{{ transliterate name }}" # transliterate name ΠŸΡ€ΠΈΠ²Ρ–Ρ‚, як справи?"; will be converted to "Privit, yak spravi?"

toJson​

toJson is used to convert a variable to a json string.

Example of toJson:

inputs:
orderJson: "{{ toJson order }}"

parseJson​

parseJson is used to parse a json string.

Example of parseJson:

inputs:
order: "{{ parseJson orderJson }}"

Workflow-Level Variable Sources​

Workflow-level variables (defined in the top-level variables section) support three resolution strategies:

Static Value / Template​

The default. Assigns a static value or template expression:

variables:
- name: "organizationId"
value: "{{ Order.OrganizationId }}"
- name: "threshold"
value: 100

From Configuration​

Loads the value from an organization configuration record:

variables:
- name: "apiKey"
fromConfig:
configName: "apps.novaposhta"
key: "apiKey"
- name: "allSettings"
fromConfig:
configName: "apps.novaposhta"
# Omit key to get the entire config object

NCalc Expression​

Evaluates an NCalc expression at runtime. The expression has access to all previously resolved variables:

variables:
- name: "baseRate"
value: 10
- name: "adjustedRate"
expression: "[baseRate] * 1.15"
- name: "isHighValue"
expression: "[orderTotal] > 5000"
tip

Variables are resolved in order, so a variable using expression can reference variables defined earlier in the list.

Complex Variables​

Extends​

Extends are used to extend the inputs of an activity. The inputs of an activity can be extended using the following syntax:

inputs:
order:
extends: "purchaseOrder"
defaultIfNull: []
mapping: # override or extend the order input
customerId: "{{ customerId }}"

Expression​

Initializes a variable using an expression.

inputs:
orderDate:
expression: "now()"

Foreach collection item​

Foreach collection item is used to iterate over a collection of items and construct a new collection of items.

Example of ForEach variables section:

activities:
- name: "UpdateCommodityStatus"
description: "Update the status of a commodity for shipped orders."
steps:
- task: "UpdateCommodityStatus"
inputs:
commodities:
foreach: "order.commodities"
item: "item"
mapping: # object mapping
commodityId: "{{item.id}}"
status: "Shipped"
commoditiesIds:
foreach: "order.commodities"
item: "item"
mapping: "{{item.id}}" # simple array mapping

Switch Cases​

Switch cases are used to define a mapping between a value and a result.

Example of Switch Cases:

inputs:
status:
switch: "{{ statusName }}"
cases:
"Shipped": 1
"Delivered": 2
"Returned": 3
default: 0

Coalesce​

Coalesce is used to return the first non-null value.

Example of Coalesce:

inputs:
status:
coalesce:
- "{{ order.status }}"
- "{{ order.statusName }}"
- "Pending"

Dynamic Dictionary Keys​

Dictionary keys in YAML mappings can contain {{ }} template expressions that are resolved at runtime. This allows you to construct output field names dynamically based on workflow variables.

Basic usage:

inputs:
translations:
"{{ targetField }}": "{{ translatedValue }}"

If targetField resolves to simplified_description_pl, the output dictionary will contain the key simplified_description_pl rather than the literal string {{ targetField }}.

Composite keys:

Multiple template expressions can appear in a single key:

inputs:
translations:
"{{ prefix }}_{{ lang }}": "{{ translatedValue }}"

With prefix = simplified_description and lang = pl, the resolved key is simplified_description_pl.

Inside foreach mappings:

Dynamic keys work in complex foreach mappings, allowing each iteration to produce entries with resolved key names:

inputs:
results:
foreach: "items"
item: "item"
mapping:
"{{ dynamicKey }}": "{{ item.value }}"
staticKey: "{{ item.name }}"

Inside extends mappings:

Dynamic keys also work in extends mapping overrides:

inputs:
order:
extends: "{{ baseOrder }}"
mapping:
"{{ targetField }}": "newValue"

Removing keys with $delete:

When a mapping extends an existing dictionary, you can remove a key by assigning the exact string $delete.

inputs:
order:
extends: "{{ baseOrder }}"
mapping:
obsoleteField: "$delete"

This remove sentinel is matched exactly. Near-matches like " $delete", "$DELETE", or nested values that merely contain $delete are treated as normal values. Explicit deletes also bypass overwrite protection, so removing an existing key is always allowed.

Fallback behavior:

If a template key resolves to null or an empty string (e.g., the referenced variable does not exist), the original literal key is preserved. This prevents entries from being silently dropped.

# If 'missingVar' is not defined, the key stays as "{{ missingVar }}"
inputs:
data:
"{{ missingVar }}": "value"
note

Dynamic key substitution does not apply inside encrypt and decrypt directives. Keys in those blocks (encryptedValue, key, initializationVector) are structural inputs and are always treated as literals.

Encrypt and Decrypt​

Encrypt and Decrypt are used to encrypt and decrypt sensitive data.

Example of Encrypt and Decrypt:

variables:
creditCard:
encrypt:
value: "{{ creditCard }}"
keyName: "stripe" # key name is used to lookup the encryption key
inputs:
creditCard:
number:
decrypt:
encryptedValue: "{{ creditCard.number }}"
keyName: "stripe" # key name is used to lookup the encryption key

Resolve​

Resolve performs an entity lookup by querying a GraphQL collection and returning a specific field (typically the entity ID). This is useful when you have a human-readable identifier (like a name or code) and need to resolve it to a database ID.

The resolve directive uses a pre-processor that batches and caches all resolve requests before the workflow step executes, preventing N+1 query issues.

inputs:
customerId:
resolve:
entity: "Contact" # Entity type to query (pluralized automatically: contacts)
filter: "name={{ customerName }}" # Lucene filter expression
field: "contactId" # Field to return (default: <entity>Id, e.g., contactId)

Properties:

PropertyTypeRequiredDescription
entitystringYesEntity type name (e.g., Contact, PackageType). Automatically pluralized for the GraphQL query.
filterstringYesLucene filter expression to find the entity. Supports template variables.
fieldstringNoField to return from the matched entity. Defaults to <entity>Id (e.g., contactId for Contact).

Example β€” Resolving a package type by name:

inputs:
commodities:
foreach: "importedCommodities"
item: "item"
mapping:
packageTypeId:
resolve:
entity: "PackageType"
filter: "name={{ item.packageTypeName }}"
weight: "{{ item.weight }}"
description: "{{ item.description }}"
info

Resolve results are cached per unique combination of entity + filter + field. When multiple items in a foreach reference the same entity (e.g., the same package type name), only one GraphQL query is executed.

$raw and $eval​

$raw and $eval are used to define raw and evaluated expressions.

Example of $raw and $eval:

inputs:
order:
$raw: |
{
"id": 1,
"customerId": 1
}
order:
$eval: |
{
"id": "{{ orderId }}",
"customerId": "{{ customerId }}"
}

Raw will use string as is and eval will parse the string as JSON object.

Template Functions​

Template functions can be used inside {{ }} expressions to transform values inline.

formatDate​

Formats a date variable using a custom format string. Returns null if the value is null, the format string is missing, or the string cannot be parsed as a date.

Syntax: {{ formatDate <variablePath> '<formatString>' ['<culture>'] }}

The optional culture argument accepts a BCP 47 locale tag (e.g., 'en-US', 'de-DE'). When omitted, InvariantCulture is used, which produces locale-neutral output and is the recommended default for machine-readable dates.

Accepted input types:

Input typeNotes
DateTimeFormatted directly
DateTimeOffsetFormatted directly, offset is preserved
String (ISO 8601 / RFC 3339)Parsed as DateTimeOffset first, then as DateTime; strings with timezone offsets (e.g., +02:00) are handled correctly
null or unparseable stringReturns null

Examples:

inputs:
# Basic date formatting
formattedDate: "{{ formatDate order.created 'yyyy-MM-dd' }}"
displayDate: "{{ formatDate order.pickupDate 'MMM dd, yyyy' }}"

# With explicit culture for locale-specific output
localizedDate: "{{ formatDate order.created 'dd.MM.yyyy' 'de-DE' }}"

# String with timezone offset β€” parsed correctly
etaFormatted: "{{ formatDate shipment.eta 'yyyy-MM-dd HH:mm' }}"

In multi-expression strings:

inputs:
subject: "Order {{ orderId }} β€” Pickup on {{ formatDate order.pickupDate 'MM/dd/yyyy' }}"

Supported format tokens: Standard .NET DateTime format strings (e.g., yyyy, MM, dd, HH, mm, ss, MMM, ddd, etc.).