Development

Error Objects for Model-Facing Tools: Fields and Tests

Antonio Evans9 min read

A JSON error object split into labeled bands: code, message, retryable, hint, field.
Key points

Key points

  1. 1A model-facing tool error needs five fields: a stable code, a one-sentence message, a retryable boolean, a hint naming the next action, and the field that failed.
  2. 2HTTP status codes and RFC 9457 problem details were built for human developers and deterministic clients, not for a model that must choose its next tool call.
  3. 3You test the schema by injecting each error into a live tool call and checking whether the model's next call changes in the way the field intends.
  4. 4You stop adding fields when a new field no longer changes that next call in tests; extra detail is token cost without behavior.

Why the HTTP error body is not enough for a model

A human developer who gets a 400 with the message "validation failed" can open the docs, inspect the payload and work out what to fix. A model cannot do that reliably. It needs the response to say exactly what failed, why, and what to do next, or it will repeat the same bad call or give up Zuplo's guide to agent-callable APIs. That gap is the whole reason a separate error contract exists for tools.

The standard HTTP error format is a good starting point, not the finish line. RFC 9457, the successor to RFC 7807, defines fields like type, title, status, detail and instance for machine-readable problem details Zuplo's RFC 9457 walkthrough. Those fields describe the failure. They do not tell the model what to try next, and they do not carry a retry decision the model can read directly.

Cloudflare makes the same point from the serving side. It now returns structured error payloads to agents with fields like error_code, retryable, retry_after and owner_action_required, and reports that structured errors cut token usage by more than 98% versus HTML error pages on a live rate-limit response Cloudflare's RFC 9457 agent error pages. Two lessons follow. Errors written for browsers waste tokens, and the fields that matter to an agent are the classification and the recovery path.

A tool error is a different artifact from an HTTP error. The HTTP status tells the transport layer what happened. The tool error tells the model what to do about it. Keep both, but do not assume one does the other's job.

The five-field schema

Here is the shape to aim for. Every field earns its place by changing what the model does next.

{
  "code": "missing_required_field",
  "message": "The request has no value for the field 'start_date'.",
  "retryable": false,
  "hint": "Call again with start_date in ISO 8601 date format, for example 2024-03-01.",
  "field": "start_date"
}

Each field has a specific job:

FieldTypeWhat the model uses it forExample
codestring, stableBranching and matching against known failuresmissing_required_field
messagestring, one sentenceUnderstanding what went wrongThe request has no value for start_date
retryablebooleanDeciding whether to call again at allfalse
hintstring, one actionChoosing the next callCall again with start_date in ISO 8601
fieldstring or pointerLocating the failed inputstart_date

The code must be stable across deploys so the model can match it against instructions and so your tests can assert on it. Speakeasy notes that application error codes follow a predictable, string-based convention and let a client branch programmatically, as in if (error.code === 'trip_too_short') Speakeasy's API error best practices. Treat the code as an API contract: renaming it is a breaking change.

The message is a single sentence for the model and any human reading logs. Keep it specific: "The email field must use [email protected] format" beats "invalid input". Zuplo's RFC 9457 example uses exactly this kind of detail in the detail field Zuplo's RFC 9457 walkthrough.

The retryable boolean is the decision the model is worst at making on its own. HTTP gives you a rough map: 408, 429, 500, 502, 503 and 504 are usually transient, while 400, 401, 403, 404 and 422 need a changed request Temporal's retry cookbook. A trained model will often retry a 500 and sometimes retry a 422, so do not make it infer this from the status code. State it.

The hint names the next action. Not a paragraph of advice, one instruction: "Call again with start_date in ISO 8601 date format" or "Do not retry; ask the user to supply an API key." Nordic APIs makes the case that agents need an explicit recovery path because guessing leads to hallucinated next steps Nordic APIs on error messages for AI agents.

The field points at the failed input, ideally as a JSON Pointer like #/start_date. RFC 9457's extension pattern already uses pointers for validation errors Zuplo's RFC 9457 walkthrough. If the failure is not tied to a field, set it to null rather than omitting the key, so the model never has to handle two shapes.

A worked example makes the cost concrete. Suppose a model calls a flight-search tool with {"origin": "LHR"} and leaves out destination. An HTTP-only response of 400 Bad Request with body "validation failed" gives the model nothing, and it typically retries the same call or stalls. The five-field error above, with field: "destination" and a hint of "Call again with destination as an IATA airport code," gives it a specific correction. Across many errors in one workflow, that difference compounds in both token spend and task completion.

Reading the error is a model decision, not a client decision

Traditional clients handle errors with code paths you write. Models handle them with reasoning over the text you hand back. That asymmetry drives two design choices.

