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)workspace.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 substrate-backed 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 %}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.