Templates
A template is text with {{ ... }} substitutions that a node evaluates when it runs. This
is how a prompt, an HTTP body or a message picks up values from earlier nodes, variables
and secrets without a line of code.
Where templates work, and where they don’t
It is the field that is templatable, not the node. The editor renders such a field as a
code editor with name completion and highlighting for unknown names — for example
template on template or system_prompt on
ai_agent.
The engine hands a node its configuration verbatim; the substitution is done by the node
itself, and only for fields declared templatable. A field without that marker takes text
literally: {{ ... }} stays two curly braces.
Node configuration
Template
Parameters
Substitutes the value delivered to the Input port.
Namespaces
| Name | What it holds |
|---|---|
inputs.<handle> |
The value the immediate predecessor delivered on that port. The name is relative to each node: inputs.input on a node in the middle of the graph is its predecessor’s output, not the workflow input. |
nodes.<id>.output |
Another node’s primary value — exactly what its success edge carries downstream. Remaining result keys sit beside it: nodes.<id>.status, nodes.<id>.branch. |
variables.<name> |
Session variables declared by a var node. Inside while_loop the loop_index counter joins them. |
secret.<NAME> |
Workspace secrets (Settings, “Secrets”). Decrypted once per run and available in any node. |
loop |
Fields of the current iteration inside an iteration shell, e.g. loop.item. |
telegram.reply_message_id |
Id of the bot’s reply message — Telegram turns only, empty until the reply is sent. |
There is a shorthand as well: {{ my_node.output }} behaves like
{{ nodes.my_node.output }}, and an input port name is available directly. When a node id
is not a valid Jinja identifier, write {{ nodes['my-node'].output }}.
- Execute + Data
- LLM
A value or a string
One rule, and it matters for fields that need a list or a number:
- the whole string is a single expression —
{{ inputs.input.items }}— and the native value comes back (a list, a number, a dict); - the expression is mixed with text, or the string contains
{% ... %}blocks, and the result is always a string.
Time and missing values
You get {{ now() }} (UTC; now('Europe/Moscow') for a zone) and the
| datetimeformat('%d.%m.%Y %H:%M') filter, aliased as | strftime. It accepts a
datetime, a unix number or an ISO string; anything it cannot read renders as an empty
string instead of failing the node. There are no modules and no import in templates — for
arbitrary logic use code_javascript.
An unknown name does not fail the node either: it renders empty. The empty paths a render
touched are recorded on the node execution under __undefined_refs__ — that is the only
trace a typo in an id leaves, so check it whenever a field comes out unexpectedly blank.
One pass, and it is a security property
Every templatable field is rendered exactly once. A value that has already been through
a render is never rendered again, even if the substitution introduced {{ }} into it.
That is deliberate: after the first pass the string legitimately carries user data, a
database row or a model answer. A second pass would execute that text as a template in a
context that contains secret.*.
Checking the result
Open a run and expand the node: its debug panel has a section with the rendered templates,
so you can see what actually went to the model or into the HTTP request, together with
__undefined_refs__ when something did not resolve. In the editor, unknown names are
highlighted in the field itself, before any run.