Skip to content

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:

MemberWhat it is
ctx.toolCallIdThe call id, matching the actions.requested / action.result stream events
ctx.sessionRead-only session info: id, channel, mode, auth
ctx.approvedByAuth 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.workspaceDirThe session's workspace directory
ctx.stateRootThe agent's durable state root, shared across every session
ctx.hostShared 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.artifactsSession-bound artifacts facade: tag auto-fills the session

ctx.host has mcp, github, slack, kv, files, and reminders. Custom metrics use the OpenTelemetry 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 use the same exposed names. See MCP connections 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.

RuleWhat happens
Existing sessionId, or a token that maps to an existing sessionSession workspace, owner check, and event recording
Token with no matching sessionScratch 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 undeclared409 session_busy until the turn finishes
Session workspace cannot be created, readScratch workspace; the outcome has scratchWorkspace: true
Session workspace cannot be created, write500 workspace_unavailable
ErrorWhen
404 unknown_toolName is not a callable server tool; the response lists available names
400 tool_not_callableAgent-execution tools cannot run on the host
400 invalid_tool_inputZod rejected the input before execute. Plain JSON Schema is forwarded unvalidated
400 invalid_continuation_tokenMalformed token, or passed together with sessionId
404 session_not_foundsessionId does not exist
409 session_busyWrite-effect call while a model turn is running
500 workspace_unavailableWrite-effect call when the session workspace cannot be created
isError: trueThe tool body threw; same envelope the model would see

See also MCP connections, Extensions, and Evals.