Skip to content

Errors Reference

SDK methods reject with ConfigureError for API, validation, permission, tool, timeout, and network failures.

All docs pages, indexed for agents: https://docs.configure.dev/llms.txt (add .md to any page URL for plain markdown).

ts
import { ConfigureError, ErrorCode } from "configure";

try {
  await profile.executeTool(toolCall);
} catch (error) {
  if (error instanceof ConfigureError) {
    console.error(error.code, error.statusCode, error.type, error.requestId);
  }
}

Error Codes

ErrorCode exports the values below. Each heading is the verbatim string on error.code.

Recovery in one line each, so a branch can be written from this table alone:

error.codeRecover withRetry?
API_KEY_MISSINGnpx configure setup, or pass apiKey to the constructorNo
AUTH_REQUIREDconfigure.auth.signInUrl() and send the user through it; discard the dead tokenNo
INVALID_INPUTFix the field named by error.paramNo
TOOL_NOT_CONNECTEDprofile.connect({ app }) and show connect_url verbatimOnly after the user connects
NETWORK_ERRORRetry only if the call was idempotent; branch on the code, retryable is undefinedIdempotent calls only
RATE_LIMITEDWait error.retryAfter seconds, then retryYes, when retryable is true
NOT_FOUNDFix the id. Also check statusCode === 404, which catches structured 404s tooNo
SERVER_ERRORRetry with backoff; report error.requestId if it persistsYes
TIMEOUTRetry only if the call was idempotent; branch on the code, retryable is undefinedIdempotent calls only
ACCESS_DENIEDprofile.tools({ connectors, actions }) with the options you intend to executeAfter enabling
TOOL_ERRORShow error.message; retry once when error.retryable === trueConditionally
PAYMENT_REQUIREDSend the user to their plan. Backoff does not helpNo
COMMIT_REQUIREDprofile.commit({ messages }), then repeat the blocked callOnly after committing

API_KEY_MISSING

Cause: no API key was provided and CONFIGURE_API_KEY was not set.

Fix: pass apiKey when constructing the client, or set CONFIGURE_API_KEY. npx configure setup writes it to .env alongside CONFIGURE_PUBLISHABLE_KEY and CONFIGURE_AGENT.

AUTH_REQUIRED

Cause: token is missing, invalid, expired, or rejected by the API.

Fix: send the user through the hosted flow and stop reusing the token. Retrying with the same token fails identically.

ts
if (error.code === "AUTH_REQUIRED") {
  return reply(`Sign in again: ${configure.auth.signInUrl()}`);
}

For message agents that need sender binding, createMessageSignInUrl() takes reason: "signin" | "reconnect" | "permissions". Over MCP, configure_connect returns the same link and needs no authorization. See Auth Flows.

INVALID_INPUT

Cause: SDK-side or API-side input validation failed. Cases that arrive as INVALID_INPUT:

  • Missing linked token or app-local externalId.
  • Invalid or reserved agent handle.
  • Malformed forget id (anything other than mem_ plus 32 hex characters). An unknown id or another agent's memory does not throw at all; forget() resolves with { deleted: false, message } because deletion is own-memories only. Check response.deleted, not just the catch block.
  • Structured backend 404s (resource_missing, agent_not_found): INVALID_INPUT with statusCode 404, not NOT_FOUND. Branch on statusCode === 404 to catch both structured and legacy 404s.

Fix: correct the input; when set, error.param names the offending field.

TOOL_NOT_CONNECTED

Cause: a connector-backed tool requires a user connection that is not available (missing connector connection). A dead grant and an account that was never connected both land here.

Fix: mint a link and show it verbatim. Never construct one yourself.

ts
const { connect_url } = await profile.connect({ app: "gmail" });
return reply(`Connect Gmail here: ${connect_url}`);

Do not retry until the user says they finished. error.suggestedAction is "connect_tool" when the app was never connected and "reconnect" when the grant died, which is the difference between rendering "Connect Gmail" and "Reconnect Gmail". See Connector repair.

NETWORK_ERROR

Cause: fetch failed before an API response was received.

Fix: retry with backoff ONLY when the call was idempotent. A read is safe to repeat. A write is not: the SDK cannot tell whether the server applied it before the connection died, and there is no idempotency key, so a blind retry can save or send twice. retryable is undefined here by design; branch on error.code.

RATE_LIMITED

Cause: the API returned a rate limit response. OTP throttling (otp_blocked) also surfaces as RATE_LIMITED, but with retryable: false.

Fix: wait error.retryAfter seconds (set by the backend on rate limits), then retry. When retryAfter is undefined, back off on your own schedule.

NOT_FOUND

Cause: the requested resource was not found. Reachable only from legacy unstructured responses; structured backend 404s arrive as INVALID_INPUT with statusCode 404.

Fix: correct the id, and branch on error.statusCode === 404 rather than on the code, so both shapes are caught.

SERVER_ERROR

Cause: the backend returned a 5xx or structured API error. It is also the fallback for any backend error type the SDK does not recognize.

Fix: retry with backoff. Report error.requestId if it persists.

TIMEOUT

Cause: the request timed out (the fetch was aborted).

Fix: retry with backoff ONLY when the call was idempotent. A read is safe to repeat. A write is not: the SDK cannot tell whether the server applied it before the connection died, and there is no idempotency key, so a blind retry can save or send twice. retryable is undefined here by design; branch on error.code.

ACCESS_DENIED

Cause: the current profile object is not allowed to access the requested resource or tool. Any tool name not in the enabled set, unknown or not, rejects with this code: executeTool() checks the enabled set first, so the INVALID_INPUT unknown-tool branch at dispatch is practically unreachable.