First, return the structured error as the tool result attached to the tool call id, not as an exception that aborts the run. n8n's guide to tool-call error handling makes this explicit: format the application error into a clean structured string, pass it back as the tool result, and associate it with the original call so the model can reason about it n8n's tool calling error handling guide. If you throw an exception, the model never sees the fields.

Second, decide which layer owns recovery. Transient transport and service errors belong to the orchestration layer, which retries silently with exponential backoff and jitter. Input validation errors belong to the model, because only the model can repair the payload n8n's tool calling error handling guide. The retryable flag encodes that split at the boundary. When it is false and the field is set, the model should fix the call. When it is true and the field is null, the model should wait or the orchestrator should retry.

This is also why the field must be visible to the model at all. A team building domain purchasing into an agent found that when the SDK's error message was hidden and only HTTP Error 422 surfaced, the agent could not self-correct; once the full body was visible, it fixed the request Pol Alvarez Vecino on HTTP errors in AI agents. Validation tightening the tool signature fixed about half of the remaining tool-call errors in another write-up, and the rest were bugs in the signature itself David Rodenas on tool call validation.

The core error objects were meant to be consistent across a whole API, not improvised per endpoint. Treblle's error handling guide stresses standardized error formats and a unified JSON structure for exactly this reason Treblle on REST API error handling. Assemble the five fields once, in one middleware, and reuse them.

Testing that the model acts on each field

The schema is only real if the model uses it. The test that proves this is a next-call assertion: inject a specific error, let the model respond, and check the shape of its next tool call. This is a behavioral test, not a schema-validity test.

The harness has four parts:

  1. A fake tool that returns a fixed error object instead of a result.
  2. A model with the tool's JSON Schema and your instructions.
  3. A capture of the model's next tool call: function name, arguments, and any text.
  4. An assertion on that call.

A minimal Python sketch, with the fake tool returning a scripted error:

def run_error_scenario(error, user_turn, max_turns=3):
    messages = [{"role": "user", "content": user_turn}]
    calls = []
    for _ in range(max_turns):
        reply = model(messages=messages, tools=[search_tool_schema])
        if not reply.tool_calls:
            break
        call = reply.tool_calls[0]
        calls.append({"name": call.name, "args": call.arguments})
        messages.append(reply)
        messages.append({"role": "tool",
                         "tool_call_id": call.id,
                         "content": json.dumps(error)})
    return calls

Then assert per field:

def test_field_is_used():
    err = {"code": "missing_required_field",
           "message": "The request has no value for 'destination'.",
           "retryable": False,
           "hint": "Call again with destination as an IATA airport code.",
           "field": "destination"}
    calls = run_error_scenario(err, "Find flights from LHR on March 1")
    assert calls[1]["args"].get("destination")  # model fixed the missing field

def test_retryable_false_stops_retry():
    err = {"code": "permission_denied",
           "message": "The API key lacks booking scope.",
           "retryable": False,
           "hint": "Do not retry; report the missing scope to the user.",
           "field": None}
    calls = run_error_scenario(err, "Book the cheapest flight")
    assert len(calls) == 1  # no second identical call

def test_retryable_true_triggers_wait():
    err = {"code": "rate_limited",
           "message": "Rate limit exceeded.",
           "retryable": True,
           "hint": "Wait 30 seconds, then call again.",
           "field": None}
    calls = run_error_scenario(err, "Find flights from LHR on March 1")
    assert any(c["name"] == "search_flights" for c in calls[1:])

Run each test against several models you ship, because a smaller model may rely on a field a larger one infers. The point is not a benchmark score. It is a boolean per field: does this field change the next call or not.

A checklist for the suite:

FieldInjected errorAssertionFails when
codeknown code plus a conflicting messagemodel branches on code, not messagemodel keys off prose only
messagevague vs specific textmodel explains the failure correctlymessage unused or misleading
retryabletrue vs false, same coderetry happens or is suppressedmodel ignores flag
hinttwo different hints for one codenext call matches the hinthint ignored
fieldcorrect vs wrong pointermodel edits the right argumentmodel edits wrong input

For code, the strongest test sends two codes with deliberately swapped messages and confirms the model's behavior follows the code. If it chases the message, the code is decoration. Speakeasy's example shows the intended contract: a client reacts to error.code, and the message is for the reader Speakeasy's API error best practices.

For hint, send the same code with two different hints ("retry with backoff" versus "ask the user for input") and confirm the next call differs. A hint that produces the same call in both cases is not earning its tokens.

For retryable, assert the count of repeated calls and check the wait behavior. Temporal's recipe is worth copying here: it maps status classes to retryable or not, honors a Retry-After header when present, and lets an explicit header override the status code Temporal's retry cookbook. Your test suite should prove your flag wins the same way.

Rate-limit errors deserve their own case. A well-formed 429 carries a Retry-After header, so a test can assert that the model waits the stated interval instead of hammering the boundary Zuplo on HTTP 429.

A stop rule for fields, with numbers

