> ## Documentation Index
> Fetch the complete documentation index at: https://opensandbox-feat-types-open-question-labels.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Tools

> Define TypeScript tools and expose them with useTool

A tool is TypeScript code the model may call while it works. Use tools for
actions such as searching an API, looking up an order, or updating an external
system. The model decides when a tool fits; your `run` function controls what
happens.

## Define your first tool

Create a tool beside the agent, for example
`opencomputer/agents/support/tools/lookup-order.ts`:

```tsx theme={null}
import { defineTool } from "@opencomputer/agent";

export const lookupOrder = defineTool({
  name: "lookup_order",
  description: "Look up an order by ID and return its current status",
  input: {
    type: "object",
    properties: {
      orderId: { type: "string", description: "The customer order ID" },
    },
    required: ["orderId"],
    additionalProperties: false,
  },
  async run({ input }) {
    const orderId = String(input.orderId);
    return { orderId, status: "processing" };
  },
});
```

Then attach it from `agent.ts`:

```tsx theme={null}
import { useModel, useTool } from "@opencomputer/agent";
import { lookupOrder } from "./tools/lookup-order";

export default function Agent() {
  useModel("anthropic/claude-sonnet-4.6");
  useTool(lookupOrder);
  return "Help customers check orders. Ask for an order ID before lookup.";
}
```

The model sees the tool's name, description, and input schema. When it calls
the tool, OpenComputer runs `run()` in the managed agent runtime and returns
the result to the model.

## Tool definition