Fix: call profile.tools() with the same connector and action options you intend to execute. See Tool Rejection Behavior.

TOOL_ERROR

Cause: a connector or provider operation failed. This is the provider's own failure, not a connection problem.

Fix: surface error.message to the user. Retry once when error.retryable === true; the backend marks transient tool failures retryable.

PAYMENT_REQUIRED

Cause: billing or quota limits blocked the request (quota_exceeded, HTTP 402). Backoff never clears it.

Fix: send the user to their plan. The REST envelope carries error.upgrade_url on this code; the SDK does not expose it on ConfigureError, so read it off the response body if you need the exact link.

COMMIT_REQUIRED

Cause: a profile read needs a follow-up commit.

Fix: commit first, then repeat the call that was blocked.

ts
await profile.commit({
  messages: [
    { role: "user", content: userText },
    { role: "assistant", content: replyText },
  ],
});
await profile.read();   // now succeeds

Commit needs at least one of messages, memories, or toolResults with real content, or it throws INVALID_INPUT before any request goes out. On the hosted MCP surface the model calls configure_profile_commit instead. Repeating the blocked call without committing fails the same way. The model-facing commit tool is not in the default tool set; it exists only for MCP and adapter runtimes via tools({ advanced: { commit: true } }).

Structured Fields

ConfigureError exposes fields directly:

  • code
  • statusCode
  • type
  • param
  • retryable
  • suggestedAction
  • docUrl
  • retryAfter
  • requestId

There is no status or details property on ConfigureError.

retryable is set only from structured backend responses ({ error: { type, code, retryable, ... } }). The backend marks the plain rate-limit code, transient tool failures, and 5xx-class errors as retryable: true. OTP throttling (otp_blocked) also surfaces as RATE_LIMITED, but with retryable: false.

Three cases leave it undefined rather than false:

  • TIMEOUT and NETWORK_ERROR, which the SDK builds locally from a caught exception and never decorates with backend details.
  • Any response whose body is a flat error string instead of the structured object. That path maps by HTTP status alone, so retryable, retryAfter, and requestId are all absent even on a 429 or a 5xx.
  • Anything passed through classifyError(), which rebuilds the error with a friendly message and keeps only code and statusCode.

So a retry loop gated on error.retryable === true never retries a local timeout or a dropped connection. Gate on the code for those, and on retryable for server-sourced failures:

ts
const RETRY_BY_CODE = new Set(["TIMEOUT", "NETWORK_ERROR"]);

const willRetry = RETRY_BY_CODE.has(error.code) || error.retryable === true;
const waitMs = (error.retryAfter ?? 0) * 1000 || backoff(attempt);

retryAfter is a number of seconds to wait before retrying. The backend sets it on rate limits; otherwise it is undefined.

suggestedAction is a closed machine-actionable enum, not free text: reauthenticate, fix_request, connect_tool, reconnect, retry, upgrade_plan, specify_agent, use_secret_key, check_permissions, use_hosted_auth, complete_approval, or commit_profile. Use it as the programmatic hook for recovery branches.

connect_tool and reconnect both arrive as TOOL_NOT_CONNECTED. The first means the user never connected the app; the second means a classified dead grant. Both are fixed by one trip through profile.connect({ app }), but only the second should say "Reconnect".

Tool Rejection Behavior

Call profile.tools() with the same connector and action options you intend to execute. tools() is synchronous and returns ConfigureToolDefinition[]; do not await it. tools() records the enabled names; profile.executeTool(toolCall: ConfigureToolCall): Promise<unknown> rejects any name outside that recorded set with ACCESS_DENIED. Before any tools() call the set holds the six default tools (read, search, remember, forget, import, connect). toolCall accepts { name, arguments }, { name, input }, or { function: { name, arguments } }; arguments can be a JSON string.

ts
profile.tools({ connectors: ["gmail"] });

await profile.executeTool({
  name: "configure_email_send",
  arguments: {},
}); // throws ConfigureError with code ACCESS_DENIED

MCP (JSON-RPC) errors

On the hosted MCP surface (https://mcp.configure.dev), tool failures arrive as JSON-RPC error objects, not ConfigureError. The machine-readable part rides in error.data: data.code names the failure, and when a minted link can fix it, data.suggested_action is an object carrying that link.

  • -32009 (commit_required): a profile read or search created a commit obligation that has not been cleared. Recover by calling configure_profile_commit with the bounded messages or tool results from this turn, then retry the blocked call. Never configure_connect (that is for sign-in), and never a bare retry.
  • -32010 (mcp_commit_plumbing_required): the transport never declared commit support. Recover by sending X-Configure-Session-Id, X-Configure-Runtime-Scope-Id, and X-Configure-MCP-Commit-Supported: 1 on every request; the ids are yours to choose and only have to be stable. Full contract: message agents.
  • -32001: no user was resolved for the call. Recover by sending Authorization: Bearer <token> for a linked user or X-User-Id for an unlinked one. configure_connect is the one tool that answers without either.
  • -32000: everything else, with data.code naming it. Refused connector calls land here (data.code is, for example, tool_not_connected or permission_needed). Recover by replying with data.connect_url verbatim; data.next_tool_call names the call that mints a fresh one (configure_connect, carrying app when a single app applies). Do not retry until the user has acted on it.
ts
const data = rpcError.data ?? {};
if (data.connect_url) return reply(`Connect it here: ${data.connect_url}`);

Personalization infrastructure for agents