# Agent SDK full documentation

> Build and serve bots and agents from versioned Markdown and TypeScript.

---

Source: /docs/building-with-agents.md

# Build with a coding agent

A coding agent in your checkout can create an Agent SDK project, call its tools, run a real turn, and check the result from the CLI. You approve the plan and any side effects.

To let a running agent change its own source, see the [self-improvement extension](/docs/guides/improve.md).

## Create an agent from a goal

Tell the coding agent what the agent is for and where people reach it, and ask it to follow the `bdk-create-agent` skill. It asks for any decisions you haven't made, shows a file plan, and scaffolds a small first version with a smoke eval.

## Add one capability

For an existing project, name the outcome and point the coding agent at the matching skill, such as `bdk-setup-slack`, `bdk-mcp-auth`, or `bdk-github`. For example:

> Add Slack mentions and DMs to this agent. Follow the bdk-setup-slack
> skill, keep replies in the source thread, and verify one local mention.

> Connect the private inventory MCP server with host OAuth. Keep its
> write tools behind a narrow server-tool wrapper.

> Add a GitHub ready-for-review trigger and replay PR 42 before posting
> anything live.

## Verify tools before tuning the prompt

Have the coding agent call tools directly and fix any that return the wrong evidence before it touches the instructions. Then it runs one real turn, reads the trace that `run` prints, and runs the smoke evals:

```bash
agent-sdk validate --dir .
agent-sdk info --dir . --json
agent-sdk tool call inspect_pr --dir . --input '{"prUrl":"https://github.com/acme/checkout/pull/42"}'
agent-sdk run --dir . --message "Is https://github.com/acme/checkout/pull/42 ready to approve?"
agent-sdk session get --file <trace-path-printed-by-run>
agent-sdk eval --dir . --tag smoke
```

