Debugging Cursor SDK Error Handling and Retry Logic: Patterns and Best Practices

The Cursor SDK distinguishes between startup failures (caught as CursorAgentError with an isRetryable flag) and run-time failures (surfaced via RunResult.status === "error"), requiring different handling strategies where only startup errors support automatic retry with exponential back-off.

The Cursor SDK provides a robust TypeScript client for orchestrating AI agents, but implementing resilient integrations requires understanding its bifurcated error model. This guide examines the source code in the cursor/plugins repository to explain how to handle transient network failures, rate limits, and run-time execution errors using the CursorAgentError hierarchy and isRetryable semantics according to the official reference documentation.

Understanding the Two Failure Axes

The SDK categorizes failures across two distinct axes that determine retry eligibility. According to cursor-sdk/skills/cursor-sdk/references/error-handling.md, conflating these categories leads to infinite retry loops or missed transient failures.

Startup Errors

Startup errors occur when the agent cannot initialize due to authentication failures, configuration issues, or network problems. These throw instances of CursorAgentError or its subclasses (AuthenticationError, RateLimitError, NetworkError). Each error object carries an isRetryable boolean flag that the backend populates to indicate whether the condition is transient.

Run-time Errors

Run-time errors happen after the agent successfully starts and performs work. These do not throw exceptions. Instead, the promise returned by run.wait() resolves to a RunResult object where status === "error". The SDK documentation explicitly warns against automatically retrying these failures; instead, you should log the run ID and inspect the conversation via Agent.getRun before deciding on a human-driven retry.

Handling Startup Failures with CursorAgentError

All SDK-thrown errors inherit from the CursorAgentError base class defined in the Cursor SDK core. Sub-classes provide specific failure classification, while the isRetryable property serves as the authoritative signal for retry decisions.

The isRetryable Flag

Do not infer retryability from the error type alone. Always check the isRetryable boolean that the backend supplies. As noted in the error handling reference, even NetworkError instances may have isRetryable === false in certain authentication contexts.

Key properties available on every CursorAgentError:

  • message – Human-readable description
  • code – SDK error code string
  • protoErrorCode – Protocol-level error code
  • isRetryable – Boolean flag indicating transient status
  • cause – Optional underlying error

Exponential Back-off Implementation

For transient startup errors (isRetryable === true), implement exponential back-off with jitter and limit attempts to 3 or fewer for cloud agents. The following pattern from cursor-sdk/skills/cursor-sdk/references/error-handling.md demonstrates the recommended approach:

import { Agent, CursorAgentError } from "@cursor/sdk";

async function runWithRetry() {
  const maxAttempts = 3;
  
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    try {
      await using agent = Agent.create({
        apiKey: process.env.CURSOR_API_KEY!,
        model: { id: "composer-2" },
        cloud: { repos: [{ url: "https://github.com/your-org/repo", startingRef: "main" }] },
      });
      
      const run = await agent.send("Summarize the repository.");
      const result = await run.wait();
      return result;
      
    } catch (err) {
      if (err instanceof CursorAgentError && err.isRetryable) {
        if (attempt === maxAttempts) throw err;
        const backoff = 2 ** attempt * 1000 + Math.random() * 500;
        await new Promise(r => setTimeout(r, backoff));
      } else {
        throw err;
      }
    }
  }
}

Managing Run-time Errors and RunResult Status

Once the agent starts successfully, failures surface through the status property rather than exceptions. Your code must handle three possible statuses: "finished", "cancelled", and "error".

When result.status === "error", extract the run ID from result.id and surface it for human triage. The orchestrator implementation in orchestrate/skills/orchestrate/scripts/core/loop.ts demonstrates this pattern by writing a handoff record rather than retrying:

if (result.status === "error") {
  console.error(`[task] ${task.name} failed – run ${result.id}`);
  await writeHandoff(task, {
    failureMode: "tool-error",
    sdkError: result.error?.message ?? "unknown",
  });
}

Practical Implementation Patterns

Retry Helper for Agent Creation

Encapsulate the retry logic in a reusable helper that specifically filters for transient network conditions. This implementation from the error handling documentation targets NetworkError and RateLimitError while respecting the isRetryable flag:

