Skip to content

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.

Available only inside a Foreach step.

  • loop.item — the current item being processed
  • loop.index — its position, starting at 1
  • loop.index0 — its position, starting at 0
  • loop.total — how many items there are
  • loop.first / loop.last — booleans for the first / last pass

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 a to: 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 %}

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.phone
  • actor.locale
  • actor.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.

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 timestamp
  • now.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.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 name
    • workspace.own_company.name — display name
    • workspace.own_company.company_id — company registration number (IČO)
    • workspace.own_company.vat — VAT number (DIČ)
    • workspace.own_company.domain, .website
    • workspace.own_company.headquartered_at.canonical_name — registered address

    These come from your workspace identity; they render empty until it’s been set up.

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_company
  • contact.primary_role
  • contact.emails, contact.phones, contact.companies — the full lists
  • Addressing helpers (pick a specific value):
    • contact.email_for(company="Acme") — the email tied to that company
    • contact.email_by_label("work") — the email with that label
    • contact.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 payload directly, e.g. {{ payload.invoice_id }}.
  • The Run API (POST /api/v1/runs) — the caller’s input object 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 real payload this 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.

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 (December 12 + 202, February). Zero-padded to match now.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: pick now.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 %}

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.

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.