Appearance
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.
text
cd my-agent
agent-sdk login
agent-sdk whoami
agent-sdk validate
agent-sdk deployThe deploy command waits until the runtime is running and prints the stable deployment URL. Open Deployed Agents to see the agent and manage it.
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:
text
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.
ts
// bot/agent.ts
import { defineAgent } from "@grok/agent-sdk";
export default defineAgent({
hosting: {
egressDomains: ["api.weather.example.com"],
secretNames: ["WEATHER_API_KEY"],
},
});text
agent-sdk deploy --dir . --slug weather-agent
agent-sdk secret set weather-agent WEATHER_API_KEY
agent-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:
bash
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 in Deployed Agents and select Sessions:
bash
agent-sdk logs --prod --once --slug weather-agentThen follow the turn:
bash
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 that Deployed Agents 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:
bash
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 Deployed Agents 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:
bash
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.
text
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.
text
export CURSOR_API_KEY="<cursor-api-key>"
export AGENT_SDK_BEARER_TOKEN="$(openssl rand -hex 32)"
agent-sdk serve \
--dir /srv/agents \
--host 127.0.0.1 \
--port 3000 \
--state-root /var/lib/agent-sdk \
--bearer-token "$AGENT_SDK_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