Pools
A pool is a named routing target that connects requests with workers. Requests wait in the pool until an available worker claims them. Create separate pools for different execution environments, such as gpu for work that needs GPUs or ios for work that needs a Mac.
See Requirements for the plan, credential, dashboard settings, and machine dependencies pools need.
Register workers to a pool
Register a worker with the Cursor CLI:
export CURSOR_API_KEY="<team service-account API key>"cd /path/to/repo # or any directory for an any-repo poolagent worker --pool my-pool startIf you supply a new name, the CLI creates the pool. If the pool name already exists, the worker joins the existing pool. If you omit the name, the worker joins the default pool.
Manage pools
Pools are durable. A pool stays registered and selectable after the last worker disconnects, so you can scale to zero and bring capacity back when requests arrive. Manage pools with the Cloud Agents API:
POST /v0/private-workers/poolsregisters a pool up front, before any worker connects.GET /v0/private-workers/poolslists pools with connected and in-use worker counts.DELETE /v0/private-workers/poolssoft-deletes a pool. It does not affect machines currently connected to the pool.
Route requests to a pool
You can select a pool in the Cursor app, on cursor.com/agents, or in the Cursor mobile app. Pool names also work in Cloud Agent integrations:
| Surface | Example |
|---|---|
| Slack | @Cursor pool=gpu fix the failing test |
| GitHub | @cursoragent pool=gpu fix the failing test |
| Linear | Include pool=gpu in the comment or issue text that triggers Cursor. |
A team admin must enable Enable Self-Hosted Pool before users can request a pool explicitly. When Require Self-Hosted Pool for All Cloud Agents is enabled, Cursor routes Cloud Agent requests to eligible pool workers unless the request targets a specific personal machine.
Pool requests match the pool name and repository. A request for pool=gpu and repo=acme/payments only routes to a connected worker with both labels. GitHub only accepts self-hosted routing requests from trusted repository collaborators.
Pools and repos
Pools come in two repo configurations. A repo-backed pool ties the pool to one repository: Cursor routes repo-scoped requests to it, and workers serve a checkout of that repo. An any-repo pool leaves source control to you.
| Repo-backed pool | Any-repo pool | |
|---|---|---|
| Source control | Cursor selects the repo for each run; workers serve existing checkouts. | You manage cloning and git state with your own image, hooks, or scripts, or pass --clone-git-repos so the worker clones on claim. |
| Routing | Requests carry a repo=<owner/repo> label and match workers serving that repo. | Requests match on the pool name alone. |
| Dashboard | Grouped under the repository. | Grouped under Any repo. |
| Good for | Pools whose workers serve one or more specific repositories. | Sandboxes and custom SCM setups. |
Repo-backed pools
Use a repo-backed pool when its workers serve specific repositories and you want requests for those repos routed automatically.
Register the pool with repo metadata
Starting a worker from a repo checkout registers the pool implicitly. Register up front when the pool should be selectable before any worker connects:
curl --request POST \ --url "https://api.cursor.com/v0/private-workers/pools" \ -u "$CURSOR_API_KEY:" \ -H "Content-Type: application/json" \ --data '{ "scope": "team", "poolName": "payments-pool", "repoOwner": "acme", "repoName": "payments", "repoUrl": "https://github.com/acme/payments" }'Clone the repository on each machine
The worker directory must be a checkout with a configured remote. The git remote provides the worker's repo=<owner/repo> label.
Start workers from the checkout
cd /path/to/paymentsagent worker --pool payments-pool startSend a request
Pick the repo and pool on cursor.com/agents, or create agents through the API with repos plus env.type: "pool" (see Create An Agent). Requests route only to workers serving that repo.
A repo-backed worker can serve more than one repository. Pass --worker-dir once per repo root. The first root is the worker's assignment identity, and every root with a git remote registers a repo=<owner/repo> routing label:
agent worker --pool app-infra \ --worker-dir "$HOME/repos/app" \ --worker-dir "$HOME/repos/infra" \ startRequests route on their selected repository and match any worker in the pool serving it.
Any-repo pools
If you prefer to manage source control yourself, create a pool without a repo attached. For these pools, you can add a .cursor/rules file in the directory where you run agent worker start. Use the rules file to teach Cursor which directories and tools are available on those machines.
Prepare a workspace directory
Point the worker at any existing directory. Your own image, hooks, or scripts handle cloning and git state:
mkdir -p "$HOME/cursor-sandboxes/default"Add a .cursor/rules file in that directory describing the directories and tools available on the machine.
Start workers with --worker-dir
An any-repo worker doesn't need a git remote:
agent worker --pool sandbox --worker-dir "$HOME/cursor-sandboxes/default" startThe worker registers without repo= routing labels.
Optionally clone on claim
Default any-repo behavior does not clone. Pass --clone-git-repos so the worker clones the claimed agent's repos on claim. This implies --mint-github-token and is limited to any-repo named pools other than default.
agent worker --pool sandbox --clone-git-repos startSend a request
Pick the pool under Any repo on cursor.com/agents, or create agents through the API with env.type: "pool" and env.name set to the pool name, and omit repos (see Create An Agent).
Add routing labels
Labels describe a worker's environment or capabilities. Add labels with repeatable CLI flags:
agent worker \ --pool gpu \ --label team=backend \ --label environment=production \ startFor managed worker images, store labels in a JSON or TOML file:
{ "team": "backend", "environment": "production", "capabilities": ["docker", "gpu"]}Pass the file when the worker starts:
agent worker --pool gpu --labels-file /etc/cursor/labels.json startSet CURSOR_WORKER_LABELS_FILE instead when your orchestrator injects the path. --label and --labels-file cannot be combined.
Set the pool with --pool, not --label. The --pool flag enables pool assignment and registers the pool=<name> label.
Cursor normally derives repo=<owner/repo> labels from the Git remotes of each --worker-dir. For multi-repo workers, prefer one --worker-dir per checkout. If a checkout does not have a usable Git remote, add its routing label manually by repeating --label repo=<owner/repo> or including repo in a labels file. A manual label controls routing only. The checkout must already exist under an exposed worker directory.
Use hooks and MCP servers
Pool workers run project hooks from .cursor/hooks.json. Enterprise workers also support team hooks and enterprise-managed hooks. See Hooks.
Cursor runs each MCP server based on its transport:
| Transport | Runs on | Use it for |
|---|---|---|
Command (stdio) | Worker | Private APIs, databases, local tools, and services reachable from the worker's network. |
HTTP or SSE (url) | Cursor | Hosted MCP servers where Cursor manages the connection, OAuth, and session. |
Add and enable personal MCP servers through the MCP dropdown in cursor.com/agents. Team admins configure shared servers under Dashboard -> Integrations & MCP. Bake command-based MCP dependencies into the worker image so every machine in the pool has the same tools.
Scale a pool with a controller
A controller is code you run that adjusts worker capacity based on demand. The Cursor CLI ships with one: agent worker controller takes care of watching the request queue, and you provide a spawn script that starts new machines. We recommend using it. To build your own custom controller instead, see Build your own controller.
The request flow
- A user selects the pool in the Cursor app, on cursor.com/agents, or in the Cursor mobile app.
- They type a request and send it.
- The request enters the request queue for that pool.
- If the pool has an available worker, that worker claims the request. Cursor routes all follow-up activity in the chat to the claimed worker.
If the pool has no available workers, the request waits in the request queue until a worker becomes available.
Run the built-in controller
agent worker controller handles pool registration, the list-then-watch loop, and claims. You provide a spawn script that boots one worker machine per invocation:
agent worker controller \ --spawn ./spawn.sh \ --api-key "$CURSOR_API_KEY" \ --pool gpu --pool defaultWarm pool (keeps five idle workers connected in gpu by pre-spawning unclaimed workers):
agent worker controller --spawn ./spawn.sh --api-key "$CURSOR_API_KEY" --pool gpu --warm-idle 5agent worker controller --warm-idle keeps process-forked workers via --spawn. It does not patch a Kubernetes WorkerDeployment. On Kubernetes, keep warm size with WorkerDeployment.spec.readyReplicas. See the Kubernetes deployment guide.
The controller runs in one of two modes:
- Claim-then-spawn (default). The controller lists and watches pending requests, claims each one, and runs the spawn script once per claim. The spawn script boots a machine that starts a worker with the claimed worker id, so the request is seated as soon as the machine connects.
- Warm pool (
--warm-idle <count>). For keeping a few always-idle workers in each pool. The controller pre-spawns unclaimed workers to holdcountidle workers per--pooland never calls claim; Cursor assigns queued requests to the warm workers. Reconciles againstGET /v0/private-workers/poolsevery 60 seconds; the pending-requests SSE stream only accelerates backfill. Run one warm controller per pool: without a claim there is no server-side lease, so concurrent controllers can transiently over-spawn.
| Flag | Description |
|---|---|
--spawn <path> | Script to run once after each successful claim, or once per missing warm worker. |
--api-key <key> | Service account API key. Also readable from CURSOR_API_KEY. The same key is used for pool registration, the queue APIs, and the spawn script's environment. |
--pool <name> | Pool to watch (repeatable). Non-default names are registered at startup. In warm mode, the pools to keep warm. Mutually exclusive with --all-pools. |
--all-pools | Watch every pool on the team. Does not register pools, and can't be combined with --warm-idle. |
--warm-idle <count> | Keep count idle workers per --pool and skip claiming. Reconciles against pool stats every 60 seconds. |
--repository <url> | Filter pending requests by repository. Required for repo-scoped API keys. In warm mode, also pins the pool idle-count to that repo's row. |
--endpoint <url> | Override the public API base (default https://api.cursor.com). Also readable from CURSOR_API_ENDPOINT. |
The spawn script receives everything it needs as environment variables:
| Variable | Set in | Description |
|---|---|---|
CURSOR_REQUEST_ID | Claim mode | Agent id of the claimed request. |
CURSOR_USER_ID | Claim mode | Cursor user id that created the request. |
CURSOR_REPO_URL, CURSOR_REPO_OWNER, CURSOR_REPO_NAME | Claim mode | Repository metadata when the request targets a repo. Unset for any-repo requests. |
CURSOR_REPO_URLS | Claim mode | JSON array of repository URLs for multi-repo requests. |
CURSOR_POOL | Both | Pool the worker should join. |
CURSOR_AGENT_WORKER_ID | Both | Worker id the machine must start with. The worker CLI reads this automatically. |
CURSOR_WORKER_NAME | Both | Display name for the worker. |
CURSOR_API_KEY | Both | The controller's API key, for the worker process. |
CURSOR_API_URL, CURSOR_API_ENDPOINT | Both | API base the controller is using. |
A minimal spawn script boots a machine or container that inherits those variables and starts a worker:
#!/usr/bin/env bashset -euo pipefaildocker run -d \ -e CURSOR_API_KEY \ -e CURSOR_AGENT_WORKER_ID \ -e CURSOR_WORKER_POOL_NAME="$CURSOR_POOL" \ your-worker-image \ agent worker --pool startSession lifecycle
Once a worker is matched to a request, Cursor forwards all agent tool calls directly to the machine. The connection has an idle timeout that defaults to 1 hour. Configure it as needed:
agent worker --pool my-pool --idle-release-timeout 600 start--idle-release-timeout (env var CURSOR_WORKER_IDLE_RELEASE_TIMEOUT) is the number of seconds the worker stays connected after a session ends, waiting for follow-up messages. If a follow-up arrives, the timer resets. When the timeout fires, the CLI exits with code 0 so a supervisor can recycle the machine. Pass 0 to disable idle-based release. Releasing a claim is a separate API: it stops preferring that machine for the agent, and does not exit the worker CLI.
Once a worker times out, Cursor marks it as freed. The machine can reset and re-enter the pool. If a user restarts a chat that has disconnected from its machine, the chat reconnects to a fresh machine from the pool. Workspace state from the original machine does not carry over.
Build your own controller
The built-in controller covers most setups. If you need custom logic, for example your own scheduling, quotas, or machine placement, you can build a controller on the same Cloud Agents API. A controller does three things: watch the request queue, claim a request, and start a worker for it.
Monitor the request queue
List the queue once to build your view of pending requests, then follow changes in real time over Server-Sent Events (SSE).
Start with GET /v0/private-workers/pending-requests. Add ?pool=<name> to watch a single pool. Paginate to completion and keep the streamCursor from the response:
curl --request GET \ --url "https://api.cursor.com/v0/private-workers/pending-requests?pool=my-pool&limit=50" \ -u "$CURSOR_API_KEY:"Then open the event stream with GET /v0/private-workers/pending-requests/stream, passing that streamCursor and the same filters. Keep your view current as events arrive: add requests from created and claimed_offline events, and drop requests when you see claimed or expired:
curl --request GET --no-buffer \ --url "https://api.cursor.com/v0/private-workers/pending-requests/stream?pool=my-pool&cursor=$STREAM_CURSOR" \ --header 'Accept: text/event-stream' \ -u "$CURSOR_API_KEY:"Cursors expire five minutes after the list that issued them. When the stream returns 410 Gone, list again and reopen the stream from the fresh streamCursor. Better yet, list again every five minutes with some jitter instead of waiting for the 410.
The number of requests in your view is the pool's queue depth. When it grows, add workers. GET /v0/private-workers/summary tells you how many workers are connected and how many are busy, so you can size the response. Treat events as hints and the list as the source of truth: event delivery is best-effort, and each new list corrects any drift. See Watch Pending Pool Requests for the full delivery guarantees and cursor rules.
Claim a request
Reserve a pending request for a specific worker before that worker starts, so two controller replicas never boot two machines for the same request. Call POST /v0/private-workers/claim with the request's id (from the list response or a created event) and the worker id you plan to start:
curl --request POST \ --url "https://api.cursor.com/v0/private-workers/claim" \ -u "$CURSOR_API_KEY:" \ --header 'Content-Type: application/json' \ --data '{ "id": "bc-00000000-0000-0000-0000-000000000002", "workerId": "pw_123" }'Start a worker for the claim
When you connect the worker, supply the same worker id.
Environment variable (preferred, because older CLI builds ignore an unknown env var):
export CURSOR_AGENT_WORKER_ID="pw_123"agent worker --pool my-pool startCLI flag:
agent worker --worker-id pw_123 --pool my-pool startRelease a claim
Use Release A Claim with the same id you passed to claim. That drops the routing preference for this machine so a replacement worker can claim the agent:
curl --request POST \ --url "https://api.cursor.com/v0/private-workers/claims/bc-00000000-0000-0000-0000-000000000002/release" \ -u "$CURSOR_API_KEY:"A waiting follow-up returns to the pool queue at the next scheduling point. A connected worker finishes its current turn. A second claim while a live claim exists is rejected; release first, then claim a new workerId.
Hibernation
A pool machine does not have to stay online while its agent is idle. After a session ends, the worker waits for follow-ups until its idle timeout fires, and keeping every machine up between turns gets expensive.
The tradeoff is workspace locality. Without hibernation, a follow-up that arrives after the machine released reacquires from the pool: the agent lands on a fresh machine and may spend its first minutes reconstructing the workspace it already had. With hibernation, the machine comes back with its workspace intact and the follow-up resumes where the agent left off.
Give the pool a reconnect window
workerReadyTimeoutSeconds controls how long Cursor waits for a claimed machine to reconnect before assigning the request to another worker. The default is 0: follow-ups reacquire immediately.
curl --request POST \ --url "https://api.cursor.com/v0/private-workers/pools" \ -u "$CURSOR_API_KEY:" \ --header 'Content-Type: application/json' \ --data '{ "scope": "team", "poolName": "gpu", "workerReadyTimeoutSeconds": 900 }'Snapshot machines when they go idle
Shorten the worker's --idle-release-timeout so machines release soon after the agent goes idle. When the worker exits (code 0 on idle release), or while Get An Agent reports a status of IDLE, snapshot the machine and stop it.
Recognize the wake-up call
When a follow-up arrives for an agent whose claimed machine is offline, Cursor waits up to the reconnect window and advertises the request as a claimed-but-offline queue entry. Your controller recognizes it two ways: List Pending Pool Requests returns the entry with claimedWorkerId and wakeTimeoutMs, and the event stream emits a claimed_offline event with the same fields.
Bring the machine back up
Restore the snapshot and start a worker with the same id before the window lapses:
export CURSOR_AGENT_WORKER_ID="<claimedWorkerId>"agent worker --pool gpu startThe follow-up resumes on the machine with its workspace intact.
Release the claim if you can't
If the machine is not coming back, for example the snapshot is gone, release the claim. The request returns to the queue immediately, and a replacement machine can claim it. If you do nothing, the window lapses on its own: the claim expires and the request is re-advertised as an unclaimed entry (a fresh created event) that any worker can serve.
Monitor workers
Start the worker management server:
agent worker --pool gpu --management-addr ":8080" startIt exposes:
| Endpoint | Purpose |
|---|---|
GET /healthz | Liveness. Returns 200 while the worker process is running. |
GET /readyz | Readiness. Returns 200 when the worker is connected and idle, and 503 while it starts or serves an agent. |
GET /metrics | Prometheus metrics for connections, retries, activity, and session lifecycle. |
The main metrics are:
cursor_self_hosted_worker_connectedcursor_self_hosted_worker_session_activecursor_self_hosted_worker_last_activity_unix_secondscursor_self_hosted_worker_connect_attempts_totalcursor_self_hosted_worker_connect_retry_totalcursor_self_hosted_worker_connect_failures_totalcursor_self_hosted_worker_session_ends_total
Use GET /v0/private-workers/pools for connected and in-use counts per pool. Use the pending-request queue for demand and autoscaling. The team-wide worker summary is not a substitute for pool-specific demand.
CLI reference
Worker flags go before the subcommand:
agent worker [options] start| Flag | Env var | Description |
|---|---|---|
--pool [name] | CURSOR_WORKER_POOL_NAME | Join a team pool instead of registering a personal My Machines worker. One agent claims the worker at a time. Optional pool name; defaults to default. |
--api-key <key> | CURSOR_API_KEY | Service account API key for pool workers. |
--worker-dir <path> | Workspace root to expose to agents. Repeatable; the first value is the assignment identity. Default: current directory. | |
--name <name> | CURSOR_WORKER_NAME | Display name for the worker. Defaults to the machine hostname. |
--label <key=value> | Add a label. Repeatable. Mutually exclusive with --labels-file. | |
--labels-file <path> | CURSOR_WORKER_LABELS_FILE | Path to a JSON or TOML labels file. Mutually exclusive with --label. |
--idle-release-timeout <sec> | CURSOR_WORKER_IDLE_RELEASE_TIMEOUT | Seconds the worker stays connected after going idle. When the timeout hits, the CLI exits with code 0 so a supervisor can recycle the machine. Default 3600; pass 0 to disable idle-based release. |
--management-addr <addr> | Listen address for /healthz, /readyz (Kubernetes probes), and /metrics (Prometheus), for example :8080. | |
--computer-use | Let claimed agents drive this machine's desktop. See Computer use and desktop sharing. | |
--display <display> | Existing X11 display to require for --computer-use, for example :0. When omitted, a reachable DISPLAY is reused or a managed desktop is started. | |
--share-desktop [mode] | Let authorized viewers watch or control the agent desktop: view or view_and_control (default). | |
--on-session-start <command>, --on-session-end <command> | Worker-host commands around a pool claim. The primary configuration path remains hooks. | |
--clone-git-repos | On claim, clone the agent's GitHub repos into the workspace. Any-repo named pools only (not default, and not a bound repo or named machine). Implies --mint-github-token. Requires git on PATH. Default: off. | |
--mint-github-token | Receive short-lived GitHub tokens during claimed runs. Pool workers only; requires team-admin enablement, and at most one credential-enabled worker per OS user or container. | |
--sync-dashboard-secrets | Receive eligible dashboard Cloud Agent secrets as environment variables during claimed runs. Pool workers only; same one-worker-per-user deployment rule. | |
--identity-socket | Expose a per-claim OIDC identity socket to claimed agents (published as CURSOR_AGENT_SOCKET) for minting short-lived Cursor identity tokens. Off by default. | |
--auth-token <token> | Pre-minted access token, for automation that exchanges an API key for a short-lived token externally. | |
--auth-token-file <path> | File containing an access token. Re-read on reconnect, so a controller can rotate a mounted token without restarting the worker. | |
--worker-id <id> | CURSOR_AGENT_WORKER_ID | Stable worker id used with claim. Prefer the env var so older CLI builds ignore it. |
--data-dir <path> | CURSOR_DATA_DIR | Base directory for logs, artifacts, and recording data. Defaults to /opt/cursor if writable, otherwise ~/.local/share/cursor-agent. |
--wait [seconds] | Wait for another worker using this data directory to stop before starting. Without a value, waits indefinitely. | |
-e, --endpoint <url> | API endpoint. Default: https://api2.cursor.sh. |
Run agent worker --help for the full list, and agent worker debug for a preflight report.
FAQ
Size a worker like a CI runner or devbox for the repository it serves. It needs enough CPU, memory, disk, and network access to clone the repository and run the builds, tests, and tools your agents use.
Yes. Project skills in .cursor/skills/ or .agents/skills/ are available
on the worker. Commit shared skills to the repository or include them in
your worker image.
A pool worker serves one agent at a time. Add workers or scale the pool when requests wait for capacity.
Next steps
- Choose where Cloud Agents run: compare managed Cloud Agents, My Machines, and Pools.
- My Machines: connect a personal laptop, devbox, or VM.
- Computer use: let agents drive a desktop and browser on your workers.
- API reference: endpoints for workers, pools, the pending-request queue, and worker tokens.
- Kubernetes deployment guide: operator and Helm chart for pools at scale.