---
title: Templating engine
description: Learn how to use variables, constants, and the Liquid templating engine to create dynamic workflows.
url: https://docs-v3-preview.elastic.dev/elastic/docs-content/pull/5399/explore-analyze/workflows/data/templating
products:
  - Elastic Cloud Enterprise
  - Elastic Cloud Hosted
  - Elastic Cloud Serverless
  - Elastic Cloud on Kubernetes
  - Elastic Stack
  - Kibana
applies_to:
  - Elastic Cloud Serverless: Preview
  - Elastic Stack: Preview since 9.3
---

# Templating engine
The workflow templating engine enables dynamic, type-safe template rendering using the [Liquid templating language](https://liquidjs.com/). It allows you to inject variables, apply transformations, and control data flow throughout your workflows.

## Syntax overview

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%}`     |


### String interpolation

Use double curly braces for basic string interpolation. Variables and expressions inside the braces are evaluated and rendered as strings.
```yaml
message: "Hello {{user.name}}!"                       # Result: "Hello Alice"
url: "https://api.example.com/users/{{user.id}}"      # Result: "https://api.example.com/users/12"
```


### Type-preserving expressions

Use the dollar-sign prefix (`${{ }}`) when you need to preserve the original data type (array, object, number, boolean).
```yaml
# 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:**
  ```yaml
  tags: "${{inputs.tags}}"
  ```
  ❌ **Invalid:**
  ```yaml
  message: "Tags are: ${{inputs.tags}}"
  ```
</important>


| 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`                   |


### Control flow

Liquid tags are control flow constructs that use the `{% %}` syntax. Unlike output expressions, tags execute logic without directly rendering a value.
**Conditionals:**
```yaml
message: |
  {% if user.role == 'admin' %}
    Welcome, administrator!
  {% else %}
    Welcome, user!
  {% endif %}
```

**Loops:**
```yaml
message: |
  {% for item in items %}
    - {{item.name}}
  {% endfor %}
```


### Escaping template syntax

Use raw tags to output literal curly brace characters without rendering them:
```yaml
value: "{%raw%}{{_ingest.timestamp}}{%endraw%}"  # Result: "{{_ingest.timestamp}}"
```


## Working with data

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

### Reference inputs

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.
```yaml
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}}
```


### Reference outputs

Access output data from previous steps using `{{steps.<step_name>.output}}`:
```yaml
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 constants

Reference workflow-level constants using `{{consts.<constant_name>}}`. Constants are defined at the workflow level and can be referenced when the workflow is triggered.
```yaml
consts:
  indexName: "my-index"
  environment: "production"

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


### Apply filters

Transform values using filters with the pipe `|` character:
```yaml
message: |
  User: {{user.name | upcase}}
  Email: {{user.email | downcase}}
  Created: {{user.created_at | date: "%Y-%m-%d"}}
```

<note>
  Workflows supports all available LiquidJS [filters](https://liquidjs.com/filters/overview.html).
</note>


### Preserve array and object types

When passing arrays or objects between steps, use the type-preserving syntax (`${{ }}`) to avoid stringification:
```yaml
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:**
  ```yaml
  tags: "${{inputs.tags}}"
  ```
  ❌ **Invalid:**
  ```yaml
  message: "Tags are: ${{inputs.tags}}"
  ```
</important>


### Use conditionals for dynamic content

Add logic to customize output based on data:
```yaml
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 %}
```


### Loop through results

Iterate over arrays to process multiple items:
```yaml
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 %}
```


## Context variables reference

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.

### Available context variables


| 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" }`        |


### Foreach loop variables

Inside `foreach` steps, you have access to additional context variables such as `foreach.item`, `foreach.index`, and more. Refer to [Foreach context variables](/elastic/docs-content/pull/5399/explore-analyze/workflows/steps/foreach#context-variables) for details.

### Trigger event data

The `event` variable contains data from the trigger. Its structure depends on the trigger type. Refer to [Trigger context](/elastic/docs-content/pull/5399/explore-analyze/workflows/triggers#trigger-context) to learn what data each trigger type provides.

## Template rendering behavior

The engine renders templates recursively through all data structures, processing nested objects and arrays.
**Input:**
```yaml
message: "Hello {{user.name}}"
config:
  url: "{{api.url}}"
tags: ["{{tag1}}", "{{tag2}}"]
```

**Rendered output:**
```yaml
message: "Hello Alice"
config:
  url: "https://api.example.com"
tags: ["admin", "user"]
```


### Type handling


| 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)       |


### Null and undefined handling


| 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                                                                   |


## Learn more

- [Liquid templating language](https://shopify.github.io/liquid/)
- [LiquidJS documentation](https://liquidjs.com/)