Picture two colleagues on a busy afternoon. You ask both to check whether an order has shipped. The warehouse system is down. The first one comes back and says, “I couldn’t reach the warehouse just now. I’ve logged it, and someone will call you back.” The second one goes quiet, and an hour later you are still waiting.
Same bad luck; only the first one had a plan. An AI employee is the same: systems go down, a step fails, a customer sends a file nobody can read. Whether the customer hears something useful depends on whether the person who designed the workflow planned for that moment. This post covers what Wetel does today when something goes wrong, what the customer sees, and how a workflow author (a developer or the manager who owns the AI employee) designs for it.
What the customer sees by default: a short generic apology
If a step fails and nothing in the workflow catches it, the run stops and the customer gets a generic reply that begins “Sorry, I ran into a problem”. The real reason is not shown in the chat. It is recorded in the run record (the run’s errorMessage and the failed step’s entry in nodeTrace) and published live as a NodeFailedEvent to anyone subscribed to the session’s events.
That default is deliberate: a raw HTTP error is not for customers. But a generic apology is the second colleague, slightly politer: it says nothing about what happens next.
Two cases look like failures and are not:
- A run waiting for the customer’s answer (status
AWAITING_INPUT) paused on purpose at anawait_replystep. It is not broken. - A turn the customer replaced by sending a newer message before the reply arrived is recorded as
INTERRUPTED, and the generic apology is suppressed for it, so nobody gets an apology for a question they have moved on from.
Route a failure to a reply you wrote
For tool and action steps, draw two outgoing edges labelled success and failure, and a failure goes down your branch instead of stopping the run. (A tool step calls a tool on a registered MCP connector; an action step calls a registered custom action, a plain REST endpoint.) In the workflow editor, the step’s card toolbar has an “Add success/failure branch” toggle.
When the call throws (a timeout, a connection failure, an HTTP error, a malformed request template), the runner follows the failure edge, writes the error message into a context variable called _lastNodeError, and the run carries on. The step’s trace entry is marked FAILED_HANDLED: not an outage, a path the author planned for.
flowchart LR A["action step:<br/>look up the order"] -- success --> B["response:<br/>here is your order status"] A -- failure --> C["response:<br/>I couldn't reach our order system,<br/>here is what happens next"] A -. "error recorded" .-> D["Run record<br/>(FAILED_HANDLED)"]Three rules from the docs:
- The pair goes together. A
failureedge needs a matchingsuccessedge on the same step; publishing one without the other is rejected. - No labels means the old behaviour. A step with neither label fires every outgoing edge when it finishes, and an unhandled failure still fails the run.
- It is only about whether the call succeeded. A system answering “no such order” normally is a success; handle that answer in the next step.
The docs describe these branches for tool and action steps only; for other step types, plan on a failure stopping the run.
Write the failure reply the way the first colleague would talk, and keep _lastNodeError (the technical error) out of it. Only promise a call-back if something in the workflow actually arranges one, such as a step that opens a ticket in your system or an approval a workspace owner acts on.
Retry a few times, then give up gracefully
A step can be retried in a loop, with a cap, and a separate edge taken when the cap runs out. The workflow docs include a complete example: a claims assistant submits a claim to a partner system that is occasionally flaky, tries three times, then hands off. The submitting step sets "maxVisits": 3, its failure edge goes to a short “trying once more” reply that loops back, and a third edge labelled loop_exhausted goes to a hand-off reply. These are its edges, from the worked example:
[ { "id": "e2", "source": "n_submit", "target": "n_confirm", "label": "success" }, { "id": "e3", "source": "n_submit", "target": "n_wait_note", "label": "failure" }, { "id": "e4", "source": "n_wait_note", "target": "n_submit" }, { "id": "e5", "source": "n_submit", "target": "n_give_up", "label": "loop_exhausted" }]The give-up reply in the example reads: “I couldn’t reach our claims system after a few tries. I’ve flagged this for a colleague, who will follow up by email today.” In your own workflow, make that sentence true: something has to do the flagging.
The docs flag the trap: every step inside the loop needs the higher maxVisits, the reply step too, or the loop stops early without reaching your give-up reply. Without the loop_exhausted edge, the fourth attempt fails the run and the customer gets the generic apology.
Loops sit inside the run’s documented ceilings of 250 step executions and 10 minutes; hitting one records BUDGET_EXCEEDED on the step that was refused, and the run fails.
When the customer sends something the workflow cannot use
Most “unreadable” input should be a branch in the workflow, not an error. The docs cover several cases:
- A fact that is simply missing. Since 2026-09-21, a
conditionstep whose expression names a value the turn did not carry evaluates tofalseand takes thefalseedge, instead of failing the run. Make thefalsebranch ask for what is missing. - An answer outside the expected categories. A classification step can reply in prose instead of a label, so the
routerafter it takes itsdefaultTarget. Point that at something useful (a clarifying question), not a dead end. - A document in a format that cannot be read. An
actionstep that extracts text from a fetched file supports PDF, DOCX, Markdown, CSV and XLSX; anything else fails the step with an error naming the supported types. A truncated extraction sets a flag (<extractTextVar>Truncated) your workflow can branch on, so it can say “this document is too long” rather than answer from half of it. - A photo that will not load. That case has its own behaviour, covered in how an AI employee reads a photo.
Say something before a slow or risky step
Once a turn has sent its first reply, it runs to completion even if the customer sends another message. If the customer sends a second message before the turn has said anything, the newer message replaces the turn: steps it had not started yet are skipped. The docs give the case to design around: a workflow that submits a form or creates a record before its first reply. If the customer sends “hello?” while it is working, that side effect may never happen, and the customer may think it did.
The fix is the first colleague’s habit: acknowledge first (“Got it, filing your application now”), then do the work. The acknowledgement commits the turn. A step that is already running, such as a call to your system, always finishes; an outbound call is never cut off halfway.
What a developer sees when a request is refused
Requests to the Wetel API that fail come back as GraphQL errors, and the stable thing to branch on is errors[].extensions.code, not the message text. Message strings are meant for people and can change. What the docs list:
NOT_FOUNDwhen a record does not exist, including when it exists for another workspace: Wetel answers “not found” rather than confirming that someone else’s record is there.- Refused or invalid input (
BAD_REQUEST,BAD_USER_INPUT,FORBIDDEN,UNAUTHENTICATED) will not succeed however often you retry it unchanged. Fix the request. INTERNAL_SERVER_ERRORis the one class worth retrying with backoff. If it repeats for the same operation, report it.- Rate limits return an error with HTTP
429semantics; back off and retry after the window, which is 60 seconds for most operations. Most limits are counted per client IP address. Starting a session (sdkStart) defaults to 30 per minute per IP; sending a message (sdkSendMessage) has no enforced limit today, which the docs say is a current state, not a guarantee.
A failure inside a running workflow is different: the message call succeeds, and the failure shows up later in the events and the run record.
How to read the run record
Query workflowRuns(sessionId) with a dashboard login and read each run’s status, errorMessage and nodeTrace. The API key and the in-browser session token cannot read run records, so internal support tooling needs a real login. The statuses worth handling separately:
| Status | What it means |
|---|---|
FAILED (on a step) | The step threw and nothing caught it; the run stopped there |
FAILED_HANDLED | A tool or action step failed and its failure edge was followed; the run carried on |
BUDGET_EXCEEDED | The run hit its 250-step or 10-minute ceiling; the named step never ran |
VISIT_LIMIT_REACHED | A loop hit its maxVisits; read the run’s status to see whether it carried on |
AWAITING_INPUT (on a run) | Waiting for the customer, not failed |
For tool, action and webhook steps, the trace entry also carries renderedRequest (the request actually sent, with recognisable secrets redacted) and, when your system answered with an error, that system’s own response message inside error. That is usually the real reason.
Do not alert on every run that is not COMPLETED: waiting runs never complete by design.
A design checklist
- Give every
toolandactionstep that talks to a system asuccessand afailureedge, and write the failure reply yourself. - Say what happens next, honestly. Promise a call-back only if a step really arranges one (a ticket, an approval, a hand-off).
- Cap every retry loop with
maxVisitson every step in the loop, plus aloop_exhaustededge to a give-up reply. - Give missing and unexpected input a branch: a useful
falsepath onconditionsteps and a usefuldefaultTargetonroutersteps. - Acknowledge before a slow or consequential side effect, so the turn is committed before the work starts.
- Have someone read the run record: a person on your team who checks failed and
FAILED_HANDLEDruns, using a dashboard login, and fixes the workflow when the same failure repeats.
What is live and what is not
Live today: the generic fallback reply, success/failure branches on tool and action steps, capped loops with a give-up edge, the run ceilings, the run record with the statuses above, and live NodeFailedEvent events. Not documented, so do not plan on it: failure branches on other step types. Not something we publish: a reliability or uptime figure; measure failures in your own run records. The changelog lists when each behaviour arrived.
Related reference docs
- Error Handling & Status Codes: error codes, the run-trace statuses, and what to retry
- Tool node: success/failure edges: the two-edge pattern on a tool step
- Action node: success/failure edges: the same pattern on a custom action step
- Workflow overview: bounded retry with a graceful give-up: the full retry example
- Response node: writing the reply, including
{{#if}}branches - Events: side effects before the first reply: why to acknowledge first
- Rate Limiting: every limit, and what has none today
- Observability: the run record and
nodeTracefields - Workflow best practices: keeping raw tool output out of customer replies
Start with the error handling reference, then add a failure branch to the one step in your workflow that calls the system most likely to be down.