Deploy an Agent SDK project
Deploying an Agent SDK project from a pushed Git ref creates a stable service. Cursor-managed hosting builds the selected ref and operates the runtime; self-hosting gives you control of the network, process, and filesystem.
Choose managed hosting or self-hosting
| Option | Use it when | You manage |
|---|---|---|
| Cursor-managed hosting | You want the shortest path from Git to a running agent | Source, declared egress, secrets, and agent behavior |
| Self-hosting | You need your own network, proxy, filesystem, or process controls | The Node process, TLS, auth, secrets, state, monitoring, and upgrades |
The rest of this guide starts with managed hosting. The self-host recipe uses the same project and HTTP API.
Deploy from Git
Commit and push the exact ref you want to run, then sign in, validate,
and deploy. The preferred CLI is agent-sdk.
cd my-agentagent-sdk loginagent-sdk whoamiagent-sdk validateagent-sdk deployThe deploy command waits until the runtime is running and prints the stable deployment URL.
deploy reads from Git; it does not upload your working tree. Inside a
checkout, it infers the HTTPS origin, current ref, agent path, and slug.
Pass them explicitly when the checkout is not the source you want:
agent-sdk deploy \ --repo https://github.com/acme/agents \ --ref main \ --path agents/weather \ --slug weather-agentThe source repository must be available through the team's repository
integration, and a nested --path must contain an installable Agent SDK
project.
Connect deployed peers
A team-owned deployment can call another agent deployed by the same team with
defineConnection({ agent: "weather-agent" }). Deploy the specialist and the
caller; Cursor-managed hosting resolves the slug and authenticates the call,
so you don't configure a peer URL, header, or secret.
Redeploy the caller after adding or changing a peer connection. An agent last deployed before hosted peer connections became available also needs one redeploy. See Peer agents for delegation, session affinity, and local development.
Privacy Mode
A hosted deployment stores a snapshot of its source repository, along with the sessions, KV, files, and logs the agent creates for your team. Whether managed hosting is available depends on the privacy setting that applies to you, which is your team's setting when the team enforces one:
| Privacy setting | Managed hosting |
|---|---|
| Privacy Mode | Available. Cursor stores deployment data the same way it does without Privacy Mode, and the agent's model requests get the same Privacy Mode handling as your other Cursor requests. |
| Privacy Mode (Legacy) | Not available. deploy, a hosted session, and an eval run all refuse. |
deploy refuses because a deployment stores a snapshot of your
repository. A hosted session refuses, and Cursor does not store the
message. An eval run refuses because it stores prompts and replies.
Switch to Privacy Mode, or ask a team admin when your team enforces
this setting.
The stored data includes the memory journal,
which agent-sdk init turns on. The journal holds user messages and the
agent's replies. With Privacy Mode, Cursor stores it only to run your
agent and doesn't train on it.
Add egress and secrets
Declare the external hosts and secret names the agent needs. Mounted
extensions add the ones they declare, so list only what your own code
calls; agent-sdk info --dir . shows the combined list and what each mount
adds. Deploy the agent, then set each value at the hidden prompt.
The next task uses the new value. You do not deploy again to apply
that value.
// bot/agent.tsimport { defineAgent } from "@grok/agent-sdk";export default defineAgent({ hosting: { egressDomains: ["api.weather.example.com"], secretNames: ["WEATHER_API_KEY"], },});agent-sdk deploy --dir . --slug weather-agentagent-sdk secret set weather-agent WEATHER_API_KEYagent-sdk secret list weather-agentCommit secret names, never values. secret set accepts names and reads
values without echoing them; secret list returns names and creation
times only. Do not pass NAME=VALUE, print a secret to verify it, or
put names beginning with reserved CURSOR_ in secretNames.
A hosted deployment receives its secrets per request, after its modules
are loaded, so code that reads process.env while a module is still
loading sees nothing. Read a secret inside the handler, tool, or callback
that needs it, and give an MCP connection its credential as a ${NAME}
template, which resolves when the connection opens. See
credentials in headers and env.
agent-sdk deploy runs the top-level code in your files one time to build
the snapshot. No secrets exist at that time. The snapshot keeps those
values.
SCM webhook channels go one step further and name their secrets in the
channel definition, as { secretName: "GITHUB_WEBHOOK_SECRET" }, so the
platform can verify each delivery's signature before any compute starts.
validate rejects a literal or getter there. The
GitHub guide shows the named form.
Enter egress hosts without schemes or paths. An HTTPS host needs no port;
add one, as clickhouse.example.com:9440, only for a protocol the proxy
reads no hostname from. The
Host MCP OAuth guide covers OAuth-backed MCP
servers on a hosted agent.
Your team egress policy limits hosting.egressDomains. agent-sdk deploy fails
when a requested host is outside that policy, and the bot that is already
running keeps the policy from its last successful deploy. A bot that lists
no egress hosts receives only the platform base. Architecture v2 stores
that decision on the release, from the hosts the release itself declares.
The server reads those hosts from the engine once the release is healthy.
It does not use a host list sent by the CLI, the portal, or MCP. An older
client that omits the optional field is marked the same way. A new release
with no stored set for that team does not start. On a shared release each
installing team has its own stored set. A release written before this change
keeps its previous behavior.
A PlanetScale Postgres host (*.pg.psdb.cloud) has no port in the
declaration. Under a restricted team mode it is a snapshot key for ports
5432 and 6432, using the team CIDR:port entries that cover every
resolved address. An address outside those entries blocks the deploy. The
snapshot does not pin the addresses from that lookup.
| Team egress mode | What agent-sdk deploy allows |
|---|---|
| No mode set | Every host in hosting.egressDomains. |
| Allow all network access | Every host in hosting.egressDomains. The bot stays limited to that list. |
| Defaults + team allowlist | The platform default list, plus the team list. |
| Team allowlist only | The team list. |
The platform base is always allowed. A team list cannot remove it. The base
is a small set of exact hosts and a few wildcards the pod needs to install
and run: Cursor API hosts, npm and Yarn registries, Slack, Origin, and the
artifact and Agent Store buckets. agent-sdk deploy --json includes those hosts
on egress.effective, together with the hosts the team policy allowed.
agent-sdk deployment --json includes egressAllowedDomains, the hosts from
hosting.egressDomains that the last successful deploy allowed.
A team entry *.example.com allows example.com, any subdomain, and a
narrower wildcard such as *.api.example.com. A bot entry *.example.com
does not add example.com. List the apex on the bot when the bot calls it.
A raw TCP host such as db.example.com:5432 is allowed only when the team
list has a public IPv4 CIDR:port entry that contains every address that
name resolves to, on the same port. Private addresses and IPv6 are refused.
When deploy blocks hosts, the error names each host, the team mode, and two
fixes: ask a team admin to add the hosts (admins get the settings link
directly), or remove the hosts from hosting.egressDomains and deploy
again.
Use the hosted agent
Verify a hosted turn
After deploy reports running, message the deployment from the CLI:
agent-sdk session create --slug weather-agent \ --message "Forecast for Paris" --waitThe deployment accepts the turn as a delivery and creates the session when
the turn starts. --wait polls the delivery until it names the session,
then prints the sessionId. Without --wait, the output has a
deliveryId and workflowId but no sessionId yet. Add --read-only to
check a deployment without side effects: read tools run and write tools are
stubbed.
You can also find the session in the deployment's recent output, which
prints a ==> ses_... (http) <== header for each session, or open the
deployment from Deployed Agents in the
dashboard and select Sessions:
agent-sdk logs --prod --once --slug weather-agentThen follow the turn:
agent-sdk session get ses_... --prod --slug weather-agentThe command lists each hosted run with its status and log lines. When the
deployment stored process output, a Process output: section prints that
output. agent-sdk logs --prod prints the same process output the dashboard shows
for a persistent deployment, under ==> process-output <==. Pass the session
ID to also print that session's process output under Session process output:
and a tool <name> line for each tool call. While the turn is still working, the session command reports how
many events have arrived and prints Turn running; runs may lag behind events.
Rerun it until the latest run shows a final status such as finished. A
high-effort turn can take several minutes, and its run can show no logs
until it ends, so a growing event count is the progress signal.
To see which tools the bot called, print the hosted events:
agent-sdk session get ses_... --prod --slug weather-agent --view eventsEach tool call is an actions.requested or action.result event. The tool
name is toolName.
To continue the same conversation, pass --session ses_... to
session create. agent-sdk session get ses_... --prod --slug weather-agent --web
opens the session in the dashboard playground. Hosted deployments don't
support agent-sdk chat, tool call, --view cost, or mcp; use
session create, logs --prod, session list --prod, and
session get --prod instead.
Send work to the deployment
A hosted agent wakes from its channels: Slack mentions and DMs, SCM events, and custom channel routes. External HTTP clients call a custom route on the stable alias URL and send the alias token on every request:
curl -X POST "$AGENT_ALIAS_URL/v1/channels/tickets/ticket" \ -H "X-Agent-Alias-Token: $AGENT_ALIAS_TOKEN" \ -H 'content-type: application/json' \ -d '{"ticketId":"ENG-142","message":"CSV export returns 500"}'The deployment answers 202 Accepted with an empty body as soon as the
request arrives, then runs the route's handler, so the caller never
receives a session ID or a result. Deliver results from the channel, as
described in Route responses.
To confirm the work started, find the new session with
agent-sdk logs --prod and follow it with agent-sdk session get --prod, as in
Verify a hosted turn.
Protect the alias token
The first deploy prints the alias token once. Store it outside terminal
transcripts; it cannot be retrieved later. If it is lost or exposed,
agent-sdk deployment rotate-token weather-agent replaces it and invalidates the
old value.
deployment --json can include short-lived access headers, so treat
that output as a credential too.
For a custom webhook whose provider cannot send the alias token, use
publicEndpoint() only when the channel verifies the provider
signature. That opens the custom channel path, not the built-in session
or tool APIs. See Webhooks.
Update the deployment
Push the new ref and deploy the same slug. The stable URL remains while the new generation replaces the previous one.
agent-sdk deploy --dir . --slug weather-agentA redeploy replaces its source, its watched repositories, and any egress
domains supplied through CLI flags. Pass every --cursor-events-repo value
again; without the flag, the deployment watches its GitHub source repository.
Keep source-controlled egress in the hosting block when possible.
Use agent-sdk deployment get weather-agent to inspect a rollout.
The CLI reference owns --no-wait,
multi-agent deployment, stop, and delete behavior.
Keep state across replacements
Managed hosting persists framework-owned session, KV, and file state
across normal runtime replacements. Files written elsewhere on the
runtime's local filesystem are disposable; use ctx.host.kv or
ctx.host.files for durable application state.
On architecture v2, the default, reminders take a prompt or a named handler. See Reminders.
Self-host securely
Run Agent SDK under Node 22.13 or newer, never Bun. One supervised process can serve every agent under a directory while a persistent state root keeps sessions across restarts.
export CURSOR_API_KEY="<cursor-api-key>"export BDK_BEARER_TOKEN="$(openssl rand -hex 32)"agent-sdk serve \ --dir /srv/agents \ --host 127.0.0.1 \ --port 3000 \ --state-root /var/lib/bdk \ --bearer-token "$BDK_BEARER_TOKEN"Before exposing the host:
-
Put TLS or an authenticating proxy in front of the loopback server.
-
Keep
--bearer-tokenon shared hosts. Use--allow-anonymousonly behind a proxy that authenticates every caller. -
Mount
--state-rooton a persistent volume outside the agent repository and back it up. -
Inject credentials at startup instead of baking them into an image.
-
Run the process under a supervisor and enable schedules on exactly one serving process per project.
Containers use the same command with --host 0.0.0.0, a mounted state
volume, and an authenticating network boundary. The
Channels reference covers
per-caller and channel-specific auth.
Observability
Use agent-sdk logs --prod for a hosted session's process output, and
agent-sdk session get <sessionId> --prod for its runs. Add --view events to
print the tool calls. OpenTelemetry covers traces and
metrics, and
session traces
cover a local conversation.
Related
- Early-alpha migration: move a hosted architecture v1 project to v2 through a reviewed pull request
- CLI reference: inspect, update, stop, delete, and rotate deployments
- GitHub: Cursor-event repositories and direct webhook delivery
- Slack: provision a dedicated hosted Slack app
- Host MCP OAuth: reach OAuth-backed MCP servers from a hosted agent
- Troubleshooting: diagnose failed or stale deployments