The workflow definition (for AI assistants)
In short: a workflow is data: one JSON object with a trigger, an optional concurrency key and a list of steps. An AI assistant (aishaa’s onboarding, or a customer’s Claude or ChatGPT through the aishaa connection) can write such a definition and save it as a draft with the workflows_save_draft tool. A draft does nothing: it only works once a person publishes it in aishaa. This article gives the complete format, all checks with their error messages, the tools and a step-by-step plan.
Where to find this
There is no separate screen for the definition. An AI assistant works with it through aishaa’s MCP connection; how to set that up is described in Connect aishaa to Claude or ChatGPT. A person checks and publishes the draft under Account > Workflows with Publish.
Who: the workflow tools are available from the Organisation admin role, for the organisations whose AI settings you manage. Only an Account admin can create or change a workflow for the whole account. The assistant always works with exactly your permissions.
The format
{
"trigger": {
"event": "<event>",
"conditions": [ { "path": "trigger.<field>", "op": "<operator>", "value": <value> } ],
"schedule": { "every": "day", "at": "09:00" }
},
"concurrencyKey": "trigger.ticketId",
"steps": [
{
"id": "<step name>",
"label": "<what the step does, in the customer's language>",
"block": "<block name>",
"inputs": { "<input>": <value or "{{reference}}"> },
"next": "<step name>",
"exits": { "<exit>": "<step name>" },
"onError": "<step name>"
}
]
}
The names are camelCase, exactly as below.
| Part | Required | Meaning |
|---|---|---|
trigger.event |
yes | The technical name of the event, such as message.understood. See Triggers and events. |
trigger.conditions |
no | List of conditions that must all be met. Left out: every event of that kind. |
conditions[].path |
yes | One of the event’s own fields, without curly brackets, starting with trigger.. |
conditions[].op |
yes | eq, neq, gt, gte, lt, lte, contains, exists, missing. |
conditions[].value |
except with exists/missing |
The fixed value, with the correct JSON type (a number as a number). |
trigger.schedule |
only with schedule.due, and then required |
every (hour, day, weekday, week, month), at (HH:mm), days (with week: monday … sunday), dayOfMonth (with month: 1 to 28), timeZone (optional, such as Europe/Amsterdam). |
concurrencyKey |
no | One of the event’s own fields (such as trigger.ticketId) for which the workflow never has two open runs at the same time. |
steps |
yes | 1 to 100 steps. The first step is where a run starts. |
steps[].id |
yes | Unique; lowercase letters, digits, - and _, starting with a letter or digit; at most 64 characters; never trigger. |
steps[].label |
no, but always fill it in | What the step does for this organisation, in a few words in its language, at most 120 characters. |
steps[].block |
yes | The technical name of the block, such as agent.reply. See Building blocks of a workflow. |
steps[].inputs |
depends on the block | The input of the block: fixed values or references {{trigger.<field>}} / {{<stepId>.<field>}}. |
steps[].next |
only for a block without exits | The next step. Left out: end of the run. |
steps[].exits |
only for a block with exits | The next step per exit. An exit left out: end of the run. |
steps[].onError |
no | The step the run goes to if this step fails. The reason is then in {{<stepId>.error}}. |
All rules for references, routing, repeating and errors are in References, routing, repeating and choosing.
Complete example
A workflow that sorts every customer message, puts on a label and replies per route, with a question to a colleague for urgent complaints:
{
"trigger": { "event": "message.understood" },
"steps": [
{ "id": "sorteer", "label": "Decide what the message is about", "block": "agent.choose",
"inputs": {
"options": ["retour", "levering", "klacht"],
"explanations": {
"retour": "wants to send something back or exchange it",
"levering": "asks where an order is or when it will arrive",
"klacht": "is unhappy with a product or the service"
},
"labels": { "retour": 12, "levering": 13, "klacht": 14 },
"input": "{{trigger.message.text}}"
},
"exits": { "retour": "antwoord-retour", "levering": "antwoord", "klacht": "dringend", "other": "antwoord" } },
{ "id": "antwoord-retour", "label": "Answer the return question", "block": "agent.reply",
"inputs": { "instruction": "Always refer to the return form on the website." },
"exits": { "failed": "taak" } },
{ "id": "dringend", "label": "Is the complaint urgent?", "block": "flow.condition",
"inputs": { "conditions": [ { "path": "trigger.urgent", "op": "eq", "value": true } ] },
"exits": { "true": "teamleider", "false": "antwoord" } },
{ "id": "teamleider", "label": "Put the urgent complaint before the team lead", "block": "ticket.update",
"inputs": { "team": 4, "urgent": "yes" }, "next": "antwoord" },
{ "id": "antwoord", "label": "Answer the message", "block": "agent.reply", "inputs": {},
"exits": { "failed": "taak" } },
{ "id": "taak", "label": "Task: write the reply manually", "block": "task.create",
"inputs": { "title": "Answer the message yourself", "text": "aishaa could not write a proposal for this message." } }
]
}
The ids of the labels (12, 13, 14) and the team (4) are examples: the real ones are in the guide from workflows_get_guide under “Labels” and “Teams”.
Checks and error messages
When saving through workflows_save_draft and when reading with workflows_get, aishaa returns all problems at once, as an English message with which an AI can improve the draft. In the editor, a person sees the same problems under Before you can publish. A draft with problems is saved, but cannot be published.
Definition and trigger
| Code | Message (as the tool gives it) | In the editor |
|---|---|---|
definition_missing |
The workflow has no definition. | The workflow has no trigger or steps yet. |
definition_invalid |
The definition is not valid JSON for a workflow: … | The draft could not be read. |
trigger_unknown |
The trigger names no known event type (‘…’). | Choose what starts the workflow. |
path_not_trigger |
A trigger condition must read from the trigger (‘trigger.…’), not ‘…’. (Or: The concurrency key must read …) | A condition of the trigger must use a field of the trigger, not ‘…’. |
trigger_reads_content |
A trigger condition reads ‘…’, the content of an object; trigger conditions read the event’s own fields only. Test it in a flow.condition step. (Or: The concurrency key reads …) | A trigger condition reads ‘…’, content that is read only by a step. Test it in a step “Condition” or “Choose a route”. |
schedule_without_trigger |
Only the event ‘schedule.due’ takes a schedule; remove trigger.schedule. | Only “On a schedule” takes a schedule. |
schedule_missing |
A workflow on the clock needs trigger.schedule: {“every”: “day”, “at”: “09:00”}. | Say when the workflow starts. |
schedule_invalid |
trigger.schedule: ‘every’ is one of: hour, day, weekday, week, month. / ‘at’ is a time as HH:mm, such as 09:00. / Every week needs ‘days’: one or more of monday, tuesday, wednesday, thursday, friday, saturday, sunday. / Every month needs ‘dayOfMonth’ from 1 to 28. / ‘…’ is not a time zone (such as Europe/Amsterdam). | The schedule is not complete: … |
You get a definition_invalid for JSON that does not fit, for example an unknown operator or a missing trigger or steps. workflows_save_draft does not save such a definition and answers with “definitionJson is not a workflow definition: … Read workflows_get_guide.”
Steps and routing
| Code | Message | In the editor |
|---|---|---|
step_empty |
A step in the list is empty (null). | There is an empty step. |
no_steps |
A workflow needs at least one step. | Add at least one step. |
too_many_steps |
A workflow has at most 100 steps. | A workflow has at most 100 steps. |
step_id_invalid |
Step id ‘…’ must be lowercase letters, digits, ‘-‘ or ‘_’. (also for more than 64 characters) | Step ‘…’: a step name is lowercase letters, digits, ‘-‘ or ‘_’. |
step_id_reserved |
A step cannot be called ‘trigger’. | A step cannot be called ‘trigger’. |
step_id_duplicate |
Step id ‘…’ is used twice. | Two steps are called ‘…’. |
step_label_too_long |
Step ‘…’: the label is longer than 120 characters. | Step …: keep what it does to at most 120 characters. |
block_unknown |
Step ‘…’: block ‘…’ does not exist. | Step ‘…’: this kind of step does not exist (…). |
exits_without_exits |
Step ‘…’: block ‘…’ has no exits; use ‘next’. | Step ‘…’: this step goes on with ‘Then’, not per outcome. |
next_with_exits |
Step ‘…’: block ‘…’ leaves by its exits (…), not by ‘next’. | Step ‘…’: this step goes on per outcome, not with ‘Then’. |
exit_unknown |
Step ‘…’: block ‘…’ has no exit ‘…’. | Step ‘…’: there is no outcome ‘…’. |
target_missing |
Step ‘…’ leads to step ‘…’, which does not exist. | Step ‘…’ leads to ‘…’, which does not exist. |
error_route_to_itself |
Step ‘…’: ‘onError’ cannot lead to the step itself. | Step ‘…’: if it fails, it cannot go to itself. |
reference_unknown |
Step ‘…’ refers to ‘…’: ‘…’ is neither the trigger nor a step. | Step ‘…’ uses ‘…’, but that step does not exist. |
answer_without_understood |
Step ‘…’: ‘agent.reply’ (or ‘case.process’) works on a customer message the analysis understood; the trigger must be ‘message.understood’. | Step “…” answers a customer message: the workflow must start on “Message read”. |
loop_without_steps |
Step ‘…’: the repeat needs its ‘each’ exit to lead to the steps for every item. | Step ‘…’: “for each item” must lead to the steps for one item. |
loop_leaks |
Step ‘…’: step ‘…’ runs for every item but is also reached outside the repeat. The steps for each item end; the repeat then goes on by ‘done’. | Step ‘…’: step ‘…’ runs for each item but is also reached outside the repeat. |
If a block is unknown, aishaa does not check the rest of that step. aishaa only checks loop_leaks when there are no other problems.
Input per block
Problems with the input of a block have the code input_required (a required input is missing) or input_invalid (an input is not right). Every message starts with Step '<id>':. The editor shows “Step ‘…’: fill in ‘…’.” or “Step ‘…’: ‘…’ is not right yet.”
| Block | Message (after Step '<id>':) |
|---|---|
flow.condition |
input ‘conditions’ must be a list of conditions with a path and an operator. |
flow.wait |
give either ‘minutes’ or ‘until’. / ‘until’ must be a moment (ISO 8601) or a reference in {{…}}. / ‘minutes’ must be a number above 0 and at most 30 days. |
flow.switch |
‘value’ (what to choose by) is required. / ‘cases’ must list 1 to 12 different values (not ‘other’), each at most 80 characters. |
flow.foreach |
‘items’ must be a reference to a list, such as {{find.records}}. / the repeat needs its ‘each’ exit to lead to the steps for every item. |
message.check |
‘check’ is hasImage, noImage, recordFound or noRecord. |
human.request |
‘title’ (the question) is required. / ‘exits’ must list 1 to 6 different answers in lowercase (letters, digits, ‘-‘, ‘_’), not ‘expired’. / ‘exitLabels’ gives each answer the words on its button (at most 60 characters), only for answers in ‘exits’. / ‘fields’ must be a JSON schema (an object). / ‘deadlineMinutes’ must be a number above 0 and at most 30 days. |
task.create |
‘title’ is required. / ‘dueMinutes’ must be a number above 0 and at most 365 days. |
plugin.action |
‘action’ must name a plugin action as ‘pluginId/operationId’. / ‘input’ must be an object of the action’s parameters. / ‘mode’ is ‘propose’ or ‘execute’. / ‘deadlineMinutes’ must be a number above 0 and at most 30 days. |
message.send |
‘message’ (the text to the customer) is required. / ‘mode’ is ‘propose’ or ‘execute’. / ‘deadlineMinutes’ must be a number above 0 and at most 30 days. |
ticket.forward |
‘to’ (an e-mail address) is required. / ‘mode’ is ‘propose’ or ‘execute’. / ‘deadlineMinutes’ must be a number above 0 and at most 30 days. |
ticket.label |
‘label’ (a label’s id) is required. |
ticket.update |
‘status’ is one of keep, open, answered, closed. / ‘important’ is one of keep, yes, no. / ‘urgent’ is one of keep, yes, no. / ‘team’ is a team’s id. |
ai.transform |
‘instruction’ (what the AI should do) is required. / ‘output’ must be a JSON schema of an object. |
data.format |
‘operation’ is one of: add_time, format_date, calculate, round, replace, upper, lower, truncate, default, join, count. / ‘value’ (what to work on) is required. / ‘unit’ is one of: minutes, hours, days, weeks, months. / ‘…’ needs ‘amount’ (a number or a reference). / ‘arithmetic’ is one of: add, subtract, multiply, divide. / ‘format’ is one of: date, date_time, time, weekday, iso, iso_date. / ‘replace’ needs ‘find’. |
record.find |
‘kind’ (which kind of record) is required. |
record.read |
‘read’ must name a record read as ‘pluginId/operationId’. / ‘record’ (which record, usually {{find.recordId}}) is required. |
plugin.read |
‘read’ must name a plugin read as ‘pluginId/operationId’. / ‘input’ must be an object of the read’s parameters. |
document.create |
‘title’ is required. / ‘text’ is required. |
agent.reply |
‘agent’ is an agent’s id, or left out for the conversation’s agent. / ‘instruction’ is text of at most 2000 characters. / ‘send’ is ‘where_granted’ or ‘propose’. |
agent.choose |
‘agent’ is an agent’s id, or left out. / ‘options’ must list 1 to 30 different options (not ‘other’), each at most 80 characters. / ‘explanations’ gives options in ‘options’ a text of at most 400 characters. / ‘labels’ gives options in ‘options’ the id of a label. / ‘input’ (what the agent reads) is required. |
agent.task |
‘agent’ is an agent’s id, or left out for the agent the answering workflow names. / ‘instruction’ (the task) is required. / ‘instruction’ is at most 4000 characters. |
case.process |
no input, no checks. |
What the check does not check
- Whether a plugin action, plugin read or record read really exists in the organisation (only the form
pluginId/operationId). That becomes clear when the workflow runs. - Whether a label, team, agent or colleague with that id exists. A wrong id makes the step fail when it runs.
- Whether a referenced field exists, and whether the step you refer to has already run.
That is why you only use ids and names from the guide from workflows_get_guide.
The MCP tools for workflows
| Tool | What it does |
|---|---|
workflows_get_guide |
Gives the guide for writing a workflow. Always read it first. |
workflows_list |
Shows the account’s workflows that this organisation sees. |
workflows_get |
Reads one workflow: the draft, the versions, what blocks publishing, and optionally the latest runs. |
workflows_save_draft |
Creates a workflow or replaces the draft of an existing one. |
Every tool has the optional parameter organisationId. Leave it out if you have access to one organisation; pass it if you have access to several.
workflows_get_guide
The guide is rebuilt every time from what is in aishaa at that moment. It always gives the current catalogue plus what the organisation has:
- the rules of the format (trigger, conditions, steps, routing, references, fixed values,
showandfields, content fields, error routes, repeating, choosing, calculating, connected systems, the schedule, answering customer messages); - every event with all its fields, their type, choice values and whether it is content;
- every block you can add, with kind, description, input (name, type, required, choice values, explanation), exits and output fields;
- under “This organisation”: the plugin actions with their parameter schema (at most 40 actions, a long schema shortened), the record kinds, the plugin reads with their schema (at most 40), the record reads with the record kinds they read, the agents (id, name and what for), the labels (id and name), the teams (id and name) and the colleagues who can be given work (user id and name).
If a part says there is nothing (“none”, “cannot be used here”), the organisation cannot use that block or you must leave out that input.
workflows_list
Per workflow: workflowId, name, description, status (Draft, Active or Paused), scope (account or an organisation), the event of the trigger, the live version, whether the draft has unpublished changes, the number of runs and failed runs in the past week, the number of open runs, the last run, and appUrl: the link to the workflow in aishaa.
workflows_get
Parameters: workflowId (required) and includeRuns (default false). Gives the name, description, status, scope, whether the workflow is locked, the draft (draft, the definition you replace with workflows_save_draft), the live version, the earlier versions (number, date, by whom), the problems that block publishing, and appUrl. With includeRuns: true also the latest 20 runs (status, version, where the run is, start, end, until when it waits, error, ticket) and, for the latest events of the trigger, whether they started a run or which condition stopped that, with the actual value.
workflows_save_draft
Parameters:
| Parameter | Required | Explanation |
|---|---|---|
definitionJson |
yes | The whole definition as JSON text. Always pass the complete definition, also for a small change. |
workflowId |
no | The workflow you are changing. Left out: a new workflow is created. |
name |
for a new workflow | The name people see, at most 200 characters. |
description |
no | What the workflow is for, in one or two sentences (at most 2,000 characters). |
wholeAccount |
no | true: a new workflow for all organisations of the account (only for an Account admin). Default: for the organisation you are working in. |
The answer contains workflowId, saved, readyToPublish (true if there are no problems), problems (the messages from the tables above), appUrl and next: “Ask the person to check the draft and publish it in the app (appUrl).” or “Fix the problems and save again.”
Good to know:
- A new workflow starts as a draft (status Draft). Saving changes nothing about what is live.
- A workflow that is locked does not accept a new draft.
- There is deliberately no tool to publish. A person checks the draft and publishes it in aishaa, through
appUrl.
A draft only works after publishing
A draft does nothing with customers or data. Only when a person clicks Publish in Account > Workflows does it become a live version (status Active) and do runs start. In the editor that person can first try it out on real events without anything happening; see Create, test and publish a workflow. A run always continues on the version on which it started.
The editor itself also has an assistant, the Building assistant, which proposes a new draft based on what someone asks. It uses the same guide as workflows_get_guide.
How to design a workflow for a customer as an AI
- Read the guide. Call
workflows_get_guidefor the right organisation. Only use events, blocks, fields, actions, reads, record kinds, agents, labels, teams and colleagues that are in it. Never make up an id. - See what is already there. Call
workflows_list. If a workflow onmessage.understoodalready answers customer messages, change that one (read it withworkflows_get) instead of putting a second one next to it. Two workflows that answer the same message each write their own proposal. - Ask what happens and when. Let the customer tell you in their own words which situation they want to automate: what comes in, what should happen then, who decides on it, and what may happen without a person looking along.
- Choose the trigger. Customer messages:
message.understood. A new conversation:ticket.created. A recording:recording.transcribed. A closed conversation or an assignment:ticket.status_changedorticket.assigned. Something at fixed times:schedule.duewith aschedule. Narrow it down with conditions on the event’s own fields (channel, topic, opening hours, new conversation). Only test content in a step. - Sort. If messages need to be handled differently, start with a choice:
flow.switchon a fixed value (channel,trigger.topicId) if possible, otherwiseagent.choosewith short options, an explanation per option and possibly a label. Always connectother. - Choose per route: reply or person. A route that gets a reply ends in
agent.reply, possibly with aninstructionfor that route. A route where a person has to decide gets ahuman.requestor aticket.updateto the right colleague or the right team. - Fetch data. Use content fields where possible (no step needed),
record.findandrecord.readfor the customer’s record,plugin.read(possibly in a chain) for other data, anddata.formatfor dates, amounts and lists. Never let the AI calculate. - Ask for approval of actions. Keep
plugin.action,message.sendandticket.forwardon"mode": "propose"until the customer trusts the workflow. Give asummary, anassigneewhere a specific colleague must decide and a suitabledeadlineMinutes. Connectrejectedandexpiredif something still needs to happen then. - Catch errors. Connect the exits
noneandfailed, and give steps where something can go wrong anonErrorto atask.createwith{{<step>.error}}in the text. A message that no workflow answers is for a colleague. - Write readably. Give every step a short, clear
idand alabelin the customer’s language. Write titles, questions, button texts (exitLabels) and summaries in that language. Only put inshowwhat helps to decide, never ids. - Save the draft. Call
workflows_save_draftwith the complete definition, a name and a description. If there areproblems, fix them and save again untilreadyToPublishistrue. - Let a person publish. Tell the customer in plain language what the workflow does, what they should check (who gets a question, which deadlines, what happens at once) and give the
appUrl. Recommend trying it out first. Never publish yourself; there is no tool for that either.
Good to know
- A workflow has no priority: every workflow whose trigger matches starts. That is why you put all of the answering of a message in one workflow.
- A workflow runs in the background on behalf of the workflow. What a person must approve is a
human.request, aplugin.actiononproposeor amessage.sendonpropose. - An
agent.replyonly sends by itself where a person has allowed the agent to send by itself on that topic and every guardrail holds; otherwise it remains a proposal. - Through the connection, an AI assistant never sees or does more than the person who is logged in.
