Guide users building apps, scripts, CI pipelines, or automations on top of the Cursor SDK - TypeScript (`@cursor/sdk`) or Python (`cursor-sdk` / `cursor_sdk`). Use when the user mentions integrating, installing, or writing code against the Cursor SDK; says `Agent.create`, `Agent.prompt`, `Agent.resume`, `agent.send`, `run.stream`, `run.messages`, `CursorAgentError`, `@cursor/sdk`, `cursor-sdk`, or `cursor_sdk`; asks to run Cursor agents programmatically from a script, CI/CD pipeline, GitHub Action, backend service, or other code outside the Cursor IDE; wants to pick between local and cloud runtime, configure MCP servers for an SDK agent, or handle streaming, cancellation, or errors; or is wiring Cursor into an automation, bot, or REST `/v1/agents` migration. Use eagerly rather than answering from memory; the SDK surface evolves and this skill is the source of truth for the external packages.
The Cursor SDK runs Cursor agents programmatically. Two language variants share the same concepts:
@cursor/sdk, npm) - docs at cursor.com/docs/sdk/typescriptcursor-sdk, pip) - docs at cursor.com/docs/sdk/pythonBoth are in public beta and follow the same Agent → Run model across local (runs on the caller's machine against cwd) and cloud (runs on a Cursor-hosted VM against a cloned repo) runtimes.
Use this skill to help someone bootstrap a working integration quickly and avoid the traps that bite new users. The canonical docs cover the full reference; this skill adds decision-making, failure-mode prevention, and ready-to-extend patterns on top.
Decide before writing any code. The wrong choice means rewriting the whole integration. Order:
@cursor/sdk, cursor-sdk, cursor_sdk, npm install, pip install, import { Agent } from "@cursor/sdk", from cursor_sdk import - go with what they named.pyproject.toml / requirements.txt / uv.lock / .py files → Python. package.json / tsconfig.json / .ts files → TypeScript. If the integration will live in a polyglot repo, ask which subdirectory it goes into.The rest of this skill shows TypeScript and Python side by side. Concepts are identical; syntax differs - camelCase vs snake_case, async-by-default vs sync-default-with-async-mirror, await using vs with.
This skill helps the user build with the SDK. It is not the place to validate, congratulate, or sell the SDK as a choice. The user's intent is the input; your job is execution.
@cursor/sdk, cursor-sdk, Agent.create, Agent.prompt, etc.): assume they know what the SDK is and have decided to use it. Skip framing, skip pep talk, go straight to producing the integration. No "good news", no "the SDK is perfect for this", no "this is almost exactly the pattern X is designed for".Avoid these specific openers (and their close cousins):
Prefer:
Almost every SDK integration collapses to one of these three shapes. Pick the one that fits the job, don't mix them.
Agent.prompt(...) - one-shotUse for fire-and-forget scripts, GitHub Actions steps, or any "send this prompt, get a result, exit" flow. No streaming, no follow-ups, no cleanup to remember. If you're reaching for this and then immediately resuming, you wanted pattern 2 instead.
TypeScript:
import { Agent } from "@cursor/sdk";
const result = await Agent.prompt("Refactor src/utils.ts for readability", {
apiKey: process.env.CURSOR_API_KEY!,
model: { id: "composer-2.5" },
local: { cwd: process.cwd() },
});
console.log(result.status, result.result);Python:
import os
from cursor_sdk import Agent, AgentOptions, LocalAgentOptions
result = Agent.prompt(
"Refactor src/utils.py for readability",
AgentOptions(
api_key=os.environ["CURSOR_API_KEY"],
model="composer-2.5",
local=LocalAgentOptions(cwd=os.getcwd()),
),
)
print(result.status, result.result)Agent.create(...) + agent.send(...) - durable with follow-upsUse when you need streaming, multi-turn conversation, or lifecycle operations (cancel, status listener). This is the shape of most non-trivial integrations.
TypeScript:
import { Agent } from "@cursor/sdk";
await using agent = await Agent.create({
apiKey: process.env.CURSOR_API_KEY!,
model: { id: "composer-2.5" },
local: { cwd: process.cwd() },
});
const run = await agent.send("Find the bug in src/auth.ts");
for await (const event of run.stream()) {
if (event.type === "assistant") {
for (const block of event.message.content) {
if (block.type === "text") process.stdout.write(block.text);
}
}
}
await run.wait();
// Follow-up keeps full conversation context.
const run2 = await agent.send("Now write a regression test for it");
await run2.wait();Python:
import os
from cursor_sdk import Agent, LocalAgentOptions
with Agent.create(
model="composer-2.5",
api_key=os.environ["CURSOR_API_KEY"],
local=LocalAgentOptions(cwd=os.getcwd()),
) as agent:
run = agent.send("Find the bug in src/auth.py")
for message in run.messages():
if message.type == "assistant":
for block in message.message.content:
if block.type == "text":
print(block.text, end="")
run.wait()
# Follow-up keeps full conversation context.
run2 = agent.send("Now write a regression test for it")
run2.wait()The Python SDK is sync by default. For servers, bots, and concurrent orchestration, use AsyncClient.launch_bridge(...) as an async context manager and AsyncAgent. There's no global async default client - instantiate one per event loop, and never mix sync and async clients in the same code path.
Agent.resume(...) - pick up an existing agent laterUse across process boundaries: a cron that continues last night's cleanup, a webhook that extends a user's agent, an interactive CLI that reloads conversation state. Runtime is auto-detected from the ID prefix - bc- is cloud, anything else is local.
TypeScript:
await using agent = await Agent.resume(previousAgentId, {
apiKey: process.env.CURSOR_API_KEY!,
});
const run = await agent.send("Also update the changelog");
await run.wait();Python:
import os
from cursor_sdk import Agent, AgentOptions
with Agent.resume(
previous_agent_id,
AgentOptions(api_key=os.environ["CURSOR_API_KEY"]),
) as agent:
run = agent.send("Also update the changelog")
run.wait()Inline MCP servers are not persisted across resume - they often carry secrets and live in memory only. Pass them again on the resume call in either language.
These trip up almost every new integration. They're all easy to prevent once you know about them.
AgentOptions doesn't require local or cloud; the SDK selects local when neither is set. The trap: if you intended a cloud agent and forgot the cloud field, you get a local agent silently - no error, just a local agent ID and a local executor.
cloud: { repos: [...] } for cloud, local: { cwd } for local - even though local is the default.cloud=CloudAgentOptions(repos=[...]) or local=LocalAgentOptions(cwd=...).Always set one of the two explicitly. The cost is one line; the cost of not noticing for an hour is much more.
A thrown CursorAgentError means the run never executed (auth, config, network). A returned result.status == "error" means the run did execute and failed. Different fixes, different exit codes, different observability.
TypeScript:
import { Agent, CursorAgentError } from "@cursor/sdk";
try {
const run = await agent.send(prompt);
const result = await run.wait();
if (result.status === "error") {
// Run started but failed mid-flight. Inspect transcript, git state, tool outputs.
console.error("run failed: " + result.id);
process.exit(2);
}
} catch (err) {
if (err instanceof CursorAgentError) {
// Didn't start. Auth, config, network. Fix environment, retry.
console.error("startup failed: " + err.message + ", retryable=" + err.isRetryable);
process.exit(1);
}
throw err;
}Python:
import sys
from cursor_sdk import CursorAgentError
try:
run = agent.send(prompt)
result = run.wait()
if result.status == "error":
# Run started but failed mid-flight. Inspect transcript, git state, tool outputs.
print("run failed: " + result.id, file=sys.stderr)
sys.exit(2)
except CursorAgentError as err:
# Didn't start. Auth, config, network. Fix environment, retry.
print(
"startup failed: " + err.message + ", retryable=" + str(err.is_retryable),
file=sys.stderr,
)
sys.exit(1)The SDK holds handles to local executors, persisted run stores, and HTTP clients. Skipping disposal leaks child processes, open databases, and (in long-running services) memory.
await using agent = await Agent.create(...) is the cleanest path. Otherwise wrap in try/finally with await agent[Symbol.asyncDispose](). Agent.prompt(...) disposes for you.with Agent.create(...) as agent: is the cleanest path. Otherwise call agent.close() in a finally. Long-running processes that rely on the module-level default client should call close_default_client() at shutdown.async with await AsyncClient.launch_bridge(...) as client: then async with await client.agents.create(...) as agent:. Agent.prompt(...) disposes for you.wait() is almost always requiredThe stream is how you observe; wait() is how you get the terminal result. You can skip streaming, but skipping wait() means you can't tell whether the run finished, errored, or was cancelled, and you'll leak the run's internal watchers. Always call wait(). If you don't want live output, call wait() alone.
run.stream() is an async iterable of SDKMessage. await run.wait() returns RunResult.run.messages() yields typed SDK messages; run.events() yields lower-level envelopes. run.wait() returns RunResult. Async equivalents are async for ... and await run.wait(). Convenience helpers: run.text() blocks on wait() and returns the final assistant text; run.iter_text() streams text-only chunks. run.stream() is an alias for run.messages().run operation is supported on every runtimeRun exposes a handful of operations - stream/messages, wait, cancel, conversation - and detached or rehydrated runs (handles you got from Agent.getRun(...) after the live event store closed) may not support all of them. Guard with supports(...):
if (run.supports("cancel")) await run.cancel();
if (run.supports("conversation")) console.log(await run.conversation());if run.supports("cancel"):
run.cancel()
if run.supports("conversation"):
print(run.conversation())run.unsupportedReason(op) (TypeScript) / run.unsupported_reason(op) (Python) tells you why.
cwd, reuses their environment and credentials. Good for dev loops and CI that already has a repo checkout.autoCreatePR: true in TypeScript; auto_create_pr=True in Python).export CURSOR_API_KEY="cursor_..." # user API key or team service-account keyBoth SDKs read CURSOR_API_KEY when no key is passed explicitly. User keys live at Cursor Dashboard → Integrations; team service-account keys live in Team Settings → Service accounts. Team Admin API keys are not yet supported.
If you're seeing 401s, the usual suspects are: key pasted with surrounding whitespace, key minted against a different environment, or the key belongs to a user without repo access for a cloud run.
Don't hardcode unusual model IDs without confirming the caller has access - model lists evolve.
TypeScript:
import { Cursor } from "@cursor/sdk";
const models = await Cursor.models.list({ apiKey: process.env.CURSOR_API_KEY! });Python:
from cursor_sdk import Cursor
models = Cursor.models.list() # falls back to CURSOR_API_KEYcomposer-2.5 is the current default for most integrations. { id: "auto" } (TS) / model="auto" (Python) lets the server pick. Cursor.models.list() returns valid IDs, per-model parameter definitions (reasoning effort, max mode), and preset variants for the calling account.
Model is required for local in both SDKs. For cloud, TypeScript falls back to a server-resolved default when omitted; Python documents it as required across both runtimes. Pass one regardless to keep behavior predictable.
Both SDKs use the same conceptual model: HTTP transport (with static headers or OAuth auth) or stdio (with command / args / env). Pass servers inline on Agent.create or agent.send for the most common case.
headers and auth are handled by Cursor's backend and redacted before the VM sees them. Stdio env values are passed into the VM - treat them like any runtime secret.For full schema and authentication options, see the SDK docs for each language and Cursor MCP.
Apply these to any integration that runs unattended:
await using (TypeScript) or with ... as agent: (Python) is the cleanest path. Non-negotiable.CursorAgentError, exit code 2 for result.status == "error", exit code 0 only for finished.run.id and agent.agentId / agent.agent_id immediately after send() before streaming. If the stream hangs, the IDs are what you need to investigate in the dashboard or via Agent.getRun(...) / Agent.get_run(...).error.isRetryable / err.is_retryable. It's the backend telling you the specific failure is safe to retry. Python errors also expose retry_after (HTTP-style seconds value or date) - honor it before falling back to exponential backoff. Blind retries can cause duplicate cloud runs.local.settingSources: []; the default in Python is no local.setting_sources. Both mean "inline config only". Opting into "all" loads project/user/team/MDM settings from the caller's environment, which is rarely what you want from a service. Setting sources have no effect on cloud agents - cloud always honors team/project/plugins.skipReviewerRequest: true (TypeScript) / skip_reviewer_request=True (Python) unless a human should be paged - it suppresses the reviewer-request step and keeps PR notifications quiet.apiKey / api_key explicitly in shared-infrastructure code instead of relying on the env var. Makes the credential dependency obvious and prevents cross-tenant mistakes.Agent.prompt(...) for true one-shots - it disposes for you and is harder to leak.You can inspect any agent or run by ID later. Cloud IDs that start with bc- auto-route to the cloud API; anything else is local.
TypeScript:
const info = await Agent.get("bc-abc123", { apiKey });
const run = await Agent.getRun(runId, { runtime: "cloud", agentId: "bc-abc123", apiKey });
// Local: you need the cwd where the agent was created.
const localList = await Agent.list({ runtime: "local", cwd: process.cwd() });Python:
from cursor_sdk import CursorClient
with CursorClient.launch_bridge(workspace=".") as client:
info = client.agents.get("bc-abc123")
run = client.agents.get_run(run_id)
# Local: filter by cwd.
local_list = client.agents.list(runtime="local", cwd=".")A cloud bc--prefixed agent ID is not a run ID. If you only have a run ID (from a log or a webhook), pass it to getRun / get_run with the runtime hint; don't confuse the two.
If the user's integration monitors, lists, or visualizes agents - dashboards of active runs, conversation replays, tool-call timelines - offer a Cursor Canvas to render it. If they accept, defer entirely to the canvas skill.
/v1/agents/*). Use it from languages without a first-party SDK, or when you want a minimal HTTP surface..cursor/hooks.json hooks. Both SDKs respect them; neither manages them. See Hooks.