| Field | Required | Purpose |
| - | - | - |
| `name` | yes | ID the model calls; letters, numbers, underscores, and hyphens only |
| `description` | yes | Explains what the tool does and when to use it |
| `input` | no | JSON Schema for tool arguments |
| `output` | no | JSON Schema describing the result; required on the result tool |
| `result` | no | `true` marks this tool as the agent's [result tool](#the-session-result); one per agent, and it must have `run` |
| `run` | yes\* | Synchronous or asynchronous implementation |
| `preview` / `apply` | no | Supply these *instead of* `run` to [wait for a person](#wait-for-a-person-before-writing) |

\* A tool has either `run`, or `preview` and `apply` — never both, and never
just one of the pair.

Descriptions are the model's documentation. Include the action, when it is
appropriate, and any important precondition.

## Execution context

The `run` function receives:

| Value | Purpose |
| - | - |
| `input` | Arguments created for the tool call |
| `sessionId` | Current session ID |
| `messageId` | Message containing the tool call |
| `toolCallId` | ID of this invocation, unique within the session |
| `agentId` | Agent executing the call |
| `signal` | Optional cancellation signal for abortable work |
| `reportProgress(metadata)` | Emits JSON-compatible progress metadata |

Use the cancellation signal with `fetch()` and report progress before slow
steps:

```tsx theme={null}
export const importCatalog = defineTool({
  name: "import_catalog",
  description: "Import the latest product catalog; use only when asked",
  async run({ signal, reportProgress }) {
    await reportProgress({ phase: "downloading" });
    const response = await fetch("https://example.com/catalog.json", {
      signal,
    });
    await reportProgress({ phase: "processing" });
    return { imported: response.ok };
  },
});
```

Tool results and progress metadata must be JSON-compatible values.

For an external write, `${sessionId}:${toolCallId}` can identify the invocation
in a downstream idempotency key. It stays the same when that invocation is
retried. A new call from the model has a new ID, even if it requests the same
action; use a business-level key when those calls must also converge.

## The session result

An agent may mark one tool as its result tool. Its latest committed output
is the session's `result`: the structured outcome an application reads
instead of parsing the final message. This tool checks a URL itself rather
than accepting a claimed status from the model:

```tsx theme={null}
import { defineTool } from "@opencomputer/agent";

const checkResult = {
  type: "object",
  properties: {
    url: { type: "string" },
    status: { type: "integer" },
  },
  required: ["url", "status"],
  additionalProperties: false,
} as const;

export const checkUrl = defineTool({
  name: "check_url",
  description: "Check an HTTPS URL and report its HTTP status",
  input: {
    type: "object",
    properties: { url: { type: "string" } },
    required: ["url"],
    additionalProperties: false,
  },
  output: checkResult,
  result: true,
  async run({ input, signal }) {
    const url = new URL(String(input.url));
    if (url.protocol !== "https:") throw new Error("Use an HTTPS URL");
    const response = await fetch(url, { method: "HEAD", signal });
    return { url: response.url, status: response.status };
  },
});
```

The build enforces three rules: an agent declares at most one result tool,
a result tool declares `output`, and it runs with `run()` rather than
[waiting for a person](#wait-for-a-person-before-writing). The schema is
recorded with the deployment, and every value the tool returns is validated
against it.

A call to the result tool succeeds only once its output is committed to
the session:

* The value `run()` returns is validated against `output` and committed
  before the model is told the call succeeded. The session's `result`
  becomes `{ turnId, callId, reportedAt, data }`, and the call's
  `tool.completed` event carries `result: true`
  ([Session events](/agents/events#tools)).
* A value that does not match `output`, or exceeds 8 KiB of JSON, fails the
  call; the model sees the reason and can call again. The previous result
  stands.
* A call from a turn that was interrupted or has ended, or from a runtime
  the session no longer runs on, is refused the same way.
* A later call replaces the entire value. Reporting does not finish the turn.
  A subsequent turn that fails or is cancelled does not clear the result, and
  the event log keeps every call.

The result says what the agent reported, not that the work succeeded.
Verify claims inside `run()`, where a tool can check that a branch exists
or a pull request is open, and throw to reject them; let the application
check what the result references. Read it from
[`GET /sessions/<id>`](/agents/api#get-and-list). For a review-ready UI, also
check current activity and whether `result.turnId` belongs to the last settled
turn. An existing result may describe earlier work.

If the commit acknowledgement is lost, the tool can fail even though its
result was saved. OpenComputer retries the commit for the same tool call;
that deduplicates the saved result, not external writes inside `run()`. Prefer
tools that verify and report existing work. Give any external write its own
idempotency key.

### Schema declarations

The result tool's `output` is copied into the deployment without evaluating
code. Use one of these forms:

* An object literal in the `defineTool()` call.
* A `const` object literal in the same module, as above.
* A named import of such a `const` from a module inside the agent directory.

Function calls, `let` bindings, namespace members such as `schemas.report`,
package imports and re-exports are not supported for this `output` schema.
The build error names the unsupported form. `input` may use these static
forms or a schema constructed when the tool module loads; it is not subject
to result-output extraction. Neither schema infers the TypeScript type of
`run`'s `input`, which remains `Record<string, unknown>`. `as const` preserves
the schema's literal types for your own typing and validation.

Write `name`, `result` and `output` as ordinary properties on the
`defineTool()` object. Spreads, computed property names, and a `result` value
other than literal `true` or `false` fail the build. A tool module whose
`result` declaration disagrees with its deployment fails to load.

## Conditional tools

`useTool()` may be conditional. This keeps sensitive or specialized actions
out of the tool set until they are relevant:

```tsx theme={null}
import { useInput, useTool } from "@opencomputer/agent";
import { issueRefund } from "./tools/issue-refund";

export default function Agent() {
  const input = useInput();

  if (input.text?.toLowerCase().includes("refund")) {
    useTool(issueRefund);
  }

  return "Resolve the support request. Confirm the order before a refund.";
}
```

`useTool()` also accepts a tool ID such as `useTool("web-search")` when the
runtime already provides that tool.

## Give an agent a sandbox shell

Select the built-in `sandbox_exec` tool when an agent needs to run commands,
inspect files, or use installed CLI packages:

```tsx theme={null}
import { useTool } from "@opencomputer/agent";

export default function Agent() {
  useTool("sandbox_exec");

  return "Use the sandbox for shell commands and keep durable work in /workspace.";
}
```

The agent runtime starts without a VM. The first `sandbox_exec` call lazily
acquires an isolated computer for the session, and later calls in that session
reuse it. Files that must survive restarts belong in `/workspace`; temporary
paths such as `/tmp` do not provide that persistence guarantee.

If your organization has an Enterprise custom package image, `sandbox_exec`
uses that image automatically. The agent definition still selects the same
tool ID, and cannot choose or override the image itself. You can see the
package image assigned to your organization under **Settings → Customize VM
packages**.

## Call external APIs safely

For authenticated HTTP APIs, declare a connection, store the key as an
OpenComputer secret, and call the connection's `fetch()` method. This keeps
the credential outside the agent runtime; see
[Secrets and outbound requests](/agents/secrets).

[Managed GitHub connections](/agents/github) are the exception: they supply
short-lived environment credentials so Git and `gh` work directly. Never
embed credentials in tool source or return them to the model.

For Google services, GitHub, or Linear, the credential belongs to whoever
connected the account and the platform refreshes it — call those with
[`callService`](/agents/services) instead.

## Ask a question

`ask` is the platform's question tool. Select it with `useTool("ask")` when a
turn may need to stop and ask a person before going on:

```tsx theme={null}
import { useInput, useTool } from "@opencomputer/agent";

export default function Agent() {
  const input = useInput();
  if (input.answer) {
    return `The answer was ${input.answer.value ?? input.answer.text}. Proceed accordingly.`;
  }
  useTool("ask");
  return "Read the request, propose an approach, and ask whether to go ahead.";
}
```

The model calls it with:

| Field | Bounds |
| - | - |
| `text` | The question, 1 to 4000 characters |
| `options` | Optional, at most six `{ label, value }` choices; each `label` and `value` 1 to 80 characters |

Calling `ask` ends the turn with an open question. The turn completes with
`outcome: "question"`, the session's `question` holds what was asked, and
`question.asked` is recorded in the [event log](/agents/events#questions).
The reply arrives as the next input with
[`answer`](/agents/inputs#answers-and-steering) set, and anything written
meanwhile arrives with it as `steering`, within
[bounds](/agents/sessions#questions). A session has at most one open
question; the turn that interprets the answer may ask again.

How the question reaches a person depends on where the session lives: an
application answers it through the [API](/agents/api#turns) or the
[React hook](/agents/react#answer-a-question); a
[Slack](/agents/slack#questions) or [Linear](/agents/linear#ask-before-acting)
session shows it in the thread with the options as buttons, and a click or a
typed reply answers it.

`ask` records that the agent asked. It does not undo or forbid anything the
turn did before asking. A turn meant only to propose should select `ask` and
tools that cannot write, and select the writing tools on the turn that
carries the answer.

The id `ask` is reserved: `defineTool({ name: "ask" })` is refused, both by
`defineTool` and by the CLI's build. `ask` runs on the default runtime for new deployments;
sessions with `executionMode: "microvm"` cannot select it.

`ask` is different from [waiting for a person](#wait-for-a-person-before-writing):
`ask` ends a turn with an open question the agent reasons about when it is
answered, while an approval tool records one exact write that runs, unchanged,
once someone approves it.

## Wait for a person before writing

Some writes should not happen because a model decided they should. Such a tool
proposes instead: the model calls it, nothing is written, and the person the
agent is talking to sees a card with the exact change and decides.

There is no separate primitive for this. Give `defineTool` a `preview` and an
`apply` instead of a `run`, and the model's call becomes a proposal. `run` is
written for you. `useTool` selects the tool exactly as it selects any other,
and the model cannot tell the difference — waiting for approval is a property
of a tool, not a different kind of thing.

```tsx theme={null}
import { defineTool } from "@opencomputer/agent";
import { billing } from "./connections/billing";

export const attach = defineTool({
  name: "attach",
  description: "Move a customer onto a plan",
  input: {
    type: "object",
    required: ["customerId", "planId"],
    properties: {
      customerId: { type: "string" },
      planId: { type: "string" },
    },
  },
  // What the person sees. It may read; it must not write.
  async preview({ input, signal }) {
    const customer = await billing
      .fetch(`/customers/${String(input.customerId)}`, { signal })
      .then((response) => response.json());
    return {
      title: `Move ${customer.name} to ${String(input.planId)}`,
      facts: [
        { label: "Today", value: customer.plan },
        { label: "After", value: String(input.planId) },
        { label: "Charged now", value: customer.nextCharge },
      ],
    };
  },
  // Runs only once somebody approves, from the arguments on the card.
  async apply({ input, decision, signal }) {
    return billing.fetch("/subscriptions", {
      method: "POST",
      headers: { "Idempotency-Key": decision.id },
      body: JSON.stringify({
        customer: input.customerId,
        plan: input.planId,
      }),
      signal,
    });
  },
});
```

The model calls `attach` like any other tool and is told the change was
recorded for approval, so it stops rather than reporting the work as done.

`apply` runs later, with the arguments that were on the card — no second pass
through the model, so what happens is what was agreed to. The proposing session
is usually gone by then, and that is fine: `apply` needs its arguments, not the
conversation.

### Always pass `decision.id`

`decision.id` identifies this approval, and it is the same value on every
attempt to carry it out. Give it to whatever you call as an idempotency key.

If a write never reports back — the runtime died mid-flight, the answer was
lost — it is recorded as unconfirmed rather than failed, because it may well
have happened. That is only recoverable if running it again is safe, and this
is what makes it safe.

### What this does and does not guarantee

Approval is a convention your code cooperates with. Nothing stops a tool from
writing inside `preview`. What the platform guarantees is narrower and
still worth having: an approved write runs **once**, from the arguments the
person agreed to, whether or not the session that proposed it still exists.

A tool that waits needs a conversation to ask in. Called from the playground,
a schedule or a webhook, there is nobody to ask, and the call fails saying so.

## Computer commands

A session gets an isolated computer when it needs one; conversation alone
does not require a computer. The model runs commands through the built-in
`shell` tool. A command runs in the session's workspace and takes `command`
and an optional `timeoutSeconds`: 120 by default, at
most 1800, and never more than the computer's remaining lifetime allows; a
value out of range fails the call before anything runs. A command is not
started on a computer that cannot host it for its full timeout; a fresh
computer is used instead.

When the timeout passes, the command and every process it started are
stopped, and the call fails as a tool error the model sees: it names the
command and the timeout and carries the output so far. The turn continues,
and files the command wrote before it was stopped remain.

A command counts as stopped only once that is confirmed: every process it
started, detached ones included, is gone, or the computer itself was
terminated. When the computer stops answering while a command runs, its
outcome is unknown, and the call fails with that as the reason; the model
is never told a command was stopped when that was not confirmed. That
computer runs no further command until the unknown one is confirmed
stopped or the computer is replaced by a fresh one with the session's
workspace, which the next command does on its own. See
[End and interrupt](/agents/api#end-and-interrupt) for cancellation guarantees
and the compatibility limit for sessions using the older runtime.

## Tools versus skills and MCP

* A tool executes TypeScript you own.
* The same tool with `preview` and `apply` instead of `run`
  [waits for a person](#wait-for-a-person-before-writing) before it does it.
* A [skill](/agents/skills) teaches the agent a reusable procedure.
* An [MCP server](/agents/mcp) supplies a remote collection of tools.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.