Loading

Templating engine

The workflow templating engine enables dynamic, type-safe template rendering using the Liquid templating language. It allows you to inject variables, apply transformations, and control data flow throughout your workflows.

The templating engine supports several syntax patterns for different use cases:

Syntax Purpose Example
Double curly braces Insert values as strings "Hello, {{name}}"
Dollar-sign prefix Preserve data types (arrays, objects, numbers) ${{myArray}}
Percent tags Control flow (conditionals, loops) {%if active%}...{%endif%}
Raw tags Output literal curly braces {%raw%}{{}}{%endraw%}

Use double curly braces for basic string interpolation. Variables and expressions inside the braces are evaluated and rendered as strings.

message: "Hello {{user.name}}!"                       # Result: "Hello Alice"
url: "https://api.example.com/users/{{user.id}}"      # Result: "https://api.example.com/users/12"
		

Use the dollar-sign prefix (${{ }}) when you need to preserve the original data type (array, object, number, boolean).

# String syntax - converts to string
tags: "{{inputs.tags}}"     # Result: "[\"admin\", \"user\"]" (string)

# Type-preserving syntax - keeps original type
tags: "${{inputs.tags}}"    # Result: ["admin", "user"] (actual array)
		
Important

The type-preserving syntax must occupy the entire string value. You cannot mix it with other text.

Valid:

tags: "${{inputs.tags}}"
		

Invalid:

message: "Tags are: ${{inputs.tags}}"
		
Feature String syntax Type-preserving syntax
Output type Always string Preserves original type
Arrays Stringified Actual array
Objects Stringified Actual object
Booleans "true" / "false" true / false
Numbers "123" 123

Liquid tags are control flow constructs that use the {% %} syntax. Unlike output expressions, tags execute logic without directly rendering a value.

Conditionals:

message: |
  {% if user.role == 'admin' %}
    Welcome, administrator!
  {% else %}
    Welcome, user!
  {% endif %}
		

Loops:

message: |
  {% for item in items %}
    - {{item.name}}
  {% endfor %}
		

Use raw tags to output literal curly brace characters without rendering them:

value: "{%raw%}{{_ingest.timestamp}}{%endraw%}"  # Result: "{{_ingest.timestamp}}"
		

This section covers common patterns for accessing and transforming data in your workflows.

Reference input parameters defined in the workflow using {{inputs.<input_name>}}. Inputs are defined at the workflow level and can be provided when the workflow is triggered manually.

inputs:
  - name: environment
    type: string
    required: true
    default: "staging"
  - name: batchSize
    type: number
    default: 100

triggers:
  - type: manual

steps:
  - name: log_config
    type: console
    with:
      message: |
        Running with:
        - Environment: {{inputs.environment}}
        - Batch Size: {{inputs.batchSize}}
		

Access output data from previous steps using {{steps.<step_name>.output}}:

steps:
  - name: search_users
    type: elasticsearch.search
    with:
      index: "users"
      query:
        term:
          status: "active"

  - name: send_notification
    type: slack
    connector-id: "my-slack"
    with:
      message: "Found {{steps.search_users.output.hits.total.value}} active users"
		

Reference workflow-level constants using {{consts.<constant_name>}}. Constants are defined at the workflow level and can be referenced when the workflow is triggered.

consts:
  indexName: "my-index"
  environment: "production"

steps:
  - name: search_data
    type: elasticsearch.search
    with:
      index: "{{consts.indexName}}"
      query:
        match:
          env: "{{consts.environment}}"
		

Transform values using filters with the pipe | character:

message: |
  User: {{user.name | upcase}}
  Email: {{user.email | downcase}}
  Created: {{user.created_at | date: "%Y-%m-%d"}}
		
Note

Workflows supports all available LiquidJS filters, plus two custom filters (json_parse and entries) documented in the next section.

In addition to the standard LiquidJS filter set, the workflow engine provides two custom filters for shapes that come up often in automation:

Filter What it does Example
json_parse Parses a JSON string into an object so you can access fields. "{{ steps.http.output.body \| json_parse }}"
entries Converts an object into an array of {key, value} pairs, which is iterable with {% for %}. "{% for kv in steps.config.output \| entries %}{{ kv[0] }}: {{ kv[1] }}{% endfor %}"

