Template variables
Most step fields accept template references written as {{ something }}.
At run time Routario replaces them with real values from the flow. Alongside the
outputs of earlier steps, a few always-available namespaces exist. The value
picker ({x}) in the editor links here for each of them.
Anything that doesn’t resolve renders as empty rather than failing the run — so referencing a value that isn’t set (a contact with no company, say) is safe.
That safety has one limit: if the empty value feeds a field the step marks
required, the run stops right there with a clear error naming the exact
reference (e.g. Required input 'path' resolved to empty — context path '{{ storage_list.files.0.path }}' is not set) instead of continuing with
nothing to work on. This is deliberate — a clear stop at the source beats a
confusing failure two steps later. See
Indexing into a list that might be empty
below for the pattern this usually points at.
loop.* — the current item in a Foreach
Section titled “loop.* — the current item in a Foreach”Available only inside a Foreach step.
loop.item— the current item being processedloop.index— its position, starting at 1loop.index0— its position, starting at 0loop.total— how many items there areloop.first/loop.last— booleans for the first / last pass
user.* — the person who ran the flow
Section titled “user.* — the person who ran the flow”The signed-in human who triggered the run (or whose behalf it ran on).
-
user.name— full display name (e.g. “Jana Nováková”) -
user.firstName,user.lastName -
user.email— handy as ato:on Send email -
user.phone -
user.role— their access role (e.g.Administrator,Manager) -
user.locale— their UI language,en/cs. Branch on it to write in their language:{% if user.locale == 'cs' %}Ahoj{% else %}Hi{% endif %}
actor.* — the person handling the step
Section titled “actor.* — the person handling the step”The actor bound to the run. Today this resolves to the same person as user.*;
the distinction exists because actor is the entity that handles a step (in
future this narrows to whoever answered a given “Ask a person”). It also exposes
contact fields directly:
actor.name,actor.email,actor.phoneactor.localeactor.id— the internal actor id (rarely needed)
Sending in the recipient’s language? When an “Ask a person” / send step targets a specific person, Routario already localizes the built-in chrome (subject, “Action required”, the Approve/Reject buttons) to their saved locale automatically. The message body you type, however, renders against
user.*(the person who ran the flow) — there is not yet a variable for the recipient’s locale. Track that enhancement in the backlog.
now.* — the current moment
Section titled “now.* — the current moment”Formatted in the workspace timezone.
now.date— today’s date (YYYY-MM-DD)now.time— the current time (HH:MM)now.timestamp— a full ISO timestampnow.year,now.month,now.day— the parts, zero-padded (2026,06,09)
now is also callable for custom formats:
{{ now().strftime('%A') }} → Monday, or with a timezone
{{ now('Europe/Prague').strftime('%H:%M') }}.
workspace.* — about the workspace
Section titled “workspace.* — about the workspace”-
workspace.name— the workspace / organisation name -
workspace.own_company.*— your own organisation’s registry details, for invoice “from” blocks and formal copy (use these inside text and email bodies):workspace.own_company.legal_name— registered legal nameworkspace.own_company.name— display nameworkspace.own_company.company_id— company registration number (IČO); a stored number with a wrong IČO check digit is left out rather than printedworkspace.own_company.vat— VAT number (DIČ)workspace.own_company.domain,.websiteworkspace.own_company.headquartered_at.canonical_name— registered address
These come from your workspace identity; they render empty until it’s been set up.
contact.* — the runner’s contact card
Section titled “contact.* — the runner’s contact card”A live view of the running user’s Contacts entry, when they have one. Renders empty when they don’t.
contact.primary_email,contact.primary_phone,contact.primary_companycontact.primary_rolecontact.emails,contact.phones,contact.companies— the full lists- Addressing helpers (pick a specific value):
contact.email_for(company="Acme")— the email tied to that companycontact.email_by_label("work")— the email with that labelcontact.phone_for(company=…),contact.phone_by_label(…)— same for phones
payload.* — data an outside caller sent in
Section titled “payload.* — data an outside caller sent in”Whatever an external system posts to start the flow, unwrapped as JSON. Three callers write it, all to the same place:
- A Webhook trigger — the POSTed
request body becomes
payloaddirectly, e.g.{{ payload.invoice_id }}. - The Run API (
POST /api/v1/runs) — the caller’sinputobject lands the same way. This works no matter which trigger the flow is configured with: the Run API can start any flow by id, so even a flow whose canvas trigger is Manual can receive a realpayloadthis way. - A Job manual action button
that launches a flow — the button’s own key/value parameters land here, the
same way. Buttons that launch an agent instead don’t use
payload— see that guide for how agent parameters travel.
{{ payload.event }}{{ payload.repository.full_name }}The value picker ({x}) always offers a payload entry, for every trigger
type — because any flow could be started via the Run API regardless of what’s
configured on its canvas. Once a flow has a completed run that actually
carried one, the picker upgrades from the bare payload ref to the real
dot-paths it saw, e.g. payload.event, payload.repository.name.
Transforming values — filters & helpers
Section titled “Transforming values — filters & helpers”Chain a filter with |. The flow-specific ones:
{{ now.date | plus_days(30) }}— shift a date forward N days{{ deal.close_date | minus_days(7) }}— …or back N days{{ now.year | plus_years(1) }}→2027,{{ now.year | minus_years(3) }}→2023— shift a year number (plain arithmetic){{ now.month | plus_months(2) }}— shift a month number, wrapping 1–12 (December12+2→02, February). Zero-padded to matchnow.month("06"). The year is not rolled — it answers “what month is N months from this one”.{{ date_offset(30) }}— today ± N days, as an ISO date (no input needed){{ week_start() }}/{{ week_end() }}— Monday / Sunday of the current week
Each
now.*field’s Modify menu in the picker matches its own granularity: picknow.date→ shift days,now.month→ shift months,now.year→ shift years.
Plus standard Jinja filters — default, upper, lower, replace, truncate,
length, join, round, int — and the matching test for a regex check:
{% if user.email is matching('@routario\\.com$') %}internal{% endif %}Pulling one value out of a block of text
Section titled “Pulling one value out of a block of text”matching tells you whether text contains something. regex_search gives you
the thing itself — the order number buried in a customer’s e-mail, a tracking code in
a forwarded message, a VAT id in a signature.
{{ description | regex_search("order\\s+([5-8]\\d{4})") }}For “Hi, my order 79623 never arrived” that renders 79623. You can then branch on
it, or feed it straight into a lookup step, without asking the customer for something
they already told you.
It returns the part in brackets. The brackets — the capture group — mark the
piece you want, so order\\s+([5-8]\\d{4}) finds the whole phrase and hands back just
the number. No brackets, and you get the whole match: {{ description | regex_search("[5-8]\\d{4}") }} → 58266.
With more than one bracketed group, add the number of the one you want:
{{ body | regex_search("invoice (\\d{4})/(\\d{4})") }} → 2026{{ body | regex_search("invoice (\\d{4})/(\\d{4})", 2) }} → 0042{{ body | regex_search("invoice (\\d{4})/(\\d{4})", 0) }} → invoice 2026/00420 means the whole match. A number higher than the groups you actually wrote also
falls back to the whole match, so a typo shows up as too much text rather than
nothing at all.
Matching ignores upper/lower case and reads across line breaks, so a number sitting on the line below its label is still found.
It never breaks the run
Section titled “It never breaks the run”Whatever you throw at it, you get an empty string back rather than a failed run:
| Situation | Result |
|---|---|
| Nothing matches | "" |
| The pattern itself is malformed | "" |
| The field is missing or empty | "" |
That is deliberate. A flow should be able to branch on “we did not find one” — send the reply that asks for an order number — instead of stopping halfway with an error. Pair it with a Branch:
{{ ticket_number != "" }}Every match, not just the first
Section titled “Every match, not just the first”regex_findall returns all of them. Add | join(", ") to print them, otherwise
you get a raw list:
{{ body | regex_findall("([5-8]\\d{4})") | join(", ") }} → 79623, 58266With one bracketed group you get that group from each match; with none, the whole matches. No matches gives you an empty list.
When two steps set the same name
Section titled “When two steps set the same name”The outputs of most steps can be read two ways: by the step’s name —
{{ read_ticket.subject }} — or bare — {{ subject }}. The bare form reads
whichever step set that name last, and the trigger counts: a flow started by
a Zendesk ticket begins with {{ ticket_id }}, {{ subject }} and the rest
already set.
So a step that returns a name an earlier step — or the trigger — already set
changes what every bare reference after it reads. A second Read Zendesk
ticket step, added to fetch the earlier ticket a follow-up continues, puts the
old ticket’s id into {{ ticket_id }}, and a note posted to {{ ticket_id }}
then lands on the old ticket.
The editor lists every reference like that under Issues as Value replaced by a later step, naming both steps. It is a warning, not an error — sometimes the latest value is the one you want. To be sure which value you get, read it from the step you mean:
{{ read_ticket.ticket_id }}A value the trigger set has no step name. To keep one safe from later steps, copy it with a Set values step right after the trigger and read it from that step instead.
Reading a value from whichever branch ran
Section titled “Reading a value from whichever branch ran”A Switch is exclusive — only one arm runs — so
after its arms reconnect with Merge, only one of
that arm’s step labels actually exists in the run’s context. first_defined() and
last_defined() take any number of candidates and return the first (or last) one
that actually has a value:
{{ first_defined(storage_read2.content, storage_read3.content, storage_read4.content) }}“Has a value” means not undefined, not None, and not an empty string. If none of
the candidates resolve, it renders as an empty string rather than erroring. See
Read a value from whichever branch ran
for the full worked example — including the pop-up step’s Attachment field, which
uses the same two functions but over bare step labels instead of dotted paths.
Indexing into a list that might be empty
Section titled “Indexing into a list that might be empty”A step that returns a list — List files (storage), any step whose output is a collection — can come back with zero items just as easily as several. Reaching straight for the first one:
{{ storage_list.files.0.path }}only resolves when at least one file matched. If the list is empty, .0 has
nothing to point at, the reference renders empty, and — because a downstream
step usually marks that field required — the run stops with the error described
above instead of continuing with no file to work on.
Two patterns handle this properly, depending on what you’re actually trying to do:
Process every result, however many there are — put a
Foreach right after the list-producing step and
work with loop.item inside its body instead of hand-indexing a position:
{{ loop.item.path }}Zero items just means the loop body never runs — no error, nothing to guard.
You specifically want “the first one, if it exists” — add a Branch checking the count before the step that indexes into position 0, and route the empty case somewhere sensible (a notification saying nothing matched) instead of letting it fall through:
{{ storage_list.count > 0 }}Only take the “yes” arm into the step that reads {{ storage_list.files.0.path }}.
Inserting a whole step’s output — [[ step ]]
Section titled “Inserting a whole step’s output — [[ step ]]”To splice the entire text of an earlier step into a message (an LLM draft, a weather summary), use the block slot with the step’s label:
Here is today's briefing:
[[ summarize ]]Unlike {{ … }}, [[ … ]] flattens a step’s structured output to readable text
and is replaced before the rest of the template renders. An unknown label is left
visible as [[ label ]] so a missing reference is obvious.