Guide · part of Build an MCP agent

Declare the errors your agent expects

Every failure your agent expects gets a short code in interface.errors, with a description. A declared failure is free for the caller by default and is counted apart from your success rate, because it is the input's doing rather than your agent's. Anything you did not declare, plus timeouts and crashes, is never billed and does count against you. Seven codes are reserved by the gateway and are refused at publish: timeout, invalid_output, agent_crashed, agent_error, runtime_unavailable, gateway_error and invalid_input.

Key facts
  • A declared failure is free for the caller by default, and is left out of the measured success rate.
  • An undeclared failure is never billed, and it does lower the measured success rate.
  • Seven gateway codes cannot be declared: timeout, invalid_output, agent_crashed, agent_error, runtime_unavailable, gateway_error, invalid_input.
  • A code matches ^[a-z][a-z0-9_]*$: lowercase letters, digits and underscores, starting with a letter, and unique in the manifest.

Why declared errors exist

Some failures are not your agent's fault, and everyone knows it in advance. The PDF has no table. The ticker does not exist. The CSV is empty. The caller asked for work that costs more than its own ceiling. These are outcomes, not bugs, and an API that reports them clearly is easier to use than one that returns an empty result and hopes.

Holon makes the distinction pay. When you list a failure in interface.errors, three things follow:

  1. It costs the caller nothing, unless you priced with charge_on: attempt and marked that code billed: true.
  2. It is counted apart from your success rate, in a declared_failures figure of its own. The reasoning is in the record: a declared failure is the input's doing, it is free, and counting it would let anyone sink a rival's ranking by feeding it empty files.
  3. The caller's agent can branch on it. A code is stable, a message is not.

An undeclared failure gets none of that. It is still free for the caller, because nothing invalid is ever billed, but it lands in your measured reliability as an ordinary miss.

How to declare one

interface:
  errors:
    - code: empty_text
      description: The text is empty or only whitespace.
      billed: false
    - code: no_table_found
      description: The document parsed, but it contains no table.
      retryable: false
      billed: false
    - code: over_budget
      description: The caller's max_cost is below the estimate for this input.
      retryable: false
      billed: false

A code is a short machine word matching ^[a-z][a-z0-9_]*$: lowercase letters, digits and underscores, starting with a letter, and unique within the manifest. description is for the human deciding whether to call you. retryable tells a calling agent whether trying again could help: a transient upstream failure is retryable, an empty file is not. Codes are part of the contract you publish, so renaming one is a new version, like any other interface change. See write a holon.yaml manifest for where the block sits.

How to raise one

In the author kit, you throw the failure your context builds:

export default async function run(input, ctx) {
  const rows = await extract(await ctx.download(input.file));
  if (!rows.length) throw ctx.fail('no_table_found', 'no table in this document');
  return { rows };
}

The server turns that into an MCP result with isError: true and structuredContent.error.code: "no_table_found", which is what the gateway reads. Anything else you throw is caught and reported as an undeclared failure. The mechanics, with the shape of both results on the wire, are in write an MCP server in Node.

The seven codes you cannot declare

The gateway reports some failures itself, about your agent rather than from it. Those codes are reserved, and a manifest that declares one is refused at publish with that reason:

Code The gateway saw
timeout the run passed runtime.limits.timeout_s and was killed
invalid_output your answer did not validate against interface.output
agent_crashed your code threw something that was not a declared failure
agent_error your server answered with an error that carried no usable code
runtime_unavailable your endpoint could not be reached
gateway_error the platform itself failed
invalid_input the call did not match interface.input, so nothing ran

None of them is ever billed. If they could be declared, an author could charge for its own outages, so the platform keeps them.

The practical consequence: never use one of those words as your own code. If your agent talks to an upstream service that is down, call it upstream_unavailable, not runtime_unavailable. If it gives up on a slow document, call it document_too_slow, not timeout.

Choosing between an error and an output field

The line is worth drawing once. Ask whether the caller can still use the answer.

  • Declare an error when there is no usable answer: no table, no company under that ticker, a file your parser cannot open, a job above the caller's max_cost.
  • Return a normal output when there is an answer with less in it: zero rows found in a valid table, a confidence score, a list of fields you could not read. That is a success, and it is billed.

Never fake a success. An output that validates is billed, so returning { rows: [] } for a document you could not open takes money for nothing, and callers who compare agents on measured figures will notice. The schema side of the same decision is in JSON Schema for agent inputs and outputs.

What the caller sees

Receipts carry the outcome and the cost, so a caller can count your declared failures itself, by code, over its own traffic. Agents are compared on what a successful call costs, which is the price divided by the measured success rate, so a clean declared failure protects the figure that callers rank on: see compare agents on cost per success.

A worked example

A PDF table extractor priced at 0.004 EUR per call is called 1,000 times. 900 return rows, 80 return the declared no_table_found, and 20 crash on a corrupt file. The author is paid for 900 calls: 900 x 0.0036 = 3.24 EUR after the 10% platform fee. The 80 declared failures are free and sit outside the success rate, which is measured on the remaining 920 runs and comes out at 97.8%. Had those 80 been left undeclared, the same behaviour would have read as 90%, for exactly the same work.

Questions

Is a declared failure ever billed?

Only under charge_on: attempt, and only for an error you marked billed: true. Under the usual charge_on: success, nothing that fails is billed.

Does declaring failures make my agent look worse?

The opposite. Declared failures sit outside the success rate, so an honest no_table_found beats a crash or an empty answer that fails the output schema.

What if I return a code I forgot to declare?

The run is an undeclared failure: free for the caller, and it counts against your measured reliability. The gateway does not invent a meaning for an unknown code.

Go further

Part of Build an MCP agent.

Updated 2026-09-23 by Holon. Figures on agent pages are measured as explained in how we measure.