Skip to content
HomeConsoleGet started

Every runstate error code with its HTTP status, SDK exception class and typical cause, plus exactly what the SDKs retry automatically and what your code must handle.

Every error from the API uses one envelope:

{
"error": {
"code": "INSUFFICIENT_BUDGET",
"message": "reservation exceeds available budget",
"requestId": "…"
}
}

Codes and their HTTP statuses are a versioned contract. Branch on code (and the SDK exception class), never on message. Include requestId when you report a problem.

The SDK classes are the same in TypeScript and Python. Both SDKs are tested against one shared mapping fixture.

Code HTTP SDK class Typical cause What to do
VALIDATION_FAILED 400 RunstateError Malformed body or parameters, missing Idempotency-Key on the raw API, payload over the size limit, settle larger than the reservation. Fix the request.
UNAUTHENTICATED 401 AuthenticationError Missing or invalid API key. Check RUNSTATE_API_KEY.
FORBIDDEN 403 AuthenticationError The key lacks the permission for this call, or isn’t allowed for this space. See key permissions.
ENTITLEMENT_EXCEEDED 403 AuthenticationError A plan ceiling was hit, such as active runs, active tasks or keys, or a space limit set above the plan. Finish or cancel work, or change plan.
SPACE_SUSPENDED 403 AuthenticationError The space was suspended by an owner. New work is refused; releasing, renewing, completing and retrying work already held still works. Resume the space in the console.
NOT_FOUND 404 NotFoundError Unknown id or name, or an id from another organization. Check the id; create the resource with ensure first.
CONFLICT 409 ConflictError Generic state conflict, such as cancelling a finished task or run, accepting a member that hasn’t succeeded, or ensure with different settings. Read the current state and decide.
CLAIM_HELD 409 ConflictError Another holder owns the key. Wait (wait: true), use tryAcquire(), or skip.
IDEMPOTENCY_CONFLICT 409 ConflictError Same task key or workKey with a different input, same usage key with a different amount, or an Idempotency-Key reused with a different request. Use a new key for different work.
SCOPE_CANCELLED 409 ScopeCancelledError The run (or an ancestor) was cancelled or completed. Stop taking work for this run.
DEADLINE_EXCEEDED 409 ConflictError The run’s deadline has passed. Stop taking work for this run.
STALE_CLAIM 409 LeaseLostError The lease behind this token was lost; someone else may own the work now. Stop; don’t retry with the old token.
CAPACITY_UNAVAILABLE 409 ConflictError The concurrency pool has no free units. Wait (wait: true, the pool default) or back off.
BACKLOG_FULL 409 ConflictError The space’s queued-message limit is reached. Consume faster or raise maxBacklog, see Limits.
INSUFFICIENT_BUDGET 409 ConflictError The budget can’t cover the reservation. Don’t start the work.
BARRIER_RELEASED 409 ConflictError Arrived at a barrier that already released. Treat as done.
BARRIER_CANCELLED 409 ConflictError The barrier was cancelled. Stop waiting.
LEASE_RENEWALS_EXHAUSTED 409 LeaseLostError The server refused further renewals of this lease. Stop; the lease is gone.
RATE_LIMITED 429 RateLimitError The space’s requestsPerMinute limit was exceeded. Back off and retry, or raise the space limit.
SERVICE_RATE_LIMITED 429 RateLimitError Your organization used its plan’s requests for this minute. Renew, release, retry, complete, cancel, detach and ack calls have a reserved share, so recovery keeps working. Back off until the next minute.
ALLOWANCE_EXHAUSTED 429 RateLimitError The shared quota’s window is used up. take() waits for you; tryTake() reports exhausted.
ALLOWANCE_COOLDOWN 429 RateLimitError A cooldown is active on the shared quota. Same as above.
CONCURRENCY_LIMITED 429 RateLimitError Your organization is at its plan’s “agents working at once” limit. Claims and pools wait through it; elsewhere, back off.
OPERATIONS_EXHAUSTED 429 RateLimitError The free plan’s monthly operations are used up. New work is refused until next month; running work can finish. Wait for the next month or change plan.
UNAVAILABLE 503 UnavailableError The service or the network failed, including after the SDK’s transport retries. Retry later.

Two classes never come from the server:

  • ConfigError (status 0): the client was constructed without an API key or space id.
  • WaitTimeoutError (status 0, code WAIT_TIMEOUT): a client-side wait ran out, in result(), wait(), acquire({ wait: true }) or take(). The work on the server is unaffected.

CursorExpiredError exists in both SDKs for a future pagination code. The server does not emit CURSOR_EXPIRED today.

Situation Retried automatically?
Network error or per-attempt timeout Yes: retries extra attempts (default 2) with 25–100 ms jitter, reusing the same Idempotency-Key. Then UnavailableError.
Any error response from the API No. It’s thrown as a typed error immediately.
CLAIM_HELD Only inside claim.acquire({ wait: true }) / claim.run(fn, { wait: true }): every ~300 ms until timeoutMs.
CAPACITY_UNAVAILABLE Inside pool.acquire() / pool.run() while wait is true (the default): every ~300 ms until timeoutMs.
CONCURRENCY_LIMITED Inside waiting claim and pool acquisition: every ~1 s. Not for quota takes or raw calls.
ALLOWANCE_EXHAUSTED, ALLOWANCE_COOLDOWN Inside quota.take() while wait is true: a durable server-side waiter, polled every ~250 ms.
Lease renewal failure Never. The lease is marked lost and renewal stops.
RATE_LIMITED, SERVICE_RATE_LIMITED, OPERATIONS_EXHAUSTED No.
Errors inside consume() while receiving Authentication, configuration and not-found errors are rethrown; SCOPE_CANCELLED ends the loop; anything else is retried after pollMs.
import { ConflictError, LeaseLostError, RateLimitError, RunstateError, WaitTimeoutError } from 'runstate-sdk';
async function reserveForCall(amount: string) {
try {
return await run.budget('research-usd').reserve({ amount });
} catch (err) {
if (err instanceof ConflictError && err.code === 'INSUFFICIENT_BUDGET') return null;
if (err instanceof RateLimitError) {
await new Promise((resolve) => setTimeout(resolve, err.retryAfterMs ?? 1000));
return reserveForCall(amount);
}
if (err instanceof LeaseLostError || err instanceof WaitTimeoutError) throw err;
if (err instanceof RunstateError) {
console.error(`runstate ${err.code} (${err.status}), request ${err.requestId}`);
}
throw err;
}
}
  • SDK calls. Each call sends a fresh Idempotency-Key, reused only across that call’s own transport retries. If you retry a whole call yourself, it is a new request. Make it safe with the domain keys: task key, message workKey, and settle/void usageKey all deduplicate.
  • Raw HTTP. Send an Idempotency-Key on every mutation (required on most). Reusing a key with the same body within 24 hours replays the stored response; with a different body it returns IDEMPOTENCY_CONFLICT.
  • After a lost response, don’t repeat a completion. Read the state instead (ticket.status()): a repeated completion after the first one landed returns STALE_CLAIM.