Error recovery
Every Pipeworx tool error is structured for self-correcting retries. Agents that read _meta recover from most failures without escalation.
What errors look like
{
"result": {
"content": [{ "type": "text", "text": "{\"error\":\"tool_error\",\"message\":\"Missing required parameter: country_code (string)\"}" }],
"_meta": {
"alternatives": [
{ "tool": "list_countries", "reason": "Find a valid country_code first" }
],
"examples": [
{ "country_code": "US" },
{ "country_code": "GB" }
],
"retry_hint": "Retry the same tool with arguments matching one of the example shapes.",
"feedback_hint": "If this looks like a bug in is_today_holiday, file pipeworx_feedback({type:\"bug\", ...}).",
"tier": "anonymous",
"cost": { "total": 1, "components": [...] }
}
}
}
The standard agent recovery loop:
- Parse the
textcontent as JSON; if it haserror, treat as failure. - Read
_meta.examples— pattern-match the failed args against a working example. Most of the time this is enough. - If you still don’t know how to recover, look at
_meta.alternatives— these are tools that solve a related need. - Use
_meta.retry_hintas natural-language guidance to the model. - If the failure looks like a Pipeworx-side issue, the
feedback_hintgives you a pre-formattedpipeworx_feedbackcall.
Error classes
Pipeworx classifies errors at the gateway in _meta:
| Class | Meaning | Should agent retry? |
|---|---|---|
error | Real tool failure | Yes — read _meta.examples and try again with corrected args |
auth_required | Missing API key for a BYO-key tool | No — surface to user; pass the key argument named in the error’s message (see below) |
user_error | Invalid args (missing required, wrong types) | Yes — read _meta.examples |
upstream_throttled | Upstream rate-limited (429) or blocked (403) | Wait + retry, or use _meta.alternatives |
| unknown tool | The tool name doesn’t exist (-32602, Unknown tool: <name>) | Not with the same name — see If a tool name doesn’t work |
The class isn’t directly exposed in _meta (it’s used for analytics), but you can usually infer it from the error message.
Two exceptions worth knowing, because both will otherwise cost you a retry loop:
Unknown toolcarries noretry_hintand noalternatives. Every other error does. The name is what failed, so re-sending it with different arguments cannot help — calldiscover_tools({task})to find the real name instead.- On
auth_required, trust themessage, not thesignup_hint. The key argument is per-tool:attom_avmreads_apiKey,altos_market_statsreads_altosKey. The genericsignup_hintcurrently says_apikey(lowercase) for every tool, and since JSON arguments are case-sensitive, passing that name gets your key silently dropped and the same “API key required” error back. Verified live 2026-08-10; tracked as a fix, but read themessageuntil it lands.
Pattern: self-correcting on schema mismatch
The most common error is “wrong arg shape.” _meta.examples is the same examples returned in tools/list — repeated here so the model has them in immediate context.
// First attempt
ct_get_study({})
// → error: "invalid_arguments"
// → message: "ct_get_study received an invalid argument: nct_id is required
// but was not provided. Required: nct_id. Retry with all required
// arguments — see _meta.examples for a valid shape."
// → _meta.examples: [{ "nct_id": "NCT04280705" }]
// Retry with corrected shape
ct_get_study({ nct_id: "NCT04280705" })
// → success
Pattern: zero results is not an error
The failure that costs you most isn’t the one that throws. A search tool that finds
nothing usually succeeds — you get a well-formed response with an empty list,
no error field, and nothing for the recovery loop above to catch:
fda_drug_events({ query: "asparafalcomine" }) // not a real drug
// → { "total": 0, "skip": 0, "limit": 0, "results": [] }
// → HTTP 200. No error. No retry_hint. No alternatives.
Nothing is broken here — the query ran and the answer is genuinely “no records.”
But if your code only branches on error, this sails through as a result, and a
model that treats an empty list as “no adverse events reported” has just told
someone a drug is clean when the truth is that the drug name was misspelled.
So check for emptiness explicitly, and treat it as a question about your input rather than an answer about the world:
const r = fda_drug_events({ query: term });
if (!r.results?.length) {
// Is the TERM wrong, or is the record genuinely absent? Resolve it first.
const resolved = rxnorm_search({ name: term });
// If the name resolves, the empty result is real — say "no records found".
// If it doesn't resolve either, the term is wrong — say that instead.
}
The distinction matters because the two cases warrant opposite answers, and only one of them is a fact about the drug.
Pattern: feedback loop on unfixable errors
If you exhaust retries and alternatives, file:
pipeworx_feedback({
type: "data_gap",
message: "ct_get_study returned 'Cannot read properties' for valid-looking NCT IDs (NCT09999999); seems to crash on missing studies instead of returning a not-found error.",
context: { tool: "ct_get_study", pack: "clinicaltrials" }
})
The Pipeworx team reviews feedback daily. Recurring patterns become bug fixes.
What you should NOT do
- Don’t retry the exact same call without reading
_meta.examples. If the call failed once with these args, it’ll fail again. - Don’t silently swallow errors. Surface them to the user with the tool name + the actual error message. Pipeworx errors are informative; hiding them makes debugging harder.
- Don’t loop infinitely. Hard-cap retries at 3 per tool per task. After that, fall back to a different tool or escalate.