Error codes, refusal verdicts and what each one actually means

Decoding the status, verdict, skipReason and errorCode on a turn — including the two failures that look identical and need completely different fixes.

14 min readUpdated 6 August 2026errors, verdicts, skip-reason, provider, diagnosis

Every attempt to answer writes a row to ai_support_turn — including the ones that produced no text at all, because why did the assistant stay silent is the question operators actually ask. That row carries four fields worth reading, and this page is what each value means.

Where to read them

Live Inbox → the case pane → Evidence tab. The AI activity list shows the last eight turns: the verdict (falling back to the status), skipReason, escalationReason, retrieval confidence, groundedness, latency, model and cost.

errorCode is returned by the session route but is not drawn in that list. For the code itself, read the backend log — every failure logs one line in this shape — or query the column:

Generation failed for <ticketId> [NO_BACKEND] via mashdiv: <message>
SELECT createdAt, status, verdict, skipReason, escalationReason,
       errorCode, LEFT(errorMessage, 200) AS errorMessage, costUsd
  FROM ai_support_turn
 WHERE ticketId = :ticketId
 ORDER BY createdAt DESC;

Six checks run before a session exists, and none of them writes a turn:

Nothing happened because Fix
aiSupportEnabled is off Settings → Guardrails
The channel is off — aiSupportTicketsEnabled or aiSupportLiveChatEnabled Settings → Channels
The ai_support extension row is switched off Admin → System → Extensions
The licence could not be verified Renew, or restore outbound access — see below
The ticket is CLOSED Reopen it
The ticket no longer exists

Only one of the six can be a fault, and it is the one worth ruling out first because on this screen it looks exactly like the switches: the licence gate fails closed, so a lapsed licence and an unreachable licence service both silence the assistant with no turn row, no error code and no verdict. The only trace is an AI_SUPPORT warning in the backend log — "…is not licensed for it; staying silent" or "License verification failed (…); staying silent". Grep for staying silent.

The extension check fails closed for the same reason: route access is not gated on extension.status by the platform, so the trigger reads the row itself and treats "cannot establish that it is on" as off.

The shipped default has tickets on and live chat off.

Four fields, four different questions

Field Answers
status Did the attempt run, and did it produce anything?
verdict What did it decide about the customer's question?
skipReason Which gate stopped it?
errorCode What broke, machine-readably?

Statuses

Status Meaning Typically bills
PENDING Recorded, not finished No — the call has not returned
STREAMING A member of the enum that nothing writes any more. The customer is shown no partial text at all — see below No
SUCCEEDED The generation succeeded — the answer may still have been withheld or undelivered Yes
REFUSED The provider returned a safeguard refusal Yes
FAILED The provider or the delivery failed Yes
CANCELLED Aborted mid-generation — a human took over, or a generated answer a barge-in is withholding Yes, if the generation had already completed
SKIPPED A gate stopped it before or instead of generating No — no call was made

The daily and monthly caps sum costUsd over every row that carries one. There is no status allowlist, and there was never a good one: the caps used to sum SUCCEEDED, REFUSED and FAILED, which reads as "every status that can have a cost" and is not. A barge-in on an install with aiSupportDraftOnBargeIn off moves a turn to CANCELLED after the generation completed and the cost is on the row — so the busier the desk, the more the ceiling under-counted. The column above says "typically" because the status describes what happened to the answer, not whether the provider billed for it.

The sum spans both turn tables. Your own questions — the admin assistant, the Handbook, Ask about this customer, Teach from this ticket, Test connection — write to ai_support_admin_turn and count against the same two ceilings your customers' answers do.

That is not an accounting quirk to work around — it is why an install with no operator articles can burn a day's budget escalating everything. The fix is articles, not a bigger cap.

SUCCEEDED is the one that misleads. It means the model produced text, not that a customer read it. A turn withheld by the groundedness gate, a copilot draft awaiting a human, and an answer a barge-in stopped from being appended are all SUCCEEDED with wasSent: false — except that when aiSupportDraftOnBargeIn is off, the barge-in turn is moved to CANCELLED instead. The text is still on the row; the status is what keeps it out of the console's draft panel. The provider had already been paid for it, and that cost counts against your caps like any other — a CANCELLED turn is withheld from the agent, not refunded to you.

STREAMING is the one you will never see. The provider is still driven in streaming mode, but the tokens are held: the post-filters that catch an invented link, a refund promise or an ungrounded claim can only run on the finished text, so nothing is put in front of the customer until they pass. The approved answer is released as one frame. A turn therefore goes from PENDING to its final status with no intermediate row state, and a customer who was promised a refund by a draft that was then discarded never read it.

Verdicts

