References, routing, repeating and choosing
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
onErrorhas{"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.formatandjoin.
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
trueorfalse:"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
stepslist. 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 haveexits. Otherwise you cannot publish. - An exit must exist for that block (for
human.requestthe answers fromexits, forflow.switchthe values fromcases, foragent.choosethe values fromoptions, each time plus the fixed exit). An exit and anextmust lead to an existing step. - Leaving it out means the end. An exit you do not name in
exits, or a step withoutnext, 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.foreachreceives a list initems, as exactly one reference:"items": "{{zoek.records}}".- The
eachexit (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}}. onErrormay 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.sendandticket.forwardthe 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.
showis 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. fieldsis a JSON schema (an object) of what the colleague fills in or adjusts;valuesgives the starting values. You read the answer later as{{<step>.fields.<field>}}.- Leave out
fieldsif 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.
