Documentation / Workflows

References, routing, repeating and choosing

Minimum role
Organisation admin
Applies per
organisation
Last updated
11 October 2026
aishaa version
80c056408

Menu path: Account > Workflows > (open workflow)

In short: a run knows the data of the event (trigger) and the output of every step that has finished. With a reference in double curly brackets, such as {{trigger.ticketId}} or {{zoek.recordId}}, you pass that data on to a next step. Every step continues through an exit (exits) or through next, never both; whatever leads nowhere ends the run. This article describes all the rules for that.

Where to find this

Account > Workflows, open a workflow. In every input field of a step you insert a reference with Insert a value; under every step you choose at Then, or per exit at If exit, where the run goes next, and at If this fails the error route. Who: from the Organisation admin role.

An input field of a step in the workflow editor with the Insert a value menu open, showing the fields of the trigger and the output fields of earlier steps, each with its English name.

What a run knows

A run has one tree of data:

  • trigger: the fields of the event that started the run, plus the content fields aishaa reads as soon as a step uses them (see Triggers and events).
  • Per step that has finished: the output of that step, under the id of the step. If a step runs again (in a repetition or a loop), the new output replaces the old one.
  • A step that failed and continued through onError has {"error": "..."} as its output.

Beyond that, a workflow does not calculate or evaluate anything: there is no code in a definition, only paths in this tree.

References

A reference is a path in double curly brackets:

  • {{trigger.<field>}}: a field of the event, such as {{trigger.channel}}, or a content field, such as {{trigger.message.text}}.
  • {{<stepId>.<field>}}: an output field of a step, such as {{zoek.recordId}} or {{verslag.text}}.
  • You go deeper into an object with dots: {{lees.data.order.status}}. You pick an item from a list by its number (starting at 0): {{zoek.records.0.title}}.
  • A path consists of letters, digits, _ and -, separated by dots. Spaces inside the brackets are allowed: {{ trigger.ticketId }}.
  • References work anywhere in an input, also deep inside objects and lists (for example in the parameters of a plugin action).

When you publish, aishaa checks that every reference starts with trigger or with the id of an existing step. aishaa does not check whether the field itself exists, or whether the step has already run. A reference to something that does not exist (yet) is empty when the workflow runs.

Type preservation for a single reference

If the whole value is exactly one reference, the step receives the value with its own type:

Input What the step receives
"amount": "{{prijs.result}}" the number, for example 12.5
"input": "{{lees.data}}" the whole object
"items": "{{zoek.records}}" the list
"assignee": "{{trigger.recordedByUserId}}" the number of the colleague
a reference to something that is missing nothing (null)

Text filling in a longer text

If a reference is part of a longer text, aishaa fills it in as text:

  • "Call customer {{trigger.customer.name}} back about ticket {{trigger.ticket.number}}" becomes, for example, "Call customer Jan Jansen back about ticket 1042".
  • If the value is missing, it becomes an empty text.
  • An object or list is filled in as JSON text. If you want a list as readable text, convert it first with data.format and join.

Fixed values

You write a fixed value as it is, with the correct JSON type:

  • numbers as a number: "deadlineMinutes": 2880, not "2880";
  • yes/no as true or false: "multiple": true;
  • objects as an object: "input": {"orderId": "{{zoek.itemId}}", "reason": "damaged"};
  • lists as a list: "cases": ["Email", "WhatsApp"];
  • ids of labels, agents and teams as a number: "label": 12, "agent": 3.

You write paths without curly brackets in only two places: the path of a condition (in the trigger and in flow.condition) and the concurrencyKey. There you write trigger.channel, not {{trigger.channel}}.

Order: the first step, next and exits

  • A run starts at the first step in the steps list. The order of the other steps in the list does not matter; the routing decides what comes next.
  • A block with exits (such as flow.condition, plugin.action, human.request) continues per exit: "exits": {"<exit>": "<stepId>"}. In the editor: If exit.
  • A block without exits (such as task.create, ticket.update, data.format) continues with "next": "<stepId>". In the editor: Then.
  • Never both. A block with exits may not have next, and a block without exits may not have exits. Otherwise you cannot publish.
  • An exit must exist for that block (for human.request the answers from exits, for flow.switch the values from cases, for agent.choose the values from options, each time plus the fixed exit). An exit and a next must lead to an existing step.
  • Leaving it out means the end. An exit you do not name in exits, or a step without next, ends the run. In the editor this is called End. Exception: inside a repetition the run then goes back to the repetition for the next item.
  • A step may lead back to an earlier step (a loop). A run stops after 2,500 executed steps.

Example of both forms:

[
  { "id": "check", "block": "flow.condition",
    "inputs": { "conditions": [ { "path": "trigger.urgent", "op": "eq", "value": true } ] },
    "exits": { "true": "dringend" } },
  { "id": "dringend", "block": "ticket.update", "inputs": { "urgent": "yes" }, "next": "taak" },
  { "id": "taak", "block": "task.create", "inputs": { "title": "Pick up urgent message" } }
]

On false the run ends, because that exit is not named.

Choosing: flow.condition, flow.switch and agent.choose

Block Chooses on Exits
flow.condition One or more conditions, all true or not. true, false
flow.switch A value, compared as text (case-insensitive) with the cases list. The first match wins. every value from cases, plus other
agent.choose The meaning of a text, judged by an AI agent. every value from options, plus other

