Expressions¶
Pipelit uses Jinja2 template expressions to pass data between nodes. Expressions let you reference the output of any upstream node or the trigger payload inside system prompts, code snippets, and extra config fields -- without writing code.
Syntax¶
The basic syntax is:
Where:
- nodeId is the unique identifier of an upstream node (e.g.,
categorizer_abc123). - portName is the name of an output port on that node (e.g.,
category,output,text).
Examples¶
{# Reference an upstream categorizer's output #}
The category is: {{ categorizer_abc123.category }}
{# Reference the trigger text #}
User said: {{ trigger.text }}
{# Use a Jinja2 filter #}
CATEGORY: {{ categorizer_abc123.category | upper }}
{# Combine multiple sources #}
Based on the {{ categorizer_abc123.category }} classification,
here is the extracted data: {{ extractor_def456.extracted }}
Where Expressions Are Available¶
Expressions are resolved in three node configuration fields:
| Field | Description | Typical Use |
|---|---|---|
| System Prompt | Agent, categorizer, router, extractor instructions | Injecting trigger data or upstream results into the LLM prompt |
| Code Snippet | Code node Python/Bash source | Templating dynamic values into code |
| Extra Config | Key-value pairs in extra_config | Dynamic URLs, parameters, or settings based on upstream output |
Resolution Timing
Expressions are resolved just before a node executes, not at workflow build time. This means the values reflect the actual runtime output from upstream nodes in the current execution.
Context Variables¶
When the orchestrator resolves expressions, the following variables are available in the template context:
Upstream Node Outputs¶
Every node that has already executed in the current run is available by its node_id. Each node's output is a dict keyed by port name:
# If categorizer_abc123 produced {"category": "billing", "raw": "..."}
# then in a template:
{{ categorizer_abc123.category }} # -> "billing"
{{ categorizer_abc123.raw }} # -> "..."
The trigger Shorthand¶
The special trigger variable refers to whichever trigger fired the current execution. This is particularly useful in workflows with multiple triggers (e.g., a chat trigger and a Telegram trigger feeding the same downstream agent).
| Property | Type | Description |
|---|---|---|
trigger.text | string | The message text from the trigger |
trigger.payload | dict | The full trigger payload (varies by trigger type) |
{# Works regardless of which trigger fired #}
You are responding to: {{ trigger.text }}
{# Access nested payload data #}
Chat ID: {{ trigger.payload.chat_id }}
Multi-Trigger Workflows
The trigger shorthand always resolves to the trigger that initiated the current execution. If a workflow has both a chat trigger and a Telegram trigger connected to the same agent, {{ trigger.text }} works correctly in both cases.
Loop Context¶
Inside a loop body, the loop variable provides information about the current iteration:
| Property | Type | Description |
|---|---|---|
loop.item | any | The current item being iterated |
loop.index | int | Zero-based index of the current iteration |
loop.total | int | Total number of items in the loop |
Jinja2 Filters¶
Standard Jinja2 filters are supported for transforming values inline:
{{ trigger.text | upper }} {# UPPERCASE #}
{{ trigger.text | lower }} {# lowercase #}
{{ trigger.text | title }} {# Title Case #}
{{ trigger.text | length }} {# character count #}
{{ trigger.text | truncate(100) }} {# truncate to 100 chars #}
{{ trigger.text | default("N/A") }} {# fallback if undefined #}
Graceful Fallback¶
If an expression cannot be resolved -- because the referenced node has not executed yet, the port does not exist, or there is a syntax error -- the original template string is returned unchanged. This prevents crashes from misconfigured expressions.
{# If node_xyz has not executed, this returns the literal string: #}
{{ node_xyz.output }}
{# -> "{{ node_xyz.output }}" (unchanged) #}
StrictUndefined with Graceful Recovery
Internally, the expression resolver uses Jinja2's StrictUndefined mode, which raises an error on undefined variables. The resolver catches this error and falls back to the original template string. This means partial resolution does not happen -- either the entire template resolves successfully, or none of it does.
Frontend Variable Picker¶
The workflow editor provides a visual way to insert expressions without typing them manually.
The { } Button¶
On System Prompt, Code Snippet, and Extra Config fields, a { } button appears next to the text area. Clicking it opens the Variable Picker popover:
- The picker performs a BFS traversal of upstream nodes from the current node.
- It displays each reachable node with its output ports.
- Clicking a port inserts
{{ nodeId.portName }}at the cursor position in the text area.
This ensures you only see variables that are actually available to the current node based on the workflow topology.
Syntax Highlighting¶
All three CodeMirror modal editors (System Prompt, Code Snippet, Extra Config) apply Jinja2 syntax highlighting:
| Element | Style |
|---|---|
{{ }}, {% %}, {# #} brackets | Bold, lighter green |
| Inner content between brackets | Bold, amber/orange |
The highlighting is implemented as a CodeMirror 6 ViewPlugin that applies decorations to regex-matched Jinja2 delimiters in visible ranges. Both light and dark theme variants are provided. Whitespace-control variants ({{-, -%}}, {%-, etc.) are also recognized.
Resolution Implementation¶
Expression resolution happens in platform/services/expressions.py and is invoked by the orchestrator before each node executes:
resolve_expressions(template_str, node_outputs, trigger)-- resolves a single string template.resolve_config_expressions(config, node_outputs, trigger)-- recursively resolves all string values in a config dict, including nested dicts and lists.
The orchestrator calls these on both system_prompt and extra_config before passing the node configuration to its component factory:
# In orchestrator.py, before executing a component:
if db_node.component_config.system_prompt:
db_node.component_config.system_prompt = resolve_expressions(
db_node.component_config.system_prompt, node_outputs, trigger
)
if db_node.component_config.extra_config:
db_node.component_config.extra_config = resolve_config_expressions(
db_node.component_config.extra_config, node_outputs, trigger
)
Short-Circuit Optimization
If the template string does not contain {{, the resolver returns it immediately without invoking the Jinja2 engine. This keeps resolution fast for nodes that do not use expressions.