Every field you add costs tokens on every error and adds a thing the model can get wrong. The stop rule is simple and testable: stop when a new field does not change the model's next tool call in your tests.

Apply it in order:

  1. Write the candidate field and the injection that would exercise it.
  2. Run the next-call assertion against the models you ship.
  3. If the next call is identical with and without the field, delete it.
  4. If the call changes only on one model, keep it and note the model dependence.

Cloudflare's production schema is a useful reference for what survives this filter. Its agent error payload carries error_code, error_name, error_category, retryable, retry_after and owner_action_required, and Cloudflare describes the schema as stable by design so agents can implement durable control flow Cloudflare's RFC 9457 agent error pages. Those fields map onto classification, recovery timing and escalation, which are exactly the decisions an agent has to make.

A rough token budget sharpens the rule. Error text enters the context window on every failure, and Cloudflare measured over 98% savings by replacing HTML with structured fields on a rate-limit error Cloudflare's RFC 9457 agent error pages. If a field you cannot justify takes even ten lines of JSON on an error the model hits several times in a run, you have paid for it repeatedly. The test suite is the only thing that tells you whether you bought behavior or noise.

Two fields are tempting and usually fail the filter. A docs_url rarely changes the next call; the model does not browse mid-turn. A second human_message duplicates the message and invites the model to produce prose instead of a corrected tool call. Keep them out unless a test shows otherwise. Links to documentation are valuable to humans, which is why Speakeasy recommends them, but human-facing fields and model-facing fields are separate budgets Speakeasy's API error best practices.

There is a policy layer beneath all of this. Postman's survey found that 89% of developers use AI in daily work but only 24% design APIs with agents in mind, per Zuplo's summary of the readiness gap Zuplo on agent-callable APIs. Error design for models is part of closing that gap, and it is a contract you have to enforce in OpenAPI, not just in prose. SmartBear's walkthrough shows how to define a reusable error response schema under components and reuse it per status code so clients and agents see one typed model SmartBear's OpenAPI error model tutorial.

When your tools span several model providers, keep the error schema identical across them and test each provider against it. Quanta Inference exposes text, image, video, audio and embedding models through one OpenAI-compatible API key, which keeps the tool loop and the error contract in one place (https://quantaintelligence.ai/inference).

FAQ

FAQ

What is the minimum set of fields a model-facing tool error needs?

Five: a stable code, a one-sentence message, a retryable boolean, a hint naming the next action, and the field that failed. Each one answers a question the model asks: what happened, how bad, should I try again, what should I change, and where is the problem. Drop any of them and the model has to guess.

Why not just use RFC 9457 problem details?

RFC 9457 is a good base for machine-readable errors, and its extension members allow validation arrays and pointers Zuplo's RFC 9457 walkthrough. It does not, however, define a retryable flag or a recovery hint, and those are the two fields that most often change a model's next call. Use the problem details shape and add the agent fields as extensions.

How do I test that the model actually reads a field?

Inject a scripted error into a fake tool, let the model respond, and assert on its next tool call. For the retryable flag, assert the number of repeated calls; for the field pointer, assert that the model edits the right argument. The test is behavioral, so it must run against the models you actually ship.

Should transient errors reach the model at all?

Usually no for transport and provider failures. The orchestration layer should retry those silently with exponential backoff and jitter, and only hand the model errors it can repair n8n's tool calling error handling guide. Validation and logic errors should reach the model, because the fix is in the call the model constructs.

When should I stop adding error fields?

When a candidate field produces no change in the model's next call across your test suite. Keep the field that changes behavior, delete the one that does not, and note any model-specific dependence. Fields like documentation links usually fail that test and add token cost on every failure.

Sources

Sources

  1. How to Design APIs That AI Agents Can Actually Use
  2. Best Practices for API Error Handling - Zuplo
  3. Slashing agent token costs by 98% with RFC 9457 ...
  4. Errors Best Practices in REST API Design - Speakeasy
  5. Retry policy from HTTP responses | AI Cookbook
  6. Designing API Error Messages for AI Agents
  7. LLM Tool Calling Error Handling: Retries and Fallbacks
  8. Handling HTTP Errors in AI Agents: Lessons from the Field
  9. Validation Fixed Half of My Tool Call Errors
  10. How to Handle and Return Errors in a REST API - Treblle
  11. HTTP 429 Too Many Requests: Causes, Headers & Retry ...
  12. Designing error models in OpenAPI for agent-safe APIs
About the author

About the author

Antonio Evans
Antonio Evans

Antonio Evans is the founder of Quanta Intelligence and a fractional CTO who builds HIPAA-ready AI systems for health and life-science companies. Before that he was technical co-founder of FriendsEAT for twenty years, CEO of Socialdraft, and a data engineer at IBM. Antonio lives with his dog Santino in Lisbon, Portugal.

All articles