Verdict Meaning
ANSWERED Text was produced and passed the filters
ESCALATED The conversation went to a person
REFUSED The provider itself declined

A crypto support agent trips cyber-category safeguards on entirely legitimate questions — "someone accessed my account", "is my seed phrase safe" — so REFUSED is a normal operating state, not an exception. It arrives as HTTP 200 with a possibly empty content array, which is why the refusal flag is checked before the text is ever read. The conversation escalates as forbidden_topic.

skipReason, decoded

Value What stopped it What to do
below_retrieval_floor Retrieval confidence was under aiSupportRetrievalFloor Write the article. It is already on Gaps.
operator_policy_undocumented:<topic> The question is about your fees, limits, timings, countries or refunds, and no article of yours covers it Write your own article on that topic
language_not_classifiable:unknown_language The question's language could not be classified and no operator source covers it See the translation stop below
daily_budget / monthly_budget A spend ceiling was reached Raise it, or wait for the window
unpriced_model A ceiling is set and something this month recorded no cost, so the ceiling cannot be enforced Price the endpoint, or set the caps to 0 — see below
no_provider No AI service is configured on this install Provider screen, then restart
turn_limit The conversation used up aiSupportMaxTurnsBeforeEscalation attempts Nothing, unless it is frequent
translation_unavailable Multilingual answering is on and the translation failed See below
nothing_to_answer The last customer message was empty Nothing
barge_in A human took the conversation over mid-generation Nothing
not_owner:<detail> The assistant did not hold the conversation Read the detail
rule One of your escalation rules matched with the action Refuse Agents → escalation rules
legal / money_issue / account_locked A built-in trigger matched the customer's message and refused the turn Nothing — these are by design

A refusing rule writes the bare value rule. The rule's name is not recorded on the turn, so do not go looking for it on the row — which of your rules matched has to be worked out from their patterns on the Agents screen.

not_owner carries the state on the row precisely because the bare value is the least useful reason in the addon — the three causes need completely different responses:

Detail Meaning
not_owner:busy A generation was already running. Harmless — the message is held and answered when it finishes.
not_owner:human A person has taken it over. Correct.
not_owner:<STATE>:<reason> Parked in a state nothing reclaims — a person has to answer or hand it back.

The third is the one to watch. Which escalation reasons the assistant may take back is in Escalation and handover.

unpriced_model — a ceiling that cannot be measured is refused, not assumed safe

"Cost unknown" and "cost nothing" are not the same number, and the caps used to treat them as one. The compatible-endpoint adapter reports no cost by design — it cannot know whether the endpoint behind it is a free local model, a metered API or a flat subscription — and any model missing from the price table reports the same. Both land on the turn row as 0, so an install answering thousands of tickets through a metered endpoint read $0.00 spent for ever and its daily ceiling never fired.

So the gate now counts them. A turn is unpriceable when it moved tokens and recorded no cost; tokens are the discriminator, because a skipped turn, a refusal before the call and a direct answer from one of your own articles all legitimately cost nothing and all move nothing. If either cap is above zero and the current month holds even one unpriceable turn, the gate refuses:

  • The turn records SKIPPED / unpriced_model and the session moves to AI_SUSPENDED.
  • The whole desk is notified — "AI support paused: spend cannot be measured" — with the number of unpriced calls and both ways out, because "raise the cap in Settings" is useless advice for this one.

Two ways out, and both are decisions only you can make:

  1. Price the endpoint. Set AI_SUPPORT_OPENAI_INPUT_USD_PER_MTOK and AI_SUPPORT_OPENAI_OUTPUT_USD_PER_MTOK to what your endpoint charges, in USD per million tokens, and restart. Both are required — half a rate card is treated as none.
  2. Set both caps to 0. Zero has always meant "no ceiling", and it is the honest answer for a local model that really is free. An install with no cap never reaches this gate.

The two budget reasons are lifted automatically: the next customer message on a session suspended for daily_budget or monthly_budget resumes it once the cap allows again. unpriced_model is not in that set, so a conversation parked by it stays AI_SUSPENDED and every later message records SKIPPED / not_owner.

After you price the endpoint or clear the caps, open the affected conversations in the Live Inbox and press Release. New conversations are unaffected, which is what makes this easy to miss.

Error codes

Two codes read identically as prose — it did not answer — and are completely different problems.

Code Where it comes from What it is
NO_BACKEND The managed gateway Capacity at the gateway. Not your account, not your key, not your balance.
INSUFFICIENT_FUNDS The gateway, HTTP 402 The gateway wallet is empty. Top it up.
PROVIDER_NOT_CONFIGURED This install A missing environment variable here. Fix .env and restart.
PROVIDER_REQUEST_FAILED Any provider A transport or upstream error with no more specific cause — DNS, refused connection, TLS, an unparseable response, or a gateway job that timed out at 504
UNEXPECTED Anywhere Something that was not a provider error at all

