Skip to content

Latest commit

History

History
230 lines (191 loc) · 12.2 KB

File metadata and controls

230 lines (191 loc) · 12.2 KB

Error codes

The canonical ApiErrorCode enum is the source of truth for every non-2xx response the gateway returns. This page maps each code to:

  • the SDK exception the Python SDK raises when it sees that error
  • the HTTP status code the gateway returns
  • when it happens

For the three-layer error model (structured exceptions → on_error hook → format_user_message / @guarded) and the boundary between developer-facing and end-user-facing wording, see Concepts → Error handling.

Gateway error codes (error field on every non-2xx response)

The canonical catalog lives in the gateway. The error slug is the stable, machine-readable identifier; message is human-safe; code is a legacy SCREAMING_SNAKE_CASE alias kept for backward compatibility.

error slugHTTPWhenSDK exception
bad_request400Generic 400 — invalid input that isn't a validation failureNullRunConfigError (or NullRunError)
unauthorized401Missing or invalid X-API-Key / expired session / HMAC mismatchNullRunAuthenticationError (NullRunAuthError for 401 specifically)
forbidden403Authenticated but not allowed (incl. CSRF mismatch, org-mismatch on /orgs/*)NullRunAuthenticationError
not_found404Resource doesn't exist or isn't visible(no exception — caller handles)
conflict409Idempotency conflict, duplicate, "already a member", "invite already pending", "cannot demote last owner", etc.NullRunError
validation_error422Request body / params failed schema validationNullRunConfigError
plan_limit_exceeded422Generic plan cap hit (workflows, seats, api_keys). Body details.resource carries which dimension.NullRunBlockedException
workflow_limit_reached422Workflow-specific active-workflow cap hitNullRunBlockedException
rate_limit_exceeded429Per-minute / per-day rate cap. Body carries retry_after (seconds).RateLimitError (carries .retry_after, .upgrade_url)
internal_error500Server-side bugNullRunBackendError (retryable)
not_implemented501Feature not yet implementedNullRunError
(also internal_error)503ApiError::ServiceUnavailable — transient downstream failure on an enforcement path. Carries retry_after.NullRunBackendError (retryable)

Phase 0.5:trial_limit_exceeded was removed — NullRun has no trial state. Lite plan is permanently free with hard limits.

Plan limit slugs (api_keys / seats / policies / executions) all surface as plan_limit_exceeded with details.resource set to the dimension name ("api_keys", "seats", "workflows", …). There is no separate slug per dimension — read details.resource.

SDK exception hierarchy (Python)

Every public SDK exception inherits from NullRunError and carries four structured fields: error_code (machine-readable, e.g. "BUDGET_HARD_BLOCKED"), user_action (imperative hint), retryable (bool), docs_url.

BreakerError (Exception)
├── NullRunError (structured base — every field above)
│ ├── NullRunDecision (marker — expected policy outcomes)
│ │ ├── NullRunBlockedException (policy / budget / loop / sensitive block)
│ │ │ ├── NullRunBudgetError (budget exhausted — BUDGET_HARD_BLOCKED)
│ │ │ └── NullRunToolBlockedError (tool in block list — TOOL_BLOCKED)
│ │ └── WorkflowPausedException (paused via control plane)
│ ├── NullRunInfrastructureError (marker — system failures)
│ │ ├── NullRunConfigError (misconfiguration, e.g. missing api_key)
│ │ ├── NullRunAuthenticationError (401 / 403)
│ │ │ └── NullRunAuthError (401 specifically)
│ │ └── NullRunTransportError (transport failures)
│ │ ├── NullRunBackendError (5xx — retryable, REDIS_UNAVAILABLE)
│ │ └── RateLimitError (429 — carries .retry_after, .upgrade_url)
└── BreakerTransportError
└── InsecureTransportError (HTTP used where HTTPS required)
BaseException
└── WorkflowKilledException (parent)
└── WorkflowKilledInterrupt (kill via control plane — BaseException,
not Exception; per the kill contract)

NullRunDecision and NullRunInfrastructureError are marker classes, not exception classes themselves. They exist so host code can except NullRunDecision to catch every expected policy outcome (budget, tool block, pause) and except NullRunInfrastructureError to catch every system failure (transport, backend 5xx, auth rejection, config error) — see Decision vs. infrastructure below for the recommended handling pattern.

NullRunBlockedException carries .workflow_id, .reason, .action ("block" / "kill" / "pause"), .tool_name (when the block is tool-scoped), and .details (free-form). There is no.message attribute — use str(exc).

Removed in SDK 0.4.0: CostLimitExceeded, ApprovalRequired, BreakerTimeout, LoopDetectedException, RetryStormException, RateLimitExceededException (no remaining callers).

Catch WorkflowKilledInterruptexplicitly and before any except Exception — it does not subclass Exception.

The default path: zero lines of error handling

For the common "run an agent and print a friendly message on failure" case, the three public helpers do the work — no try/except NullRunError required.

importnullrunfromnullrunimportinit_or_die, guarded, protect, shutdowninit_or_die(api_key="nr_live_...") # exits cleanly if api_key missing@guarded# catches NullRunError,@protect# prints catalog wording,defmy_agent(prompt): # sys.exit(1)returncall_llm(prompt)
if__name__=="__main__":
try:
print(my_agent("hello"))
finally:
shutdown()
HelperCatchesFor
init_or_die(api_key=...)NullRunError raised by init() (typically a config / auth family code)Startup; one-shot script entry point
@guardedAny NullRunError raised inside the wrapped functionStandard agent loop
with nullrun.handle():Any NullRunError raised inside the blockRegion of code (e.g. a graph invoke)

All three propagate WorkflowKilledInterrupt (BaseException) unchanged and let non-NullRunError exceptions surface as honest tracebacks. For the full design rationale and the boundary between "what NullRun tells the developer" and "what the developer tells their end users", see Concepts → Error handling.

Decision vs. infrastructure

The public exception hierarchy splits NullRunError into two marker subclasses by what kind of event the exception represents. The split is additive — every existing except NullRunError: and except NullRunBlockedException: clause keeps matching. New code can use the marker classes to write a two-branch handler that captures the right behaviour for each category.

MarkerWhat it coversWhy it matters
NullRunDecisionExpected policy outcomes — budget cap, tool block, loop detection, workflow pause, per-workflow rate limitThe enforcement layer is doing its job. UX explains the decision and (where applicable) offers an upgrade or alternative action.
NullRunInfrastructureErrorSystem failures — network unreachable, gateway 5xx, auth rejection, config errorThe SDK could not reach or query the policy engine. UX is a generic "service unavailable"; operators triage via error_code, retryable, and for transport errors, source / endpoint.

Recommended handler shape

importnullrunfromnullrunimport (
NullRunDecision,
NullRunInfrastructureError,
)
try:
result=agent.run(message)
exceptNullRunDecisionasd:
# Expected — surface to the user, log to product analytics,# tag the conversation with d.error_code for cohort analysis.returnd.user_message() ifhasattr(d, "user_message") elsestr(d)
exceptNullRunInfrastructureErrorase:
# System failure — alert ops, retry with backoff, do NOT# surface internal text to the end user. The catalog has a# generic message for every infrastructure error code.sentry.capture_exception(e)
returnnullrun.format_user_message(e)
exceptWorkflowKilledInterrupt:
# Operator kill (BaseException, not Exception) — re-raise# unless you are the top of the agent loop.raise

Mapping decision subclasses to HTTP

When you build a server-framework integration (FastAPI, aiohttp, Telegram bot, Slack handler), map each category to the right HTTP status:

CategoryHTTP statusNotes
NullRunDecision — budget exhausted (BUDGET_HARD_BLOCKED)429 (or backend 402)Honour .retry_after from the RateLimitError if set; budget-exhausted NullRunBudgetError exposes the same field via .details.retry_after
NullRunDecision — tool blocked (TOOL_BLOCKED)403User did nothing wrong, but the action is forbidden
NullRunDecision — workflow paused503Set Retry-After from .resume_after
NullRunDecision — consume overbudget (CONSUME_OVERBUDGET)422Subclass NullRunConsumeOverbudgetError; actual cost > reservation + ε
NullRunDecision — chain error (CHAIN_ORG_MISMATCH / CHAIN_MAX_DURATION_EXCEEDED)409 / 402Subclass NullRunChainError; chain_id mismatch / expired
NullRunDecision — workflow inactive (WORKFLOW_INACTIVE)403Subclass NullRunWorkflowInactiveError; workflow paused or killed in cross-org scenario
NullRunInfrastructureError — rate-limit Redis (RATE_LIMIT_REDIS_UNAVAILABLE)503NullRunRateLimitRedisError. Fail-CLOSED — do not retry blindly
NullRunInfrastructureError — protocol too old (PROTOCOL_TOO_OLD)400NullRunProtocolError. Carries .min_required_version; SDK must be upgraded past the protocol's minimum supported version
NullRunInfrastructureError (any other subclass)503The failure is on our side, not the user's
WorkflowKilledInterrupt503Special ASGI middleware required — see Use with FastAPI

Every NullRunDecision subclass carries .status_code (the wire HTTP status the backend returned). The FastAPI integration maps this field to the response status automatically; in custom integrations read exc.status_code rather than hard-coding the default above.

The NullRun SDK ships a reference FastAPI integration that applies this mapping for you — see Use with FastAPI for a one-line setup.

HTTP status summary

StatusMeaningSDK action
200OK
400Bad requestInspect message, fix request
401Bad API key / HMACRefresh key / check NULLRUN_SECRET_KEY
403ForbiddenCheck role / scope
404Not foundCaller handles (workflow/policy may have been deleted)
409ConflictInspect message (already-member, invite-already-pending, etc.)
422Validation / plan limitInspect details (for plan limits, details.resource + details.current + details.limit)
429Rate limitHonour Retry-After; check upgrade_url
5xxGateway errorRetry with backoff; sensitive tools fail-closed

When the gateway is unreachable, the SDK raises NullRunTransportError with source set to one of NETWORK_ERROR, GATEWAY_ERROR, BREAKER_OPEN, AUTH_ERROR. See ADR-008 for the full rationale.

See also