Finish with the project's TypeScript check, because a passing turn doesn't prove the project type-checks. Only `run` and `eval` need a Cursor credential, and [Environment variables](/docs/reference/cli.md#environment-variables) lists the ways to provide one.

## Improve a working agent

Once a smoke turn passes, follow [Hillclimbing](/docs/hillclimbing.md) to improve the agent against fixed inputs.

---

Source: /docs/changelog.md

# Changelog

## 0.3.0 (2026-10-23)

- GA Release
- `persistent` option is removed from `defineAgent`

## 0.2.29 (2026-10-23)

- Bug Fixes

---

Source: /docs/deployment.md

# Deploy an Agent SDK project

Deploy a pushed Git ref as a stable service on Cursor-managed hosting, or
self-host it when you need your own network or process controls.

## Deploy from Git

Commit and push the ref you want to run, then deploy it:

```bash
cd my-agent
agent-sdk login
agent-sdk validate
agent-sdk deploy
```

The command waits until the agent is running and prints its URL. Open [Deployed Agents](https://cursor.com/dashboard/deployed-agents) to see the agent and manage it.

`deploy` builds the pushed ref, so uncommitted changes don't ship. Pass the source when the checkout is not the one you want:

```text
agent-sdk deploy \
  --repo https://github.com/acme/agents \
  --ref main \
  --path agents/weather \
  --slug weather-agent
```

### Connect deployed peers

To call another agent your team deployed, add
`defineConnection({ agent: "weather-agent" })` to the caller and redeploy
it. See [Peer agents](/docs/guides/agent-to-agent.md).

### Privacy Mode

Managed hosting stores your repository and the agent's data, so it depends
on your privacy setting:

| 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. |

Switch to Privacy Mode, or ask a team admin when your team enforces the
setting.

## Add egress and secrets

Declare the hosts and secret names your code calls. `agent-sdk info --dir .`
shows them combined with what mounted extensions add.

```ts
// bot/agent.ts
import { defineAgent } from "@grok/agent-sdk";

export default defineAgent({
  hosting: {
    egressDomains: ["api.weather.example.com"],
    secretNames: ["WEATHER_API_KEY"],
  },
});
```

After you deploy, set each value at the hidden prompt. The next task uses
the new value without another deploy.

```bash
agent-sdk secret set weather-agent WEATHER_API_KEY
agent-sdk secret list weather-agent
```

Commit secret names, never values. Don't pass `NAME=VALUE` to `secret set`
or use a name that starts with `CURSOR_`.

Hosted secrets arrive after your modules load, so read each one inside the
handler, tool, or callback that needs it. Give an MCP connection a
[`${NAME}` template](/docs/reference/connections.md#credentials-in-headers-and-env)
instead.

SCM channels take each credential as a named secret, such as
`{ secretName: "GITHUB_WEBHOOK_SECRET" }`. The channel reads the environment
variable with that name locally and the deployment secret on Cursor hosting.
On Cursor hosting, Cursor rejects a badly signed delivery before it reaches your agent. `validate`
fails on a literal or a getter.

Write each egress host as a lowercase hostname with no scheme or path, such as `api.example.com`, and add a port such as `clickhouse.example.com:9440` only for raw TCP. `agent-sdk deploy` rejects any other form, and the error quotes that host.

Your team's egress policy limits which hosts you can list in
`hosting.egressDomains`. If you list a host that the policy blocks,
`agent-sdk deploy` fails and the error names that host. A deployment that
is already running keeps the policy from its last successful deploy. Ask a
team admin to add the host, or remove it and deploy again.

| 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. |

No team list can remove the platform base, which includes Cursor's API and
the npm registry. `agent-sdk deploy --json` lists every allowed host under
`egress.effective`.

A team entry `*.example.com` allows `example.com`, every subdomain, and
narrower wildcards such as `*.api.example.com`. An agent entry
`*.example.com` skips the apex, so list `example.com` too when the agent
calls it.

A raw TCP host such as `db.example.com:5432` needs a team `CIDR:port` entry
that covers every public IPv4 address it resolves to. If your team
restricts egress, a PlanetScale Postgres host (`*.pg.psdb.cloud`) needs a
team `CIDR:port` entry for ports 5432 and 6432.

## Use the hosted agent

### Verify a hosted turn

```bash
agent-sdk session create --slug weather-agent --message "Forecast for Paris" --wait
agent-sdk session get ses_... --prod --slug weather-agent
agent-sdk session get ses_... --prod --slug weather-agent --view events
agent-sdk logs --prod --slug weather-agent
```

Without `--wait`, the command returns before the session exists, so the
output has no `sessionId` yet. A long turn can show `running` with no logs
for several minutes. If the event count keeps going up, the turn is still
working.

### Send work to the deployment

Call a custom route on the stable alias URL with the alias token:

```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"}'
```

See [Route responses](/docs/reference/channels.md#route-responses) for what the
caller gets back. To confirm the work started, run
`agent-sdk session list --prod --slug weather-agent`.

### Protect the alias token

The first deploy prints the alias token once. If it leaks or you lose it, run `agent-sdk deployment rotate-token weather-agent`. Output from `deployment --json` can include access headers, so guard it the same way.

Use [`publicEndpoint()`](/docs/guides/webhooks.md) only for a webhook provider
that can't send the token and whose signature the channel verifies.

## Update the deployment

Push the new ref and deploy the same slug. Each redeploy replaces the
watched repositories, so pass every `--cursor-events-repo` value again, or
the deployment watches only its GitHub source repository. Run
`agent-sdk deployment get weather-agent` to check a rollout.

## Keep state across replacements

Sessions, `ctx.host.kv`, and `ctx.host.files` survive restarts and
redeploys. Anything else the agent writes to disk can disappear.

## Self-host securely

Run Agent SDK under Node 22.13 or newer, never Bun. One process can serve
every agent under a directory:

```bash
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 you expose the host:

- Put TLS or an authenticating proxy in front of the server.
- Keep `--bearer-token` unless a proxy authenticates every caller.
- Keep `--state-root` on a backed-up persistent volume outside the
  repository.
- Turn on schedules in only one process per project.

In a container, use `--host 0.0.0.0` behind an authenticating network
boundary.

---

Source: /docs/evals.md

# Evals

An eval sends a fixed input to the real agent and checks which tools ran and what the answer looks
like. To improve an agent and lock each win with an eval, follow [Hillclimbing](/docs/hillclimbing.md).

Evals run real tools, so [guard any writes](#keep-side-effects-out-of-eval-sessions) first.

## Protect a tool decision

The case below passes only if the agent calls the inspection tool and doesn't approve.

```ts
// evals/readiness.eval.ts
import { defineEval, includes } from "@grok/agent-sdk/evals";

export default defineEval({
  tags: ["smoke"],
  async test(t) {
    await t.send("Is https://github.com/acme/checkout/pull/42 ready to approve?");

    t.succeeded();
    t.calledTool("inspect_pr");
    t.notCalledTool("approve_pr");
    t.check(t.reply, includes(/ready|blocked|approve/i));
  },
});
```

Evals live in `evals/` beside `bot/`. Set how many cases run at once:

```ts
// evals/evals.config.ts
import { defineEvalConfig } from "@grok/agent-sdk/evals";

export default defineEvalConfig({
  maxConcurrency: 20,
});
```

```bash
agent-sdk eval --dir . --list
agent-sdk eval --dir . --tag smoke
```

## Check answer shape or quality

When the output has a contract, check its shape:

```ts
import { matches } from "@grok/agent-sdk/evals";
import { z } from "zod";

const verdict = JSON.parse(t.reply ?? "{}");
t.check(verdict, matches(z.object({ ready: z.boolean(), blockers: z.array(z.string()) })));
```

When correctness depends on meaning, use a judge. [Judges](/docs/reference/evals.md#judges) covers
models and grader options.

```ts
t.judge.factuality("The lint step failed on src/sidebar.ts.").atLeast(0.8);
```

## Test a conversation

Several `t.send` calls in one case share a session. An assertion on a returned turn checks only that
turn, and an assertion on `t` checks the whole conversation.

```ts
const first = await t.send("Inspect PR 42.");
first.calledTool("inspect_pr");

const followUp = await t.send("Now summarize only the blockers.");
followUp.usedNoTools();
t.check(followUp.message, includes(/blocker/i));
t.succeeded();
```

## Pin realistic inputs

Seed the files the agent reads instead of depending on your checkout:

```ts
await t.send("Review pr/diff.patch and report blockers.", {
  workspaceFiles: {
    "pr/diff.patch": [
      "diff --git a/app/routes/search.ts b/app/routes/search.ts",
      "+res.send(`<h1>Results for ${req.query.q}</h1>`);",
    ].join("\n"),
  },
});
```

Save a GitHub pull request's webhook payloads as fixtures:

```bash
agent-sdk github replay https://github.com/acme/checkout/pull/42 \
  --events '*' --dry-run --fixtures-dir fixtures/github
```

[Datasets and fixtures](/docs/reference/evals.md#datasets-and-fixtures) covers loaders for larger sets.

## Run and debug a regression suite

Run one case while you iterate, then the smoke set before review:

```bash
agent-sdk eval --dir . readiness
agent-sdk eval --dir . --tag smoke
```

Each run writes a folder under `evals/` in the project state directory, or under `--artifacts <dir>`
if you pass it. Open `evals/<case-id>.json` in that folder to see a failed case's input, assertions,
tool calls, and final text. Model turns need a
[Cursor credential](/docs/reference/cli.md#environment-variables).

## Run evals in CI

```bash
# CURSOR_API_KEY comes from the CI secret store.
agent-sdk eval --dir . --json --no-stream \
  --junit reports/evals.xml \
  --artifacts reports/evals \
  > reports/evals.json
```

Run `--tag smoke` on pull requests and the full suite on a schedule. The
[CLI reference](/docs/reference/cli.md#eval) covers every flag.

## Keep side effects out of eval sessions

Eval sessions carry `purpose: "eval"`. Check it where the tool writes, and return the decision
without the external action:

```ts
async execute({ findings }, ctx) {
  if (ctx.session.purpose === "eval") {
    return { posted: false, reason: "eval", count: findings.length };
  }

  return await postFindings(findings);
}
```

The tool call stays in the trajectory, so the eval can still assert that the agent chose to post.

---

Source: /docs/guides/agent-to-agent.md

# Peer agents

A peer connection lets one agent ask an independent specialist for help. The
specialist keeps its own instructions, tools, and sessions. On Cursor hosting,
team-owned agents reach each other by deployment slug, and Agent SDK handles
routing and authentication for you.

## Delegate a question to a specialist

Ask the concierge about the weather, and it hands the question to the weather
agent and returns the answer.

```ts
// bot/mcp-connections/weather.ts
import { defineConnection } from "@grok/agent-sdk/connections";

export default defineConnection({
  agent: "weather-agent",
  description: "Delegate weather questions to the weather specialist.",
});
```

The filename, `weather.ts`, becomes the
[MCP server name](/docs/reference/connections.md#peer-mcp-connection), and `agent`
is the specialist's lowercase deployment slug. Give the concierge a one-way
routing rule, so the agents never pass a request back and forth:

```md
When a request needs current weather, ask the `weather` peer. Return its
answer.
```

Deploy both agents from pushed Git refs:

```bash
agent-sdk deploy --dir ./agents/weather-agent --slug weather-agent
agent-sdk deploy --dir ./agents/concierge --slug concierge
```

## Continue the specialist's session

The first `ask` returns `{ status, sessionId, reply }` as text. Pass that
`sessionId` on the next `ask` to continue the specialist's earlier work.

```ts
const first = await ctx.host.mcp.callTool("weather", "ask", {
  message: "What's the weather in Paris right now?",
});
const firstText = first.content.find((part) => part.type === "text");
if (firstText?.text === undefined) {
  throw new Error("weather ask returned no text payload");
}
const { sessionId } = JSON.parse(firstText.text) as { sessionId: string };

await ctx.host.mcp.callTool("weather", "ask", {
  message: "How does that compare with tomorrow?",
  sessionId,
});
```

If the specialist is still working, `ask` returns `status: "running"`, and
`check` waits for the answer.

```ts
await ctx.host.mcp.callTool("weather", "check", {
  sessionId,
  waitSeconds: 20,
});
```

## Call a peer tool directly

Use `call_tool` when you want to run a tool on a peer agent directly, without
starting a model turn on that agent. The peer needs at least one server tool,
and the call follows the
[direct-call rules](/docs/reference/tools.md#call-a-tool-without-a-model-turn).

```ts
const result = await ctx.host.mcp.callTool("weather", "call_tool", {
  toolName: "get_forecast",
  input: { city: "Paris" },
});
const part = result.content.find((item) => item.type === "text");
if (part?.text === undefined) {
  return { ok: false, forecast: null };
}
const payload = JSON.parse(part.text) as {
  isError?: boolean;
  result?: unknown;
};

return {
  ok: result.isError !== true && payload.isError !== true,
  forecast: payload.result ?? null,
};
```

## Try it locally

To try it locally, run both projects in one process:

```bash
agent-sdk dev ./agents
agent-sdk chat --url http://127.0.0.1:3000/concierge \
  --message "What's the weather in Paris right now?"
```

Locally, `agent` is the specialist's mount slug, usually its directory name.

## Peer or subagent?

Use a [subagent](/docs/reference/subagents.md) to split one agent's job into roles.
Use a peer when the specialist is a complete agent you also run on its own.

| | Subagent | Peer |
| --- | --- | --- |
| Context | Child role inside one agent | Independent agent and session |
| Tools and connections | Inherits the parent project | Owns its project surface |
| Reachability | Parent only | Local co-hosted agents, or declared deployments on the same team |

## Other hosts

To self-host, run the peers under one `agent-sdk` process, and if turns run on
remote workers, protect it with `--bearer-token` and set `--public-url` as
[Deployment](/docs/deployment.md) describes.

---

Source: /docs/guides/bitbucket.md

# Bitbucket agents

A Bitbucket agent wakes on pull request, comment, and push events from
Bitbucket Cloud or Data Center, then writes its results back to the
repository. Declare `onPullRequest`, `onPullRequestComment`, or `onPush`
on `bitbucketChannel()` for each event that should wake it.

## Connect a repository

Name the repository hook secret and list the repositories that may wake
the agent.

```ts
// bot/channels/bitbucket.ts
import { bitbucketChannel } from "@grok/agent-sdk/channels/bitbucket";

export default bitbucketChannel({
  repos: ["acme/widgets"],
  webhookSecret: { secretName: "BITBUCKET_WEBHOOK_SECRET" },
});
```

Add a repository webhook for
`https://<your-host>/<slug>/v1/channels/bitbucket` with the same secret.
On Cursor hosting, a delivery with the wrong secret gets a `401` and
never reaches your agent. For Data Center, set the REST base and the API token:

```ts
export default bitbucketChannel({
  repos: ["PLATFORM/api"],
  webhookSecret: { secretName: "BITBUCKET_WEBHOOK_SECRET" },
  token: () => process.env.BITBUCKET_TOKEN ?? "",
  apiBaseUrl: "https://bitbucket.example.com/rest/api/1.0",
});
```

For Bitbucket Cloud, omit `apiBaseUrl`. To set the secrets on a deployment, see
[Add egress and secrets](/docs/deployment.md#add-egress-and-secrets).

For Bitbucket Cloud on Cursor hosting, you can skip the repository
webhook. Set `cursorAccount: { repos: ["acme/widgets"] }`, and events and
`ctx.bitbucket` calls go through your signed-in Cursor account.

## Review a pull request

When someone opens a pull request, or pushes new commits to a Data Center
pull request, the agent reads the diff and posts its review as a comment
on the pull request.

```ts
// bot/channels/bitbucket.ts
import {
  bitbucketChannel,
  defaultBitbucketAuth,
} from "@grok/agent-sdk/channels/bitbucket";
import { postPullRequestComment } from "../lib/bitbucket.js";

export default bitbucketChannel({
  repos: ["acme/widgets"],
  webhookSecret: { secretName: "BITBUCKET_WEBHOOK_SECRET" },
  onPullRequest: async (ctx, pullRequest) => {
    if (pullRequest.draft) {
      return null;
    }
    const hasNewCommits =
      pullRequest.action === "updated" &&
      pullRequest.previousHeadSha !== undefined &&
      pullRequest.previousHeadSha !== pullRequest.headSha;
    if (pullRequest.action !== "created" && !hasNewCommits) {
      return null;
    }

    const diff = await ctx.bitbucket.api.getPullRequestDiff(pullRequest.id);
    return {
      // `auth` starts a model turn running as the event sender.
      auth: defaultBitbucketAuth(ctx),
      message: `Review ${ctx.repository.fullName}#${pullRequest.id}.`,
      context: [`<bitbucket_diff>${diff}</bitbucket_diff>`],
    };
  },
  events: {
    async "message.completed"(event, channel, ctx) {
      if (ctx.session.purpose === "eval") {
        return;
      }
      if (event.data.finishReason === "tool_call" || event.data.text === "") {
        return;
      }
      const pullRequestId = channel.state.pullRequestId;
      if (
        !channel.state.eventType?.startsWith("pullrequest:") ||
        pullRequestId === null
      ) {
        return;
      }
      await postPullRequestComment(channel.state, pullRequestId, event.data.text);
    },
  },
});
```

`postPullRequestComment` is a small helper you write in
`bot/lib/bitbucket.ts` that posts a comment through the Bitbucket API
with `BITBUCKET_TOKEN`. It uses the repository and pull request from the
session state, so the model never picks where the review goes.

Each hook returns `{ auth }`, `{ task }`, or `null`, as
[handler results](/docs/reference/channels.md#handler-results) describes.
All events on the same pull request go to the same session, so the agent
sees the whole conversation.

## Answer a pull-request comment

Skip the agent's own comments, and answer a comment that mentions the agent:

```ts
const AGENT_USERNAME = "cursor-reviewer";

onPullRequestComment: (ctx, comment) => {
  if (comment.author?.username === AGENT_USERNAME) {
    return null;
  }
  if (!comment.body.includes(`@${AGENT_USERNAME}`)) {
    return null;
  }
  return {
    auth: defaultBitbucketAuth(ctx),
    message: comment.body,
  };
},
```

Add this property to the same `bitbucketChannel()` as the review handler.

## Triage a push to `main`

Mark the new commit as in progress, then start the triage turn:

```ts
import {
  bitbucketChannelStateForEvent,
  defaultBitbucketAuth,
} from "@grok/agent-sdk/channels/bitbucket";
import { setPushTriageStatus } from "../lib/bitbucket.js";

onPush: (ctx, push) => {
  const change = push.changes.find(
    candidate =>
      candidate.name === "main" && candidate.after !== undefined
  );
  if (change?.after === undefined) {
    return null;
  }
  const sha = change.after;

  return {
    // `task` runs this host sequence after the webhook is acknowledged.
    task: async ({ send }) => {
      await ctx.bitbucket.setBuildStatus({
        sha,
        key: "cursor/push-triage",
        state: "INPROGRESS",
        name: "Cursor push triage",
        description: "Investigating push to main",
      });

      await send(`Triage push ${sha} on ${ctx.repository.fullName}.`, {
        auth: defaultBitbucketAuth(ctx),
        title: `Bitbucket push ${sha}`,
        state: {
          ...bitbucketChannelStateForEvent({
            repository: ctx.repository,
            headSha: sha,
            eventType: ctx.eventType,
          }),
        },
      });
    },
  };
},
```

When the turn ends, update that same status:

```ts
async "message.completed"(event, channel, ctx) {
  if (
    ctx.session.purpose !== "eval" &&
    channel.state.eventType === "repo:push" &&
    channel.state.headSha !== null &&
    event.data.finishReason !== "tool_call"
  ) {
    await setPushTriageStatus(
      channel.state,
      "SUCCESSFUL",
      "Push triage complete"
    );
    return;
  }

  // Continue with the pull-request reply branch from the first example.
},
async "turn.failed"(_event, channel, ctx) {
  if (
    ctx.session.purpose === "eval" ||
    channel.state.eventType !== "repo:push" ||
    channel.state.headSha === null
  ) {
    return;
  }
  await setPushTriageStatus(
    channel.state,
    "FAILED",
    "Push triage failed"
  );
},
```

This status reports the triage run, not the health of the pushed commit.

## Test with replay

Replay a pull request you can read against your local agent, or save the
payloads as fixtures. For Data Center, pass the full pull request URL.
[`bitbucket replay`](/docs/reference/cli.md#replay-other-webhooks) lists
every flag.

```text
agent-sdk dev
agent-sdk bitbucket replay acme/widgets#42 --events '*'
agent-sdk bitbucket replay acme/widgets#42 --events '*' --dry-run --fixtures-dir fixtures/bitbucket
```

---

Source: /docs/guides/cloud-agents.md

# Cloud agents

The `cursor-cloud-agents` extension lets an Agent SDK agent hand coding work to
Cursor cloud agents. A cloud agent works in a repository checkout and pushes
branches and pull requests while your agent keeps talking to the person.

## Mount it

```ts
// bot/extensions/cloud.ts
import cursorCloudAgents from "@grok/agent-sdk/extensions/cursor-cloud-agents";

export default cursorCloudAgents({
  cloud: {
    repos: [{ url: "https://github.com/acme/api", startingRef: "main" }],
    autoCreatePR: true,
  },
});
```

Launches run under the host's signed-in account (`agent-sdk login`,
`CURSOR_API_KEY`, or `CURSOR_SERVICE_ACCOUNT_KEY`) and spend its cloud-agent
budget.

## What the agent can do

With the extension mounted, the agent can launch a cloud agent on a repository
or pull request, check its status, read its conversation and edits, steer or
stop it, and list the agents it launched. A bundled skill teaches the model when
to delegate and how to write a brief a cloud agent can act on alone.

Each caller can only see and steer the cloud agents they launched. If every
caller uses the same bearer token, the extension treats them all as one person.
Use [authored auth](/docs/reference/channels.md#auth-policies) to keep callers
apart.

## Configure it

| Option | What it controls |
| --- | --- |
| `cloud` | Launch defaults for repositories, environment, and pull request behavior. |
| `cloud.repos` | The repositories the agent may hand work to, each with a default branch or pull request. Without it, launches run with no repository, so secrets in `cloud.envVars` never reach a repository you didn't list. |
| `cloud.teamMcpServers` | Up to 50 team MCP server names that launched agents may use, matched case-insensitively. Omit it to allow every team server the account can use. Personal servers and `cloud.mcpServers` aren't filtered. |
| `model` | The model launched agents run. Omit it to use the account default. |
| `apiKey` | A Cursor API key to launch with instead of the host's own. |

## Adjust it

To remove a capability such as sending follow-ups, mount the extension as a
directory and disable that tool. See
[Adjust a mounted extension](/docs/reference/extensions.md#adjust-a-mounted-extension).

---

Source: /docs/guides/convert-automation.md

# Convert a Cursor Automation

Convert a dashboard Automation when you want to edit its behavior as code, test
it locally, or deploy it as an Agent SDK project. To change only the prompt or
the trigger, stay in the dashboard.

## Convert and run locally

`init --from automation:<url>` creates a project with the Automation's prompt,
model, and any schedules, channels, and account connections it can represent.
It prints warnings and setup steps for the rest.

```text
agent-sdk login
agent-sdk init --from \
  automation:https://cursor.com/automations/0a1b2c3d-0000-1111-2222-333344445555

cd nightly-triage
agent-sdk dev
```

Try the agent in the playground before you reconnect a live trigger. The
[CLI reference](/docs/reference/cli.md#convert-automation) covers the other
options.

## Reconnect account-backed actions

For an HTTP or SSE MCP server, the converter writes a connection by name,
without its URL or credentials. Connect the same server under MCP in Cursor,
sign in on the local host, and the agent uses its tools through your account.

```ts
// bot/mcp-connections/linear.ts
import { defineConnection } from "@grok/agent-sdk/connections";

export default defineConnection({
  cursorAccount: true,
  servers: ["Linear"],
});
```

## Bring a Slack trigger online

Once you create the [Slack app](/docs/guides/slack.md), the agent answers mentions in the
same thread. For a channel watch, also subscribe the app to channel posts:

```text
agent-sdk slack create --dir . --channel-posts
```

The converter keeps the watch allowlist. If a warning says the Automation also
filtered messages by text, add that check to the channel's `onChannelPost` hook.

## Bring a GitHub trigger online

The generated GitHub channel keeps simple filters such as PR actions, branches, and repositories, and a hook whose condition depended on dashboard-only context returns `null` until you replace it. This converted CI trigger wakes only when the suite fails:

```ts
import {
  defaultGitHubAuth,
  githubChannel,
} from "@grok/agent-sdk/channels/github";

export default githubChannel({
  cursorAccount: { repos: ["acme/api"] },
  webhookEvents: ["check_suite"],
  onCheckSuite: (ctx, suite) => {
    if (suite.conclusion !== "failure") {
      return null;
    }
    return { auth: defaultGitHubAuth(ctx) };
  },
});
```

Replay a failing PR before you connect live delivery:

```text
agent-sdk github replay acme/api#42 --dir . --events '*' --conclusion failure
```

Cursor-account delivery carries PR and comment events. Push, issue, and CI
triggers need webhook delivery through a GitHub App or `github forward`, as the
[GitHub guide](/docs/guides/github.md) explains.

## Work through the checklist

The converter doesn't carry every behavior over. Before you turn on a live
trigger, go through the warnings and setup steps it prints:

- A setup step names something you have to connect or configure.
- A `Verify:` step names a dashboard action with no matching tool. Run the agent
  and confirm the same external write happens.
- A `.todo.md` file marks a connection you have to replace.
- Credentials, webhook registration, and Automation memory never carry over.

Keep the source Automation enabled while you work, and when you [deploy](/docs/deployment.md), disable it before you turn on the same live triggers. The converter doesn't support Cursor-managed Automations.

---

Source: /docs/guides/github.md

# GitHub agents

A GitHub agent wakes from repo events, reads the latest PR and CI state,
then writes back reviews, comments, checks, or pushes. Every wake for the
same pull request resumes one session, so the open, later comments, and
CI results share one conversation.

Connect GitHub in Cursor for the repos you care about (Settings or
[cursor.com/dashboard](https://cursor.com/dashboard)), then sign the host
in with `agent-sdk login`. `cursorAccount` reuses that connection for
events and API calls, so you don't need a public webhook URL, a GitHub
App, or a personal access token.

For a complete reviewer or issue triager, start from the
[review and triage templates](/docs/templates/reviews.md).

## Review a pull request

When someone opens a pull request or marks it ready for review, the agent
starts a turn, reads the change, and posts its verdict as a review.

```ts
// bot/channels/github.ts
import { defaultGitHubAuth, githubChannel } from "@grok/agent-sdk/channels/github";

export default githubChannel({
  // Skip the agent's own comments so replies don't re-trigger it.
  botName: "my-agent",
  cursorAccount: { repos: ["acme/checkout"] },
  onPullRequest: (ctx, pr) => {
    if (pr.draft) {
      return null;
    }
    if (pr.action !== "opened" && pr.action !== "ready_for_review") {
      return null;
    }
    // `auth` starts a model turn running as the sender.
    return { auth: defaultGitHubAuth(ctx) };
  },
});
```

```ts
// bot/tools/submit_review.ts
import { parseGitHubPrContinuationKey } from "@grok/agent-sdk/channels/github";
import { defineTool } from "@grok/agent-sdk/tools";
import { z } from "zod";

export default defineTool({
  description:
    "Post the review decision on this session's pull request. Call once.",
  inputSchema: z.object({
    verdict: z.enum(["approve", "request_changes"]),
    summary: z
      .string()
      .describe("One or two sentences explaining the verdict."),
  }),
  async execute({ verdict, summary }, ctx) {
    if (ctx.session.purpose === "eval") {
      return { posted: false, reason: "eval" };
    }
    const ref = parseGitHubPrContinuationKey(ctx.session.continuationKey ?? "");
    if (ref === undefined) {
      throw new Error("submit_review requires a GitHub pull request session");
    }

    let event: "APPROVE" | "REQUEST_CHANGES" = "REQUEST_CHANGES";
    if (verdict === "approve") {
      event = "APPROVE";
    }
    const octokit = await ctx.host.github.getOctokit();
    await octokit.rest.pulls.createReview({
      owner: ref.owner,
      repo: ref.repo,
      pull_number: ref.number,
      event,
      body: summary,
    });
    return { posted: true, pr: `${ref.owner}/${ref.repo}#${ref.number}` };
  },
});
```

The event only tells you that the pull request changed. Read the title,
files, and checks during the turn.

## Answer a mention

`onComment` replaces the default `@botName` gate. Return a turn when the
comment is for this agent, then post the reply from a tool.

```ts
onComment: (ctx, comment) => {
  if (!comment.body.includes("@my-agent")) {
    return null;
  }
  return { auth: defaultGitHubAuth(ctx) };
},
```

```ts
// bot/tools/reply.ts
import { parseGitHubPrContinuationKey } from "@grok/agent-sdk/channels/github";
import { defineTool } from "@grok/agent-sdk/tools";
import { z } from "zod";

export default defineTool({
  description: "Post a comment on this session's pull request.",
  inputSchema: z.object({
    body: z.string().describe("Markdown to post on the PR timeline."),
  }),
  async execute({ body }, ctx) {
    if (ctx.session.purpose === "eval") {
      return { posted: false, reason: "eval" };
    }
    const ref = parseGitHubPrContinuationKey(ctx.session.continuationKey ?? "");
    if (ref === undefined) {
      throw new Error("reply requires a GitHub pull request session");
    }

    const octokit = await ctx.host.github.getOctokit();
    await octokit.rest.issues.createComment({
      owner: ref.owner,
      repo: ref.repo,
      issue_number: ref.number,
      body,
    });
    return { posted: true };
  },
});
```

The tool reads the pull request from the session, so the model never
picks where the comment goes.

## Triage a failing check

When CI fails, this hook labels the pull request with host code and no
model turn. Return `{ auth: defaultGitHubAuth(ctx) }` instead when the
agent should read the logs and decide what to do.

```ts
onCheckSuite: (ctx, suite) => {
  if (suite.conclusion !== "failure") {
    return null;
  }
  // `task` runs host code instead of a model turn.
  return { task: () => labelFailure(ctx, suite) };
},
```

```ts
async function labelFailure(ctx, suite) {
  const pr = suite.pullRequests[0];
  if (pr === undefined) {
    return;
  }
  const octokit = await ctx.github.getOctokit();
  await octokit.rest.issues.addLabels({
    owner: ctx.repository.owner,
    repo: ctx.repository.name,
    issue_number: pr.number,
    labels: ["ci-failed"],
  });
}
```

## Events

Declare a hook for each event that should wake the agent:
`onPullRequest`, `onComment`, `onIssue`, `onCheckSuite`, `onCheckRun`,
`onWorkflowRun`, `onStatus`, and the catch-all `onEvent`. Each hook
returns `{ auth }`, `{ task }`, or `null`, as
[handler results](/docs/reference/channels.md#handler-results) describes.

## Choose watched repositories

The examples above list their repositories with
`cursorAccount: { repos: [...] }`, and every repository in one channel
must share one GitHub owner. To keep the list out of source, set
`cursorAccount: true` and pass `--repo` to `agent-sdk serve`. A hosted
deployment watches the repositories you pass to `agent-sdk deploy` in
both cases.

```text
agent-sdk serve --dir . --cursor-events \
  --repo acme/checkout --repo acme/payments
agent-sdk deploy --dir . \
  --cursor-events-repo acme/checkout \
  --cursor-events-repo acme/payments
```

Each deploy replaces the previous list, so pass every repository each
time. Use [`agent-sdk deployment update`](/docs/reference/cli.md#deployment-update)
to change the list without redeploying.

## Call GitHub from deterministic code via `ctx`

With `cursorAccount`, hooks (`ctx.github.getOctokit()`) and tools (`ctx.host.github.getOctokit()`) share one credential. Hooks can also use `ctx.thread.post()`, `ctx.repository`, and `ctx.conversation`.

This hook reads the pull request, skips a draft, and posts before the model turn:

```ts
onPullRequest: async (ctx, pr) => {
  if (pr.action !== "opened") {
    return null;
  }
  const octokit = await ctx.github.getOctokit();
  const { data } = await octokit.rest.pulls.get({
    owner: ctx.repository.owner,
    repo: ctx.repository.name,
    pull_number: pr.number,
  });
  if (data.draft) {
    return null;
  }
  await ctx.thread.post(`Reviewing ${pr.url}`);
  return { auth: defaultGitHubAuth(ctx) };
},
```

## Batch events

GitHub can send many events for one pull request within a few seconds. Coalesce them per pull request:

```ts
const pending = new Map();

function enqueue(pr, flush) {
  const key = `${pr.owner}/${pr.repo}#${pr.number}`;
  const existing = pending.get(key);
  if (existing !== undefined) {
    clearTimeout(existing);
  }
  pending.set(
    key,
    setTimeout(() => {
      pending.delete(key);
      flush();
    }, 3_000)
  );
}
```

Persist that buffer across restarts, and drop it when the pull request closes.

## Test with replay

`agent-sdk github replay` reads a pull request you can access and posts
GitHub-shaped events to your channel.

```text
agent-sdk github replay https://github.com/acme/checkout/pull/42 --dir . --action opened

# Replay everything the channel listens for, with CI failing
agent-sdk github replay acme/checkout#42 --dir . --events '*' --conclusion failure

# Save the payloads as fixtures instead of posting them
agent-sdk github replay acme/checkout#42 --dir . --events '*' --dry-run --fixtures-dir fixtures/github
```

## Other hosts

The same hooks work on GitHub Enterprise Server, and on github.com when
you already receive GitHub webhooks yourself. Point the channel at your
server:

```ts
export default githubChannel({
  api: {
    apiBaseUrl: "https://github.example.com/api/v3",
  },
  credentials: {
    token: { secretName: "GITHUB_TOKEN" },
    webhookSecret: { secretName: "GITHUB_WEBHOOK_SECRET" },
  },
});
```

Add a repository webhook for
`https://<your-host>/<slug>/v1/channels/github` with the same secret.
On Cursor hosting, a delivery with a missing or wrong signature gets a
`401` and never reaches your agent. Event pull and `github replay` only
reach github.com, so test Enterprise Server with saved fixtures.

Pass each credential as `{ secretName: "NAME" }`. When you run locally, the channel reads the environment variable with that name, and on Cursor hosting it reads the deployment secret with that name. To set those secrets on a deployment, see [Add egress and secrets](/docs/deployment.md#add-egress-and-secrets).

## Related

- [Build a PR reviewer](/docs/quickstart.md) covers the full loop.
- Ask a coding agent to follow the `bdk-github` skill for the GitHub setup steps.

---

Source: /docs/guides/gitlab.md

# GitLab agents

A GitLab agent wakes on merge request, note, and pipeline events from
GitLab.com or a self-managed server, then writes its results back to the
project. Declare `onMergeRequest`, `onNote`, or `onPipeline` on
`gitlabChannel()` for each event that should wake it.

## Connect a project

Name the project hook secret and list the projects that may wake the
agent. Set `apiBaseUrl` for a self-managed server, and leave it out for
GitLab.com.

```ts
// bot/channels/gitlab.ts
import { gitlabChannel } from "@grok/agent-sdk/channels/gitlab";

export default gitlabChannel({
  projects: ["acme/platform"],
  webhookSecret: { secretName: "GITLAB_WEBHOOK_SECRET" },
  apiBaseUrl: "https://gitlab.example.com/api/v4",
});
```

Add a project webhook for
`https://<your-host>/<slug>/v1/channels/gitlab` with the same secret. On
Cursor hosting, a delivery with the wrong secret gets a `401` and never
reaches your agent. `ctx.gitlab` calls the API with the token in
`GITLAB_TOKEN`. To set both secrets on a deployment, see
[Add egress and secrets](/docs/deployment.md#add-egress-and-secrets).

For GitLab.com on Cursor hosting, you can skip the project webhook. Set
`cursorAccount: { projects: ["acme/platform"] }`, and events and
`ctx.gitlab` calls go through your signed-in Cursor account.

## Review a merge request

Read the latest diff, then post the finished review as a note:

```ts
// bot/channels/gitlab.ts
import {
  createGitLabApi,
  defaultGitLabAuth,
  gitlabChannel,
} from "@grok/agent-sdk/channels/gitlab";
import { postMergeRequestNote } from "../lib/gitlab.js";

export default gitlabChannel({
  projects: ["acme/platform"],
  onMergeRequest: async (ctx, mergeRequest) => {
    if (mergeRequest.draft) {
      return null;
    }
    const hasNewCommits =
      mergeRequest.action === "update" && mergeRequest.oldrev !== undefined;
    if (
      mergeRequest.action !== "open" &&
      mergeRequest.action !== "reopen" &&
      !hasNewCommits
    ) {
      return null;
    }

    const api = createGitLabApi(
      ctx.gitlab,
      ctx.project.pathWithNamespace
    );
    const diffs = await api.listMergeRequestDiffs(mergeRequest.iid);
    return {
      // `auth` starts a model turn running as the event sender.
      auth: defaultGitLabAuth(ctx),
      message: `Review ${ctx.project.pathWithNamespace}!${mergeRequest.iid}.`,
      context: [`<gitlab_diffs>${JSON.stringify(diffs)}</gitlab_diffs>`],
    };
  },
  events: {
    async "message.completed"(event, channel, ctx) {
      if (ctx.session.purpose === "eval") {
        return;
      }
      if (event.data.finishReason === "tool_call" || event.data.text === "") {
        return;
      }
      const mergeRequestIid = channel.state.mergeRequestIid;
      if (
        (channel.state.eventType !== "merge_request" &&
          channel.state.eventType !== "note") ||
        mergeRequestIid === null
      ) {
        return;
      }
      await postMergeRequestNote(
        channel.state.projectPath,
        mergeRequestIid,
        event.data.text
      );
    },
  },
});
```

`postMergeRequestNote` is a helper you write that posts through the GitLab API with `GITLAB_TOKEN`. It uses the project and merge request stored from the event.

## Answer a note

Skip the agent's own notes, and answer a note that mentions the agent:

```ts
const AGENT_USERNAME = "cursor-reviewer";

onNote: (ctx, note) => {
  if (note.author?.username === AGENT_USERNAME) {
    return null;
  }
  if (!note.body.includes(`@${AGENT_USERNAME}`)) {
    return null;
  }
  return {
    auth: defaultGitLabAuth(ctx),
    message: note.body,
  };
},
```

Add this property to the same `gitlabChannel()` as the review handler.

## Triage a failed pipeline

When a pipeline fails, the example marks the commit status as `running`
and starts a triage turn. When the turn finishes, it sets the status to
success or failure.

```ts
// bot/channels/gitlab.ts
import {
  defaultGitLabAuth,
  gitlabChannel,
  gitlabChannelStateForEvent,
} from "@grok/agent-sdk/channels/gitlab";
import { setPipelineTriageStatus } from "../lib/gitlab.js";

export default gitlabChannel({
  projects: ["acme/platform"],
  webhookSecret: { secretName: "GITLAB_WEBHOOK_SECRET" },
  onPipeline: (ctx, pipeline) => {
    if (pipeline.status !== "failed" || pipeline.sha === undefined) {
      return null;
    }
    const sha = pipeline.sha;
    return {
      // `task` runs host code, and `send` starts the model turn.
      task: async ({ send }) => {
        await ctx.gitlab.setCommitStatus({
          sha,
          state: "running",
          name: "cursor/pipeline-triage",
          description: "Investigating failed pipeline",
        });
        await send(`Triage failed GitLab pipeline ${pipeline.id}.`, {
          auth: defaultGitLabAuth(ctx),
          state: {
            ...gitlabChannelStateForEvent({
              project: ctx.project,
              headSha: sha,
              eventType: ctx.eventType,
            }),
          },
        });
      },
    };
  },
  events: {
    async "message.completed"(event, channel, ctx) {
      if (
        ctx.session.purpose === "eval" ||
        channel.state.eventType !== "pipeline" ||
        channel.state.headSha === null ||
        event.data.finishReason === "tool_call"
      ) {
        return;
      }
      await setPipelineTriageStatus(channel.state.projectPath, channel.state.headSha, "success");
    },
    async "turn.failed"(_event, channel, ctx) {
      if (
        ctx.session.purpose === "eval" ||
        channel.state.eventType !== "pipeline" ||
        channel.state.headSha === null
      ) {
        return;
      }
      await setPipelineTriageStatus(channel.state.projectPath, channel.state.headSha, "failed");
    },
  },
});
```

`setPipelineTriageStatus` is a small helper you write in
`bot/lib/gitlab.ts`. It updates the same `cursor/pipeline-triage` status
through the GitLab API with `GITLAB_TOKEN`, using the project and commit
from the session state, so the model never picks where it writes.

Each hook returns `{ auth }`, `{ task }`, or `null`, as
[handler results](/docs/reference/channels.md#handler-results) describes.
Every event and note on one merge request resumes the same session.

## Test with replay

Replay a merge request you can read against your local agent, or save the
payloads as fixtures.
[`gitlab replay`](/docs/reference/cli.md#replay-other-webhooks) lists every
flag.

```text
agent-sdk dev
agent-sdk gitlab replay acme/platform!42 --events pipeline --conclusion failed
agent-sdk gitlab replay acme/platform!42 --events '*' --dry-run --fixtures-dir fixtures/gitlab
```

---

Source: /docs/guides/improve.md

# Self-improvement

The `improve` extension lets an agent turn "change how you work" into a change
to its own source. The agent writes a brief, a Cursor cloud agent edits a
checkout and opens a pull request, and the agent reports the pull request.

## Mount it

Mount [Cloud agents](/docs/guides/cloud-agents.md) with the agent's own repository in
`cloud.repos`, then mount `improve` with its source directory.

```ts
// bot/extensions/cloud.ts
import cursorCloudAgents from "@grok/agent-sdk/extensions/cursor-cloud-agents";

export default cursorCloudAgents({
  cloud: {
    repos: [{ url: "https://github.com/acme/agents", startingRef: "main" }],
    autoCreatePR: true,
  },
});
```

```ts
// bot/extensions/improve.ts
import improve from "@grok/agent-sdk/extensions/improve";

export default improve({
  repo: "https://github.com/acme/agents",
  path: "agents/concierge",
  ref: "main",
});
```

`improve` adds the `improve__yourself` skill and no tools.

## Ask for one change

Say what to change, how to tell it worked, and what must not change.

> Improve yourself. Stop opening incident replies with a summary. Start with
> the decision, keep the existing evidence bullets, and preserve every current
> eval.

The agent launches one cloud agent and reports its URL. When the pull request
opens, the agent tells you what changed and which checks passed.

## Review, merge, then deploy

The agent can open a pull request, but it can't merge or deploy. Someone on your
team reviews and merges it, and the change goes live on your next deploy.
Afterward, repeat the original request to confirm the change.

## Configure it

- `repo` is the source repository URL.
- `path` is the agent's directory in that repository.
- `ref` is the branch new pull requests target, `main` by default.
- `notes` adds guidance to every brief, such as a required test command.
- `deploy` tells the skill who deploys a merged change and when.

Validation checks only the shape of `repo` and `path`, so make sure they match
the deployed agent and `cloud.repos`.

---

Source: /docs/guides/jev.md

# Use Jev in review tools

> [!WARNING]
> Experimental. `@grok/agent-sdk/extensions/jev` is experimental. Its
> exports and behavior can change in any release, without a deprecation
> period.

Use Jev when a review workflow needs a typed decision instead of prose. Jev
returns a choice, a score, or a probability, and your TypeScript decides what
happens next.

Set `TYPESAFE_API_KEY`, then mount the extension. The default model is
`jev-latest`.

```ts
// bot/extensions/jev.ts
import jev from "@grok/agent-sdk/extensions/jev";

export default jev();
```

On Cursor hosting, the mount adds `api.typesafe.ai` to egress and
`TYPESAFE_API_KEY` to the expected secrets. Without the mount,
[add `api.typesafe.ai`](/docs/deployment.md#add-egress-and-secrets) to
`hosting.egressDomains` yourself.

## Start only the review turns you need

Call Jev from a channel hook before a model turn starts. This hook asks Jev
whether a new pull request needs security review, based on its title, body, and
file names. Below the threshold, the hook returns `null`, so the agent doesn't
run or post anything.

```ts
// bot/channels/github.ts
import {
  defaultGitHubAuth,
  githubChannel,
} from "@grok/agent-sdk/channels/github";
import { above, decide } from "@grok/agent-sdk/extensions/jev";

export default githubChannel({
  cursorAccount: { repos: ["acme/checkout"] },
  onPullRequest: async (ctx, pr) => {
    if (pr.action !== "opened" && pr.action !== "ready_for_review") {
      return null;
    }

    const octokit = await ctx.github.getOctokit();
    const files = await octokit.paginate(octokit.rest.pulls.listFiles, {
      owner: ctx.repository.owner,
      repo: ctx.repository.name,
      pull_number: pr.number,
    });
    const answers = await decide({
      state: {
        title: pr.title,
        body: pr.body,
        files: files.map(file => file.filename),
      },
      questions: {
        review: {
          type: "boolean",
          instructions: "Does this change touch auth, permissions, secrets, or external input?",
        },
      },
    });

    if (!above(answers.review, 0.8)) {
      return null;
    }
    return { auth: defaultGitHubAuth(ctx) };
  },
});
```

## Filter findings before you post them

Let the model draft a finding, then ask Jev whether it's a real bug in the new
code. The tool posts only the drafts that clear your threshold.

```ts
// bot/tools/post_finding.ts
import { parseGitHubPrContinuationKey } from "@grok/agent-sdk/channels/github";
import { above, decide } from "@grok/agent-sdk/extensions/jev";
import { defineTool } from "@grok/agent-sdk/tools";
import { z } from "zod";

export default defineTool({
  description: "Post one security finding on this session's pull request.",
  inputSchema: z.object({
    summary: z.string(),
    draft: z.string().describe("The finding, in one or two sentences."),
  }),
  async execute({ summary, draft }, ctx) {
    if (ctx.session.purpose === "eval") {
      return { posted: false, reason: "eval" };
    }

    const ref = parseGitHubPrContinuationKey(ctx.session.continuationKey ?? "");
    if (ref === undefined) {
      throw new Error("post_finding requires a GitHub pull request session");
    }

    const answers = await decide({
      state: { summary, draft },
      questions: {
        real: {
          type: "boolean",
          instructions: "Is the draft an exploitable bug in the new code?",
        },
      },
    });

    if (!above(answers.real, 0.85)) {
      return { posted: false, reason: "clean" };
    }

    const octokit = await ctx.host.github.getOctokit();
    await octokit.rest.pulls.createReview({
      owner: ref.owner,
      repo: ref.repo,
      pull_number: ref.number,
      event: "COMMENT",
      body: draft,
    });
    return { posted: true };
  },
});
```

## Approve changes by risk tier

Ask Jev for one risk tier. Approve only a confident `very-low` or `low`.

```ts
import { decide, needsHuman } from "@grok/agent-sdk/extensions/jev";

const answers = await decide({
  state: { title, summary },
  questions: {
    risk: {
      type: "choice",
      instructions: "What risk tier is this pull request?",
      criteria: {
        "very-low": "docs, formatting, or a mechanical rename",
        low: "a local change with tests and no new trust boundary",
        medium: "auth, billing, or a behavior change callers depend on",
        high: "a likely exploit, data loss, or a broken public contract",
      },
    },
  },
});

const tier = answers.risk.choice;
if (needsHuman(answers.risk) || tier === "medium" || tier === "high") {
  return { verdict: "hold", tier };
}
return { verdict: "approve", tier };
```

Your tool resolves the pull request from the session and posts that verdict, as the [Agentic Owners template](/docs/templates/agentic-owners.md) does.

## Write tools with Jev

Call `decide` to get the answer map, or `evaluate` to get `{ answers }`. Use a
boolean for a yes-or-no gate, a choice for a closed set such as risk tiers, and
a score for an ordered rubric. `above` returns `true` when a probability or
score meets your threshold. `needsHuman` returns `true` when the answer's
confidence falls below `0.7` by default. Calibrate both on pull requests you've
already labeled.

Ask Jev one question per call, and write the rule that combines the answers in
your own code.

## Let the agent ask Jev

The mount also gives the model a read-only tool named `<namespace>__evaluate`,
which is `jev__evaluate` with the mount shown earlier. The model can call it
during a turn, and it never posts or approves anything. When the answer and the
write belong together, call `decide` inside a project tool instead.

## Choose what to mount

Mounting Jev also adds two skills. `jev__questions` teaches the model to write
atomic questions and read the answers, and `jev__gated-write` teaches it to put
`decide` and the write in one server tool. Set `harnessTools: false` to keep Jev
inside your own tools, or `skills: false` if your agent has its own Jev
instructions.

```ts
export default jev({ harnessTools: false, skills: true });
```

Neither switch removes the exported helpers, so project tools can still import
`decide`, `above`, and `needsHuman`.

---

Source: /docs/guides/mcp-oauth.md

# Connect a private MCP server with OAuth

Host MCP OAuth lets the machine running your agent authorize a remote
MCP server without putting tokens in your source or workspace.

## Use a private MCP tool locally

Declare the server and authorize it once in the browser.

```ts
// bot/mcp-connections/inventory.ts
import { defineConnection } from "@grok/agent-sdk/connections";

export default defineConnection({
  url: "https://mcp.example.com/inventory",
  oauth: true,
  advertiseTools: true,
});
```

```bash
agent-sdk mcp oauth inventory
```

The filename is the connection name. If you change the server URL, run
`mcp oauth` again.

## Connect on first use

Without a prior `mcp oauth`, the first call to the server pauses the
turn, and the playground shows **Connect**. Finish the browser flow,
and the call retries in the running process.

A turn waits for Connect only when a person can click it:

- A playground or HTTP chat turn.
- A Slack message, when the channel shows Connect through
  `slackChannel({ toolApprovals: true })` or `buildToolApprovalEvents`
  in `events`.

The wait stops after 10 minutes or when the turn is interrupted.
Schedules, reminders, webhooks, evals, and Slack channels without
Connect don't wait. A required connection fails the call with
`Reconnect <connection>`, and an `optional` one is skipped, even in the
playground. Authorize those ahead of time with `mcp oauth`.

## Keep privileged tools behind a wrapper

The model never sees a connection under `bot/host-connections/`. A
server tool reaches it through `ctx.host.mcp` and exposes one narrow
operation.

```ts
// bot/host-connections/inventory.ts
import { defineConnection } from "@grok/agent-sdk/connections";

export default defineConnection({
  url: "https://mcp.example.com/inventory",
  oauth: true,
  tools: ["lookup_stock"],
});
```

```ts
// bot/tools/get_stock.ts
import { defineTool } from "@grok/agent-sdk/tools";
import { z } from "zod";

export default defineTool({
  description: "Read stock for one approved region.",
  inputSchema: z.object({
    sku: z.string(),
    region: z.enum(["us-east", "us-west"]),
  }),
  async execute(input, ctx) {
    const result = await ctx.host.mcp.callTool(
      "inventory",
      "lookup_stock",
      input
    );
    return {
      ok: result.isError !== true,
      stock: result.structuredContent ?? null,
    };
  },
});
```

## Use an OAuth server from a hosted agent

Tokens from `mcp oauth` stay on the machine that authorized them. For a
Cursor-hosted agent, add the server under MCP in the Cursor dashboard,
reach it through a
[`cursorAccount: true` connection](/docs/reference/connections.md#cursor-account-mcp-connection),
and connect it from [Deployed Agents](https://cursor.com/dashboard/deployed-agents).
Anyone who can use the agent can use that connection, so narrow its
`servers` list.

## Choose where credentials live

| Connection | Credentials live in | Use it when |
| --- | --- | --- |
| `cursorAccount: true` | Cursor's backend | You connected the server in the Cursor dashboard |
| `url` with `oauth: true` | The serve host | The serve host should own access |
| `url` with `headers` | Your secrets | The server takes an API key or bearer token |

Host OAuth can't share a connection with per-session `auth`, which
gives each caller its own downstream identity.

## Related

- Follow the `bdk-mcp-auth` skill, then run `agent-sdk mcp oauth <connection>`.
- [MCP connections](/docs/reference/connections.md)

---

Source: /docs/guides/migrate.md

# Migrate an early-alpha project to architecture v2

Use this guide only for an Agent SDK project deployed on architecture v1 during
the early alpha. Every other project already runs on v2.

## Check whether your project needs migration

Your project needs migration if any of these is true:

- `package.json` depends on `@cursor/july`.
- A script or CI job runs the early-alpha CLI.
- The agent sets `architecture: "v1"` or `runtime: "grokbot"`.
- The project has `bot/storage.ts` or `agent/storage.ts`.
- An SCM channel credential is a literal value.
- Code uses a `/v1` continuation selector.
- A reminder uses an anonymous `run` or `until` function.
- A peer connection reads its URL from a `*_MCP_URL` secret.
- `agent-sdk deployment get <slug>` doesn't print `architecture: v2`.

`agent-sdk migrate` checks the source for these signals and prints
`Nothing to migrate.` when it finds none. It doesn't check scripts, CI jobs, or
the hosted deployment.

To swap the early-alpha package, run:

```bash
npm uninstall @cursor/july
npm install @grok/agent-sdk@latest
```

An existing `agent/` folder still works.

## Start the migration

Commit and push the source first. `agent-sdk migrate` starts a Cursor cloud
agent on that pushed ref, and the agent opens a pull request without deploying.

```bash
agent-sdk login
agent-sdk migrate --dir .
```

To migrate a pushed source outside your checkout, pass `--repo`, `--ref`, and
`--path`. The [CLI reference](/docs/reference/cli.md#migrate) lists every flag.

You can also ask a coding agent in your checkout to follow the bundled `bdk-migrate-v2` skill. If the project
relies on a v1 capability that has no v2 path, the agent stops and names the
blocker.

## Review the pull request

Before you merge, check that the pull request:

- Uses `@grok/agent-sdk` and `agent-sdk`, and removes `architecture: "v1"`.
- Moves durable state to `ctx.host.kv` or `ctx.host.files`, reads secrets inside
  the handler that needs them, and expects hosted routes to return `202`.
- Passes every SCM credential as `{ secretName: "NAME" }`. `agent-sdk validate`
  warns about a literal or `process.env` getter while `architecture: "v1"` is
  set, and fails once you remove it.
- Replaces each peer URL secret, such as `BUILDER_MCP_URL`, with
  `defineConnection({ agent: "<slug>" })`. Check each slug and any TODO the
  agent left.
- Lists unsupported behavior, follow-ups, and the checks it ran or handed off.

## Choose a cutover

| Cutover | What happens | Choose it when |
| --- | --- | --- |
| Same slug | v2 copies the v1 secrets and replaces v1 once it starts. You can't undo it, and it deletes v1's hosted sessions, KV, files, and eval runs. | A short outage and an empty v2 state are fine. |
| New slug | v1 keeps serving while you test v2. Move senders to the new URL, then delete v1. | You use Slack, webhooks, or hosted state. |

With a new slug, point the trial at a test repository so the two deployments
don't both act on the same events.

## Deploy after merge

Deploy from the merged, pushed ref. Reuse every `--cursor-events-repo` value
from v1, and move every `--allow-domain` value into `hosting.egressDomains`.

```bash
agent-sdk deploy --dir . --slug <existing-or-new-slug>
```

Then follow the checklist that `agent-sdk migrate` printed:

1. Deploy each agent folder separately by running
   `agent-sdk deploy --dir <agent-folder>` once for each one.
2. For each Slack agent, run
   `agent-sdk slack create --env prod --dir <agent-folder>`. Then uninstall the
   old app and invite the new bot to its channels.
3. For a [peer connection](/docs/guides/agent-to-agent.md), deploy both agents to the same
   team. Then delete each peer URL secret the pull request lists with
   `agent-sdk secret delete <slug> NAME`.
4. For a new slug, add each secret with `agent-sdk secret set <slug> NAME`. It
   applies without a redeploy.

Finally, [verify a hosted turn](/docs/deployment.md).

---

Source: /docs/guides/opentelemetry.md

# OpenTelemetry

Agent SDK sends traces, metrics, and logs to an OTLP collector when you
set the `OTEL_*` variables. Record your own with `@opentelemetry/api`.

```bash
export OTEL_EXPORTER_OTLP_ENDPOINT=https://otel.example.com
export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer%20$OTEL_TOKEN"
export OTEL_SERVICE_NAME=incident-agent
agent-sdk serve --dir . --dev
```

- The default protocol is `http/protobuf`. `http/json` also works, and
  `grpc` logs a warning and falls back to `http/protobuf`.
- Metrics use delta temporality. Grafana Cloud and Prometheus reject
  delta, so set `OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE=cumulative`
  when you export to either one.
- If you don't set an endpoint, Agent SDK doesn't export anything.
- You can create a meter, tracer, or logger at the top of
  `bot/agent.ts`, and it will use Agent SDK's providers.

## Variables

| Variable | Effect |
| --- | --- |
| `OTEL_EXPORTER_OTLP_ENDPOINT` | Base URL. Appends `/v1/traces`, `/v1/metrics`, `/v1/logs`. |
| `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` | Traces URL, used as written. Overrides the base URL. |
| `OTEL_EXPORTER_OTLP_METRICS_ENDPOINT` | Metrics URL, used as written. Overrides the base URL. |
| `OTEL_EXPORTER_OTLP_LOGS_ENDPOINT` | Logs URL, used as written. Overrides the base URL. |
| `OTEL_EXPORTER_OTLP_HEADERS` | Sent on every export. URL-encode values. Reread on each export. |
| `OTEL_EXPORTER_OTLP_PROTOCOL` | `http/protobuf` (default) or `http/json`. |
| `OTEL_SERVICE_NAME` | `service.name`. Defaults to the agent name, then `unknown_service:node`. |
| `OTEL_RESOURCE_ATTRIBUTES` | `service.namespace`, `service.version`, `deployment.environment.name`. |
| `OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE` | `delta` (default), `cumulative`, or `lowmemory`. |
| `OTEL_LOGS_EXPORTER` | `otlp` exports logs. Unset, `none`, and `console` do not. |
| `OTEL_SDK_DISABLED` | `true` turns export off. |
| `OTEL_TRACES_EXPORTER` | `none` turns traces off. |
| `OTEL_METRICS_EXPORTER` | `none` turns metrics off. |
| `OTEL_LOG_USER_PROMPTS` | `1` adds prompt text to log records. |
| `OTEL_LOG_TOOL_CONTENT` | `1` adds tool payloads to log records. |

## Hosted agents

```ts
export default defineAgent({
  hosting: {
    egressDomains: ["otlp-gateway-prod-us-east-0.grafana.net"],
    secretNames: [
      "OTEL_EXPORTER_OTLP_ENDPOINT",
      "OTEL_EXPORTER_OTLP_HEADERS",
      "OTEL_SERVICE_NAME",
    ],
  },
});
```

`service.name` is fixed by the first export, so redeploy to change it.

## Record and tag

```ts
import { metrics, trace } from "@opentelemetry/api";
import { logs } from "@opentelemetry/api-logs";
import { currentConversationId } from "@grok/agent-sdk/otel";

const tracer = trace.getTracer("incident-agent");
const lookups = metrics.getMeter("incident-agent").createCounter("incident.lookups");
const logger = logs.getLogger("incident-agent");

lookups.add(1, { "cursor.conversation.id": currentConversationId() });
```

Spans started during a turn get `cursor.conversation.id`, and a metric
gets it only when you set it.

To tag a whole turn, call `ctx.telemetry` from a tool. Spans in the
turn get the trace keys, Agent SDK's log records get the log keys, and
`cursor.token.usage` gets the metric keys. Outside a tool, use
`telemetry` from `@grok/agent-sdk/telemetry`.

```ts
async execute({ key }, ctx) {
  ctx.telemetry.setTraceAttributes({ "work.item": key });
  ctx.telemetry.setMetricAttributes({ "work.item": key });
  ctx.telemetry.setLogAttributes({ "work.item": key });
  return await jira.getIssue(key);
}
```

Grafana renames a `.` in a metric key to `_`.

```promql
sum by (work_item) (cursor_token_usage_total)
```

- Each distinct metric value is a new series, so never use a per-turn
  ID, a secret, an email, or a raw user ID.
- A session holds up to 32 keys of each kind, with keys up to 128
  characters and values up to 256.

## Limits

- Agent SDK doesn't support custom span processors, samplers, views,
  your own OpenTelemetry SDK, or library auto-instrumentation. It reads
  `OTEL_TRACES_SAMPLER` and `OTEL_METRIC_EXPORT_INTERVAL` once at
  startup, so restart the process after you change either one.
- Import Agent SDK before any other OpenTelemetry setup. Otherwise the
  turn's tags may not reach your spans.

## Migrating from serve({ otel })

Delete `serve({ otel })`, `bot/otel.ts`, `bot/instrumentation.ts`, and
any `defineOtel` call. Set these variables instead.

| Old option | Set this instead |
| --- | --- |
| `serviceName` | `OTEL_SERVICE_NAME` |
| `serviceVersion` | `service.version` in `OTEL_RESOURCE_ATTRIBUTES` |
| `resourceAttributes` | `OTEL_RESOURCE_ATTRIBUTES`, comma-separated, URL-encoded values |
| `exporters[].url` | `OTEL_EXPORTER_OTLP_ENDPOINT` (appends `/v1/traces`, `/v1/metrics`, `/v1/logs`) |
| `exporters[].url` with `signals` | `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT`, `OTEL_EXPORTER_OTLP_METRICS_ENDPOINT`, or `OTEL_EXPORTER_OTLP_LOGS_ENDPOINT`, used as written |
| `exporters[].headers` | `OTEL_EXPORTER_OTLP_HEADERS`, comma-separated, URL-encoded values |
| `exporters[].protocol` | `OTEL_EXPORTER_OTLP_PROTOCOL`, or `OTEL_EXPORTER_OTLP_TRACES_PROTOCOL`, `OTEL_EXPORTER_OTLP_METRICS_PROTOCOL`, `OTEL_EXPORTER_OTLP_LOGS_PROTOCOL` |
| `exporters[].signals` | The per-signal URL. `OTEL_TRACES_EXPORTER=none`, `OTEL_METRICS_EXPORTER=none`, or `OTEL_LOGS_EXPORTER=none` stops that signal. |
| `traces: false` | `OTEL_TRACES_EXPORTER=none` |
| `traces: true` | Leave `OTEL_TRACES_EXPORTER` unset. Traces start when an endpoint is set. |
| `traces.sampleRatio` | `OTEL_TRACES_SAMPLER=parentbased_traceidratio` and `OTEL_TRACES_SAMPLER_ARG`, before process start |
| `metrics: false` | `OTEL_METRICS_EXPORTER=none` |
| `metrics: true` | Leave `OTEL_METRICS_EXPORTER` unset. Metrics start when a metrics URL is set. |
| `metrics.exportIntervalMillis` | `OTEL_METRIC_EXPORT_INTERVAL`, milliseconds, before process start |
| `logs: false` | Unset `OTEL_LOGS_EXPORTER`, or set `none` |
| `logs: true` | `OTEL_LOGS_EXPORTER=otlp` |
| `logs.userPrompts` | `OTEL_LOGS_EXPORTER=otlp` and `OTEL_LOG_USER_PROMPTS=1` |
| `logs.toolContent` | `OTEL_LOGS_EXPORTER=otlp` and `OTEL_LOG_TOOL_CONTENT=1` |
| `otel: false` | No endpoint, or `OTEL_SDK_DISABLED=true` |

Set the hosted values after deploy:

```bash
agent-sdk secret set my-agent OTEL_SERVICE_NAME OTEL_EXPORTER_OTLP_ENDPOINT OTEL_EXPORTER_OTLP_HEADERS
```

---

Source: /docs/guides/pagerduty.md

# PagerDuty agents

A PagerDuty agent wakes when an incident triggers, investigates it, and writes what it found back to
the incident. `pagerdutyChannel()` verifies PagerDuty's v3 webhooks and sends every later event for
that incident to the session that started the investigation.

## Investigate a new incident

When an incident triggers, the agent starts a turn with the incident's title, service, urgency, and
link.

```ts
// bot/channels/pagerduty.ts
import { defaultPagerDutyAuth, pagerdutyChannel } from "@grok/agent-sdk/channels/pagerduty";

export default pagerdutyChannel({
  deliverReplies: true,
  onIncident: (ctx, event) => {
    if (event.eventType !== "incident.triggered") {
      return null;
    }
    return {
      // `auth` starts a model turn running as the event's PagerDuty actor.
      auth: defaultPagerDutyAuth(ctx),
      message: `Investigate PagerDuty incident #${event.incident.number}: ${event.incident.title}`,
      context: [`Service: ${event.incident.service?.summary ?? "unknown"}`],
    };
  },
});
```

With `deliverReplies: true`, the channel posts each reply as a note on the incident. It doesn't post
anything until it knows the agent's own PagerDuty user, because each note comes back as a webhook
event and would otherwise start another turn. Handlers you write in `events` replace the built-in
ones.

Hooks also get `ctx.pagerduty`, a REST client with the channel's credentials. Use the incident from
the event or channel state when you write, so the model can't choose the target.

```ts
onIncident: async (ctx, event) => {
  const incident = await ctx.pagerduty.getIncident(event.incident.id);
  // …
},
```

On Cursor hosting, declare PagerDuty's API host and the token:

```ts
// bot/agent.ts
import { defineAgent } from "@grok/agent-sdk";

export default defineAgent({
  hosting: {
    egressDomains: ["api.pagerduty.com"],
    secretNames: ["PAGERDUTY_TOKEN"],
  },
});
```

## Follow the incident in one session

People acknowledge, annotate, reassign, and resolve an incident while the agent investigates. Return
a dispatch for those events too, and each one reaches the session already working on the incident.

```ts
onIncident: (ctx, event) => {
  const followUps = ["incident.acknowledged", "incident.reassigned", "incident.resolved"];
  if (event.eventType !== "incident.triggered" && !followUps.includes(event.eventType)) {
    return null;
  }
  return {
    auth: defaultPagerDutyAuth(ctx),
    message: `${event.eventType} by ${ctx.agent?.summary ?? "PagerDuty"}`,
  };
},
onNote: (ctx, note) => {
  return {
    auth: defaultPagerDutyAuth(ctx),
    message: `Note on the incident: ${note.content}`,
  };
},
```

Add both to the same `pagerdutyChannel()` call. A re-trigger after a resolve is a new PagerDuty
incident, so it starts a new session. `ctx.incidentKey` carries the alert's dedup key, so the agent
can recall what it found the last time the same alert fired.

## Ignore your own actions

PagerDuty sends the agent's own changes back as webhook events, such as a note it posted or an
incident it resolved. The channel acknowledges those events without starting a turn, and the same
action by a person still arrives.

```ts
export default pagerdutyChannel({
  // Events caused by the agent's own PagerDuty user are skipped.
  // The user comes from, in order: selfUserIds, PAGERDUTY_USER_ID,
  // or the user PAGERDUTY_TOKEN acts as (GET /users/me).
  selfUserIds: ["PBOT001"],
  onIncident: (ctx) => ({ auth: defaultPagerDutyAuth(ctx) }),
});
```

A user-level API key or an OAuth user token acts as one user. An account-level key or an app token
acts as no user, so the channel uses the user whose email matches `PAGERDUTY_FROM_EMAIL`. A
read-only agent with no token sets `PAGERDUTY_USER_ID` instead. When none of these names a user, the
channel skips nothing.

## Events

Each hook receives one family of v3 events:

| Hook          | Events                                                                                                                                                                       |
| ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `onIncident`  | `incident.triggered`, `.acknowledged`, `.unacknowledged`, `.escalated`, `.reassigned`, `.delegated`, `.resolved`, `.reopened`, `.priority_updated`, `.incident_type.changed` |
| `onNote`      | `incident.annotated`                                                                                                                                                         |
| `onResponder` | `incident.responder.added`, `incident.responder.replied`                                                                                                                     |
| `onEvent`     | Everything else, such as status updates and workflow events                                                                                                                  |

Without `onIncident`, a triggered or reopened incident starts a turn. The channel acknowledges every
other event until you add its hook. Each hook returns `{ auth }`, `{ task }`, or `null`, as
[Handler results](/docs/reference/channels.md#handler-results) describes.

## Connect

Create a v3 webhook subscription in PagerDuty for the services the agent should respond to, and
point it at `https://<your-host>/<slug>/v1/channels/pagerduty`. Set the channel's secrets on the
deployment, or as environment variables locally.

| Variable                   | Used for                                                                                                               | Override                                 |
| -------------------------- | ---------------------------------------------------------------------------------------------------------------------- | ---------------------------------------- |
| `PAGERDUTY_WEBHOOK_SECRET` | Verifying `X-PagerDuty-Signature`. On Cursor hosting, a delivery with a bad signature gets a `401`.                    | `webhookSecret: { secretName }`          |
| `PAGERDUTY_USER_ID`        | The agent's own PagerDuty user ID. Optional when `PAGERDUTY_TOKEN` names a user.                                       | `selfUserIds: [ids]` or `{ secretName }` |
| `PAGERDUTY_TOKEN`          | Reply notes, `ctx.pagerduty`, and finding the agent's user. Use a REST API key, or an OAuth token as `Bearer <token>`. | `token: { secretName }`                  |
| `PAGERDUTY_FROM_EMAIL`     | The `From` user for writes with an account-level key or app token.                                                     | `from: { secretName }`                   |
| `PAGERDUTY_API_BASE_URL`   | The REST host, such as `https://api.eu.pagerduty.com`. Defaults to `https://api.pagerduty.com`.                        | `apiBaseUrl`                             |

`createPagerDutyClient()` takes the same options for code outside the channel.

```bash
agent-sdk deploy --dir . --slug incident-responder
agent-sdk secret set incident-responder PAGERDUTY_WEBHOOK_SECRET PAGERDUTY_TOKEN
```

The next delivery picks up a new value without another deploy. Until the webhook secret is set, the
route accepts only loopback deliveries under `agent-sdk dev`.

### Several subscriptions

PagerDuty gives each webhook subscription its own signing secret. Put one secret on each line of
`PAGERDUTY_WEBHOOK_SECRET`:

```text
<signing secret of subscription 1>
<signing secret of subscription 2>
```

A delivery passes when its signature matches any line. Set the value from that file:

```bash
agent-sdk secret set incident-responder PAGERDUTY_WEBHOOK_SECRET < pagerduty-webhook-secrets.txt
```

To rotate a secret, add the new one before PagerDuty signs with it, and remove the old one once
nothing signs with it.

## Test locally

`agent-sdk pagerduty replay` posts synthesized v3 deliveries for one incident to the running agent,
in order. Without `PAGERDUTY_WEBHOOK_SECRET`, `agent-sdk dev` admits them unsigned from loopback.
With it set, replay signs them the way PagerDuty does.

```bash
agent-sdk dev &
agent-sdk pagerduty replay --events triggered,acknowledged,annotated,resolved \
  --comment "Rolled back the 09:30 deploy."
```

Each event joins the session the trigger started. Pass an incident ID to start another session,
`--dry-run` to print the bodies, or `--fixtures-dir fixtures/pagerduty` to save them as fixtures for
[evals](/docs/evals.md).

To post a delivery you saved from the webhook log, sign it:

```bash
export PAGERDUTY_WEBHOOK_SECRET=local-test-secret
agent-sdk dev &

body=fixtures/pagerduty/incident-triggered.json
signature=$(openssl dgst -sha256 -hmac "$PAGERDUTY_WEBHOOK_SECRET" -hex < "$body" | awk '{print $NF}')
curl -X POST http://127.0.0.1:3000/<slug>/v1/channels/pagerduty \
  -H 'content-type: application/json' \
  -H "x-pagerduty-signature: v1=$signature" \
  --data-binary @"$body"
```

## Related

- [Channels reference](/docs/reference/channels.md)
- [Webhooks and custom channels](/docs/guides/webhooks.md)
- [Evals](/docs/evals.md)

---

Source: /docs/guides/slack.md

# Slack Agents

Agent SDK makes it easy to build agents that work in Slack.

A Slack agent wakes when someone mentions it, sends it a DM, or posts in a channel it watches. From
there, it keeps the conversation in one thread and can reply all at once, stream the answer live, or
decide for itself when to post updates. [Connect](#connect) sets up the Slack app.

## Answer a mention or DM

Mention the bot in a channel or send it a DM, and it will answer in a thread on that message. Any
follow-up in the same thread continues the conversation where it left off. All messages in one DM
share a session by default. Set `continuation: "slack.dmthread/v2"` to give each top-level DM its
own session and thread.

```ts
// bot/channels/slack.ts
import { slackChannel } from "@grok/agent-sdk/channels/slack";

export default slackChannel({
  // Reads TRIAGE_SLACK_BOT_TOKEN and TRIAGE_SLACK_APP_TOKEN, so two bots
  // on one host don't collide.
  envPrefix: "TRIAGE",
});
```

## Post one reply with a status chip

By default, Slack shows a status chip under the thread while the turn runs, with idle messages until
reasoning or tool activity gives it more to show. When the turn finishes, the chip disappears and
one complete reply lands in the thread.

```ts
export default slackChannel({
  reply: {
    mode: "post",
    status: {
      // First line of each reasoning block on the chip.
      reasoning: true,
      // Tool names as they start ("Running grep, glob...").
      tools: true,
      idle: ["Checking the request...", "Reviewing recent activity..."],
    },
  },
});
```

## Stream a live reply

In stream mode, the reply appears immediately and grows as the model writes. Slack shows its generic
"is working..." state until the stream ends, while optional reasoning and tool cards keep progress
inside the same message.

```ts
export default slackChannel({
  reply: {
    mode: "stream",
    // Thinking card inside the streamed message.
    reasoningCard: true,
    // One card per tool in that same message.
    toolCards: true,
  },
});
```

## Let the agent own every message

Use agentic mode when the agent should decide how to communicate as well as what to say. The bot
reacts to the parent message with eyes and shows a thinking chip, then the agent chooses when to
post progress and how to deliver the final reply.

`slack-communication` gives those posts a Slack-aware voice by default. Use `tone` to set the voice,
level of detail, formatting, and structure of updates.

```ts
// bot/extensions/slack.ts
import slackCommunication from "@grok/agent-sdk/extensions/slack-communication";

export default slackCommunication({
  tone: "Be concise but warm. Use bullets for evidence and end with the next action.",
});
```

```ts
export default slackChannel({
  // `post-update` writes mid-turn progress and the final reply.
  reply: { mode: "agentic" },
});
```

### Show steps as cards or as a checklist

The agent calls `post-card` to show one step of its work, such as "Scan the repo", with an `id`, a
`title`, a `status`, and optional `details`. Calling it again with the same `id` updates that card,
for example from `in_progress` to `complete`.

`cards` sets how Slack draws these steps:

| `cards`                             | What Slack shows                            | Use it when                                   |
| ----------------------------------- | ------------------------------------------- | --------------------------------------------- |
| `{ display: "timeline" }` (default) | A separate card in the thread for each step | One or two short steps                        |
| `{ display: "plan" }`               | One checklist for every step in the turn    | Several steps that readers track in one place |

```ts
export default slackChannel({
  reply: {
    mode: "agentic",
    cards: { display: "plan", title: "Release check" },
  },
});
```

- `title` is optional and can be up to 256 characters.
- `pending` only works when `display` is `"plan"`.

## Wake on a channel post

This example watches `#triage-alerts`, so any substantial top-level message starts a thread even
when nobody mentions the bot.

Mentions work in any channel the bot has joined. To listen without a mention, list between 1 and 10
channel IDs that start with `C` or `G`, plus optional `#name` entries for readability. On its own,
`"*"` watches thread replies in every joined channel but never wakes on top-level posts.

```ts
export default slackChannel({
  engagement: {
    channelPosts: {
      allow: ["C0123ABCDEF", "#triage-alerts"],
      posts: "top-level",
      debounceMs: 15_000,
    },
  },
  onChannelPost: async (_ctx, message) => {
    if (message.markdown.length <= 20) {
      return null;
    }
    // `{}` starts a model turn as the sender.
    return {};
  },
});
```

When you create the app, pass `--channel-posts` and invite the bot to every channel in the
allowlist. Add the `channels:read` scope when you use `#name` entries. On Cursor hosting,
`debounceMs` has no effect.

## Control who can message

By default, the agent doesn't reply to people outside your workspace, such as Slack Connect guests.
Set `blockExternals: false` only if those people should be able to use the agent.

Anyone in a thread can continue its session by default. To keep the conversation between the agent
and the person who started it, set `respondTo: "author"`.

```ts
export default slackChannel({
  policy: { blockExternals: false, respondTo: "author" },
});
```

## Events

Mentions and DMs wake the agent automatically. Declare `onAppMention` or `onDirectMessage` to filter
them. Like `onChannelPost`, each handler returns `{}` to start a turn or `null` to skip the message.
This handler only starts a turn for DMs that begin with `triage:`:

```ts
onDirectMessage: async (_ctx, message) => {
  if (!message.markdown.startsWith("triage:")) {
    return null;
  }
  return {};
},
```

## Call Slack from deterministic code via `ctx`

A hook can post a fixed acknowledgement before the model turn starts, using `ctx.thread.post()`,
`ctx.slack.channelId`, and `ctx.slack.threadTs`, while tools call the Slack Web API with
`ctx.host.slack.getClient()`.

```ts
onAppMention: async (ctx, message) => {
  if (message.markdown.trim() === "") {
    return null;
  }
  await ctx.thread.post("Looking at this now.");
  return {};
},
```

## Connect

```bash
agent-sdk login
agent-sdk slack create --dir .
agent-sdk slack doctor --prefix TRIAGE
agent-sdk serve --dir . --dev
```

After you sign in, `slack create` opens **Add Slack to this agent** in the browser and writes the
resulting tokens to `.env.local` without printing them. You can open the same wizard from
**Integrations** on [Deployed Agents](https://cursor.com/dashboard/deployed-agents). `doctor`
catches missing credentials before you start the agent. The
[CLI reference](/docs/reference/cli.md#slack) covers each `slack` command.

If your workspace requires admin approval, keep `slack create` running and open the **Request
approval** link it prints. Installing from Deployed Agents doesn't send that request. After an admin
approves, select **Retry** in the **Add Slack** wizard there.

### Choose the bot scopes

A Cursor-managed app requests these bot scopes by default:

`app_mentions:read`, `assistant:write`, `channels:history`, `channels:read`, `chat:write`,
`files:read`, `files:write`, `groups:history`, `groups:read`, `im:history`, `im:read`, `im:write`,
`mpim:history`, `reactions:read`, `reactions:write`, `usergroups:read`, `users:read`,
`users:read.email`

To request a different list, set `botScopes` in `slack-manifest.json` at the root of the agent
project. The app then requests exactly that list:

```json
{
  "botScopes": [
    "app_mentions:read",
    "assistant:write",
    "channels:history",
    "chat:write",
    "groups:history",
    "im:history",
    "reactions:read",
    "pins:read"
  ]
}
```

Then run `agent-sdk slack create` again (add `--env prod` for the production app), and Slack updates
and reinstalls the app.

- The list must include `app_mentions:read`, `assistant:write`, `channels:history`, `chat:write`,
  `groups:history`, `im:history`, and `reactions:read`.
- Cursor rejects scopes in the Slack Marketplace forbidden families: `admin.*`, `identity.*`,
  `search:read*`, `triggers:*`, `apps_configuration:*`, and `workflow.steps:execute`.

The person who runs `slack create` grants Cursor three user scopes on their own account so it can
create and install the app, and `botScopes` doesn't change them.

If you already own the Slack app, run `agent-sdk slack init --manual --dir . --name "Triage"` and
follow the `bdk-setup-slack` skill for installation and tokens.

---

Source: /docs/guides/webhooks.md

# Webhook agents

A custom channel turns an event from another system into an agent
conversation and sends the result back to the system that started it.
Use one when the built-in [HTTP API](/docs/reference/http-api.md) doesn't
match the provider's payload, conversation key, or reply path.

A file at `bot/channels/<id>.ts` mounts its routes under
`/<slug>/v1/channels/<id>`.

## Receive a webhook and send the answer back

When a ticket system sends a new message, this route starts a turn and
answers right away. The agent keeps working after the HTTP request ends,
and its answer appears as a comment on the ticket.

```ts
// bot/channels/tickets.ts
import { defineChannel, POST } from "@grok/agent-sdk/channels";
import { z } from "zod";
import { postTicketComment } from "../lib/tickets.js";

type TicketState = {
  ticketId: string;
};

export default defineChannel<TicketState>({
  state: { ticketId: "" },
  routes: [
    POST("/ticket", {
      description: "Answer a new ticket message",
      bodySchema: z.object({
        ticketId: z.string(),
        message: z.string(),
      }),
      handler: async (_request, { send, waitUntil, body }) => {
        waitUntil(
          send(body.message, {
            state: { ticketId: body.ticketId },
            title: `Ticket ${body.ticketId}`,
          })
        );
        return new Response(null, { status: 202 });
      },
    }),
  ],
  events: {
    async "message.completed"(event, channel) {
      if (event.data.finishReason === "tool_call" || event.data.text === "") {
        return;
      }
      await postTicketComment(channel.state.ticketId, event.data.text);
    },
  },
});
```

`postTicketComment` wraps the ticket provider's SDK. The write lives in
the event handler, so the webhook request doesn't stay open while the
model works.

On Cursor hosting, the caller always gets an empty `202` response, and
the platform ignores whatever your handler returns. Send results and
failures back through the provider's API instead. See
[Route responses](/docs/reference/channels.md#route-responses).

## Keep one conversation per ticket

Without a conversation key, every delivery creates a new session. Pass
the ticket ID as the `continuationToken`, so every message on one
ticket resumes the same conversation.

```ts
waitUntil(
  send(body.message, {
    continuationToken: `ticket:${body.ticketId}`,
    state: { ticketId: body.ticketId },
    title: `Ticket ${body.ticketId}`,
  })
);
```

## Keep one hosted session per thread

On Cursor hosting, each request to a custom route starts a new session,
even when the handler passes the same `continuationToken`. To join
requests, the sender or a small relay in front of the deployment adds
the same `X-BDK-Session-Key` value to each one, and those requests run
in one session in arrival order.

```bash
curl -X POST "$AGENT_ALIAS_URL/v1/channels/tickets/ticket" \
  -H "X-Agent-Alias-Token: $AGENT_ALIAS_TOKEN" \
  -H "X-BDK-Session-Key: ticket:ENG-142" \
  -H 'content-type: application/json' \
  -d '{"ticketId":"ENG-142","message":"CSV export returns 500"}'
```

Hosting stores the value with the request, so don't put secrets or
personal data in it.
[Hosted session keys](/docs/reference/channels.md#hosted-session-keys)
lists the auth, format, and retry rules.

## Prepare evidence before the turn

Webhook payloads often carry little more than an object ID. This
handler fetches the full ticket with a server tool and writes it to
`ticket.json`, so the model starts from a fixed snapshot.

```ts
handler: async (_request, { callTool, send, waitUntil, body }) => {
  const prepared = await callTool("prepare_ticket", {
    ticketId: body.ticketId,
  });
  if (prepared.isError) {
    await postTicketComment(
      body.ticketId,
      "I couldn't load this ticket, so I didn't draft a reply."
    );
    return new Response(null, { status: 202 });
  }

  waitUntil(
    send("Read ticket.json, then draft the reply.", {
      continuationToken: `ticket:${body.ticketId}`,
      state: { ticketId: body.ticketId },
      workspaceFiles: {
        "ticket.json": JSON.stringify(prepared.result, null, 2) ?? "null",
      },
    })
  );
  return new Response(null, { status: 202 });
},
```

The `message.completed` handler from the first example still delivers
the answer.

## Validate payloads with Zod

The route validates the request and passes the handler a typed `body`
or `query`. `POST`, `PUT`, and `PATCH` routes need a Zod `bodySchema`,
and `GET` routes need a `querySchema`. Use `z.unknown()` for a variable
payload that your own parser narrows.

## Use handler context

Await `send` instead of passing it to `waitUntil` when later handler
code needs the session, for example to call
`session.waitForCompletion()`.
[Handler arguments](/docs/reference/channels.md#handler-arguments) lists
every helper.

## Protect public webhooks

Custom routes accept direct loopback requests by default and reject
forwarded ones, so a tunnel or reverse proxy can't quietly make a local
endpoint public. Under `--dev`, a provider channel with no webhook
secret (GitHub, GitLab, Bitbucket, or PagerDuty) accepts only local
requests, so set that secret in the provider and in your shell before
you test through a tunnel.

Use `bearerAuth()` when the sender can attach an authorization header.
Webhook providers often can't attach the agent alias token, so
`publicEndpoint()` lets a custom channel skip it on Cursor hosting.
That opens the route to every caller, so verify the provider's
signature before you start a turn or call a tool.

```ts
import { defineChannel, POST, publicEndpoint } from "@grok/agent-sdk/channels";
import { z } from "zod";
import { verifyProviderSignature } from "../lib/tickets.js";

export default defineChannel({
  auth: [publicEndpoint()],
  routes: [
    POST("/", {
      description: "Signed ticket webhook",
      bodySchema: z.unknown(),
      handler: async request => {
        const rawBody = await request.text();
        const signature = request.headers.get("x-provider-signature");
        if (!verifyProviderSignature(rawBody, signature)) {
          return new Response(null, { status: 202 });
        }

        // Parse and dispatch only after the signature passes.
        return new Response(null, { status: 202 });
      },
    }),
  ],
});
```

With `publicEndpoint()`, the handler drops a badly signed delivery
silently. If the provider sends a shared secret header or signs the raw
body with HMAC-SHA256, use `sharedSecretAuth()` or
`hmacSignatureAuth()` instead. Hosting checks those policies before the
handler runs and answers a bad delivery with `401`.

`publicEndpoint()` opens only this channel's routes, and the built-in
session and tool APIs stay private. See
[Auth policies](/docs/reference/channels.md#auth-policies).

## Test locally

Run the agent with `--dev` and post the payload the provider will send.
The session appears in the playground while the reply goes out.

```text
agent-sdk serve --dir . --dev

curl -sS -X POST \
  http://127.0.0.1:3000/<slug>/v1/channels/tickets/ticket \
  -H 'content-type: application/json' \
  -d '{"ticketId":"ENG-142","message":"CSV export returns 500"}'
```

Keep sample payloads under `fixtures/` and pass them with
`--data-binary @fixtures/<event>.json`.

## Related

- [Channels reference](/docs/reference/channels.md)
- [Review and triage templates](/docs/templates/reviews.md), including a
  signed tracker webhook

---

Source: /docs/hillclimbing.md

# Hillclimbing

Hillclimbing improves an agent one measured change at a time. Hold the inputs fixed, change one
thing, and keep it only when the same run gets better without moving the behavior you froze.

A coding agent in your checkout can run this loop by following the `bdk-hillclimb` skill.

## Run one measured round

First agree on the agent, one to three fixed inputs, the metric to improve, and the behavior that
must not change, such as the output shape, side effects, and existing evals.

Then run one round:

1. Run the agent the way a user would, on saved or local inputs, without deploying or posting live.
2. Read the trajectory and name the main failure in one sentence.
3. Change one lever.
4. Rerun the same fixtures.
5. Compare correctness, efficiency, and contract fit.
6. Keep, revert, or narrow the change.

Don't swap a fixture mid-round, or the comparison means nothing.

## Choose the smallest lever

Start with the earliest place that can remove the failure. Prepare evidence on the host before the
turn, then change the shape of that evidence, then the instructions or skills, then remove or gate
tools that invite wandering, and change the framework only when the agent can't express the fix.

Fixing the host or evidence often improves quality and latency together, because the model searches
less for ground truth.

## Match the fix to the failure

When the agent picks the wrong tool, such as a PR reviewer that calls `approve_pr` before
`inspect_pr`, pin one PR and tighten only the instruction that owns that decision, so you know which
edit fixed it.

```ts
t.calledTool("inspect_pr");
t.notCalledTool("approve_pr");
```

When a run wanders or runs long, it often repeats reads or makes the model find evidence the host
already has. Move that preparation ahead of the turn, and keep the change only if the answer still
finds the same issue.

```text
before: 14 tool calls, 92 seconds, 6 repeated reads
after:   5 tool calls, 31 seconds, 0 repeated reads
```

When the answer is weak, freeze the public shape, compare both runs against known-good findings, and
edit the instruction or skill that owns the missing judgment.

```text
fixture: saved checkout PR diff
success: report both gold findings with file paths
freeze:  findings JSON shape; no review post; existing smoke evals
```

## Lock every kept win

Each kept change needs an eval that would have failed before it, such as a tool-choice assertion, a
`maxToolCalls` bound, an output-shape check, or a quality score.

```bash
agent-sdk eval --dir . --tag smoke
```

Don't loosen an existing eval so that a new change passes. If an older eval starts failing, the
change broke something that used to work, and you should fix the change instead.

---

Source: /docs/index.md

# Agent SDK

Agent SDK lets you define, test, evaluate, and deploy agents as code. It uses the Cursor SDK harness
that powers Bugbot, Security Review, Approval Bot, and Rollouts.

An Agent's behavior is defined in a folder of Markdown and TypeScript that combines deterministic
code with model judgment. Agent SDK makes it easy to build useful agents, with APIs to interact on
Slack, pull requests, issue trackers, Cursor, and the other places where you get work done.

After you deploy an agent, open [Deployed Agents](https://cursor.com/dashboard/deployed-agents) to
manage it.

## Get Started with your Coding Agent

Paste this prompt into your coding agent. It installs the CLI, asks what your agent is for, and
scaffolds a first project from the Markdown version of these docs.

```text
Set up my first Agent SDK project.

Read https://cursor.com/docs/agent-sdk.md first. Every page under
https://cursor.com/docs/agent-sdk/ is also available as Markdown when you
add .md to its URL.

1. Check that Node is 22.13 or newer, then run:
   npm install --global @grok/agent-sdk
2. Read the docs index that ships with the package:
   "$(npm root -g)/@grok/agent-sdk/dist/docs/llms.txt"
3. Run agent-sdk login, and wait while I finish the browser sign-in.
4. Ask me what the agent should do and where people will reach it. Then
   follow the bdk-create-agent skill at
   "$(npm root -g)/@grok/agent-sdk/skills/create-agent/SKILL.md"
   to scaffold the project with agent-sdk init.
5. Run agent-sdk validate --dir . and one agent-sdk run turn, then show me
   the reply.
```

## Why Agent SDK

Your instructions, tools, and evals are files you review, test, and version like the rest of your
code. You write the instructions and tools, and Cursor runs the model loop that calls them.

## When to use Agent SDK

|                    | Cursor Automations                            | Grok Bot                                                        | Agent SDK                                                                                          |
| ------------------ | --------------------------------------------- | --------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
| **You define**     | A prompt, model, and trigger in the dashboard | Instructions, skills, and connected tools in the app            | A versioned Markdown and TypeScript project                                                        |
| **Where it runs**  | Cursor-managed                                | Cursor-managed                                                  | Cursor-managed hosting or your infrastructure                                                      |
| **Choose it when** | The job fits a schedule or event trigger      | You want to configure and talk to an agent without writing code | You want code around model judgment that your team can version control, review, test and hillclimb |

## Get started

Install `@grok/agent-sdk`, then scaffold and run a project. The package was `@cursor/bdk` before.

```bash
npm install --global @grok/agent-sdk
agent-sdk init ./my-bot
cd my-bot
agent-sdk dev
```

[Project layout](/docs/reference/project-layout.md) explains every folder under `bot/`.

## Next steps

- [Build your first PR reviewer agent](/docs/quickstart.md) step by step.
- Start from a [review or triage template](/docs/templates/reviews.md) or the
  [owners-policy approver](/docs/templates/agentic-owners.md).
- [Convert a Cursor Automation](/docs/guides/convert-automation.md) into an Agent SDK project.
- [Build agents with agents](/docs/building-with-agents.md) using a coding agent.
- Look up commands in the [CLI reference](/docs/reference/cli.md) and deployed-agent roles in
  [Access](/docs/reference/access.md).

## Architecture

![Agent SDK architecture](/docs/architecture.svg)

---

Source: /docs/quickstart.md

# Build a PR reviewer with Agent SDK

You'll build a reviewer that rates each pull request as `trivial`, `moderate`, or `large`, then approves safe changes or asks for a human review.

When it's deployed, you'll manage it in [Deployed Agents](https://cursor.com/dashboard/deployed-agents).

## Build it with a coding agent

Paste this prompt into your coding agent to build the reviewer from this page. The sections after
it walk through the same steps by hand.

```text
Build the PR reviewer from https://cursor.com/docs/agent-sdk/quickstart.md
in ./pr-reviewer. Fetch that page and follow each step in order.

- If agent-sdk isn't on PATH, install it with
  npm install --global @grok/agent-sdk. Wait while I finish agent-sdk login.
- Use the code from the page as written. Ask me for a GitHub repository and
  a pull request I didn't author, and use them in place of acme/checkout#42.
- Run both tool-call checks, and confirm the complexity each one reports.
- Ask me before any step that posts a review on GitHub. Skip the deploy step.

Finish with agent-sdk validate --dir . and the project's TypeScript check,
then tell me what passed.
```

## Prerequisites

You need Node 22.13 or newer and a Cursor login for model turns:

```bash
npm install -g @grok/agent-sdk
agent-sdk login
```

## Create and run the project

```bash
agent-sdk init ./pr-reviewer
cd pr-reviewer
agent-sdk dev
```

In a second terminal, confirm the agent replies:

```bash
agent-sdk run --dir . --message "Introduce yourself in one sentence."
```

## Add review instructions

Replace `bot/instructions.md`:

```md
# PR reviewer

Call `inspect_pr` before you judge a pull request, then act on the
complexity it reports:

- `trivial`: approve with `submit_review` if the diff matches the title
  and nothing looks risky.
- `moderate`: approve only when you understand every patch and see no
  risk. Otherwise request a human review and name the files that worry you.
- `large`: request a human review right away and point the reviewer at
  the biggest files.

Never approve a draft. End with one sentence that gives the verdict and why.
```

## Share the URL parser

Both tools parse a pull request URL, so put the parser in `bot/lib/github.ts`, where Agent SDK doesn't look for tools.

```ts
export function parsePrUrl(prUrl: string) {
  const [owner, repo, pulls, number] = new URL(prUrl).pathname.split("/").filter(Boolean);
  const pull_number = Number.parseInt(number ?? "", 10);
  if (pulls !== "pull" || owner === undefined || repo === undefined || Number.isNaN(pull_number)) {
    throw new Error(`Not a pull request URL: ${prUrl}`);
  }
  return { owner, repo, pull_number };
}
```

## Add the inspect tool

Create `bot/tools/inspect_pr.ts`. It skips patches on a large pull request and caps the rest at 3,000 characters.

```ts
import { defineTool } from "@grok/agent-sdk/tools";
import { z } from "zod";
import { parsePrUrl } from "../lib/github.js";

function rateComplexity(linesChanged: number, changedFiles: number) {
  if (linesChanged <= 25 && changedFiles <= 2) {
    return "trivial";
  }
  if (linesChanged <= 400 && changedFiles <= 15) {
    return "moderate";
  }
  return "large";
}

export default defineTool({
  description: "Fetch a pull request's title, patches, and complexity.",
  inputSchema: z.object({ prUrl: z.string().describe("GitHub pull request URL") }),
  async execute({ prUrl }, ctx) {
    const pull = parsePrUrl(prUrl);
    const octokit = await ctx.host.github.getOctokit();
    const { data: pr } = await octokit.rest.pulls.get(pull);
    const { data: files } = await octokit.rest.pulls.listFiles({ ...pull, per_page: 100 });
    const complexity = rateComplexity(pr.additions + pr.deletions, pr.changed_files);

    return {
      title: pr.title,
      draft: pr.draft ?? false,
      complexity,
      files: files.map((file) => ({
        path: file.filename,
        additions: file.additions,
        deletions: file.deletions,
        patch: complexity === "large" ? undefined : file.patch?.slice(0, 3000),
      })),
    };
  },
});
```

## Verify complexity classification

Call the tool without a model turn:

```bash
# Expect "complexity": "trivial"
agent-sdk tool call inspect_pr --dir . --input '{"prUrl":"https://github.com/facebook/react/pull/35623"}'

# Expect "complexity": "large" and no patch fields
agent-sdk tool call inspect_pr --dir . --input '{"prUrl":"https://github.com/facebook/react/pull/25229"}'
```

## Add the review tool

Create `bot/tools/submit_review.ts`:

```ts
import { defineTool } from "@grok/agent-sdk/tools";
import { z } from "zod";
import { parsePrUrl } from "../lib/github.js";

export default defineTool({
  description: "Approve the pull request, or comment asking for a human review.",
  inputSchema: z.object({
    prUrl: z.string().describe("GitHub pull request URL"),
    verdict: z.enum(["approve", "request_human_review"]),
    summary: z.string().describe("One or two sentences on why."),
  }),
  async execute({ prUrl, verdict, summary }, ctx) {
    const approve = verdict === "approve";
    const octokit = await ctx.host.github.getOctokit();
    await octokit.rest.pulls.createReview({
      ...parsePrUrl(prUrl),
      event: approve ? "APPROVE" : "COMMENT",
      body: approve ? summary : `Human review needed. ${summary}`,
    });
    return { posted: true, verdict };
  },
});
```

You need GitHub write access and a pull request you didn't author.

## Review a pull request

```bash
agent-sdk run --dir . --message "Review https://github.com/acme/checkout/pull/42"
```

Confirm `inspect_pr` runs before `submit_review`, then check the review on GitHub.

## Trigger reviews from GitHub

Create `bot/channels/github.ts`:

```ts
import { defaultGitHubAuth, githubChannel } from "@grok/agent-sdk/channels/github";

const REVIEW_ACTIONS = new Set(["opened", "reopened", "ready_for_review"]);

export default githubChannel({
  botName: "agent-sdk-pr-reviewer",
  cursorAccount: { repos: ["acme/checkout"] },
  onPullRequest: (ctx, pr) => {
    if (!REVIEW_ACTIONS.has(pr.action) || pr.draft) {
      return null;
    }

    return {
      auth: defaultGitHubAuth(ctx),
      title: `Review ${ctx.repository.fullName}#${pr.number}`,
      context: [`Review ${pr.url}.`],
    };
  },
});
```

With `agent-sdk dev` running, replay an open pull request. Pull access is enough:

```bash
agent-sdk github replay https://github.com/acme/checkout/pull/42 --dir . --action opened
```

Open the playground and confirm `Review acme/checkout#42` shows both tool calls and the verdict.

## Deploy the reviewer

Follow [Deployment](/docs/deployment.md) to run `agent-sdk deploy`, then manage the reviewer in [Deployed Agents](https://cursor.com/dashboard/deployed-agents).

## Where to go next

- Turn these two PRs into regression checks with [Evals](/docs/evals.md).
- Read about typed tools in [Tools](/docs/reference/tools.md).
- Set up fixtures and forwarding with the [GitHub guide](/docs/guides/github.md).

---

Source: /docs/reference/access.md

# Access

[Deployed Agents](https://cursor.com/dashboard/deployed-agents) calls the three access levels Viewer, Editor, and Owner, and the API calls them `viewer`, `deployer`, and `admin`. `admin` is Owner, and a write also accepts `owner` for that role and `editor` for Editor.

| Dashboard | API value | What it allows |
| --- | --- | --- |
| Viewer | `viewer` | Open the bot |
| Editor | `deployer` | Deploy, stop, and restart |
| Owner | `admin` | Editor, plus secrets and who else has access |

A new deploy gives the person who deployed it Owner and the team that owns the bot Viewer. Deploying that bot again does not change who has access. A team that already has access keeps it.

A deploy from a service account, such as CI, gives Owner to the person who created that account. The service account is not an Owner. If the account has no creating person, the team is Viewer and a team Admin can manage the bot.

A team Admin can manage every bot that team owns, including a bot the team can only view. The same person cannot manage a bot another team published, including a catalog bot installed into their team.

An Owner changes who has access in [Deployed Agents](https://cursor.com/dashboard/deployed-agents).

---

Source: /docs/reference/agent-config.md

# Agent config

`bot/agent.ts` default-exports `defineAgent(config)`, which sets the model
that runs the agent and the defaults for a local run. Every field is optional on
the root agent.

```ts
import { defineAgent } from "@grok/agent-sdk";

export default defineAgent({
  model: {
    id: "grok-4.5",
    params: [
      { id: "effort", value: "high" },
      { id: "fast", value: "true" },
    ],
  }, // optional; this is the default
});
```

## Agent fields

| Field | Type | Meaning |
| --- | --- | --- |
| `model` | string or `{ id, params }` | Cursor model for turns. Defaults to `grok-4.5` with `effort=high` and `fast=true`. Subagents omit it to inherit. |
| `name` | string | Display name. Defaults to the package or directory name. |
| `description` | string | What the agent is for. Required on subagents, where the parent model reads it to decide when to delegate. |
| `instructions` | string | Inline instructions for subagents and generated configs. Prefer `instructions.md`. |
| `local` | `{ cwd?, workspaceDir?, sandbox? }` | Defaults for a local run. See [Local options](#local-options). |
| `hosting` | `{ egressDomains?, secretNames? }` | The egress allowlist and expected secret names that `agent-sdk deploy` reads. Mounted [extensions](/docs/reference/extensions.md#build-an-extension) add their own. |
| `concurrency` | `{ maxRunningTurns? }` | Agent-wide limit on concurrent turns. See [Concurrency](#concurrency). |
| `builtinTools` | `{ reminders? }` | Opt-in tools that Agent SDK provides. See [Built-in tools](#built-in-tools). |
| `tools` | `ToolName[]` | Allowlist of built-in tools. Omit it for the full standard toolset. See [Harness tools](#harness-tools). |
| `fs` | `FileSystem` or `(ctx) => FileSystem` | Custom file tree for the session. `root` is the deprecated name. See [Custom filesystem](#custom-filesystem). |
| `skills` | `false` or `(ctx) => FileSystem` | Replaces the skills tree mounted at `/agent/skills`, or turns that mount off. |

The `persistent` field was removed. Discovery rejects a config that still sets it, and the error is `persistent was removed. Delete it from defineAgent.`, so `validate`, `dev`, and `deploy` fail until you delete the field. A deployment that is already running keeps serving until you stop or delete it.

## Model

Effort and speed are params, not id suffixes, so Agent SDK rejects an id like
`grok-4.5-fast`. Use the object form shown above to set params, or a plain
string when you don't need any:

```ts
model: "composer-2.5",
```

## Local options

`local.workspaceDir` points every session at one shared working directory, for
agents that work inside an existing checkout. It takes precedence over `cwd`,
and a per-send `workspaceDir` wins over both. Sessions share that tree, so one
turn's files are visible to the next.

`local.sandbox` runs the agent inside Cursor's local sandbox. It's off by
default, so shell commands auto-approve and inherit the serve process
environment, including the host's credentials. Turn it on when turns read
untrusted input, such as webhook payloads, PR diffs, or inbound chat. The
sandbox limits the tools themselves, so an injected prompt can't talk the model
past it.

Agent SDK runs the file tools (`read`, `edit`, `delete`, `grep`, `glob`, `ls`)
in the serve process, outside the sandbox. With `local.sandbox` on, they refuse
a path outside the session workspace with `Path is outside the workspace`. With
it off, they can reach any path the serve process can.

### Local cwd

`local.cwd` sets the parent directory for local session workspaces. Each
session uses `<cwd>/<sessionId>` unless a per-send `workspaceDir` overrides it.

Session workspaces are real Cursor project directories, so the local agent reads
`AGENTS.md` and `.cursor` config from every ancestor directory. When you omit
`cwd` and the agent sits inside another git repo, such as a monorepo package,
Agent SDK uses a per-project directory under `~/.cache` instead, so the
enclosing checkout's rules, skills, and MCP servers stay out of the turn. An
agent at its own git root keeps its workspaces inside the project. Point `cwd`
at a checkout only when the agent should inherit that tree.

## Custom filesystem

`fs` is mounted at `/`, and Agent SDK mounts `/tmp` and `/agent/skills` over it. Set `fs` or the deprecated `root`, not both. A tree that already has `/agent/skills` fails session start unless `skills` is `false`.

## Harness tools

Use `tools` to limit which built-in Cursor tools the model can call.
When you set it, the model gets only the tools you list. An empty list turns
off every built-in tool, and new platform tools stay off until you add them.

```ts
export default defineAgent({
  model: "composer-2.5",
  // Read-only triage: no shell, no edits, no subagents.
  tools: ["read", "grep", "glob", "ls"],
});
```

Agent SDK always adds `"mcp"` to a configured allowlist, because the server
tools in `bot/tools/` reach the model over MCP. With `local.sandbox` on, MCP
tool calls fail closed. An enclosing checkout can also add MCP servers, as
[Local cwd](#local-cwd) explains.

Use the Cursor SDK's public tool names, such as `"shell"`, `"read"`,
`"edit"`, `"grep"`, `"glob"`, `"ls"`, and `"task"`. An unknown name fails the
turn with a `ConfigurationError`, and `"SemanticSearch"` or `"semSearch"`
fails validation.

Two names have broader effects:

- `"shell"` also grants shell input. Tools with `execution: "agent"` need it
  to run their scripts, and discovery warns when your allowlist blocks them.
- `"task"` lets the root agent start subagents. Each subagent keeps its own
  toolset.

The allowlist doesn't isolate the serve host, so pair it with `local.sandbox`
for untrusted input.

## Migrate off semantic search

Semantic search is removed, and `agent-sdk validate` reports
`Semantic search has been removed. Use Grep.` while a project still uses it.
[A turn fails or misbehaves](/docs/troubleshooting.md#a-turn-fails-or-misbehaves) lists what to
delete and what to use instead.

## Concurrency

`concurrency.maxRunningTurns` caps how many model turns run at once across the agent's sessions. It takes a positive integer up to 200. Omit it for no limit.

When every slot is busy, a new turn queues in arrival order instead of failing. The stream records a `turn.queued` event with the queue
position, `GET /v1/sessions` reports `queued: true`, and the turn starts as
soon as a slot frees.

A queued turn still counts as running. A follow-up preempts it, and a direct
write-effect tool call gets `409 session_busy`.

```ts
export default defineAgent({
  concurrency: { maxRunningTurns: 3 },
});
```

## Built-in tools

`builtinTools` opts into tools that Agent SDK provides. They behave like your
own server tools in turns, direct calls, `info`, and the playground. A tool you
write with the same name wins, with a warning.

`builtinTools: { reminders: true }` adds `reminders_create`, `reminders_list`,
and `reminders_cancel`, bound to the current conversation. A session without a
continuation key can't set reminders. See
[Schedules and reminders](/docs/reference/schedules.md#reminders).

## Serve programmatically

`serve(dirOrProject, options)` embeds the server in your own process:

```ts
import { serve } from "@grok/agent-sdk";

const handle = await serve("./my-agent", {
  port: 3000,
});
console.log(`listening on ${handle.url}`);
// handle.callTool(...), handle.dispatchSchedule("heartbeat"),
// handle.createReminder(...), handle.agents, await handle.close()
```

`handle.agents` lists each mount as `{ slug, url, info }`. `handle.project` is
deprecated.

`serve()` takes the same host settings as the [CLI](/docs/reference/cli.md) `serve` flags,
plus `discovery` for project-loading options and `mode: "single" | "multi"`.
Pass `apiKey`, or sign in the same way as the CLI, as
[Environment variables](/docs/reference/cli.md#environment-variables) describes.

## Inspect a project in tests

`loadProject(dir)` reads one project without serving it. Its `info` matches the
`GET /v1/info` body.

```ts
import { loadProject } from "@grok/agent-sdk";

const { info } = await loadProject("./my-agent", { strict: true });
expect(info.tools.map((t) => t.name)).toContain("inspect_pr");
```

`loadProject` throws `ProjectLoadError` when discovery reports an error, and
`{ strict: true }` makes it throw on a warning too. `error.diagnostics` lists
the findings, and each `path` is relative to the project directory. `serve`
accepts the loaded project or a directory path.

## Related

- [Instructions](/docs/reference/instructions.md)
- [Sessions](/docs/reference/sessions.md)

---

Source: /docs/reference/artifacts.md

# Artifacts

Artifacts are durable outputs such as reviewed pull requests or
reports. Records persist across sessions, update by key, and can be
listed, streamed, or downloaded.

## Artifact kinds

Declare kinds in `bot/artifacts.ts`:

```ts
import { z } from "zod";
import { defineArtifacts } from "@grok/agent-sdk/artifacts";

export default defineArtifacts({
  kinds: {
    "reviewed-pr": {
      description: "A pull request this agent reviewed.",
      schema: z.object({ url: z.string(), verdict: z.string() }),
    },
  },
  agentTool: true,
});
```

| Option | Contract |
| --- | --- |
| `kinds` | Kind names, each with a non-empty `description` and optional Zod `schema` |
| `agentTool` | Expose `tag_artifact` to the model. Requires at least one declared kind |
| `max` | Retention cap, default `1000`. Only local and self-hosted servers evict, oldest-updated first |

With declared kinds, `tag` rejects any other kind, and without them it accepts any kind name and defaults an omitted kind to `"artifact"`. `tag` stores `data` as the kind's schema parsed it, with defaults and coercions applied.

## Tag artifacts from host code

Host code gets an `ArtifactsApi` with `tag` and `list`:

```ts
await ctx.artifacts.tag({
  kind: "reviewed-pr",
  key: prUrl,
  title: `Reviewed ${prUrl}`,
  data: { url: prUrl, verdict: "approve" },
});
```

`tag` returns an `ArtifactRecord`.

| Field | Contract |
| --- | --- |
| `kind` | Declared or free-form kind |
| `data` | Required JSON data, validated by the kind's schema |
| `key` | Optional upsert key, unique across the agent and all kinds |
| `title` | Optional display title |
| `sessionId`, `turnId` | Optional session or turn attribution |
| `source` | `"host"` or `"model"`. `tag` defaults to `"host"` |
| `contents` | Tag only. String or bytes served by the content route |
| `contentType` | Tag only. MIME type for `contents` |
| `id` | Record only. Stable ID derived from `key`, or a generated ID |
| `content` | Record only. `{ size, contentType? }` metadata |
| `createdAt`, `updatedAt` | Record only. ISO-8601 timestamps |

Tool, hook, and channel-event contexts are session-bound and fill
`sessionId` and the current `turnId`. Channel routes, channel
`onStart`, schedules, and reminders get an unbound API, so pass
`sessionId` to attribute an artifact.

Re-tagging a key without `contents` keeps its payload unless
`sessionId` changes.

## Expose `tag_artifact` to the model

With `agentTool: true`, the model gets a generated `tag_artifact` tool
that accepts the declared kinds and validates data with their schemas.
Kind descriptions tell the model what each kind is for. An authored
`tag_artifact` tool takes precedence.

## List artifacts

`list({ kind?, sessionId? })` returns matching records, newest update
first. HTTP callers use the
[artifact routes](/docs/reference/http-api.md#list-and-download-artifacts) to list
and download.

## Stream artifact tags

Tagging with a `sessionId` emits
[`artifact.tagged`](/docs/reference/sessions.md#stream-events) on that session with
`id`, `kind`, `key`, `title`, `data`, and `source`.

---

Source: /docs/reference/channels.md

# Channels

A channel connects an external surface to agent sessions. A file at `bot/channels/<id>.ts` defines channel `<id>` and mounts its routes under `/v1/channels/<id>`, and the built-in HTTP channel serves the [HTTP API](/docs/reference/http-api.md) routes and is always available.

## Define a custom channel

Use `defineChannel` with one or more typed routes:

```ts
import { defineChannel, POST } from "@grok/agent-sdk/channels";
import { z } from "zod";

export default defineChannel({
  routes: [
    POST("/review", {
      description: "Review a pull request on this channel",
      bodySchema: z.object({
        prUrl: z.string().url(),
      }),
      handler: async (_request, { send, waitUntil, body }) => {
        waitUntil(
          send(`Review ${body.prUrl}`, {
            continuationToken: `pr:${body.prUrl}`,
          })
        );
        return new Response(null, { status: 202 });
      },
    }),
  ],
});
```

This route is `POST /v1/channels/<id>/review`. Calls for the same pull request
pass the same [continuation token](/docs/reference/sessions.md#session-identifiers), so they
share one conversation.

## Route responses

On Cursor hosting, the deployment
answers every custom route with an empty `202 Accepted` as soon as the request
arrives, then runs the handler. The caller never sees the handler's `Response`,
so a route can't return a session ID, a result, or an error status. A request
that fails the alias token, `sharedSecretAuth`, or `hmacSignatureAuth` check
still gets `401`.

Deliver results through [`events`](#events) handlers, a callback to the sender,
or a tool the turn calls. Local `serve` returns the handler's response, so
return `new Response(null, { status: 202 })` to match hosting.

## Route verbs and schemas

Route paths must begin with `/`.

`GET` requires a `querySchema`.

`POST`, `PUT`, and `PATCH` require a `bodySchema`. They also accept a `querySchema`.

`DELETE` takes a `bodySchema`, a `querySchema`, either, or neither.

Plain JSON Schema objects don't type-check, so use `z.object({})` or
`z.unknown()` for an open surface. The host validates input before the handler
runs, and declared schemas appear in `GET /v1/info` for the playground and
slash commands.

## Handler arguments

A handler receives the Fetch `Request` and these arguments:

| Member | Contract |
| --- | --- |
| `send(message, options?)` | Start or resume a session on this channel |
| `getSession(sessionId)` | This channel's session, or `null` |
| `receive(channel, input)` | Hand work to another channel |
| `callTool(name, input, options?)` | [Call a server tool](/docs/reference/tools.md#call-a-tool-without-a-model-turn) without a model turn |
| `body`, `query`, `params` | Validated inputs and `:param` path segments |
| `auth` | The `AuthContext` from the route's auth chain |
| `requestIp` | The TCP peer address, or `null` |
| `host` | Host services such as MCP, storage, telemetry, and reminders |
| `waitUntil(promise)` | Keep work running after the handler returns |
| `sessionUrls(request, sessionId)` | Playground and trace URLs for a session |
| `artifacts` | List or tag [artifacts](/docs/reference/artifacts.md) |

### `send` options

| Option | Contract |
| --- | --- |
| `continuationToken` | Channel-local key that resumes its session, or creates one when new |
| `admission` | `"preempt"` (the default) interrupts a busy turn. `"coalesce"` queues behind it |
| `workspaceFiles` | Relative files to add before the next turn |
| `workspaceDir` | Absolute local working directory |
| `auth` | Session principal. Defaults to the request principal |
| `state` | Starting channel state for a new session |
| `title`, `purpose`, `dryRun`, `asOf` | Set up a new session, as in the [`POST /v1/session` body](/docs/reference/http-api.md#start-a-session) |
| `deliveryId` | Names this send on the session's [events](/docs/reference/sessions.md#track-which-follow-up-reached-the-agent), up to 256 characters. Defaults to a fresh `dlv_…` ID |

`send` returns a `ChannelSession` with `id`, the current `continuationToken`,
`isNew`, and `coalesced: true` for a coalesced call. On Cursor hosting, it
returns the ID before the session exists.

## Events

The `events` map subscribes the channel to stream events for its sessions. Keys
are types from the [event vocabulary](/docs/reference/sessions.md#stream-events), or `"*"`.
Each handler receives `(event, channel, ctx)` with `channel.state`, session
info, and host services, so it can post progress and replies. `state` on the
channel definition is each new session's starting JSON, unless a route passes
`state` to `send`, and handlers update it through `channel.state`.

## Start and stop a channel

`onStart(args)` runs when the channel mounts, and `onStop()` runs when the host
stops. Besides the route helpers, `onStart` receives
`emitAssistantMessage(sessionId, text)`, which appends assistant text without a
model turn, and `hasContinuationSession`, `isContinuationBusy`, and
`interruptContinuation`, which check or stop the session behind a token.

## Auth policies

Every route runs the channel's `auth` array, which defaults to `[localDevStrict()]`. The first policy to return an `AuthContext` admits the request, and if every policy returns `null`, the route answers `401`.

| Policy | Admits |
| --- | --- |
| `localDevStrict()` | Direct loopback requests with a loopback hostname and no forwarding headers |
| `localDev()` | The same, with any hostname |
| `loopbackOnly()` | Any loopback peer, including relays that add forwarding headers |
| `bearerAuth(tokenOrVerify)` | A matching bearer token, or one the verifier accepts |
| `sharedSecretAuth({ header, secret })` | A header that matches the named secret |
| `hmacSignatureAuth({ header, secret, prefix?, separator? })` | A hex HMAC-SHA256 signature of the raw body under the named secret, or any of several signatures split by `separator`. The secret can hold up to 32 values, one per line, and any of them verifies |
| `allowAll()` | Every caller, as an anonymous principal |
| `publicEndpoint()` | Every caller, with no alias token on hosting. The handler must verify the provider's signature |

Use `allowAll()` only for a public surface or one protected upstream. `publicEndpoint()` applies only to custom channel routes and doesn't
open the built-in session or tool API.

The admitted `AuthContext` becomes the request principal, which
[owns the sessions it creates](/docs/reference/sessions.md#session-contents).

On a channel without its own `auth`, `--bearer-token` swaps the default for
`bearerAuth(...)` and `--allow-anonymous` swaps it for `allowAll()`.

## Prebuilt channels

Import each one from `@grok/agent-sdk/channels/<name>`.

| Function | Details |
| --- | --- |
| `slackChannel()` | [Slack](/docs/guides/slack.md) |
| `githubChannel()` | [GitHub](/docs/guides/github.md) |
| `gitlabChannel()` | [GitLab](/docs/guides/gitlab.md) |
| `bitbucketChannel()` | [Bitbucket](/docs/guides/bitbucket.md) |
| `pagerdutyChannel()` | [PagerDuty](/docs/guides/pagerduty.md) |
| `deploymentsChannel()` | Deployment events filtered by source, environment, or event name |

Use `defineChannel` for other platforms, such as Discord.

## Handler results

Hooks on the GitHub, GitLab, Bitbucket, and PagerDuty channels, such as
`onPullRequest` or `onIncident`, return one of three results:

| Return value | What happens |
| --- | --- |
| `{ auth }` | Runs the model turn with that auth. It can also set the turn's `message`, `title`, or `context` |
| `{ task }` | Runs your function on the host. That function can call `send` to start a model turn |
| `null` | Ignores the event |

## Hosted session keys

On Cursor hosting, each request to a `defineChannel` route starts a new session
unless it carries `X-BDK-Session-Key`. Requests with the same value on one
channel share one session, in arrival order. The handler's
`continuationToken` doesn't change that choice, and local `serve` ignores the
header.

| Rule | Contract |
| --- | --- |
| Auth | The alias token or `sharedSecretAuth()`. Routes admitted by `hmacSignatureAuth()` or `publicEndpoint()` answer `400` |
| Value | 1 to 256 printable ASCII characters with no space or comma, or the request gets `400`. Hosting stores the value, so keep secrets and personal data out of it |
| Scope | One channel on one deployment. Prebuilt channels ignore the header |
| Retries | Hosting doesn't deduplicate. A retry runs a second turn |

## Related

- [Webhooks](/docs/guides/webhooks.md)
- [Sessions](/docs/reference/sessions.md)

---

Source: /docs/reference/cli.md

# Agent SDK CLI

The CLI needs Node 22.13 or newer. Run `agent-sdk <command> --help` to see
every flag for that command.

```text
agent-sdk <command>
```

`agent-sdk help --all` prints every command, and
`agent-sdk help <command> --json` prints one command's flags as JSON.

Commands are named `agent-sdk <resource> <verb>`, such as
`agent-sdk deployment list`, apart from workflow verbs such as `init`, `dev`,
and `deploy`. Older names, such as `agent-sdk deployments` or
`agent-sdk secrets set`, still run and print the new form on stderr. The `bdk`
command remains available.

## Command catalog

### Development commands

| Command | Contract |
| --- | --- |
| `help`, `--help`, `-h` | Print the command index, or one command's help |
| [`serve`](#serve) | Serve one or more agents over HTTP |
| [`dev`](#dev) | Serve agents with local-development behavior |
| [`docs`](#docs) | Serve the bundled documentation |
| [`init`](#init) | Scaffold a project, or convert an Automation |
| [`migrate`](#migrate) | Open a pull request that migrates an early-alpha project |
| [`skills list`, `skills install`](#skills-install) | List or refresh the bundled coding-agent skills |
| [`info`](#info) | Print a project's discovered parts or deployment manifest |
| [`validate`](#validate) | Check project diagnostics and set the exit status |
| [`version`](#version) | Print the installed package version |
| [`update`](#update) | Upgrade the installed CLI |
| [`login`, `logout`, `whoami`](#login-logout-whoami) | Store, remove, or show the Cursor credential |

### Session and eval commands

| Command | Contract |
| --- | --- |
| [`chat`](#chat) | Talk to a running agent, or reattach a session |
| [`run`](#run) | Run turns on a local or running agent |
| [`tool call`](#tool-call) | Call a server tool without a model turn |
| [`logs`](#logs) | Follow or dump agent logs |
| [`session list`](#session-list) | List sessions, or their costs |
| [`session get`](#session-get) | Inspect a session or a saved trace |
| [`session create`](#session-create) | Start or continue a hosted session |
| [`eval`](#eval) | Run evals, or list, get, and cancel remote eval batches |

### Managed hosting commands

| Command | Contract |
| --- | --- |
| [`deploy`](#deploy) | Deploy an agent |
| [`deployment list`, `deployment get`](#deployment-list) | List deployments, or inspect one |
| [`deployment stop`, `start`, `kill`, `unlock`, `cancel-runs`, `upgrade`, `rollback`, `delete`, `rotate-token`](#deployment-lifecycle) | Change a deployment's state, release, or alias token |
| [`deployment update`](#deployment-update) | Replace a deployment's event repositories |
| [`secret`](#secret) | Set, list, or delete deployment secrets |

### Connection and channel commands

| Command | Contract |
| --- | --- |
| [`mcp serve`, `mcp install`](#mcp) | Connect an MCP client to an agent |
| [`mcp oauth`](#mcp-oauth) | Authorize an MCP connection |
| [`slack`](#slack) | Provision and check Slack channel apps |
| [`github`](#github) | Forward, replay, and inspect GitHub webhooks |
| [`gitlab`, `bitbucket`, `pagerduty`](#replay-other-webhooks) | Replay and inspect GitLab, Bitbucket, or PagerDuty webhooks |

## Targets {#choose-a-target}

| Target | Selection | Commands |
| --- | --- | --- |
| Ephemeral local server | Omit `--url` and `--prod` | `run`, `tool call`, `eval` |
| Running server | Pass `--url <baseUrl>` | `chat`, `run`, `tool call`, `eval`, `logs`, `session list`, `session get`, `mcp serve` |
| Default local server | Omit `--url` and `--prod` to use `http://127.0.0.1:3000` | `chat`, `logs`, `session list`, `session get` |
| Hosted deployment | Pass `--prod` | `session list`, `session get`, `logs`. `session create` is always hosted. |

A hosted deployment supports only the commands in its row. `chat`, `run`,
`tool call`, `eval`, `mcp serve`, and `mcp install` exit with a usage error
when you pass `--prod`. To test a hosted agent, start a turn with
[`session create`](#session-create) and inspect it with
[`session get`](#session-get).

`--url` and `--prod` are mutually exclusive. An explicit `--url` must include
the slug for a multi-agent server, such as `http://127.0.0.1:3000/pr-approver`.
Otherwise `--slug` picks the agent, and under `--prod` it defaults to the
`--dir` basename. `--json` prints one JSON document on stdout, including
errors. Without it, output is text, even when piped.

## Local servers

### Serve agents {#serve}

`serve` mounts every project discovered under `--dir`:

```text
agent-sdk serve [--dir <path>] [--port <n>] [--host <host>]
  [--mode multi|single] [--dev] [--api-key <key>]
  [--state-root <path>]
  [--bearer-token <secret> | --allow-anonymous]
  [--allow-anonymous-cursor-github]
  [--allow-anonymous-cursor-account-mcp]
  [--public-url <url>]
  [--peers-url <url>]
  [--no-schedules] [--no-playground] [--no-docs]
  [--cursor-events --repo <owner/name>]...
```

`serve` listens on `127.0.0.1:3000`, or the next free port when that one is
taken. `--mode multi`, the default, mounts each project at `/<slug>/v1/*`, and
`--mode single` mounts one project at `/v1/*`. `--dev` turns off schedule and
reminder firing and admits unsigned loopback GitHub deliveries.

`--bearer-token` requires a token on routes that don't declare their own
authentication. `--allow-anonymous` admits every caller as one principal, so
use it only behind a trusted network boundary. The slugs `v1`, `playground`,
and `docs` are reserved.

### Develop locally {#dev}

`dev` is `serve --dev` with an optional project path:

```text
agent-sdk dev
agent-sdk dev ./agent-sdk-pr-reviewer --port 3000
```

It accepts every `serve` option. Don't combine the path with a different
`--dir`.

### Serve the docs {#docs}

```text
agent-sdk docs [--port <n>] [--host 127.0.0.1] [--print]
```

`docs` serves the bundled documentation on a loopback port until you press
Ctrl-C. `--print` prints the URL without opening a browser.

## Sessions and turns

### Chat with an agent {#chat}

```text
agent-sdk chat [--url <baseUrl>] [--message <text>]
  [--session <id> | --resume] [--continuation-token <token>]
  [--slug <slug>] [--json] [--text] [--no-color]
```

On a TTY, `chat` opens a REPL and sends any `--message` first. Without a TTY, it
sends `--message` as one turn, or reads newline-delimited messages until EOF.
`--session <id>` reattaches a stored session, and `--resume` picks the most
recent one.

### Run turns {#run}

```text
agent-sdk run [--dir <path>] [--message <text>]...
  [--messages-file <path>] [--url <baseUrl>]
  [--session <id>] [--continuation-token <token>]
  [--trace <file> | --no-trace] [--json]
  [--timeout-ms <n>] [--no-stream] [--slug <slug>] [--dry-run]
```

Repeat `--message` for a multi-turn run. `run` prints the compact trajectory,
writes an NDJSON trace unless you pass `--no-trace`, and exits `1` when the
trajectory fails. It has no default timeout.

### Call a tool {#tool-call}

```text
agent-sdk tool call <tool> [--input <json>]
  [--dir <path> | --url <baseUrl>]
  [--session <id>] [--slug <slug>] [--json]
```

`--input` takes JSON and defaults to `{}`. `--session` runs the tool in that
session's workspace and records the call. The command exits `1` unless the
response has `ok: true`. See
[Tools](/docs/reference/tools.md#call-a-tool-without-a-model-turn) for error codes.

### List sessions {#session-list}

```text
agent-sdk session list [--url <baseUrl> | --prod] [--since <duration|instant>]
  [--view sessions|cost] [--slug <slug>] [--team <id>]
  [--bearer-token <token>] [--limit <n>] [--page-token <token>] [--json]
```

`--limit` sets the page size, 20 by default and at most 100. With `--prod`,
it lists the most recently active hosted sessions, and `--since` reads back
to a cutoff such as `24h` or an ISO-8601 instant.
`--view cost` works only against a running server.

### Inspect a session {#session-get}

```text
agent-sdk session get <sessionId> [--url <baseUrl> | --prod]
  [--slug <slug>] [--team <id>]
  [--view trajectory|events|cost] [--json [--fields <a,b.c>]]
  [--trace <file.ndjson>] [--web [--print]]

agent-sdk session get --file <file.ndjson> [--json]
```

`session get` prints a compact trajectory, or raw [events](/docs/reference/sessions.md#stream-events) with `--view events`. `--view cost` and `--trace` work only locally. `--file` reads a saved trace and exits `1` when that trajectory failed, and `--web` opens the session in the playground. For a hosted deployment:

```bash
agent-sdk session list --prod
agent-sdk session get <sessionId> --prod --slug <slug> --view events
```

### Start a hosted session {#session-create}

```text
agent-sdk session create --message <text> [--session <ses_...>]
  [--slug <slug>] [--team <id>] [--wait [--timeout-ms <n>]]
  [--read-only] [--as-of <instant>] [--component <key>] [--dry-run] [--json]
```

`session create` sends one `--message` to the hosted deployment. Without
`--wait`, it exits `0` once the deployment accepts the turn, before a new
session has an ID. `--wait` polls until the session starts. It exits `3` when
`--timeout-ms` passes first and `1` when the delivery is skipped or fails.

`--session` continues a session that `session create` started. `--read-only`
stubs write tools, and `--as-of` freezes the session's clock at an ISO-8601
instant. See [Verify a hosted turn](/docs/deployment.md#verify-a-hosted-turn).

### Follow logs {#logs}

```text
agent-sdk logs [sessionId] [--url <baseUrl> | --prod]
  [--session <sessionId>] [--slug <slug>] [--team <id>] [--once] [--json]
```

`logs` follows output until Ctrl-C, and `--once` prints the current buffer and
exits. With `--prod`, it follows the hosted deployment's recent sessions and
prints a `==> <sessionId> (<channel>) <==` header when the output switches
sessions. A session ID follows one hosted session and requires `--prod`.

## Evals

### Run evals {#eval}

`eval` discovers `evals/**/*.eval.{ts,js}` and runs the selected cases:

```text
agent-sdk eval [--dir <path>] [evalId...]
  [--list] [--tag <tag>]... [--json] [--verbose]
  [--timeout-ms <n>] [--strict] [--max-concurrency <n>]
  [--junit <path>] [--artifacts <dir> | --no-artifacts]
  [--skip-report] [--no-stream]
  [--url <baseUrl>] [--no-wait] [--slug <slug>]
```

Select an exact case such as `weather/nyc`, a file prefix such as `weather`,
several IDs, or none for every case. Put an ID that is also a subcommand word
after `--`, as in `agent-sdk eval -- list`. The project needs an
[eval configuration](/docs/reference/evals.md#eval-configuration).

`--strict` fails a scored case below its soft threshold. The command exits `1`
for failed cases and `2` for invalid options or no matching cases. A running
server prints an Eval ID when it accepts the batch, and `--no-wait` returns at
that point. These commands require `--url`:

```text
agent-sdk eval list --url <baseUrl> [--limit <n>] [--page-token <token>]
agent-sdk eval get <evalId> --url <baseUrl>
agent-sdk eval cancel <evalId> --url <baseUrl>
```

`eval get` exits `3` while the batch runs and `1` when it failed or was
cancelled.

## Projects

### Scaffold a project {#init}

```text
agent-sdk init [directory] [--template <name>] [--var id=value]... [--json] [--dry-run]
agent-sdk init [directory] --from automation:<url-or-uuid> [--dry-run] [--json]
```

Without a directory, `init` prints the setup guide. With one, it adds missing
scaffold files, installs dependencies, and tries to put `agent-sdk` on `PATH`.
On a TTY, it also asks the template's questions and starts login when needed.

`--template` takes `agentic-owners`, `agents-md`, `code-wiki`, `demo`,
`security-help`, `security-reviewer`, `thermo-quality-review`, `thermo-review`,
`triage-jira`, or `triage-linear`. `triage` selects `triage-linear`.
`--var id=value` answers a template question without a prompt. `--dry-run`
lists the files without writing them.

#### Convert an Automation {#convert-automation}

`--from automation:` takes a Cursor Automation dashboard URL or UUID and
writes to `./<automation-name>` unless you name a directory. Generated files
don't overwrite existing paths, but the `npm install` that follows can run
lifecycle scripts from an existing `package.json`. See
[Convert a Cursor Automation](/docs/guides/convert-automation.md).

### Migrate an early-alpha project {#migrate}

```text
agent-sdk migrate [--dir <path>] [--repo <https-url>] [--ref <git-ref>]
  [--path <agent-path>] [--no-wait] [--json] [--api-key <key>]
```

`migrate` applies only to projects that deployed with `architecture: "v1"`
during the early alpha. It starts a Cursor cloud agent on a pushed Git ref
with the bundled `bdk-migrate-v2` skill, and the agent opens a pull request
with the source changes. The command never deploys or changes secrets.

With nothing to change, it prints `Nothing to migrate.` and exits `0`, and it exits `1` if the run ends without a pull request. `deploy` refuses a project that sets `architecture: "v1"` and points to `migrate`. See [Migrate an early-alpha project](/docs/guides/migrate.md).

### Install and list coding-agent skills {#skills-install}

```text
agent-sdk skills install [--dry-run] [--json]
agent-sdk skills list [--json [--fields <a,b.c>]] [--limit <n>] [--page-token <token>]
```

Package installation copies the bundled skills, `skills install` replaces the installed copies and removes stale ones, and `skills list` prints each skill's bundled and installed versions. The `bdk-cli` skill teaches agents to run `agent-sdk` safely.

### Inspect a project {#info}

```text
agent-sdk info [--dir <path>] [--view info|manifest] [--json [--fields <a,b.c>]]
```

Text output lists what discovery found, such as tools, channels, and
diagnostics. `--view manifest` summarizes one project's deployment metadata
without a network request.

### Validate a project {#validate}

```text
agent-sdk validate [--dir <path>]
```

`validate` prints every discovered project's diagnostics and exits `1` when
any diagnostic has error severity. `validate` fails when an SCM webhook secret
or GitHub credential is a literal or a getter, and the error message shows the
`{ secretName }` form to use. A project that still sets `architecture: "v1"`
gets the same finding as a warning.

## Credentials and versions

### Manage credentials {#login-logout-whoami}

```text
agent-sdk login [--api-key <key>] [--key-name <name>]
agent-sdk whoami [--json]
agent-sdk logout
```

`login` stores a supplied API key, or opens browser sign-in and stores the key
it creates. `whoami` shows the active credential and its source. `logout`
removes the stored credential but doesn't revoke the key, so revoke it in the
Cursor dashboard.

### Print the version {#version}

```text
agent-sdk version [--json]
agent-sdk --version
```

### Update the CLI {#update}

```text
agent-sdk update
```

`update` upgrades a recognized global or project install to npm's `latest`
tag, or prints a manual upgrade command. Published installs check for updates
daily unless an [update-check variable](#environment-variables) is set.

## Managed hosting

Every command here that changes a deployment takes `--dry-run`, which runs
the reads, stops at the first change, prints that request with secrets
redacted, and exits `0`.

`secret delete`, `image delete`, `slack destroy`, and the `deployment`
commands marked No in the [lifecycle table](#deployment-lifecycle) can't be
undone. On a terminal they ask you to type the slug or name, and a mismatch
exits `1`. Without a terminal, they exit `2` unless you pass `--yes`.

### Deploy agents {#deploy}

```text
agent-sdk deploy [--dir <path>] [--slug <slug> | --all] [--team <id>]
  [--repo <https-url>] [--ref <git-ref>] [--path <agent-path>]
  [--cursor-events-repo <repo>]... [--no-wait] [--json]
```

Deploying needs a team with agent hosting turned on, and a team service-account key with agent access can deploy. The CLI infers `--repo`, `--ref`, and `--path` from the current checkout, and the repository must use HTTPS.

`deploy` deploys one agent. On a folder of several agents, pass `--slug` to
pick one. `--all`, or picking several agents, prints one
`agent-sdk deploy --dir` command per agent and exits `1`.

`--cursor-events-repo` sets the repositories whose Cursor events the
deployment receives. Write `owner/name` for GitHub,
`gitlab.com/<group>/<project>` for GitLab, or
`bitbucket.org/<workspace>/<repo>` for Bitbucket. A GitLab path can include
subgroups. Repeat the flag once per repository. Each deploy replaces the whole list. Without the flag, the deployment watches its GitHub
source repository.

`deploy` waits until the deployment runs and exits nonzero if it fails, unless
you pass `--no-wait`. The first deploy of a slug can print an alias token
once. Slugs use lowercase letters, digits, `_`, and `-`, start with a letter
or digit, and have at most 64 characters. See [Deployment](/docs/deployment.md)
for egress hosts and the alias token.

### Inspect deployments {#deployment-list}

```text
agent-sdk deployment list [--team <id>] [--limit <n>] [--page-token <token>]
  [--json [--fields <a,b.c>]]
agent-sdk deployment get <slug> [--team <id>] [--json [--fields <a,b.c>]] [--web]
```

`deployment list` shows each deployment's slug, status, and update time, and `deployment get` adds the release, watched repositories, routes, secret names, and latest error. `--web` opens the deployment in [Deployed Agents](https://cursor.com/dashboard/deployed-agents), and an early-alpha deployment shows the steps to migrate it.

### Change a deployment {#deployment-lifecycle}

```text
agent-sdk deployment <command> <slug> [--team <id>] [--json] [--dry-run]
```

| Command | What it does | Undoable? |
| --- | --- | --- |
| `stop` | Stop the deployment. `--cancel-runs` also cancels active runs and reminder wakes. | Yes |
| `start` | Resume a stopped deployment. A kill lock stays. | Yes |
| `kill` | Lock deploys, stop the deployment, and cancel its runs and reminder wakes. To recover, run `unlock` and then `start`. | No |
| `unlock` | Remove the kill lock so deploys, upgrades, and rollbacks work again. Traffic stays stopped. | Yes |
| `cancel-runs` | Cancel active runs and reminder wakes. | No |
| `update` | Replace the [event repositories](#deployment-update). | Yes |
| `upgrade` | Move to a ready release, the newest unless you pass `--release`. | Yes |
| `rollback` | Move to an earlier ready release, the previous one unless you pass `--release`. | Yes |
| `delete` | Remove the deployment and free its slug. | No |
| `rotate-token` | Replace the alias token. The old one stops working at once. | No |

### Replace event repositories {#deployment-update}

```text
agent-sdk deployment update <slug> (--event-repo <owner/name>... | --no-event-repos)
  [--team <id>] [--dry-run] [--json]
```

The repositories you pass replace the deployment's whole event repository
list, and `--no-event-repos` clears it.

### Manage deployment secrets {#secret}

```text
agent-sdk secret set <slug> NAME [NAME2 ...]
  [--team <id>] [--from-argv] [--json]
agent-sdk secret list <slug> [--team <id>] [--limit <n>] [--page-token <token>] [--json]
agent-sdk secret delete <slug> NAME [--team <id>] [--yes] [--dry-run] [--json]
```

On a TTY, `secret set` prompts for each hidden value. With one name,
redirected stdin is the whole value minus one trailing newline, so a PEM key
keeps its line breaks:

```bash
agent-sdk secret set my-bot GITHUB_APP_PRIVATE_KEY < ./app.private-key.pem
```

With several names, give one line per name. `set` rejects `NAME=VALUE` arguments unless you pass `--from-argv`, because shell history keeps them. Names use `UPPER_SNAKE_CASE`, and the `CURSOR_` prefix is reserved. `secret list` returns names, never values.

## MCP connections

### Connect an MCP client {#mcp}

```text
agent-sdk mcp serve --url <baseUrl> [--bearer-token <token>]

agent-sdk mcp install --url <baseUrl> [--bearer-token <token>]
  [--name <server-name>] [--dry-run] [--json]
```

`mcp serve` proxies newline-delimited JSON-RPC from stdin to the agent's MCP
endpoint and reserves stdout for MCP messages. `mcp install` adds the agent to
`~/.cursor/mcp.json`. An entry made with `--bearer-token` contains that token,
so treat every printed or written copy as a credential.

### Authorize MCP OAuth {#mcp-oauth}

```text
agent-sdk mcp oauth <connection> [--dir <path>]
  [--slug <slug>] [--team <id>]
```

`<connection>` is the basename under `bot/mcp-connections/` or
`bot/host-connections/`. A `url` connection with `oauth: true` opens a browser
PKCE flow and saves tokens bound to that URL, so authorize again after you
change it. A `cursorAccount` connection exits nonzero and points to the
cursor.com dashboard. See [Host MCP OAuth](/docs/guides/mcp-oauth.md).

## Channel helpers

### Configure Slack {#slack}

```text
agent-sdk slack setup
agent-sdk slack create [--dir <path>] [--name <name>] [--env prod]
  [--slack-team <id>] [--team <id>] [--icon <url-or-file>]
  [--prefix <prefix> | --no-prefix] [--channel-posts] [--json]
agent-sdk slack destroy [--dir <path>] [--env prod] [--yes] [--dry-run]
  [--slack-team <id>] [--team <id>] [--json]
agent-sdk slack icon <url-or-file> [--dir <path>] [--env prod]
  [--slack-team <id>] [--team <id>] [--json]
agent-sdk slack init --manual [--dir <path>] [--name <name>]
  [--prefix <prefix> | --no-prefix] [--channel-posts]
  [--install | --no-install] [--slack-team <id>] [--env dev|prod]
agent-sdk slack manifest [--dir <path>] [--name <name>]
  [--env dev|prod|both] [--channel-posts] [--dry-run]
agent-sdk slack doctor [--dir <path>]
  [--prefix <prefix> | --no-prefix] [--channel <id>]... [--json]
```

`slack create` opens the
[Deployed Agents](https://cursor.com/dashboard/deployed-agents) wizard, writes
the token pair to `.env.local`, and runs `doctor`, which checks both tokens and
Socket Mode. It makes the development app unless you pass `--env prod`. The
token prefix defaults to the project directory name in upper snake case.
`--channel-posts` adds the message events that `engagement.channelPosts`
needs. See [Slack](/docs/guides/slack.md) for manual setup.

### Test GitHub webhooks {#github}

```text
agent-sdk github forward [--dir <path>] [--slug <slug>] [--channel <id>]
  [--repo owner/repo | --org <org>] [--events a,b,c]
  [--url <url>] [--host <host>] [--port <n>]
  [--secret <secret>] [--install]
agent-sdk github replay <pr-url-or-owner/repo#N>
  [--events a,b,c | '*'] [--action <action>] [--conclusion <result>]
  [--comment <body>] [--context <name>] [--dir <path>]
  [--slug <slug>] [--channel <id>] [--url <url>]
  [--secret <secret>] [--dry-run] [--fixtures-dir <dir>] [--json]
agent-sdk github events [--dir <path>] [--host <host>] [--port <n>] [--json]
agent-sdk github doctor [--install] [--json]
```

`forward` sends live deliveries to discovered channels through `gh webhook forward`. It needs repository admin access, or organization owner access for `--org`, and it uses the GitHub CLI's own login, so unset `GITHUB_TOKEN` and `GH_TOKEN` first. `doctor` checks the GitHub CLI and its webhook extension.

`replay` reads a pull request with read access, builds webhook payloads, and
posts them to matching channels. Its events are `pull_request`, the default,
`issue_comment`, `pull_request_review_comment`, `check_run`, `check_suite`,
`workflow_run`, and `status`. `--dry-run` prints the payloads without posting,
and `--fixtures-dir` also writes them as fixtures. See
[GitHub](/docs/guides/github.md) for channel setup.

### Test GitLab, Bitbucket, and PagerDuty webhooks {#replay-other-webhooks}

```text
agent-sdk gitlab replay <mr-url-or-group/project!N> [replay flags]
agent-sdk bitbucket replay <pr-url-or-workspace/repo#N> [replay flags]
agent-sdk pagerduty replay [incident-id] [replay flags]
agent-sdk <gitlab|bitbucket|pagerduty> events [--dir <path>] [--json]
agent-sdk <gitlab|bitbucket> forward [--dir <path>]
```

Replay takes the same flags as `github replay`, including `--fixtures-dir`. `events` lists the discovered channels, and `forward` prints the webhook and tunnel recipe for live deliveries.

| Command | Replay events | Default event | Read credential | `--secret` default |
| --- | --- | --- | --- | --- |
| `gitlab replay` | `merge_request`, `note`, `pipeline`, `push` | `merge_request` | `GITLAB_TOKEN` | `GITLAB_WEBHOOK_SECRET` |
| `bitbucket replay` | `pullrequest:created`, `pullrequest:updated`, `pullrequest:comment_created`, `repo:push`, in Cloud or Data Center format | `pullrequest:created` | `BITBUCKET_TOKEN` | `BITBUCKET_WEBHOOK_SECRET` |
| `pagerduty replay` | `incident.triggered`, `.acknowledged`, `.annotated`, `.escalated`, `.resolved`, `.reopened` | `incident.triggered` | None | `PAGERDUTY_WEBHOOK_SECRET` |

PagerDuty replay's incident ID defaults to `PREPLAY1`, and `--comment` is the
note body for `annotated`. Without a secret, its deliveries are unsigned, and
`agent-sdk dev` admits them from loopback. See [GitLab](/docs/guides/gitlab.md),
[Bitbucket](/docs/guides/bitbucket.md), and [PagerDuty](/docs/guides/pagerduty.md)
for channel setup.

## Environment variables {#environment-variables}

The CLI tries `--api-key`, `CURSOR_API_KEY`, `CURSOR_API_KEY_FILE`,
`CURSOR_SERVICE_ACCOUNT_KEY`, then the stored `agent-sdk login` credential, in
that order.

| Variable | Contract |
| --- | --- |
| `CURSOR_API_KEY` | Cursor credential. |
| `CURSOR_API_KEY_FILE` | Path to a Cursor credential file. |
| `CURSOR_SERVICE_ACCOUNT_KEY` | Team service-account credential. |
| `CURSOR_API_BASE_URL` | Backend for login, account, deployment, and event commands. |
| `CURSOR_BACKEND_URL` | Backend for agent turns. Set it with `CURSOR_API_BASE_URL` for a non-default backend. |
| `AGENT_SERVE_CONFIG_DIR` | CLI config directory for stored credentials and update state. |
| `AGENT_SERVE_NO_UPDATE_CHECK`, `NO_UPDATE_NOTIFIER`, `CI` | Turn off update checks when set to a value other than `0`. |
| `GITHUB_WEBHOOK_SECRET` | Default secret for GitHub forwarding and replay. |
| `GITHUB_APP_ID`, `GITHUB_APP_PRIVATE_KEY`, `GITHUB_APP_INSTALLATION_ID` | GitHub App credentials for outbound API calls. |
| `GITHUB_TOKEN`, `GH_TOKEN` | Tokens for outbound GitHub calls. |
| `GITLAB_TOKEN`, `GITLAB_WEBHOOK_SECRET` | GitLab API token and default webhook token. |
| `GITLAB_API_BASE_URL` | GitLab REST base URL for self-managed hosts. |
| `BITBUCKET_TOKEN`, `BITBUCKET_WEBHOOK_SECRET` | Bitbucket API token and default signing secret. |
| `BITBUCKET_API_BASE_URL` | Bitbucket Data Center REST base URL. |
| `SLACK_BOT_TOKEN`, `SLACK_APP_TOKEN` | Slack tokens for one agent. Multi-agent servers use `<PREFIX>_SLACK_BOT_TOKEN` and `<PREFIX>_SLACK_APP_TOKEN`. |

## Exit status

Every command uses these exit codes, and `agent-sdk help --json` lists them
as `exitCodes`.

| Code | Meaning |
| --- | --- |
| `0` | `ok` |
| `1` | `failed`, `aborted`, `invalid_argument`, `rate_limited`, or `unavailable` |
| `2` | `usage`, for a bad command, flag, or argument |
| `3` | `pending`, while the work still runs |
| `4` | `not_found` |
| `5` | `unauthenticated` or `permission_denied` |
| `6` | `conflict` or `failed_precondition` |

## Related

- [Project layout](/docs/reference/project-layout.md)
- [Sessions, events, and streaming](/docs/reference/sessions.md)
- [Deployment](/docs/deployment.md)
- [Early-alpha migration](/docs/guides/migrate.md)

---

Source: /docs/reference/connections.md

# MCP connections

An MCP connection gives the agent tools from an MCP server. Each file under
`bot/mcp-connections/` default-exports `defineConnection` from
`@grok/agent-sdk/connections`, and its filename becomes the server name. A
connection reaches a remote HTTP server, a local stdio process, the signed-in
Cursor account's connectors, or a peer agent by slug.

Put a server in `bot/host-connections/` when only host code should call it.
Host connections use the same shape and support `agent-sdk mcp oauth`, but the
model and playground don't see them.

## Remote MCP server

Point a connection at a remote server with a URL.

```ts
import { defineConnection } from "@grok/agent-sdk/connections";

export default defineConnection({
  url: "https://mcp.linear.app/mcp",
  headers: { authorization: "Bearer ${LINEAR_TOKEN}" },
});
```

## Credentials in headers and env

Write a credential as a `${NAME}` template in `headers` on a remote connection
or in `env` on a stdio one.

```ts
export default defineConnection({
  command: "slack-mcp-server",
  args: ["--transport", "stdio"],
  env: { SLACK_MCP_XOXB_TOKEN: "${SLACK_BOT_TOKEN}" },
});
```

On Cursor hosting, your secrets aren't in `process.env` yet when the connection
file loads. Agent SDK resolves a `${NAME}` template each time the connection
opens, so the template works locally and on hosting. Reading
`process.env.NAME` at the top of the file only works locally.

`agent-sdk validate` warns when the environment doesn't define a template's
variable. An unset variable resolves to an empty value, which the server
reports as an auth failure.

## Host MCP OAuth

For a server that speaks OAuth, set `oauth: true` and follow
[Connect a private MCP server with OAuth](/docs/guides/mcp-oauth.md).

## Per-session auth

For an HTTP or SSE connection whose credential depends on who the session is
for, declare an `auth` callback instead of static headers. The headers it
returns override the static ones.

```ts
export default defineConnection({
  url: "https://api.cursor.com/v1/mcp/plugins",
  auth: async (session) => ({
    headers: { Authorization: `Bearer ${await grantFor(session)}` },
  }),
  advertiseTools: true,
});
```

- The callback runs on the host before every turn, including reminder fires
  and follow-ups after a restart, so the model never sees or chooses the
  account. A callback that throws fails the turn.
- Derive the identity from `session.auth`, `session.id`, or your channel's
  session state, not from `session.continuationKey`, which the HTTP channel
  rotates after every follow-up.
- `host.mcp` calls from server tools send only the static headers, and `auth`
  can't be combined with `oauth: true`.

## Advertise tools {#advertise-tools}

Set `advertiseTools: true` when the model should call an MCP server's tools by
name. Agent SDK keeps each tool's description, schemas, and MCP annotations,
and renames invalid or colliding names.

A listing or auth failure, an invalid tool name, or a name collision fails the
turn. Set `optional: true` to skip an unavailable connection instead.
Advertised tools behave like server tools, and
[direct tool calls](/docs/reference/tools.md#call-a-tool-without-a-model-turn) use the same
names.

In a dry-run session, read-only MCP tools run, Agent SDK stubs write tools, and
tools without effect annotations are unavailable. A read-only tool gets the
same transient retry as an authored read, and a tool with no effect annotation
runs once.

## Filter connection tools

Set `tools` to serve only the listed names, or `disallowedTools` to drop names.
When both list a name, `disallowedTools` wins. The model and `host.mcp` see
only what remains, and a filter the model sees requires `advertiseTools: true`.

```ts
export default defineConnection({
  url: "https://mcp.example.com/inventory",
  advertiseTools: true,
  tools: ["search_skus", "get_stock"],
});
```

```ts
export default defineConnection({
  url: "https://mcp.example.com/inventory",
  advertiseTools: true,
  disallowedTools: ["place_order", "delete_sku"],
});
```

Names are the server's `tools/list` names, and Agent SDK skips unknown names. A
filter that matches nothing fails the turn unless `optional: true`. Add
`effects: "read"` to keep only the listed tools the server marks as reads. On a
host connection, a name list limits `host.mcp` without advertising anything.

## Local stdio MCP server

Run a local MCP server as a child process with `command`.

```ts
export default defineConnection({
  command: "node",
  args: ["--import", "tsx", "mcp/units-server.ts"],
  // env, cwd
});
```

To run TypeScript in the agent environment, define the tools on the connection
instead. Here `tools` takes an object of handlers rather than a list of names.
Agent SDK packages them as a stdio MCP server, so you write only `execute`.

```ts
export default defineConnection({
  tools: {
    probe_agent_tool: {
      description: "Prove the tool ran in the agent environment.",
      async execute({ label }) {
        return { ok: true, label };
      },
    },
  },
});
```

## Cursor account MCP connection

`{ cursorAccount: true }` forwards the MCP connectors that the signed-in Cursor
account already authorized in the dashboard, such as Linear, Notion, and Slack.
Each tool runs on the Cursor backend with the account's stored OAuth
credentials, so raw tokens never reach the serving host, session workspaces, or
traces.

```ts
// bot/mcp-connections/account.ts
export default defineConnection({
  cursorAccount: true,
  advertiseTools: true,
});
```

The agent gets every connected HTTP or SSE connector, the same as
`servers: "*"`, unless you pass a list such as `servers: ["Linear"]`. Name the
file `account.ts`, because `cursor.ts` collides with the IDE's `cursor` MCP
namespace. The host must be [signed in](/docs/reference/cli.md#environment-variables), or
`serve` fails at startup.

> [!CAUTION]
> Anyone who can talk to the agent can drive these connectors. `serve` refuses
> to start with `--allow-anonymous` and account connections unless you also pass
> `--allow-anonymous-cursor-account-mcp`, which belongs only behind a trusted
> boundary such as an SSO proxy. Prefer `--bearer-token` on shared hosts.

## Peer MCP connection {#peer-mcp-connection}

`{ agent: "<slug>" }` addresses another agent without a URL, headers, or
secrets. On a local multi-agent serve, the target is another mount slug. On
Cursor hosting, it's the lowercase deployment slug of another agent on the same
team.

The model gets the peer's `ask` and `check` tools, plus `call_tool` when the
peer has server tools, so it can delegate work:

```ts
export default defineConnection({
  agent: "weather-agent",
  description: "Delegate weather questions to the weather agent.",
});
```

Declare the connection only on the caller. In a local multi-agent serve,
unknown slugs and self-references fail at startup. A single-agent serve omits
peers with a warning unless its hosting platform sets `--peers-url`. See
[Peer agents](/docs/guides/agent-to-agent.md#delegate-a-question-to-a-specialist).

## Call MCP from host code

Server tools reach every connection, including host connections, through
`ctx.host.mcp`. Channel and schedule handlers use `args.host.mcp`.

```ts
// bot/tools/search_linear.ts
import { defineTool } from "@grok/agent-sdk/tools";
import { z } from "zod";

export default defineTool({
  description: "Search Linear issues.",
  inputSchema: z.object({ query: z.string() }),
  async execute({ query }, ctx) {
    return ctx.host.mcp.callTool("linear", "list_issues", { query });
  },
});
```

`host.mcp.names()` lists connection names, and `listTools(name)` and
`callTool(name, tool, args)` call into a named connection.

## Related

- [Tools](/docs/reference/tools.md)
- [Peer agents](/docs/guides/agent-to-agent.md)

---

Source: /docs/reference/evals.md

# Evals

Evals run fixed cases against an agent and record whether its turns,
tools, events, and output meet a contract. The
[Evals guide](/docs/evals.md) covers the regression workflow.

## Eval discovery and case IDs

Eval files live under the project-root `evals/` directory and end in
`.eval.ts` or `.eval.js`. The path under that directory becomes the
eval ID, so `evals/readiness.eval.ts` becomes `readiness`,
`evals/builds/api.eval.ts` becomes `builds/api`, and
`evals/builds/index.eval.ts` becomes `builds`.

Export one `defineEval` that sets exactly one of `test` or `cases`. A
case ID adds one path segment to the file ID:

```ts
export default defineEval({
  tags: ["smoke"],
  cases: [
    {
      id: "checkout",
      async test(t) {
        await t.send("Review checkout PR 42.");
        t.succeeded();
      },
    },
    {
      id: "search",
      async test(t) {
        await t.send("Review search PR 7.");
        t.succeeded();
      },
    },
  ],
});
```

Those cases are `prs/checkout` and `prs/search` when the file is
`evals/prs.eval.ts`. Case IDs must be unique single path segments.

A file can also export an array of `defineEval` calls, named with
zero-padded indexes such as `sql/0000`.

`iterations` repeats one datapoint 1 to 100 times. With
`iterations: 3`, `weather/nyc` runs as `weather/nyc/1` through
`weather/nyc/3`, and selecting `weather/nyc` runs all three.

## Eval configuration

Every suite needs `evals/evals.config.ts` with `maxConcurrency`:

```ts
import { defineEvalConfig } from "@grok/agent-sdk/evals";

export default defineEvalConfig({
  maxConcurrency: 20,
  judge: { model: "gpt-5.4-mini" },
});
```

| Option | Contract |
| --- | --- |
| `maxConcurrency` | Required. Concurrent authored datapoints, from 1 to 200 |
| `timeoutMs` | Per-case timeout. A case or file value wins, then the CLI flag, then this config |
| `judge` | Default model for `t.judge` |
| `reporters` | Objects notified as cases and runs complete |
| `maxPlaygroundRuns` | Server-side batches kept in playground history. Defaults to 20 |

A case can override `description`, `tags`, `timeoutMs`, `iterations`,
`judge`, `reporters`, and `metadata`. Case metadata merges over file
metadata, and case reporters add to the file's reporters.

## Send turns in a case

`await t.send(message, options?)` runs one turn and waits until it
finishes or fails. Sends in one test share a session.

The returned turn has the assistant `message`, `sessionId`, `events`,
ordered `toolCalls`, `ok`, and a one-based `index`. Assertions on a turn
inspect only that turn, and assertions on `t` inspect the whole case.
The case also exposes `t.reply`, `t.events`, `t.turns`, `t.sessionId`,
`t.iteration`, `t.iterations`, and the timeout `t.signal`.

On the first send only, `workspaceFiles` seeds the session workspace
from a map of relative paths to content, and `workspaceDir` sets an
absolute working directory for a new local session.

`turn.expectOk()` throws when the turn failed, so later steps that
depend on it don't run.

## Trajectory assertions

Assertions record failures without stopping the test.

| Assertion | Checks |
| --- | --- |
| `t.succeeded()` | Run finished without failure |
| `t.calledTool(name, matcher?)`, `t.notCalledTool(name)` | A matching tool request occurred, or none did |
| `t.loadedSkill(name)` | Agent opened a named skill |
| `t.toolOrder(names)`, `t.eventOrder(matchers)` | Tool requests or event groups appeared in relative order |
| `t.usedNoTools()`, `t.maxToolCalls(max)` | No tool was requested, or the count stayed under the bound |
| `t.noFailedActions()` | No tool result failed |
| `t.calledSubagent(name, matcher?)` | Matching subagent call occurred |
| `t.taggedArtifact(kind?, predicate?)` | Matching durable artifact was tagged |
| `t.event(type, matcher?)`, `t.notEvent(type, matcher?)` | A matching session event occurred, or none did |
| `t.eventsSatisfy(label, predicate)` | Predicate accepted the event stream |
| `t.check(value, expectation)` | Value met an expectation builder |
| `t.score(name, value)` | Recorded a numeric score from 0 to 1 |

`t.requireToolCall`, `t.requireInputRequest`, and `t.require` return the
matched value and stop the rest of the test when nothing matches.

## Tool and event matchers

Tool matchers can narrow by `input`, `output`, `status`, and `count`.
Values can be literals, regular expressions, or predicates. Object
literals match partially:

```ts
t.calledTool("inspect_pr", {
  input: { verdict: "review" },
  count: 1,
});
```

Tool status is `completed`, `failed`, `pending`, or `rejected`.
Subagent matchers accept `output`, `status`, `count`, and `callId`.
Event matchers accept `data` and `count`.

## Expectation builders

`includes(string | RegExp)`, `equals(value)`, `matches(schema)`, and
`satisfies(predicate, label)` are hard gates by default, and
`similarity(expected)` is a soft score. `matches` accepts Standard
Schema implementations such as Zod.
`normalizedSimilarity(value, expected)` returns the same 0-to-1 value
that `similarity` uses.

## Severity and verdicts

Every assertion returns a severity handle:

```ts
t.calledTool("search").soft();
t.check(t.reply, similarity("Expected answer")).atLeast(0.8);
t.judge.closedQA("names the failing step").gate(1);
```

- `.gate(threshold?)` fails the case on a miss.
- `.soft(threshold?)` records a value and never fails the case.
- `.atLeast(threshold)` sets a soft bar, and a miss gives the case a
  `scored` verdict.

Case verdicts are `passed`, `failed`, `scored`, or `skipped`. A `scored`
case fails the run only under `--strict`.

## Judges

Judges grade what deterministic checks can't express:

```ts
t.judge.factuality(expected).atLeast(0.8);
t.judge.summarizes(expected).atLeast(0.8);
t.judge.closedQA("names the root cause").gate(1);
t.judge.sql(expected).atLeast(0.9);
```

Each judge grades `t.reply` unless you pass `{ on }` with another value.
Set judge options in the project config, the eval, the case, or the
call, and the nearest value wins.

`t.judge.model(prompt)` returns a raw model reply for custom grading.
Agent output is untrusted input, so wrap it with `fenceUntrusted` and
include `EVAL_JUDGE_INJECTION_GUARD` in the judge prompt.

## Judge host

A judge needs a judge model and a Cursor credential from
`agent-sdk login`, `CURSOR_API_KEY`, or `CURSOR_SERVICE_ACCOUNT_KEY`.
Without a usable credential the runner skips the case, and without a
judge model it fails the case and names where to set one.

## Datasets and fixtures

Load committed JSON, JSONL, or YAML relative to the project root:

```ts
import { defineEval, includes } from "@grok/agent-sdk/evals";
import { loadYaml } from "@grok/agent-sdk/evals/loaders";

const rows = await loadYaml<Array<{ prompt: string; expected: string }>>(
  "evals/data/cases.yaml",
);

export default rows.map(row =>
  defineEval({
    async test(t) {
      await t.send(row.prompt);
      t.check(t.reply, includes(row.expected));
    },
  })
);
```

## Reporters and results

Built-in reporters include `JUnit({ filePath, suiteName? })` and
`Artifacts({ dir })`. A custom reporter can implement `onRunStart`,
`onEvalComplete`, and `onRunComplete`.

JSON output has run totals and one result per case. Each result has the
case ID, verdict, assertions, session ID, inputs, final text, tool
calls, metrics, logs, duration, metadata, tags, and any error or skip
reason.

Each local run writes `summary.json`, `results.jsonl`, and
`evals/<case-id>.json` under `evals/<stamp>/` in the project state
directory. `--state-root` doesn't move them, and `--artifacts <dir>` or
`--no-artifacts` does.

## Select cases

```text
agent-sdk eval --dir . --list
agent-sdk eval --dir .
agent-sdk eval --dir . builds/checkout
agent-sdk eval --dir . --tag smoke
```

An ID matches itself and its descendants. Repeated IDs or tags combine
with OR, and a case must match both groups when you pass both.
[Run evals](/docs/reference/cli.md#eval) covers runs on a server.

## Suite summary cases

A case tagged `EVAL_SUMMARY_TAG` aggregates its file, so selecting it by
ID or with `--tag summary` also runs every other case in that file.

```ts
import { defineEval, EVAL_SUMMARY_TAG } from "@grok/agent-sdk/evals";

export default defineEval({
  cases: [
    {
      id: "checkout",
      async test(t) {
        await t.send("Review checkout PR 42.");
        t.succeeded();
      },
    },
    {
      id: "overall",
      tags: [EVAL_SUMMARY_TAG],
      async test(t) {
        await t.send("Summarize the checkout review.");
        t.succeeded();
      },
    },
  ],
});
```

---

Source: /docs/reference/extensions.md

# Extensions

An extension packages capabilities that would otherwise live under
`bot/`, such as tools, skills, and connections. One mount file adds them
under a namespace without copying their source into your project.

Extensions and Cursor plugins run as trusted host code. A namespace prevents name collisions but doesn't sandbox the package, so review the package before mounting it.

## Mount an extension

Install the package and add one file under `bot/extensions/`. The
filename becomes the namespace, so this mount contributes names such as
`ci__failed_jobs` and `ci__triage`. The `__` separator is reserved, so
root contributions can't impersonate a mounted one.

```ts
// bot/extensions/ci.ts
import ci from "@acme/ci-triage";

export default ci({
  org: "acme",
  token: () => process.env.BUILDKITE_TOKEN ?? "",
});
```

Pass `token` as a function so it reads `BUILDKITE_TOKEN` when a tool
calls it.

The extension validates its settings at load, so a missing or mistyped
setting fails `agent-sdk validate`.

Extension instructions follow the root agent's instructions.
The root agent still needs its own `bot/instructions.md`.

## Inspect a mount

Run discovery after mounting or upgrading an extension:

```text
agent-sdk validate --dir .
agent-sdk info --dir . --json
```

`info` lists each mount and every namespaced name it adds. The same
data appears under `extensions` in `GET /v1/info`.

`info` also shows the egress hosts and secret names each mount adds to
a hosted deployment, and `agent-sdk deploy` names every extension that
adds egress.

## Adjust a mounted extension

To replace or remove one contribution, mount as a directory. Put the
package mount in `extension.ts` and overlay the short name beneath it.

```text
bot/extensions/ci/
  extension.ts
  tools/
    job_log.ts
    retry_job.ts
```

```ts
// bot/extensions/ci/tools/job_log.ts
import { defineTool } from "@grok/agent-sdk/tools";
import {
  jobLogInput,
  readRedactedLog,
} from "../../../lib/ci.js";

export default defineTool({
  description: "Read a redacted CI log.",
  inputSchema: jobLogInput,
  execute: readRedactedLog,
});
```

```ts
// bot/extensions/ci/tools/retry_job.ts
import { disableTool } from "@grok/agent-sdk/extensions";

export default disableTool();
```

The first file replaces `ci__job_log`, and the second removes
`ci__retry_job`. Use the matching `disable*` helper for another slot, or
generic `disable()` when the file location already identifies it.
Validation fails when an overlay names a contribution the package
doesn't provide.

## Mount a Cursor plugin

`cursorPlugin()` mounts a vendored Cursor or Claude plugin the same way. Skills, commands, agents, and MCP servers become Agent
SDK skills, subagents, and connections.

```ts
// bot/extensions/notion.ts
import { cursorPlugin } from "@grok/agent-sdk/extensions";

export default cursorPlugin({
  path: "plugins/notion-workspace",
  variables: {
    NOTION_TOKEN: "${NOTION_TOKEN}",
  },
});
```

`path` is relative to the project root. `variables` fills the plugin's
MCP placeholders with `${NAME}` templates read when the connection
opens. An unresolved placeholder fails
validation instead of mounting a partial server.

Plugin rules and hooks aren't mounted. Overlay a generated
`mcp-connections/<server>.ts` file to replace or disable one plugin
server. OAuth-backed plugin servers use
[Host MCP OAuth](/docs/guides/mcp-oauth.md).

## Extension contents

| Ships under the namespace | Ignored |
| --- | --- |
| Instructions, tools, skills, MCP and host connections | `agent.ts`, storage, OpenTelemetry |
| Hooks, channels, schedules, and subagents | Playground code, custom sandbox backends |
| Artifact kinds and workspace seed files | Nested extensions |

`agent-sdk validate` reports unsupported paths.

## Build an extension

Lay out the package like the shareable part of `bot/`. The root
`extension.ts` supplies a description and validates the consumer's
settings.

```text
@acme/ci-triage/
  package.json
  extension.ts
  instructions.md
  tools/
    failed_jobs.ts
    job_log.ts
  skills/
    triage.md
  lib/
    buildkite.ts
```

```ts
// extension.ts
import { defineExtension } from "@grok/agent-sdk/extensions";
import { z } from "zod";

export default defineExtension({
  root: import.meta.dirname,
  description: "Investigate failing Buildkite jobs.",
  config: z.object({
    org: z.string(),
    token: z.function().args().returns(z.string()),
    logTailLines: z.number().int().positive().default(200),
  }),
  hosting: () => ({
    egressDomains: ["api.buildkite.com"],
    secretNames: ["BUILDKITE_TOKEN"],
  }),
});
```

Package modules import this definition and read the validated
`extension.config`, with defaults applied. `hosting` adds its hosts and secret names to the mounting
agent's deployment. Publish the built entry point with its slot
directories and prose assets beside it.

Keep extension instructions and tool descriptions namespace-neutral
because the consumer chooses the prefix. [Project layout](/docs/reference/project-layout.md)
covers each slot's contract.

---

Source: /docs/reference/hooks.md

# Hooks

A hook subscribes to selected session events and can run a side effect after each matching event is recorded. Hooks run in the serving process for every session of the agent. They cannot change the turn, the prompt, or the reply. A handler that throws is logged and skipped.

Treat the event as read-only. Later subscribers see the same object. A hook is the wrong surface for anything that must run before the model or must fail a turn. See [Hook boundaries](#hook-boundaries).

`defineHook` is unrelated to [Cursor Agent hooks](https://cursor.com/docs/agent/hooks), the `.cursor/hooks.json` scripts that can observe, block, or modify the agent loop. Those still run inside a local session workspace.

## Author a hook

Author `bot/hooks/<name>.ts` with `defineHook` from `@grok/agent-sdk/hooks`. This hook meters tokens and pages on failure:

```ts
// bot/hooks/usage.ts
import { metrics } from "@opentelemetry/api";
import { defineHook } from "@grok/agent-sdk/hooks";

const meter = metrics.getMeter("acme");
const inputTokens = meter.createCounter("acme.tokens.input");
const turnFailed = meter.createCounter("acme.turn.failed");

export default defineHook({
  events: {
    async "turn.completed"(event, ctx) {
      if (ctx.session.purpose === "eval" || event.data.usage === undefined) {
        return;
      }
      inputTokens.add(event.data.usage.inputTokens);
    },
    async "turn.failed"(event, ctx) {
      const pagerUrl = process.env.PAGER_WEBHOOK_URL;
      if (
        pagerUrl === undefined ||
        ctx.session.purpose === "eval" ||
        event.data.status === "cancelled"
      ) {
        return;
      }
      turnFailed.add(1, { channel: ctx.channel.id });
      await fetch(pagerUrl, {
        method: "POST",
        headers: { "content-type": "application/json" },
        body: JSON.stringify({
          agent: ctx.agent.name,
          session: ctx.session.id,
          message: event.data.message,
        }),
        signal: AbortSignal.timeout(5_000),
      });
    },
  },
});
```

Any module under `bot/hooks/` is a hook named by its path without the extension. `*.test.ts` and `*.spec.ts` are skipped. The default export must be `defineHook(...)`, names can't contain `__`, and an empty `events` map is skipped with a warning. `agent-sdk validate` reports all three. An extension mounts its hooks as `<ns>__<name>`, and `disableHook()` removes one. See [Adjust a mounted extension](/docs/reference/extensions.md#adjust-a-mounted-extension).

`agent-sdk init` scaffolds `bot/hooks/memory.ts`. Delete it to opt out of the journal. See [Memory](/docs/reference/memory.md).

Read hosted secrets inside the handler, not at module scope. See [Add egress and secrets](/docs/deployment.md#add-egress-and-secrets). Give outbound calls a timeout, because a stalled request blocks later handlers on that session.

## Events and payloads

Keys are event types from the [event vocabulary](/docs/reference/sessions.md#stream-events), or `"*"` for every event. A typed key narrows `event.data`, a `"*"` handler receives the union so you switch on `event.type`, and every event carries `{ type, index, sessionId, turnId?, at, data }`. Skip `*.appended` deltas when you want the final text.

| Event | `event.data` |
| --- | --- |
| `message.received` | `{ text }` |
| `turn.completed` | `{ result?, usage?, cost? }`. `usage` has `inputTokens`, `outputTokens`, `cacheReadTokens`, `cacheWriteTokens`, and optional `reasoningTokens`. `cost` has `totalUsd` and the `model` it was priced against |
| `turn.failed` | `{ message, status? }`. `status` is `"error"` or `"cancelled"` when present |
| `actions.requested` | `{ calls: [{ callId, toolName, args?, parentCallId? }], parentCallId? }`. One envelope per model step. `parentCallId` marks subagent work |
| `actions.completed` | `{ results: [{ callId, toolName, output?, isError, stubbed?, parentCallId? }], parentCallId? }`. Emitted when every call in that batch has a result |
| `action.result` | `{ callId, toolName, output?, isError, stubbed?, parentCallId? }`. Live per-call envelope. `stubbed` means a dry-run session answered a write without running it |
| `action.tool_retry` | `{ callId, toolName, attempt, reason, connection? }`. A read is running again after a transient failure. `attempt` is the try about to run |

The types are `SessionEvent`, `SessionEventType`, and `HookContext`, exported from `@grok/agent-sdk`.

## Handler context

| Member | What it is |
| --- | --- |
| `ctx.session` | Read-only session info: `id`, `channelId`, `mode` (`chat` or `task`), `purpose` (`live` or `eval`), `auth`, plus `title` and `sdkAgentId` when set |
| `ctx.agent` | `{ name }` of the agent the event belongs to |
| `ctx.channel` | `{ id, continuationToken }`. The token is `null` when the session can't take follow-ups |
| `ctx.host.kv` | Durable JSON, shared by every session of the agent. Prefix keys with `ctx.session.id` for per-session state |
| `ctx.host.files` | Durable files, bound to this session. Pass `{ scope: "deployment" }` for agent-wide files |
| `ctx.host.mcp`, `ctx.host.github`, `ctx.host.slack` | The same shared clients tools get |
| `ctx.host.reminders` | Per-session [reminders](/docs/reference/schedules.md#reminders) |
| `ctx.artifacts` | Session-bound [artifacts](/docs/reference/artifacts.md) facade. `tag` fills in `sessionId` and `turnId` |
| `ctx.stateRoot` | Absolute path of the local state root. It resets when a hosted deployment is replaced. Keep derived state in `kv` or `files` |

## Hook dispatch

A hook runs after the event is recorded. It never delays the model.

| Rule | What happens |
| --- | --- |
| Same session | Events dispatch one at a time |
| Eval sessions | The same stream fires. Skip metering or paging when `ctx.session.purpose === "eval"` |
| Host restart | Recorded events are not replayed into hooks, so a mirror needs no dedupe |
| Slow handler | Holds the next event's handlers on that session, not the model. Keep handlers short and queue anything slow |

## Hook boundaries

| You want to | Use instead |
| --- | --- |
| Add context before the model runs | The channel's `send` message and `workspaceFiles`, `instructions.md`, skills, or `sandbox/workspace/` seed files |
| Reply on Slack, comment on a PR, or post any other delivery | The channel's `events` map, or the Slack and GitHub packs |
| Show PR progress (merge-box check, sticky banner) | `githubChannel({ progress: { commitStatus, banner } })`. See [GitHub agents](/docs/guides/github.md) |
| Gate a change on behavior | [Evals](/docs/evals.md) |
| Observe every session for audit, metrics, or a transcript mirror | A hook |

To act on a turn's decision, such as a label or a ticket, have the model record it with an authored [tool](/docs/reference/tools.md). A `turn.completed` hook then reads the recorded decision and acts on it through `ctx.host`.

## Test a hook

A hook definition is a plain object, so a unit test calls `hook.events["turn.completed"]` directly with an event and a stub `HookContext`. Discovery skips `*.test.ts`, so the test can live next to the hook.

| Command | What it reports |
| --- | --- |
| `agent-sdk validate --dir .` | Discovery errors and the empty-handlers warning |
| `agent-sdk info --dir . --json` | Loaded hooks under `agents[].hooks` |
| `agent-sdk run --dir . --message "…"` | Serve log on stderr, including `hook "<name>" handler for <event> threw: …` |

On hosting, read the same log with [`agent-sdk logs`](/docs/reference/cli.md#logs).

See also [Sessions](/docs/reference/sessions.md#stream-events), [OpenTelemetry](/docs/guides/opentelemetry.md), and [Channels](/docs/reference/channels.md#events).

---

Source: /docs/reference/http-api.md

# HTTP API

In the default multi-agent layout each agent serves its routes at
`/<slug>/v1/*`, and `--mode single` serves them at `/v1/*`.

Routes use the agent's HTTP auth chain, which defaults to
`localDevStrict()`. `--bearer-token` and `--allow-anonymous` replace it
with bearer or anonymous access. Built-in JSON
routes return errors as `{ ok: false, error: "<code>", message? }`, and
MCP uses JSON-RPC.

## Host routes

These routes need no auth.

| Route | Contract |
| --- | --- |
| `GET /` | HTML index of mounted agents |
| `GET /v1/agents` | JSON index of mounted agents, multi-agent layout only |
| `GET /<slug>/playground`, `GET /<slug>/playground/assets/:file` | The agent's playground page and assets |
| `GET /docs`, `GET /docs/*` | Documentation site |

`--no-playground` removes the index and playground routes, and
`--no-docs` removes the docs.

## Start a session

`POST /v1/session` opens a durable conversation.

```bash
curl -X POST http://127.0.0.1:3000/<slug>/v1/session \
  -H 'content-type: application/json' \
  -d '{"message":"What can you do?"}'
# {"ok":true,"sessionId":"ses_…","continuationToken":"http:…",
#  "playgroundUrl":"…?sessionId=ses_…","traceUrl":"…/v1/session/ses_…/events"}
```

| Body field | Contract |
| --- | --- |
| `message` | Required user message |
| `title` | Display title |
| `dryRun` | Run read tools and stub write tools |
| `asOf` | ISO-8601 instant with a timezone. Sets `ctx.now()` and rejects omitted, relative, or later declared tool time arguments |
| `workspaceFiles` | Relative files added to the session workspace |
| `purpose` | `"eval"` marks regression traffic |

## Send a follow-up

`POST /v1/session/:sessionId` continues an existing conversation.

```bash
curl -X POST http://127.0.0.1:3000/<slug>/v1/session/ses_… \
  -H 'content-type: application/json' \
  -d '{"continuationToken":"http:…","message":"Make it shorter."}'
```

The route accepts any chat session, including one a custom channel created. Each accepted follow-up returns a new continuation token. A message to a busy session interrupts the active turn. A stale token or a task session returns `409`.

## Stream or replay session events

`GET /v1/session/:sessionId/stream` is the live NDJSON feed.

```bash
curl -N 'http://127.0.0.1:3000/<slug>/v1/session/ses_…/stream?startIndex=0'
```

It replays from `startIndex`, which defaults to `0`, then follows new
events. To reconnect without duplicates, pass the last index you
received plus one.

`GET /v1/session/:sessionId/events` returns a one-shot NDJSON dump, or
`{ sessionId, events, playgroundUrl }` with `?format=json`.
[Stream events](/docs/reference/sessions.md#stream-events) lists the event types.

## Manage sessions

| Route | Contract |
| --- | --- |
| `POST /v1/session/:sessionId/stop` | Interrupt the active turn without sending another message |
| `GET /v1/sessions` | List sessions the caller owns |
| `GET /v1/session/:sessionId/cost` | Per-turn token usage and estimated cost |

Under `serve --dev` on loopback, the list includes every principal's
sessions.

## Call a tool without a model turn

| Route | Contract |
| --- | --- |
| `POST /v1/tools/:toolName` | Run a server tool on `{ input, sessionId?, continuationToken? }` |

[Tools](/docs/reference/tools.md#call-a-tool-without-a-model-turn) covers session
binding, busy sessions, and error codes.

## Discovery routes

| Route | Contract |
| --- | --- |
| `GET /v1/info` | Everything discovery found, such as the model, tools, channels, and diagnostics |
| `GET /v1/tools` | Server and advertised MCP tools as `{ name, title?, source? }`, optionally for one `session` or `continuationToken` |
| `GET /v1/tools/:name` | One tool's schemas, effect rules, execution mode, and source. An unknown name returns `404` with the available names |
| `GET /v1/logs?after=N` | Recent server logs and the next polling cursor |

`connectionErrors` names any connection that couldn't list its tools.

## List and download artifacts

`GET /v1/artifacts` returns the caller's `{ artifacts }`, newest-updated
first, filtered by `kind`, `sessionId`, and a positive `limit`.
`GET /v1/artifacts/:id/content` downloads an artifact's file or blob.
[Artifacts](/docs/reference/artifacts.md) covers tagging and record fields.

## Call custom channel routes

Authored routes mount under `/v1/channels/<id>` with their declared
methods and paths, behind each channel's auth chain.
[Route responses](/docs/reference/channels.md#route-responses) covers what a hosted
caller gets back.

## MCP endpoint

Both MCP routes use stateless streamable HTTP, so they take `POST` and
return `405` for `GET` and `DELETE`.

`/v1/mcp` serves `ask`, `check`, and `call_tool` when server tools
exist, behind the session API auth chain. `/v1/mcp/tools` serves only
deterministic server tools, behind the CLI-level loopback, bearer, or
anonymous auth chain. [MCP connections](/docs/reference/connections.md#peer-mcp-connection)
connects one agent to another.

## Playground eval routes

| Route | Contract |
| --- | --- |
| `GET /v1/dev/evals` | List discovered cases and eval config |
| `GET /v1/dev/evals/runs` | List recent run snapshots, newest first |
| `POST /v1/dev/evals/runs` | Start `{ filterIds?, tags?, timeoutMs?, verbose? }`. Returns `202`, `404` for no match, or `409` while a run is active |
| `GET /v1/dev/evals/runs/:runId` | Progress and the final snapshot |
| `POST /v1/dev/evals/runs/:runId/cancel` | Cancel an active run. Returns `409` when it already ended |

Poll a run until it is `completed`, `failed`, or `cancelled`. A case must match one entry from each of `filterIds` and
`tags` that you pass.

## Dev-mode routes

These routes exist only under `serve --dev`, where schedules and
reminders never fire on their own. `POST /v1/dev/schedules/:scheduleId`
dispatches a schedule once and returns `{scheduleId, sessionIds}`.
`GET /v1/dev/reminders` lists reminders, and
`POST /v1/dev/reminders/:reminderId` fires one.

## Status codes

| Code | Meaning |
| --- | --- |
| `202` | Accepted for asynchronous work |
| `400` | Invalid body, query, or tool input |
| `401` | No auth policy admitted the request |
| `403` | The caller doesn't own the resource |
| `404` | A named resource doesn't exist, or no eval case matches |
| `405` | The route doesn't accept this method |
| `409` | The resource's state rejects the request, such as a stale token or busy write |
| `500` | A write-effect tool can't create its session workspace (`workspace_unavailable`) |

## Related

- [Sessions](/docs/reference/sessions.md)
- [Channels](/docs/reference/channels.md)
- [Deployment](/docs/deployment.md)

---

Source: /docs/reference/instructions.md

# Instructions

Instructions are the always-on prompt for every turn. A root agent
requires them, and a subagent may inline `instructions` in `agent.ts`
instead.

## Authoring forms

| Form | Use it when |
| --- | --- |
| `bot/instructions.md` | Most agents |
| `bot/instructions.ts` | Generated prompts. Default-export `defineInstructions({ markdown })` or a string |
| `bot/instructions/` | A long prompt split across files, joined in filename order |

```ts
// bot/instructions.ts
import { defineInstructions } from "@grok/agent-sdk";

export default defineInstructions({
  markdown: "You are the on-call assistant for the payments team.",
});
```

The `markdown` string is fixed when the file loads. See
[Add egress and secrets](/docs/deployment.md#add-egress-and-secrets) for
reading secrets inside a handler.

## Delivery

A local turn sends the instructions, with a short identity section, as the system prompt, and a turn in the cloud includes the same text in the first user message. Nothing is written to the workspace as `AGENTS.md`, but a parent directory's `AGENTS.md` can still reach a local session. See
[Local cwd](/docs/reference/agent-config.md#local-cwd).

## Contents

Keep instructions to a few lines that cover identity, when to use each
tool, and the output shape. A short version of the
[quickstart](/docs/quickstart.md) reviewer:

```md
# PR reviewer

You review GitHub pull requests. Be specific and brief.

1. Call `inspect_pr` first. Never judge a change you haven't fetched.
2. Match your review to the complexity it reports.
3. Never approve a draft.

End with one sentence: the verdict and why.
```

Name the tools and the decision rule, such as "use X before answering
about Y". State the output contract, including length and format, so
your [evals](/docs/evals.md) can gate it. Move occasional multi-step
workflows into [skills](/docs/reference/skills.md), which load on demand.

---

Source: /docs/reference/limits.md

# Limits

Cursor caps deployments for some teams. If your team has caps, these are the default values.

| Limit | Default | What counts |
| --- | --- | --- |
| `owner.deployments` | 20 | Each slug, running or stopped |
| `owner.deploysPerDay` | 100 per 24 hours | Each accepted deploy, including redeploys |

The error looks like this:

```json
{
  "error": "Your team has 20 of 20 deployments. Delete one with `agent-sdk deployment delete <slug>`, or contact support to raise the limit. Redeploying an existing slug doesn't count.",
  "code": "limit_exceeded",
  "limit": "owner.deployments",
  "max": 20,
  "current": 20,
  "docsUrl": "https://cursor.com/docs/bdk/reference/limits"
}
```

To free a slot, run `agent-sdk deployment delete <slug>`, or wait until the
oldest counted deploy is 24 hours old. A deploy can also answer `503` with a
`Retry-After` header, and you can retry it after that many seconds.

---

Source: /docs/reference/memory.md

# Memory

A memory hook appends one record per turn to a journal. The model reads
the journal with its file tools when its instructions send it there.

## Memory is on by default

`agent-sdk init` scaffolds `bot/hooks/memory.ts`:

```ts
import { memoryHook } from "@grok/agent-sdk/memory";

export default memoryHook();
```

It also adds a `## Memory` section to `bot/instructions.md`. The model
has no recall tool, so that section tells it when to read `memory/` and
to treat records as untrusted history. To add memory to an existing
project, copy both from a new `agent-sdk init` project. To turn memory
off, delete both.

## Journal records

After each turn, the hook appends one JSON line to
`memory/journal.jsonl`:

```json
{"at":"2026-10-01T16:42:09.118Z","sessionId":"5f0c2a9e-7d1b-4c43-9a6e-2b8f1e0d7c35","channelId":"slack","status":"completed","title":"Checkout flaky test","userMessage":"Why does acme/checkout fail on main?","result":"The cart test races the price service mock...","usage":{"inputTokens":18234,"outputTokens":912,"cacheReadTokens":15002,"cacheWriteTokens":0}}
```

| Field | Contract |
| --- | --- |
| `at` | ISO-8601 time the turn finished |
| `sessionId` | Session that ran the turn |
| `channelId` | Channel that started the session |
| `status` | `"completed"` or `"failed"` |
| `title` | Session title, if set |
| `userMessage` | The turn's message, truncated |
| `result` | Final assistant text or failure message, truncated |
| `usage` | Token usage for completed turns |

As the journal grows, older records move to `journal-*.jsonl` files
beside it, so point the model at the whole `memory/` directory.

## Read memory

Local turns find the journal at `memory/` in the session workspace. On
Cursor hosting, Agent SDK tells the model where the journal is at the
start of each session. A session on a server you run yourself can't
reach the journal. Agent SDK rejects `workspaceFiles` and
`sandbox/workspace/` seed files under the reserved `memory/` path.

Local servers keep the journal in the project state directory. `agent-sdk run` and `agent-sdk eval` start with empty memory unless you pass `--state-root`, and hosted deployments keep the journal across redeploys.

## Caveats

- All sessions and users share one journal, so one person's messages
  can surface in another's session.
- Turning memory off keeps existing records. Locally, delete `memory/`
  in the project state directory to clear them. On Cursor hosting,
  [Privacy Mode](/docs/deployment.md#privacy-mode) applies.
- Anyone who can message the agent writes to the journal, so keep the
  scaffolded rule against following instructions found in records.
- Records can be dropped or arrive late. Keep state your code depends
  on in `ctx.host.kv` or `ctx.host.files`.

## Options

| `memoryHook` option | Contract |
| --- | --- |
| `maxTextLength` | Truncation cap for `userMessage` and `result`. Default `2000` characters |
| `backend` | A `MemoryBackend` that replaces the default journal |

A `MemoryBackend` has `appendTurn(record, { stateRoot, agentName })`
and an optional `prepareSession({ stateRoot, agentName })` that runs
when a session starts. The model still looks in `memory/`, so tell it
how to read your records.

---

Source: /docs/reference/playground.md

# Playground

`agent-sdk serve` hosts a web playground at
`http://127.0.0.1:3000/playground`. When one server hosts multiple
agents, each agent's playground is at `/<slug>/playground` and `/`
lists them.

## Playground surfaces

| Surface | What you can do |
| --- | --- |
| Chat | Text and reasoning stream live, and tool calls appear inline with arguments, output, and errors |
| Slash commands | Run a custom channel route without path parameters from the composer. A `drive` route becomes `/drive <pr-url>`. GitHub and Slack ingress routes are excluded |
| Try | Call a custom channel route from the Agent tab and copy the request as curl |
| Runs | Search the sessions you own and eval runs, then open them in Chat, Trace, Raw, or Evals |
| Evals | List and run filesystem evals |
| Agent | Inspect discovered tools, skills, subagents, MCP connections, channels, and hooks |
| Raw | Inspect the selected session's event stream |
| Logs | Recent server log lines from `GET /v1/logs` |

In `--dev` on loopback, or with `--allow-anonymous`, the session list
includes every principal.

## Remote access

To open the playground through a tunnel or LAN address, start the
server with `--bearer-token <secret>` and paste the token into the
navbar's token field. See [Auth policies](/docs/reference/channels.md#auth-policies).

---

Source: /docs/reference/project-layout.md

# Project layout

An Agent SDK project keeps agent files under `bot/`, and each file's
path determines how Agent SDK loads it. [Evals](/docs/evals.md) live in
`evals/` at the project root, beside `bot/`.

```text
my-agent/
├── package.json
├── bot/
│   ├── agent.ts
│   ├── instructions.md
│   ├── tools/
│   │   └── approve_pr.ts
│   ├── skills/
│   │   └── pr-review.md
│   ├── mcp-connections/
│   │   └── linear.ts
│   ├── subagents/
│   │   └── reviewer/
│   │       ├── agent.ts
│   │       └── instructions.md
│   ├── extensions/
│   │   └── ci.ts
│   ├── channels/
│   │   └── github.ts
│   └── hooks/
│       └── audit.ts
└── evals/
    ├── evals.config.ts
    └── readiness.eval.ts
```

The root agent's name is the `name` in `package.json`, or else the directory name. When one server hosts multiple agents, each slug is the agent's directory name, and it must match `[A-Za-z0-9][A-Za-z0-9_-]*` and can't be `v1`, `playground`, or `docs`.

## Folder reference

| Path under `bot/` | What it is | Reference |
| --- | --- | --- |
| `agent.ts` | `defineAgent({ model?, local? })` | [Agent config](/docs/reference/agent-config.md) |
| `instructions.md` | Always-on system prompt, required on the root agent | [Instructions](/docs/reference/instructions.md) |
| `tools/<name>.ts` | One typed tool named after the file | [Tools](/docs/reference/tools.md) |
| `skills/*` | Procedures loaded on demand | [Skills](/docs/reference/skills.md) |
| `mcp-connections/<name>.ts` | MCP servers for the model, server tools, and handlers | [MCP connections](/docs/reference/connections.md) |
| `host-connections/<name>.ts` | MCP servers for host code and `mcp oauth`. The model never sees them | [MCP connections](/docs/reference/connections.md) |
| `subagents/<id>/` | Child agent directory with a required `description` | [Subagents](/docs/reference/subagents.md) |
| `extensions/<ns>.ts` or `extensions/<ns>/` | Extension or Cursor plugin mount. Its contributions become `<ns>__<name>` | [Extensions](/docs/reference/extensions.md) |
| `channels/<name>.ts` | HTTP routes under `/v1/channels/<name>` | [Channels](/docs/reference/channels.md) |
| `hooks/*.ts` | Observe-only event subscribers | [Hooks](/docs/reference/hooks.md) |
| `artifacts.ts` | Artifact kinds, the `tag_artifact` opt-in, and retention | [Artifacts](/docs/reference/artifacts.md) |
| `schedules/*` | Cron-driven runs | [Schedules](/docs/reference/schedules.md) |
| `reminders/*.ts` | Named reminder handlers | [Reminders](/docs/reference/schedules.md#reminders) |
| `sandbox/workspace/**` | Seed files copied into each local session workspace | [Sessions](/docs/reference/sessions.md#local-session-workspace) |
| `lib/` | Shared imports, never discovered | None |

Every eval project needs `evals/evals.config.ts`. Each
`evals/**/*.eval.ts` file is a suite whose case IDs come from its path.

A `.ts` file in a discovery folder loads as a definition, and an
unrecognized directory produces a validation warning. Agent SDK still
loads existing projects rooted at `agent/`.

## Inspect discovery

```text
agent-sdk validate --dir .    # diagnostics, non-zero exit on errors
agent-sdk info --dir .        # discovered surface
agent-sdk info --dir . --json # same shape as GET /v1/info
```

`serve` refuses to start on errors. Warnings do not block startup. When `info` omits a tool or channel, the file is in the wrong directory or has the wrong extension.

---

Source: /docs/reference/schedules.md

# Schedules and reminders

A schedule or a reminder lets an agent act without an inbound message.

## Schedules vs reminders

| | Schedule | Reminder |
| --- | --- | --- |
| Defined | At deploy time, in `bot/schedules/*` | At runtime, with `createReminder` or `host.reminders` |
| Scope | The whole agent | One conversation |
| Session | Starts a new task session, or hands off through `receive` | Wakes an existing conversation |
| Cadence | 5-field cron, in UTC, to the minute | `every`, `cron`, `delay`, or `at` |
| Dev mode | Manual dispatch only | Manual dispatch only, with timers off |

## Schedules

### Markdown schedules

A Markdown file with `cron:` frontmatter is a fire-and-forget task:

```md
---
cron: "0 9 * * 1-5"
---

Pull open incidents and post a summary to the metrics endpoint.
```

Each fire starts a task-mode session with the body as its prompt, and
the session runs to `session.completed` or `session.failed` without
follow-ups.

### Schedule handlers

A `run` handler can call tools or hand work to a channel, whose delivery
events then fire:

```ts
import { defineSchedule } from "@grok/agent-sdk/schedules";
import webhook from "../channels/webhook.js";

export default defineSchedule({
  cron: "*/30 * * * *",
  async run({ receive, waitUntil, appAuth, host }) {
    waitUntil(
      receive(webhook, {
        message: "Report any new critical alerts.",
        auth: appAuth,
      })
    );
  },
});
```

`defineSchedule` takes exactly one of `markdown` or `run`. `appAuth` is a
schedule-scoped principal for work the agent does on its own behalf. The
handler also receives `callTool`, for server-tool calls without a model
turn, and `host`, which holds `host.mcp`, `host.github`, `host.slack`,
and `host.reminders`.

### Dispatch a schedule

Schedules fire on their cron cadence on Cursor hosting and under
production `agent-sdk serve`. Run them in exactly one `serve` process
per project, or pass `--no-schedules` to turn them off. `serve --dev`
fires nothing on its own, so dispatch by hand:

```bash
curl -X POST http://127.0.0.1:3000/<slug>/v1/dev/schedules/heartbeat
```

The playground and `handle.dispatchSchedule("heartbeat")` dispatch the
same way.

### Schedules on Cursor hosting

Each deploy replaces the hosted schedules with the ones in
`bot/schedules/`, so a schedule you delete stops after your next deploy.

A `run` handler must finish in 26 minutes, including the work it passes
to `receive` and `waitUntil`. Module memory doesn't carry over to the
next fire, so keep state in `ctx.host.kv` or `ctx.host.files`. Each scheduled time
starts at most one run, and a stopped deployment skips its fires.

A schedule's name is its path under `bot/schedules/` without the
extension. On Cursor hosting, a name has 1 to 100 letters, digits, `.`,
`_`, `-`, or `/`, and starts with a letter or digit. A deploy can hold
at most 64 schedules, and `deploy` rejects one that breaks either rule.

## Reminders

A reminder belongs to one channel continuation, and it stays armed
across restarts. A recurring reminder works like `setInterval`, and a
one-shot reminder works like `setTimeout`.

Reminders take a prompt or a named handler. A prompt reminder sends
`prompt` into the conversation when it fires, and `until` tells the
model when to cancel it:

```ts
await handle.createReminder({
  purpose: "ci_recheck",
  channelId: "drive",
  continuationToken: "pr:acme/checkout#42",
  delay: "2h",
  prompt: "Re-check CI. Only act if still failing.",
  until: "Cancel once CI is green or the PR is merged.",
});
```

When host code must decide what happens on each tick, write a named
handler. It starts a model turn only if it calls `followup`:

```ts
// bot/reminders/ci_recheck.ts
import { defineReminder } from "@grok/agent-sdk/reminders";

export default defineReminder({
  async run({ args, followup }) {
    await followup({
      message: `Re-check CI for ${String(args.prUrl)}.`,
    });
    return { action: "delivered" };
  },
});
```

Pass its default export as `handler`, with serializable `args`:

```ts
import ciRecheck from "./bot/reminders/ci_recheck.js";

await handle.createReminder({
  purpose: "ci_recheck",
  channelId: "drive",
  continuationToken: "pr:acme/checkout#42",
  every: "30m",
  handler: ciRecheck,
  args: { prUrl: "https://github.com/acme/checkout/pull/42" },
});
```

Channel handlers, tools, and schedule runs get the same API as
`host.reminders`. `builtinTools: { reminders: true }` gives the model
`reminders_create`, `reminders_list`, and `reminders_cancel` for the
current conversation. See
[built-in tools](/docs/reference/agent-config.md#built-in-tools).

`--dev` doesn't fire reminders on its own. Dispatch one by hand, or call
`handle.dispatchReminder(id)` from code:

```bash
curl http://127.0.0.1:3000/<slug>/v1/dev/reminders             # list
curl -X POST http://127.0.0.1:3000/<slug>/v1/dev/reminders/<id>  # fire one
```

Cancel a reminder when its subject ends, for example on
`pull_request.closed`. Keep wake prompts generic so the agent re-reads
live state.

---

Source: /docs/reference/sessions.md

# Sessions, events, and streaming

A session keeps one conversation, its workspace, and an append-only
record of every message and tool call.

## Session contents

Each session also has a channel, an authenticated caller, an NDJSON
event stream, and state that survives a restart. Sessions belong to the
principal that created them. Another caller gets `403` from that
session's routes.

## Session identifiers

| Identifier | Use it for |
| --- | --- |
| `continuationToken` | Continue a conversation through its channel |
| `sessionId` | Stream events, inspect state, or run a session-bound tool call |

Each channel picks its token format, such as a Slack thread or
`pr:owner/repo#1`. The built-in HTTP API returns an opaque token and
rotates it after each accepted follow-up, so a stale HTTP token returns
`409`.

## Session modes

HTTP sessions, channel `send`, Slack, and MCP `ask` create `chat`
sessions, which wait in `session.waiting` for follow-ups. Markdown
schedules and fire-and-forget dispatch create `task` sessions, which end
in `session.completed` or `session.failed`. A follow-up to a task
session returns `409`.

## Follow-ups

A follow-up starts a turn right away on an idle session. On a busy one, the channel decides what happens.

| Path | Busy-session policy |
| --- | --- |
| HTTP playground, `POST /v1/session/:id`, MCP `ask` | Preempt (the default). Interrupts the running turn, then runs the new message |
| Slack mentions, DMs, alert-watch | Coalesce. The running turn may pick up the follow-up at a tool boundary, and the rest become one turn when it finishes |

Pass `admission: "coalesce"` on `send()` to use the Slack policy from
other callers.

`POST /v1/session/:id/stop` interrupts a turn without sending a message.
The stream records `turn.failed` with `status: "cancelled"`, and the
session can take another follow-up. A Slack message that is only `stop`
or `@agent stop` does the same for its thread and clears pending
coalesced follow-ups.

### Track which follow-up reached the agent

Every `send()` returns a `deliveryId`. Pass your own, such as a webhook
delivery ID, or omit it to get a fresh `dlv_…` ID. A coalesced follow-up
reaches the agent in one of two ways.

| Path | Event | Field |
| --- | --- | --- |
| Steered into the running turn | `nudge.delivered` | `data.deliveryId` |
| Fell back to the next turn | That turn's `message.received` | `data.deliveryIds` |

`message.received` lists every send its prompt carries in arrival order,
so one turn can carry several IDs.

```ts
// In a route or channel hook: name the steer after the webhook delivery.
await send(`Incident ${incident.id} was acknowledged.`, {
  continuationToken: incident.id,
  admission: "coalesce",
  deliveryId: webhookId,
});

// In the channel's `events` map:
const steered = new Set<string>();
const prompted = new Set<string>();
const events: ChannelEventHandlers = {
  "nudge.delivered": async (event) => {
    if (event.data.deliveryId !== undefined) {
      steered.add(event.data.deliveryId);
    }
  },
  "message.received": async (event) => {
    for (const id of event.data.deliveryIds ?? []) {
      prompted.add(id);
    }
  },
};
```

In a local Slack session, a `nudge.delivered` event with no `deliveryId`
came from a new Slack message, not from a `send()` call.

## Stream events

Each NDJSON line is a `{ type, index, sessionId, turnId?, at, data }`
envelope. The `index` increases within one session, and `at` is an
ISO-8601 timestamp.

| Phase | Events | What they tell you |
| --- | --- | --- |
| Session | `session.started`, `session.waiting`, `session.completed`, `session.failed` | Creation, readiness, and task completion |
| Input | `message.received`, `nudge.delivered` | A prompt was accepted or reached a running turn. Both carry [delivery IDs](#track-which-follow-up-reached-the-agent) |
| Turn | `turn.queued`, `turn.started`, `turn.completed`, `turn.failed` | Queueing under [`maxRunningTurns`](/docs/reference/agent-config.md#concurrency), then status, result, and token usage |
| Steps | `step.started`, `step.completed` | Model step boundaries and duration |
| Reasoning | `reasoning.appended`, `reasoning.completed` | Streamed reasoning blocks |
| Reply | `message.appended`, `message.completed` | Text deltas and finished assistant messages |
| Tools | `actions.requested`, `actions.completed`, `action.result`, `action.tool_retry` | Tool names, arguments, outputs, and errors. `action.tool_retry` marks a read retried after a transient failure |
| Subagents | `subagent.called`, `subagent.completed` | Delegated work |
| Artifacts | `artifact.tagged` | An [artifact](/docs/reference/artifacts.md) was tagged |

`actions.requested` and `actions.completed` bracket each model step's
tool batch, and `action.result` carries each call live. Read
`turn.completed.data.usage` for token counts.
[HTTP API](/docs/reference/http-api.md#stream-or-replay-session-events) shows how to
follow the stream.

## Local session workspace

Agent SDK creates a workspace before the first local turn.

| Source path | Lands as |
| --- | --- |
| `skills/*` | `.cursor/skills/<name>/SKILL.md` |
| agent tools (`execution: "agent"`) | Scripts listed in the system prompt |
| `sandbox/workspace/**` | Seed files |
| per-send `workspaceFiles` | Files written before the turn |

On a local turn, Agent SDK sends the instructions as the system prompt, and on a turn in the cloud it includes them in the first user message. Nothing is written into the session workspace as `AGENTS.md`. See
[Instructions](/docs/reference/instructions.md).

A channel can give one session a different directory, such as a PR
worktree. [Agent config](/docs/reference/agent-config.md#local-cwd) covers what parent
directories add.

## Session storage

Local session state lives under `--state-root`, in a subdirectory named
for the slug on a slugged mount. Deleting a session directory removes
the session from the server.

## Inspect a saved event stream

`agent-sdk session get --file <state-root>/traces/<sessionId>.ndjson`
prints a saved trace's tool calls, reply, and token usage in the same
JSON shape as `run`. For a session on a running server,
`agent-sdk session get <sessionId> --view events` prints the raw event
list. The playground's **Trace** view shows a timeline.

## Related

- [HTTP API](/docs/reference/http-api.md)
- [Hooks](/docs/reference/hooks.md)
- [Tools](/docs/reference/tools.md)

---

Source: /docs/reference/skills.md

# Skills

A skill is an on-demand procedure in the `SKILL.md` convention. Agent SDK advertises each skill by its description, and the model loads a skill only when the task calls for it. Skills give an agent a multi-step workflow without carrying it in the always-on
[instructions](/docs/reference/instructions.md).

## Authoring forms

| Form | Reach for it when |
| --- | --- |
| `bot/skills/<name>.md` | Static Markdown. `description` frontmatter is optional and falls back to the first body line |
| `bot/skills/<name>/SKILL.md` plus siblings | A packaged directory with reference files. Requires `description` frontmatter |
| `bot/skills/<name>.ts` | Generated content, with `defineSkill` from `@grok/agent-sdk/skills` |

Use flat Markdown for static content:

```md
---
description: Use when a pull request needs a structured approval checklist.
---

# PR review checklist

1. Call `inspect_pr` and confirm required checks passed.
2. Summarize the PR title, author, and remaining risks.
3. Call `approve_pr` only after an explicit request.
```

Use TypeScript when the content must be generated or include inline
sibling files:

```ts
import { defineSkill } from "@grok/agent-sdk/skills";

export default defineSkill({
  description: "Research unfamiliar topics before answering.",
  markdown: "Gather evidence first, then answer with the key facts.",
  files: { "references/checklist.md": "# Checklist\n\n- Find sources.\n" },
});
```

## Skill delivery

Local sessions get each skill at `.cursor/skills/<name>/SKILL.md` in the session workspace, and the agent lists and loads them from there.

## Skills, instructions, and tools

Instructions stay in context on every turn. Skills hold procedures,
checklists, and house style that the model needs only sometimes. Put
anything that must be correct every time in a typed
[tool](/docs/reference/tools.md), where the model can't paraphrase it.

A description is a routing hint, so say when to use the skill, as the checklist example does. The model sees only the description before it decides to load the skill.

---

Source: /docs/reference/subagents.md

# Subagents

A subagent is a specialist child agent the model can delegate to mid-turn. Each one lives under `bot/subagents/<id>/` with a required `agent.ts`.

Add `instructions.md`, or inline `instructions` in `agent.ts`, for a custom prompt. Omit `model` to inherit the parent's model.

The parent model reads `description` to decide whether to delegate, so write it the way you write a [skill description](/docs/reference/skills.md#skills-instructions-and-tools).

The parent delegates through the `task` tool. A parent that limits its [built-in tools](/docs/reference/agent-config.md#harness-tools) must list `"task"`.

The stream records `subagent.called` and `subagent.completed`. `subagent.completed` has `isError: true` when the subagent failed or was cancelled.

```text
bot/subagents/researcher/
├── agent.ts            # description (required), model (optional)
└── instructions.md     # optional custom system prompt
```

```ts
// bot/subagents/researcher/agent.ts
import { defineAgent } from "@grok/agent-sdk";

export default defineAgent({
  description:
    "Background research: climate history, records, comparisons across many cities.",
  // model: omit to inherit the parent's model
});
```

## Subagent rules

Subagents inherit the parent's execution surface. Discovery warns about
and ignores these directories inside a subagent: `tools/`, `skills/`,
`extensions/`, `mcp-connections/`, `host-connections/`, `channels/`,
`schedules/`, `reminders/`, `hooks/`, `sandbox/`, and nested
`subagents/`.

The parent's [instructions](/docs/reference/instructions.md) should say when to
delegate, such as "When a request needs background research, delegate
to the `researcher` subagent."

## Subagents and peers

Subagents split one job into roles inside a single agent. When the
specialist is independently useful, with its own tools, sessions, and
playground, make it a full agent. See
[Peer agents](/docs/guides/agent-to-agent.md#peer-or-subagent).

---

Source: /docs/reference/tools.md

# Tools

A tool is a typed action the model can call, such as hitting an API, running a query, or writing a file. Each file in `bot/tools/` defines one tool, and the filename is the name the model sees. Server tools run on the serving host, and agent tools run as scripts next to the agent.

## Define a server tool

By default, `execute` runs in-process on the serving host with full
access to `process.env` and your `bot/lib/` code. `execution: "server"`
is the default.

A tool needs a filename slug (the model-facing name), a `description`
written for the model, an optional `inputSchema`, and the code that
runs it. With a Zod schema, Agent SDK validates the input and infers its type. With plain JSON Schema, `execute` gets raw JSON.

`prompt` from `@grok/agent-sdk` or `@grok/agent-sdk/prompt` dedents a
template literal so a multi-line description, reminder, or error can
sit indented in TypeScript. Blank lines and relative indentation stay.
`prompt.lines` returns the same text as `string[]`, one entry per line.

```ts
context: prompt.lines`
  Merged PR detected: ${pr.url} by ${author}.

  Call plan_deploy, then follow its nextStep.
`
```

```ts
import { prompt } from "@grok/agent-sdk";
import { defineTool } from "@grok/agent-sdk/tools";
import { z } from "zod";

export default defineTool({
  description: prompt`
    Inspect a pull request before approval.
    Prefer this over guessing from the title alone.
  `,
  inputSchema: z.object({ prUrl: z.string().url() }),
  async execute({ prUrl }) {
    return { prUrl };
  },
});
```

### Tool context

`execute` receives a `ctx` with the runtime accessors:

| Member | What it is |
| --- | --- |
| `ctx.toolCallId` | The call id, matching the `actions.requested` / `action.result` stream events |
| `ctx.session` | Read-only session info: id, channel, mode, auth |
| `ctx.approvedBy` | Auth of whoever approved a `needsApproval` call, same shape as `ctx.session.auth`. On Slack, `attributes.user_id` is the person who clicked Approve. Unset when the call needed no approval |
| `ctx.workspaceDir` | The session's workspace directory |
| `ctx.stateRoot` | The agent's durable state root, shared across every session |
| `ctx.host` | Shared host services (see below) |
| `ctx.send(channelId, message, options?)` | Start or resume a session on any channel. `auth` defaults to this call's session principal |
| `ctx.getSession(channelId, sessionId)` | Look up a session on a channel |
| `ctx.artifacts` | Session-bound [artifacts](/docs/reference/artifacts.md) facade: `tag` auto-fills the session |

`ctx.host` has `mcp`, `github`, `slack`, `kv`, `files`, and `reminders`.
Custom metrics use the [OpenTelemetry](/docs/guides/opentelemetry.md) API.

### Return values

`execute` may return a string, a JSON-shaped value, or `{ content: [...], isError? }`. Use object literals or `type` aliases, since an `interface` return fails typecheck with no index signature. If `execute` throws, the model gets the error and the turn keeps going.

### Declare structured output

Set `outputSchema` when MCP clients need a structured result contract. Use a
Zod schema or plain JSON Schema:

```ts
const Weather = z.object({
  city: z.string(),
  temperatureF: z.number(),
});

export default defineTool({
  description: "Read the current weather.",
  inputSchema: z.object({ city: z.string() }),
  outputSchema: Weather,
  async execute({ city }) {
    return { city, temperatureF: 72 };
  },
});
```

Agent SDK advertises the schema and does not validate results against it.
A plain object fills MCP `structuredContent`. An envelope must set
`structuredContent` next to `content` and `isError`.

### Control dry-run effects

Set `effect` to `"read"`, `"write"`, `"idempotent"`, or a predicate over
the validated input. A dry-run session runs reads and stubs writes and
idempotent calls. A tool with no `effect` is a write. `"idempotent"` can
change something outside the session, and repeating the same arguments
has no further effect.

```ts
export default defineTool({
  description: "Publish a release.",
  inputSchema: z.object({ version: z.string() }),
  effect: "write",
  dryRunResult: ({ version }) => ({ published: false, version }),
  async execute({ version }) {
    return publishRelease(version);
  },
});
```

Set `dryRunResult` so a stubbed write returns the real shape. Without it, the write returns "Operation acknowledged." Traces mark the result as stubbed.

### Retry a transient read or idempotent call

If a read or an idempotent call fails with a transient error, Agent SDK
tries it up to 3 times before the model sees the error. It runs writes,
and tools that don't declare an effect, only once. Agent SDK retries an
MCP tool when the tool's annotations mark it read-only, or when the tool
sets `idempotentHint: true` together with an explicit `destructiveHint`.
If the call still fails, the tool error is the last attempt's message.

### Bound time arguments under `asOf`

`timeArgs` names the input fields that carry time bounds. In a session
created with `asOf`, each named argument must be an absolute ISO-8601
instant with a timezone, no later than that frozen instant. A missing,
relative, or later value is refused. Sessions without `asOf` ignore it.

```ts
export default defineTool({
  description: "Query telemetry between two instants.",
  inputSchema: z.object({ since: z.string(), until: z.string() }),
  effect: "read",
  timeArgs: ["since", "until"],
  async execute({ since, until }) {
    return queryTelemetry(since, until);
  },
});
```

Every name in `timeArgs` must exist in the input schema. A session with
`asOf` refuses to start otherwise, because a misspelled name would check
nothing.

## Define an agent tool

Set `execution: "agent"` when the tool must run next to the checkout.
The script receives JSON on stdin and prints its result on stdout.

```ts
export default defineTool({
  description: "Echo a message from the agent workspace.",
  execution: "agent",
  inputSchema: z.object({ message: z.string() }),
  script: `#!/usr/bin/env bash
set -euo pipefail
python3 -c 'import json,sys; print(json.load(sys.stdin)["message"])'
`,
});
```

Agent SDK copies the scripts into the workspace and lists them in the system prompt.

## Tools from an MCP connection, advertised by name

Set `advertiseTools: true` on a connection to expose its MCP tools by
name. Add per-session `auth` when credentials depend on the caller.
Model calls and [direct tool calls](#call-a-tool-without-a-model-turn)
use the same exposed names. See
[MCP connections](/docs/reference/connections.md#advertise-tools) for the full
contract.

## Call a tool without a model turn

You can call any server tool yourself, with your own input and no model turn. Validation and execution match a model-initiated call, and no Cursor API key is needed.

Over HTTP, with the same auth chain as the session API:

```bash
curl -X POST http://127.0.0.1:3000/<slug>/v1/tools/inspect_pr \
  -H 'content-type: application/json' \
  -d '{"input":{"prUrl":"https://github.com/acme/checkout/pull/42"}}'
# {"ok":true,"toolName":"inspect_pr","callId":"tool_inspect_pr_…",
#  "isError":false,"result":{…},"durationMs":12}
```

From the CLI. Without `--url`, this starts an ephemeral server:

```text
agent-sdk tool call inspect_pr --dir . \
  --input '{"prUrl":"https://github.com/acme/checkout/pull/42"}'
```

Programmatically, `callTool(toolName, input, options?)` is available on
the serve handle, on channel route handlers and `onStart` args, and on
schedule `run` handlers, so a channel can mix deterministic calls with
model turns:

```ts
const outcome = await handle.callTool("inspect_pr", {
  prUrl: "https://github.com/acme/checkout/pull/42",
});
// { toolName, callId, isError, result, durationMs }
```

Omit both identifiers to run against an ephemeral workspace that is
removed when the call returns. Pass `sessionId` (HTTP body, `--session`,
or `options.sessionId`) or `continuationToken` (`<channelId>:<key>` as
`/v1/sessions` lists it) to bind the call. The two are mutually
exclusive.

| Rule | What happens |
| --- | --- |
| Existing `sessionId`, or a token that maps to an existing session | Session workspace, owner check, and event recording |
| Token with no matching session | Scratch workspace; the token's channel id and key are the call identity |
| Busy session, `effect: "read"` (or an advertised MCP tool marked read-only) | Runs alongside the turn, reading the workspace as the turn left it; recorded under its own `turnId` |
| Busy session, write, idempotent, or undeclared | `409 session_busy` until the turn finishes |
| Session workspace cannot be created, read | Scratch workspace; the outcome has `scratchWorkspace: true` |
| Session workspace cannot be created, write | `500 workspace_unavailable` |

| Error | When |
| --- | --- |
| `404 unknown_tool` | Name is not a callable server tool; the response lists available names |
| `400 tool_not_callable` | Agent-execution tools cannot run on the host |
| `400 invalid_tool_input` | Zod rejected the input before `execute`. Plain JSON Schema is forwarded unvalidated |
| `400 invalid_continuation_token` | Malformed token, or passed together with `sessionId` |
| `404 session_not_found` | `sessionId` does not exist |
| `409 session_busy` | Write-effect call while a model turn is running |
| `500 workspace_unavailable` | Write-effect call when the session workspace cannot be created |
| `isError: true` | The tool body threw; same envelope the model would see |

See also [MCP connections](/docs/reference/connections.md), [Extensions](/docs/reference/extensions.md), and [Evals](/docs/evals.md).

---

Source: /docs/templates/agentic-owners.md

# Review pull requests with owners policies

This agent risk-tiers every pull request in your repository, requests
the owners as reviewers, and approves what your policy allows. Rules
live in `.agentic-owners/policy.yml` files at any depth. The closest policy to a changed file wins. The agent reads
policies from the PR's base ref, so a PR can't exempt itself.

## Scaffold and run

```bash
agent-sdk init ./owners-agent --template agentic-owners
```

Set your repositories in `bot/lib/config.ts` (`repos: ["acme/api"]`),
then run:

```bash
cd owners-agent
agent-sdk login
agent-sdk dev
```

PR events stream through your Cursor account, so you don't need a
webhook or GitHub App. The agent reviews a PR when it's opened,
reopened, pushed to, marked ready for review, or retargeted, and skips
drafts. Open a PR or replay a real one:

```bash
agent-sdk github replay https://github.com/acme/api/pull/123 --dir .
```

Deploy with `agent-sdk deploy`.

## Policies

With no policy files, the agent approves very-low and low risk PRs and
holds medium and above with a comment. Commit policies to route
reviews:

```yaml
# .agentic-owners/policy.yml at any depth; the closest policy wins.
version: 1
policies:
  - paths: ["docs/**"]
    always_approve: true
  - paths: ["src/api/**"]
    team: platform # bare slug in the repo's org; or acme/platform, or user: alice
    prompt: api-review.md # optional extra rubric, same directory
  - paths: ["src/billing/**"]
    users: [alice, bob]
    mode: hard # the agent never approves; a person must
```

`paths` are relative to the policy's parent directory, so
`src/.agentic-owners/policy.yml` matches `api/**` against `src/api/**`.
A leading `../` reaches a sibling tree inside the repo.

| Key | Meaning |
| --- | --- |
| `paths` | Required globs |
| `team`, `teams` | Owner teams, as a slug in the repo's org or an `org/team` handle |
| `user`, `users` | Owner GitHub logins, without `@` |
| `prompt` | Extra rubric file beside the policy |
| `mode` | `advisory`, the default, approves low risk. `hard` requires a person to approve. `notify` never blocks and doesn't request its owners |
| `human_review_required` | `true` is the older spelling of `mode: hard` |
| `self_review` | A listed `user:` owner's own PRs clear this rule |
| `always_approve` | Satisfied at every risk tier. Allows no other key except `paths` |

- A rule without owners must set `always_approve: true`.
- `global_exclusions` in the root policy opts paths out entirely.
- The template rejects `delegated_automations`.

## What one review does

- One model turn per PR head grades the diff against the base rubric
  and matched rule prompts, then calls `record_review` with `very_low`,
  `low`, `medium`, `medium_high`, or `high`. The model never writes to
  GitHub.
- The host recomputes the policy from trusted state. It approves when
  every matched rule is `always_approve` or `notify`. Otherwise it
  approves only when the author's `self_review` clears every matched
  `hard` rule at very low or low risk, or every rule that can require
  review at medium and above.
- When the agent holds, it requests up to two owners, never the author,
  and updates one status comment in place.
- An approval binds to the head SHA. A later hold dismisses the agent's
  earlier approvals.
- The agent holds when the PR edits `.agentic-owners/` or `CODEOWNERS`,
  or when the diff is empty or GitHub truncated the diff or file list.
  `self_review` never clears a governance edit.
- A broken policy file fails closed and comments with the validation
  issues.

## Rubric

Edit `bot/lib/owners/prompts/base-review.md` to change the risk tiers.
Put per-area guidance in policy `prompt:` files.

## Evals

`agent-sdk eval` runs three smoke reviews without writing to GitHub.
They cover a docs-only change, an auth deletion, and a prompt injection.

---

Source: /docs/templates/reviews.md

# Review and triage templates

Each template is one model turn. A review template reads the diff and posts one comment, and the triage template comments on a ticket you already filed.

| Template | `--template` id | What it posts | Channel |
| --- | --- | --- | --- |
| Security reviewer | `security-reviewer` | One comment on exploitable bugs, such as injection, authz bypass, secret leaks, SSRF, and RCE | GitHub |
| Thermo review | `thermo-review` | One comment on bugs, breaking side effects, and unintended feature exposure | GitHub |
| Thermo quality review | `thermo-quality-review` | One comment on structure, such as missing abstractions, deep nesting, and files past 1,000 lines | GitHub |
| Triage | `triage-linear`, `triage-jira`, or `triage` | A `Triage:` comment, a bug priority, and a duplicate link when titles match | Linear or Jira |

`triage` is short for `triage-linear`. No review template comments on style, and the quality template doesn't argue about behavior.

The [Agentic Approver](/docs/templates/agentic-owners.md) template is separate. It applies an owners policy instead of posting one findings comment.

## Scaffold

```text
agent-sdk init ./security-reviewer --template security-reviewer \
  --var repos=acme/api
```

Swap the folder and the `--template` id for the row you want. `--var repos` is only for `security-reviewer`. Name repositories already connected in Cursor, separated by commas, and they must share one owner. `init` writes that list to `bot/lib/repos.ts`.

## Try it locally

Connect GitHub in Cursor for the repositories you watch (Settings or [cursor.com/dashboard](https://cursor.com/dashboard)), then sign the host in with `agent-sdk login`. The [GitHub guide](/docs/guides/github.md) covers `cursorAccount`.

```text
agent-sdk dev --cursor-events --repo acme/api
agent-sdk github replay https://github.com/acme/api/pull/12 --dir .
```

You need pull access to replay. The channel skips drafts, writes the diff to `pr/diff.patch`, and starts a turn. The model calls `post_findings` once, and that call writes the GitHub comment. Chat replies stay in the playground.

```text
agent-sdk eval
```

Eval sessions skip the GitHub write. Edit `bot/instructions.md` for what counts as a finding, and edit `evals/review.eval.ts` for the smoke cases.

## Deploy

From the agent's git checkout:

```text
agent-sdk login
agent-sdk deploy --cursor-events-repo acme/api
```

Repeat `--cursor-events-repo` for each repository, and every repository must share one owner. The deployment watches those repositories through your team's Cursor GitHub connection, and you can change the list later without a redeploy:

```text
agent-sdk deployment update my-agent --event-repo acme/api --event-repo acme/web
```

## Triage a tracker

The template runs after someone files a ticket, so keep filing them as you do now. Connect Linear or Jira under MCP in the Cursor dashboard, then run `agent-sdk dev` and open `/queue/issue` in the playground.

```bash
curl -X POST http://127.0.0.1:3000/triage-agent/v1/channels/queue/issue \
  -H 'content-type: application/json' \
  -d '{
    "identifier": "ENG-142",
    "title": "CSV export returns 500",
    "description": "Export has returned 500 since this morning."
  }'
```

For create webhooks, set `LINEAR_WEBHOOK_SECRET` or `JIRA_WEBHOOK_SECRET` and point the provider at `/v1/channels/webhook` on the deployed alias. `record_triage` comments on the issue that started the session. Edit `bot/skills/triage-rubric.md` for kinds and what `urgent` means on your team. Eval sessions skip tracker writes.

---

Source: /docs/troubleshooting.md

# Fix common agent problems

Start with three checks:

1. Run `agent-sdk validate --dir .`.
2. Confirm the expected agent and connections in
   `agent-sdk info --dir . --json`.
3. Reproduce the problem once, then read the serve log or the
   [session trace](#read-the-session-trace).

## Serve or the playground won't work

| What you see | What to do |
| --- | --- |
| The playground is blank or lists no agents | Start `agent-sdk serve`, because the playground gets its agents from the server. |
| Agent-file edits don't appear | Open the URL that `serve --dev` prints, and press Enter in its terminal to reload. |
| Sessions exist but the list is empty | The list shows only your own sessions, so use the identity that created them. |

## A turn fails or misbehaves

| What you see | What to do |
| --- | --- |
| Reads and greps keep failing with `NGHTTP2_FRAME_SIZE_ERROR` | Run the agent under Node 22.13 or newer, never Bun. |
| `Semantic search has been removed. Use Grep.` | Delete `search` from a custom `fs`, drop `SearchQuery` and `SearchResult`, and remove `SemanticSearch` or `semSearch` from `tools` and `bot/tools/`. |
| The turn reports a missing API key | Set `CURSOR_API_KEY`, `CURSOR_API_KEY_FILE`, or `CURSOR_SERVICE_ACCOUNT_KEY`, or run `agent-sdk login`. |
| Login works but the model host rejects the key | Point `CURSOR_API_BASE_URL` and `CURSOR_BACKEND_URL` at the same API host. |
| The first turn reports `[resource_exhausted]` | The model is usually out of capacity. If a GitHub event started the turn, check for a GitHub `429` rate-limit response first. |
| Replies quote an ancestor `AGENTS.md` or unrelated repository rules | Set `defineAgent({ local: { cwd } })` so the agent doesn't inherit the parent checkout. `/v1/info` shows the current `local.cwd`. |
| The model lists IDE meta-tools but never calls named MCP tools | Set `advertiseTools: true` on the local connection. |
| Server tools, skills, or seed files are missing | Check the discovered project in `/v1/info`, and follow the warning from `validate`. |
| `validate` and `run` pass but CI type-checking fails | Return JSON-shaped tool results, such as object literals or `type` aliases, instead of `interface` types. |

## HTTP returns 401, 403, or 409

| What you see | What to do |
| --- | --- |
| `401` outside local development | Send the bearer token, alias token, or channel credential. Default custom routes accept only direct loopback calls. |
| `403` on a stream or follow-up | Use the identity that created the session. |
| A custom route fails through a tunnel or LAN | Protect the host with `--bearer-token` or channel auth. Use `--allow-anonymous` only behind a proxy that authenticates every caller. |
| `409` on a follow-up | Refresh the rotating `continuationToken`. Task sessions don't accept follow-ups. |
| `409 session_busy` from `call --session` | Wait for the turn, omit `--session`, or mark a read-only tool `effect: "read"` so it can run alongside the turn. |

## A hosted deployment fails or looks stale

| What you see | What to do |
| --- | --- |
| `validate` or `deploy` reports `architecture "v2" requires a named secret` | Replace the literal or getter with a [named secret](/docs/deployment.md#add-egress-and-secrets), and set its value with `agent-sdk secret set <slug> NAME`. |
| Local changes don't appear after deploy | `deploy` builds the pushed ref, so commit and push your changes, then deploy again. |
| The deployment never reaches `running` | Run `agent-sdk deployment get <slug>`, and fix the error it reports. |
| A secret or egress change has no effect | Confirm the name with `secret list`, check that the `hosting` block is on the pushed ref, and redeploy. |
| A hosted schedule doesn't fire | Check that the deployment is running and that the schedule is in `bot/schedules/` on the deployed ref. |
| A custom route returns `429` | Wait for the time in `Retry-After`, then send it again. |

## GitHub deliveries fail

| What you see | What to do |
| --- | --- |
| `github forward` gets `401` on every delivery | Clear the token variables so the forwarder uses your `gh` login, as in `GITHUB_TOKEN= GH_TOKEN= agent-sdk github forward`. |
| `Hook already exists` | Stop the stale forwarder. GitHub allows one per repository. |
| Direct webhooks are rejected outside `--dev` | Set the same `GITHUB_WEBHOOK_SECRET` on the signer and the server. |
| You can't create a forwarder | Use `agent-sdk github replay <pr-url>`, which needs only pull access. |

## Slack doesn't respond

| What you see | What to do |
| --- | --- |
| Logs say `channel idle ... missing credentials` | Run `agent-sdk slack create --dir <agent>`, then check the prefix with `agent-sdk slack doctor`. |
| `slack create` waits for workspace approval | Keep the CLI running and open the **Request approval** link it prints. After an admin approves, select **Retry** in the [Deployed Agents](https://cursor.com/dashboard/deployed-agents) **Add Slack** wizard. |
| The bot ignores ordinary channel posts | Configure `engagement.channelPosts`, create the app with `--channel-posts`, and invite it to each allowed channel. |

## MCP authorization fails

| What you see | What to do |
| --- | --- |
| `must be defineConnection({ url, oauth: true })` | Add `oauth: true` to the connection, or fix the filename you passed to `mcp oauth`. |
| Local auth works but hosted calls return unauthorized | Your local OAuth tokens don't reach a deployment. Use a `cursorAccount: true` connection, as described in [Host MCP OAuth](/docs/guides/mcp-oauth.md#use-an-oauth-server-from-a-hosted-agent). |
| The model invents `mcp_auth` instead of calling the server | Set `advertiseTools: true` for named local tools, or expose a wrapper through `ctx.host.mcp`. |

## Schedules or reminders stall

| What you see | What to do |
| --- | --- |
| A schedule or reminder never fires under `--dev` | `--dev` doesn't fire them on its own, so trigger one through its `/v1/dev/schedules/<id>` or `/v1/dev/reminders/<id>` route. |
| A reminder is disarmed with `handler_lost_on_restart` | Recreate handler-form reminders after a restart, or use a prompt-form reminder. |
| One schedule fires on several self-hosted processes | Pass `--no-schedules` to every serving process but one. |

## Read the session trace

```bash
agent-sdk session list --prod --slug <slug>
agent-sdk session get <sessionId> --prod --slug <slug> --view events
agent-sdk session get --file <trace-path>
```

Find a hosted session with `session list`, then print its events with
`--view events`. For a local run, pass the trace path from `agent-sdk run`
to `--file`. Pair each `actions.requested` event with its `action.result`
to find the call that failed.