The difference in behaviour matters as much as the label:

PROVIDER_NOT_CONFIGURED Everything else
Turn status SKIPPED FAILED
skipReason no_provider
Escalates? No Yes, as provider_error

A missing key is a configuration state, not an incident, so it does not fill your queue with handovers. A gateway failure is an incident, and the customer must not be left watching a typing indicator, so it goes to a person.

ProviderRequestError.code is a hardcoded literal — PROVIDER_REQUEST_FAILED — so every gateway failure recorded that one value on the turn and printed that one word in the log, whether it was a capacity problem, an exhausted balance or a bad request.

The precise cause did arrive: the gateway adapter passes the upstream error.code into the error's third constructor argument. Nothing read it.

It is now preferred over the generic literal, which is what turns PROVIDER_REQUEST_FAILED into NO_BACKEND or INSUFFICIENT_FUNDS on the turn record and in the log — three states an operator has to act on completely differently. On a NO_BACKEND the log line also spells out that this is capacity at the gateway rather than your account or your key.

If you are looking at a PROVIDER_REQUEST_FAILED from before that change, the real cause is in errorMessage on the same row.

The operator-policy gate

The retrieval floor catches "we have nothing on this subject". It does not catch "we have plenty on this subject, none of it written by this operator" — measured at confidence 0.80 for "what are your withdrawal fees", answered confidently from a chain-specific documentation page belonging to somebody else's install.

So five topics can only ever be answered from an article you wrote:

fees · limits · processing_time · countries · refunds

The test at that gate is the source, not the score. A high-confidence retrieval from the shipped documentation fails it, deliberately, and records operator_policy_undocumented:<topic>.

The topic patterns are English. A Spanish fee question that was never rendered into English is not recognised as a fee question, the gate does not fire, and the assistant states a fee it has no business stating — fluently, in Spanish, in your voice.

That is the worst failure this system can produce and it fails silently. Hence the two defences below.

The customer does not get an eleven-word brush-off. On a policy refusal they are told why there is no single answer and where the real one lives, with no number in it: "fees depend on the currency and the network you're using", "you'll see the exact fee on the Withdraw page before you confirm it". The page is only offered when the route catalogue says it exists on this install.

The translation stop

Answer customers who write in a language your documentation is not in.

Settings → Retrieval. With it off, behaviour is byte-identical to an install that never had the feature — and the language check still runs, because turning translation off does not stop non-English customers writing in. A non-English question is marked as such and the policy gate refuses anything it cannot classify, which is where language_not_classifiable:unknown_language comes from.

With it on, a non-English question is rendered into English on the cheap model before retrieval, because the corpus is English and BM25 matches words: a Spanish question shares almost no tokens with any passage, falls under the retrieval floor, and escalates for a question the documentation answers perfectly.

When that translation fails, the turn stops. skipReason: translation_unavailable, verdict ESCALATED, reason no_documentation.

It does not degrade and carry on with the original text, and the reason is the gate above: the same English rendering is what lets the policy gate read the question at all. Continuing would mean answering a question no gate in the system was able to read.

Once a translation succeeds, the strict posture is dropped — with a proper English rendering, "no policy topic" is a real answer rather than an absence of one. Leaving it on would escalate every ordinary non-English question, "where is my deposit" included, on any install without operator articles. That is not a safety gate, it is an outage.

citationMode — and why groundedness reads differently per provider

Value Meaning
NATIVE The provider returned structured citation spans
MARKER They were recovered from [n] markers the model typed, intersected against the documents actually sent
(null) No citations were produced at all

An OpenAI-compatible provider has no native citations. Documents are numbered in the prompt, the model is required to mark each factual sentence, and the markers are then extracted and range-checked — a model that types [9] when six documents were sent has invented the citation, and clamping it to [6] would attach a real URL to a claim that document does not support. The turn records MARKER, and the customer sees a heading rather than a verbatim quote, because showing a fake verbatim quote would be worse than showing none.

This changes how groundedness reads. The score is per-sentence coverage: what share of factual sentences a citation actually reaches. Under NATIVE that is span overlap; under MARKER it is whether the sentence contains a marker; with neither, it falls back to a ratio, which is all that can be said.

The managed gateway declares that it supports citations and then picks an inference backend per job — two of which cannot produce native citations. So the field describes an intention and citationMode describes what actually happened.

When documents were sent, no citations came back and no [n] markers were found either, groundedness cannot be measured — so the floor is not applied to that answer. The URL intersection and the refund tripwire still are, and those are the two checks that actually protect you from a costly sentence.