flow.condition and flow.switch cost nothing and are predictable. Only use agent.choose when someone has to understand the text. Details in Building blocks: deciding, repeating and waiting and Building blocks: agents.

The exit of a flow.switch or agent.choose is the value exactly as you wrote it in the list. So write the key in exits literally the same: "cases": ["Email"] goes with "exits": {"Email": ...}.

Repeating: flow.foreach

  • flow.foreach receives a list in items, as exactly one reference: "items": "{{zoek.records}}".
  • The each exit (must be connected) leads to the steps for one item. In those steps the current item is {{<foreachId>.item}}, and its position is {{<foreachId>.index}} (starting at 0) or {{<foreachId>.position}} (starting at 1).
  • The steps for one item end where they lead nowhere. The run then goes back to the repetition, which takes the next item.
  • After the last item the run continues through done.
  • The steps for one item may only be reachable through each. If a step from before or after the repetition leads to them, or if they lead to what comes after the repetition, you cannot publish.
  • At most 200 items; the rest is skipped ({{<foreachId>.skipped}}).

See Building blocks: deciding, repeating and waiting for a complete example.

Catching errors: onError and {{stepId.error}}

Every step may have "onError": "<stepId>" (in the editor If this fails; default Stop the run).

  • If the block fails, or keeps giving errors until aishaa stops retrying it, the run goes to that step instead of failing.
  • The failed step is recorded as failed with the exit error. Its output is {"error": "..."}; later steps read the reason as {{<stepId>.error}}.
  • onError may not lead to the step itself.
  • Without onError, a failed step ends the run as failed.

Note the difference with an exit such as failed. Blocks such as plugin.action, plugin.read, record.read, message.send and agent.reply have failed as a normal outcome: the step has then not failed, it continues through that exit. onError only catches real errors of a step, such as a colleague who does not exist, a conversation that is missing or an ai.transform that does not answer in the schema.

Use onError where a next step can still help, for example a task for a colleague:

[
  { "id": "verslag", "block": "ai.transform", "inputs": { "instruction": "Summarise.", "input": { "t": "{{trigger.recording.transcript}}" },
    "output": { "type": "object", "properties": { "text": { "type": "string" } } } },
    "next": "document", "onError": "hulp" },
  { "id": "document", "block": "document.create", "inputs": { "title": "Report", "text": "{{verslag.text}}" } },
  { "id": "hulp", "block": "task.create",
    "inputs": { "title": "Write the report manually", "text": "The automatic report did not work: {{verslag.error}}", "assignee": "{{trigger.recordedByUserId}}" } }
]

Do not put onError everywhere: only where it makes a difference.

Never two at once: concurrencyKey

"concurrencyKey": "trigger.ticketId" (in the editor Never twice at once for) makes sure the workflow never has two runs at the same time for the same value.

  • The key is a path without curly brackets to one of the event’s own fields, starting with trigger.. A content field is not allowed.
  • If an event with the same value arrives while a run of this workflow is open (running, waiting or paused), that event starts nothing. It is not kept or started later.
  • If the value is empty, there is no limit. A value longer than 200 characters only counts up to 200 characters.
  • Two events at the same moment cannot both start a run.

Use it when a workflow may only run once at a time per conversation (or per customer), for example a workflow that waits and sends reminders.

Step names (id)

  • Unique within the workflow.
  • Lowercase letters, digits, - and _, starting with a lowercase letter or a digit. At most 64 characters.
  • Never trigger.
  • References use the id, so choose a short, meaningful id (zoek-dossier, verslag) and do not change it when other steps refer to it.

In the editor the id is called Step name.

Labels

  • label (in the editor What this step does) is optional, at most 120 characters.
  • It says what the step does for this organisation, in a few words in the organisation’s language: “Find the customer’s mortgage file”, not the name of the block.
  • People read the workflow, the runs and the turn on the ticket by their labels. Without a label, aishaa shows the name of the block.
  • The label does not change how the workflow works, with one exception: for message.send and ticket.forward the label is the title of the proposal in To do.
  • In the editor, a reference to an output field reads as the name of that field together with the label of the step, instead of as a technical path.

show and fields when asking a person

For human.request, show and fields decide what the colleague sees and fills in.

  • show is what helps to decide: a report an AI step wrote, a summary, an amount. Use readable keys in the colleague’s language: {"Report": "{{verslag.text}}", "Amount": "{{totaal.result}}"}.
  • Never put ids in show (ticketId, messageId, actionId): the question is already linked to the ticket.
  • fields is a JSON schema (an object) of what the colleague fills in or adjusts; values gives the starting values. You read the answer later as {{<step>.fields.<field>}}.
  • Leave out fields if the colleague only chooses an answer.
  • Give every answer a readable button text in exitLabels, and connect every answer that has to go somewhere.

Good to know

  • A workflow has at most 100 steps.
  • A run always continues on the version on which it started, even if you publish a new version in the meantime.
  • A failed run can be retried from the step where it went wrong; see Following runs and handling errors.

Related

Last updated on 11 October 2026. Checked against aishaa version 80c056408. Something wrong or missing? Email support@aishaa.nl.