Parse a JSON string returned as a string body:

- name: parse_response
  type: data.set
  with:
    parsed: "{{ steps.http_call.output.body | json_parse }}"
		
Note

The inverse of json_parse is the standard LiquidJS json filter, which serializes a value to a JSON string.

Iterate an object's keys:

- name: summarize_config
  type: console
  with:
    message: |
      {% for kv in steps.config.output | entries %}
        {{ kv[0] }}: {{ kv[1] }}
      {% endfor %}
		

Liquid is excellent for small inline transformations: field access, defaults, formatting, string concatenation. When a transformation grows (filtering a large array, grouping by a key, parsing a JSON payload into named outputs, extracting fields with regex), reach for a data.* step instead. Data steps give you explicit, testable transformation with their own named output and their own execution log entry.

Size of transformation Use
A field access, a default, a format (fits in one expression) Liquid
Filter, group, parse, regex-extract, or multi-field map A data.* step

When passing arrays or objects between steps, use the type-preserving syntax (${{ }}) to avoid stringification:

steps:
  - name: get_tags
    type: elasticsearch.search
    with:
      index: "config"
      query:
        term:
          type: "tags"

  - name: create_document
    type: elasticsearch.request
    with:
      method: POST
      path: /reports/_doc
      body:
        # Preserves the array type, doesn't stringify it
        tags: "${{steps.get_tags.output.hits.hits[0]._source.tags}}"
		
Important

The type-preserving syntax must occupy the entire string value. You cannot mix it with other text.

Valid:

tags: "${{inputs.tags}}"
		

Invalid:

message: "Tags are: ${{inputs.tags}}"
		

Add logic to customize output based on data:

steps:
  - name: send_message
    type: slack
    connector-id: "alerts"
    with:
      message: |
        {% if steps.search.output.hits.total.value > 100 %}
        ⚠️ HIGH ALERT: {{steps.search.output.hits.total.value}} events detected!
        {% else %}
        ✅ Normal: {{steps.search.output.hits.total.value}} events detected.
        {% endif %}
		

Iterate over arrays to process multiple items:

steps:
  - name: summarize_results
    type: console
    with:
      message: |
        Found users:
        {% for hit in steps.search_users.output.hits.hits %}
        - {{hit._source.name}} ({{hit._source.email}})
        {% endfor %}
		

The workflow engine provides context variables that you can access using template syntax. These variables give you access to workflow metadata, execution details, trigger data, and step outputs.

Variable Description Example value
workflow.name Name of the current workflow "My Workflow"
workflow.id Unique identifier of the workflow definition "abc-123"
execution.id Unique identifier for this specific run "exec-456"
execution.startedAt ISO timestamp when execution began "2024-01-15T10:30:00Z"
event Data from the trigger that started the workflow { "user": { "id": "u-123" }, "params": { "target": "host-1" } }
inputs.<name> Input parameters passed at trigger time inputs.severity resolves to "high"
consts.<name> Constants defined at the workflow level consts.api_url resolves to "https://api.example.com"
steps.<step_name>.output Output data from a completed step steps.search.output.hits.total resolves to 42
steps.<step_name>.error Error details if a step failed { "message": "Connection timeout", "code": "ETIMEDOUT" }

Inside foreach steps, you have access to additional context variables such as foreach.item, foreach.index, and more. Refer to Foreach context variables for details.

The event variable contains data from the trigger. Its structure depends on the trigger type. Refer to Trigger context to learn what data each trigger type provides.

The engine renders templates recursively through all data structures, processing nested objects and arrays.

Input:

message: "Hello {{user.name}}"
config:
  url: "{{api.url}}"
tags: ["{{tag1}}", "{{tag2}}"]
		

Rendered output:

message: "Hello Alice"
config:
  url: "https://api.example.com"
tags: ["admin", "user"]
		
Type Behavior
Strings Processed as templates: variables are interpolated, and filters are applied
Numbers, Booleans, Null Returned as-is
Arrays Each element is processed recursively
Objects Each property value is processed recursively (keys are not processed)
Case Behavior
Null values Returned as-is
Undefined variables Returned as empty string in string syntax and as undefined in type-preserving syntax
Missing context properties Treated as undefined