Before that fallback existed, this install discarded 100% of generated answers and recorded ungrounded on every one — blaming the model for an infrastructure fault and pointing the operator at the wrong fix ("write more documentation"). One of the discarded answers cost $0.2839.

The log line to look for: "No citations came back from … despite sending N document(s)".

aiSupportGroundednessFloor defaults to 0.4, and that number was re-calibrated when the metric changed meaning. Measured after the change: a well-sourced answer about enabling two-factor authentication came back with four native citations and scored 0.43, because a support answer is mostly procedural prose — "open the app", "scan the code" — and only some of those sentences sit inside a cited span. At the old floor of 0.6 it was withheld and the customer got a handover instead of the answer, at a cost of $0.25.

An answer that asserts facts with nothing behind it scores 0 and is refused at any floor above zero.

The effort parameter and the small fast model

aiSupportEffort says how much deliberation a question deserves. It reaches the managed connection on every call; on your own key it is omitted for any model that rejects it — the cheapest tier does, with a 400, and so does any model the pricing table does not recognise. Omitting it costs default behaviour; sending it to a model that refuses it costs the whole turn, so the adapter drops it rather than gambling.

That distinction matters most on the internal models. aiSupportRerankModel and aiSupportClassifierModel both default to the small fast tier (mashdiv-lite, against mashdiv-core for answering), and they run on reranking, triage and the handover brief — three calls per conversation that nobody reads directly and that would otherwise dominate the bill.

Two related facts from the same adapter, in case you are debugging a 400 yourself:

  • No temperature, top_p or top_k is sent, anywhere. The current top-tier models reject all three. Tone is a prompt concern.
  • Citations and structured output cannot be combined — that pairing is a 400. Which is why every machine-readable decision the agent makes comes from a tool call rather than a response format.

aiSupportMaxTokens defaults to 4,000 because extended thinking tokens count against it: a smaller ceiling can be consumed entirely by thinking and return empty content. That is a default, not a floor — the field accepts 1,024 to 64,000, on the settings screen and in the backend coercer alike, so nothing stops you saving a value low enough to have answers cut off mid-sentence and thrown away unsent. If you lower it, lower it a little and watch. (The own-key adapter's hard minimum is 1,024, so 1,024 is the real floor everywhere.)

Test connection

Runs the whole path a customer's question takes and reports each step.

It is not a string check and not a ping. It runs the whole path a customer's question takes, and ok is false unless the generation step succeeded — because that is the step that corresponds to a customer actually being answered. A green tick over a mute assistant is a state this endpoint used to report as success, and a buyer whose first impression is that does not file a bug.

What it actually reports

There is no three-valued verdict. Each step comes back as a tick or a cross with a sentence of detail, and the screen draws them as a list under the summary line. The steps are not the same on every provider:

Provider Steps
Managed connection Gateway credentialsAccountAnswering
Your own key API keyAnswering
Any compatible endpoint EndpointAnswering

Only the managed connection has a middle step, because it is the only one with something to ask: /v1/account returns your plan, balance and remaining spend cap, and the detail line prints all three. That is the check that separates the gateway wallet is empty from the credential is wrong — on the other two providers both arrive as a failed Answering step and you read the detail to tell them apart.

What the details actually say, so you know what you are looking at:

Step result The detail you get
Credentials failed The variable that is not set, named: "ANTHROPIC_API_KEY is not set in the backend .env", or the endpoint URL for the compatible provider
Credentials passed The first few characters of the loaded key, or the endpoint URL. Never the credential itself
Account failed The gateway's own message — unreachable, or the credentials refused
Account passed "Plan … Balance $… Spend cap remaining $…"
Answering failed The upstream message verbatim: a rejected key, an exhausted balance, a refused connection, a timeout, or "The model refused a trivial request"
Answering passed Which model answered and how long it took, plus the cost when the provider reports one

A failed step short-circuits the ones after it, so the last line in the list is the one to read.

The backend reads .env at boot. Editing the file and pressing Test without restarting reports the old state, which is indistinguishable from a bad key — and this is the screen an operator opens precisely because the assistant is not answering, so it is the mistake most likely to be made.

Two more things about it worth knowing:

  • It spends money, and the spend is recorded. It performs a real generation, returns costUsd, and files a row against the same ceilings a customer's answer counts against — including when the probe fails, because a failing probe still ran the generation that billed and is the one an operator presses again immediately. It is refused with a 429 when a cap is already reached: there is no point learning the provider works if the budget will not let it answer anybody.
  • Read the step, not the summary. The steps that pass before the failing one are exactly why a single "Connected" was so misleading.

Related: Escalation and handover for what each escalationReason means, Troubleshooting for the symptom-shaped version of this page, Provider for the .env variables, and Cost and budgets for what a turn costs.