import { Agent, CursorAgentError, NetworkError, RateLimitError } from "@cursor/sdk";

export async function createWithRetry(opts: Parameters<typeof Agent.create>[0]) {
  const maxAttempts = 3;
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    try {
      return await Agent.create(opts);
    } catch (e) {
      const retryable =
        e instanceof CursorAgentError &&
        e.isRetryable &&
        (e instanceof NetworkError || e instanceof RateLimitError);
      
      if (!retryable || attempt === maxAttempts) throw e;
      
      const backoff = 2 ** attempt * 1000 + Math.random() * 500;
      await new Promise(r => setTimeout(r, backoff));
    }
  }
  throw new Error("unreachable");
}

CI/CD Exit Code Conventions

The SDK defines specific exit codes to signal execution outcomes to CI/CD pipelines. As documented in cursor-sdk/skills/cursor-sdk/references/patterns.md, use these conventions:

  • 0 – Success (result.status === "finished")
  • 1 – Permanent startup failure (non-retryable CursorAgentError)
  • 2 – Run finished with status: "error"
  • 75 – Transient retryable failure (POSIX EX_TEMPFAIL)

The following GitHub Action pattern demonstrates proper exit code handling:

import { Agent, CursorAgentError } from "@cursor/sdk";

async function main() {
  await using agent = Agent.create({ /* …options… */ });
  
  try {
    const run = await agent.send("Review the PR …");
    const result = await run.wait();
    
    if (result.status !== "finished") {
      console.error(`run ended as ${result.status}`);
      process.exit(2);
    }
    console.log("✅ review complete");
    
  } catch (err) {
    if (err instanceof CursorAgentError) {
      console.error(`startup failed: ${err.message}`);
      process.exit(err.isRetryable ? 75 : 1);
    }
    throw err;
  }
}

Orchestrator Integration Patterns

For sophisticated workflows using the orchestrator skills, failure modes drive planner decisions through handoff specifications. The orchestrate/skills/orchestrate/scripts/handoffs.md file defines categories including cap-hit, oom, network-drop, tool-error, and unknown that determine whether the planner should retry, back-off, or abandon the task.

When integrating with the orchestrator loop in orchestrate/skills/orchestrate/scripts/core/loop.ts, never auto-retry run-time errors. Instead, populate the handoff structure with the failure mode and let the planner's hygiene logic (defined in orchestrate/skills/orchestrate/prompts/loop-hygiene.md) decide the next action.

Summary

  • Distinguish failure types: Catch CursorAgentError for startup issues; check RunResult.status for run-time failures.
  • Respect isRetryable: Always rely on this boolean flag rather than inferring retryability from error class names.
  • Limit startup retries: Use exponential back-off with jitter and cap attempts at 3 for cloud agents.
  • Never auto-retry run-time errors: Log the run ID and surface dashboard links for human triage when result.status === "error".
  • Use standard exit codes: Return 75 for transient retryable failures, 1 for permanent startup failures, and 2 for run-time errors to integrate cleanly with CI/CD systems.

Frequently Asked Questions

How do I differentiate between a retryable network error and a permanent authentication failure?

Both errors extend CursorAgentError, so you must inspect the isRetryable property rather than using instanceof checks alone. According to the Cursor SDK error handling reference, an AuthenticationError typically has isRetryable === false, while a NetworkError usually has isRetryable === true, though the backend ultimately controls this flag based on context.

The documentation recommends 3 or fewer attempts for cloud agents when handling transient startup failures. Implement exponential back-off with jitter (e.g., 2 ** attempt * 1000 + Math.random() * 500 milliseconds) to avoid overwhelming the API during recovery periods.

Why should I not automatically retry when RunResult.status === "error"?

Run-time errors indicate the agent performed work but failed during execution, potentially consuming tokens or modifying state. Blindly retrying could duplicate expensive operations or amplify failures. Instead, log the result.id and surface a dashboard link for human inspection via Agent.getRun before deciding whether to retry.

Which exit code should my CI pipeline use for a transient rate limit error?

Exit with code 75 (POSIX EX_TEMPFAIL) to signal that the failure is transient and the job should be retried by the CI scheduler. For permanent startup failures, use code 1; for successful runs that encountered execution errors, use code 2.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →