# Application tools

> Execute a tool in your application, then resume the waiting run.

## Register a tool

Look up the exact name first with `GET` `/v1/tools?name=<name>`.

```bash title="Terminal"
curl -sS --get "$PANTHEON_BASE_URL/v1/tools" \
  -H "Authorization: Bearer $PANTHEON_API_KEY" \
  --data-urlencode "name=echo_tool"
```

Read the matches from `data`. If none exist, create the tool with `POST` `/v1/tools`.

Otherwise, use the matching tool's `id` to add a version.

Creating an existing name returns HTTP 409 `idempotency_conflict`. The error details carry `tool_id`.

Tool creation returns `ToolResponse` with these fields. Additional definition fields are omitted here.

```json title="Selected JSON response fields"
{
  "id": "tool_123",
  "name": "echo_tool",
  "display_name": "Echo tool",
  "latest_version": 1,
  "status": "active",
  "created_at": "2026-09-08T12:00:00Z",
  "updated_at": "2026-09-08T12:00:00Z",
  "latest_definition": {
    "id": "tool_definition_123",
    "tool_id": "tool_123",
    "name": "echo_tool",
    "version": 1
  }
}
```

Deployments bind `latest_definition.id` through `tool_definition_ids`.

## Tool options

Set `side_effect` and `confirmation` when creating a tool or version.

| Value             | Meaning                             |
| ----------------- | ----------------------------------- |
| `none`            | Accepted for side_effect. Default.  |
| `workspace_write` | Accepted for side_effect.           |
| `external_write`  | Accepted for side_effect.           |
| `never`           | Accepted for confirmation. Default. |
| `policy`          | Accepted for confirmation.          |

## Add a version

`POST` `/v1/tools/{{id}}/versions` accepts only these fields:

| Field           | Meaning                 |
| --------------- | ----------------------- |
| `description`   | Accepted request field. |
| `executor`      | Accepted request field. |
| `input_schema`  | Accepted request field. |
| `output_schema` | Accepted request field. |
| `side_effect`   | Accepted request field. |
| `confirmation`  | Accepted request field. |

Omit `name` and `display_name`. Both are rejected with "Extra inputs are not permitted".

Set `TOOL_ID` to the matching tool's id before running this request.

```bash title="Terminal"
curl -sS -X POST "$PANTHEON_BASE_URL/v1/tools/$TOOL_ID/versions" \
  -H "Authorization: Bearer $PANTHEON_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "description": "Echo a message.",
  "executor": "application",
  "input_schema": {
    "type": "object",
    "properties": {
      "message": {
        "type": "string"
      }
    },
    "required": [
      "message"
    ],
    "additionalProperties": false
  },
  "output_schema": {
    "type": "object",
    "properties": {
      "echoed": {
        "type": "string"
      }
    },
    "required": [
      "echoed"
    ],
    "additionalProperties": false
  },
  "side_effect": "none",
  "confirmation": "never"
}'
```

## Handle a tool call

### Track the tool

`tool.started` identifies the call.

### Receive the pause

`agent.custom_tool_use` contains the application tool input.

### Wait for idle

`session.status_idle` contains `requires_action`.

### Resume

Send the result. Watch for `tool.completed` and `run.completed`.

## Correlate the call

Use `tool_call_id` to update one UI item through the tool lifecycle.

Use `execution_idempotency_key` to execute the application tool once.

Use the durable SSE id as `in_reply_to_event_id`.

```yaml title="SSE"
event: agent.custom_tool_use
id: evt_custom_tool_123
data: {"id":"evt_custom_tool_123","sequence":3,"type":"agent.custom_tool_use","workspace_id":"workspace_123","session_id":"session_123","run_id":"0123456789abcdef0123456789abcdef","created_at":"2026-09-03T12:00:02Z","content":{"tool_call_id":"call_123","pending_input_id":"pending_123","execution_idempotency_key":"evt_custom_tool_123","name":"echo_tool","input":{"message":"hello"}},"data":{"tool_call_id":"call_123","pending_input_id":"pending_123","execution_idempotency_key":"evt_custom_tool_123","name":"echo_tool"}}
```

## Resume the run

Reuse the same `Idempotency-Key` when retrying this result.

```bash title="Terminal"
curl -sS -X POST "$PANTHEON_BASE_URL/v1/sessions/$SESSION_ID/runs/$RUN_ID/resume" \
  -H "Authorization: Bearer $PANTHEON_API_KEY" \
  -H "Idempotency-Key: resume-evt-custom-tool-123" \
  -H "Content-Type: application/json" \
  -d '{
    "events": [{
      "type": "user.custom_tool_result",
      "in_reply_to_event_id": "evt_custom_tool_123",
      "content": {"echoed": "hello"}
    }]
  }'
```

A waiting run stays resumable for 30 minutes by default.

## Per-operation result schemas

Register an `output_schema` with top-level `oneOf` or `anyOf` branches.

Each branch must be an object schema. Nested combinators are not supported.

Give each operation a distinct `operation` value with `const` and its own required fields.

This example accepts separate create and delete results:

```bash title="output_schema"
{
  "oneOf": [
    {
      "title": "Create result",
      "type": "object",
      "properties": {
        "operation": {
          "const": "create"
        },
        "id": {
          "type": "string"
        },
        "title": {
          "type": "string"
        }
      },
      "required": [
        "operation",
        "id",
        "title"
      ],
      "additionalProperties": false
    },
    {
      "title": "Delete result",
      "type": "object",
      "properties": {
        "operation": {
          "const": "delete"
        },
        "id": {
          "type": "string"
        },
        "deleted": {
          "type": "boolean"
        }
      },
      "required": [
        "operation",
        "id",
        "deleted"
      ],
      "additionalProperties": false
    }
  ]
}
```

A create result contains `operation`, `id`, and `title`.

A delete result contains `operation`, `id`, and `deleted`.

Send the matching object as the result's `content`. Publish a new deployment after changing a tool version.

When no branch matches, resume returns HTTP 422 with `tool_result_invalid`.

Each `error.details` entry includes a `path` JSON Pointer and a `reason`.

The reason names the closest branch, its index, and the failed rule.

For example, a missing title points to `/events/0/content/title` and identifies `Create result` at `oneOf[0]`.

The run remains waiting. Correct the result and retry before its deadline.

## Decline a call

Return a structured tool error. The error code must use uppercase letters, digits, or underscores.

```json title="JSON"
{
  "events": [{
    "type": "user.custom_tool_result",
    "in_reply_to_event_id": "evt_custom_tool_123",
    "error": {
      "code": "USER_CANCELLED",
      "message": "The user declined this action.",
      "retryable": false
    }
  }]
}
```

## Reconnect

Save the last durable SSE id. Reconnect with `Last-Event-ID`.

If live startup fails, the stream stays open with durable events and `X-Pantheon-Live-Events: unavailable`. Otherwise that header is `live`. If you receive `stream.resync` after a mid-stream live failure, reconnect with that id, with backoff if resyncs repeat within seconds of connecting.

Pantheon replays persisted events before it resumes live output.

## Pitfalls

> **Warning**
>
> - **Tool creation returns idempotency_conflict:** The name already exists. Read tool_id from error details, then add a version.
> - **Confirmation always is rejected:** This release does not accept always. Gate the call in your application.
> - **Resume returns event_expired:** The wait deadline passed. Start a new run.
> - **Resume returns event_already_resolved:** Treat the original result as accepted. Do not execute the tool again.
> - **Resume returns tool_result_invalid:** Match the tool's output schema. Keep each operation's shape unambiguous.
> - **A reconnect misses live tokens:** Ephemeral deltas are not replayed. Rebuild final text from durable message events.
