> ## Documentation Index
> Fetch the complete documentation index at: https://docs.herm.run/llms.txt
> Use this file to discover all available pages before exploring further.

# Long-running tools

> Start asynchronous application-owned work and return its final result to an agent session.

Custom tools execute in your application. For work that cannot finish while the
agent is waiting, return a `running` result promptly, continue the external job,
then send a final result when the session is idle.

This keeps tool calling from blocking the chat for the full lifetime of an
external job. The kickoff releases the pending tool call so the agent can respond
and the conversation can continue while your application finishes the work in
the background.

## How the flow works

1. The stream emits `agent.custom_tool_use`.
2. Your application starts the external workflow.
3. Within five minutes, post `user.custom_tool_result` with `status: "running"`.
4. That kickoff resolves the blocked custom tool call and lets the current agent turn continue.
5. When the external workflow finishes and the session is idle, post a second result with `status: "completed"` or `"failed"`.
6. Herm updates the original running result, emits `session.tool_updated`, and starts a new agent turn with the final text.

<Warning>
  The final result starts a new turn. If another turn is running, the API returns
  `409 invalid_state`. Final custom tool results cannot use
  `delivery: "when_idle"`; wait for `session.status_idle` and retry from your
  application.
</Warning>

## Start the workflow

The pending tool call allows approximately five minutes for a response. Send the
kickoff well before that deadline:

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X POST "https://api.herm.run/v1/sessions/session_123/events" \
  -H "Content-Type: application/json" \
  -H "x-api-key: $PRISM_API_KEY" \
  -d '{
    "events": [
      {
        "type": "user.custom_tool_result",
        "tool_use_id": "custom_toolu_123",
        "content": [
          { "type": "text", "text": "Render job started. I will report when it finishes." }
        ],
        "status": "running",
        "is_error": false
      }
    ]
  }'
```

`status: "running"` is not a progress-only event. It is the response to the
pending tool call, so the original agent turn resumes immediately with the
kickoff text as tool output. Herm does not poll or supervise the external job.

Store the `sessionId`, `tool_use_id`, and the accepted result event `id` with
your job record.

## Complete the workflow

After observing `session.status_idle`, send the final result with the same
`tool_use_id`:

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X POST "https://api.herm.run/v1/sessions/session_123/events" \
  -H "Content-Type: application/json" \
  -H "x-api-key: $PRISM_API_KEY" \
  -d '{
    "events": [
      {
        "type": "user.custom_tool_result",
        "tool_use_id": "custom_toolu_123",
        "content": [
          { "type": "text", "text": "Render completed. The video is available at https://example.com/render/123." }
        ],
        "status": "completed",
        "is_error": false
      }
    ]
  }'
```

For failures, use `status: "failed"`, set `is_error: true`, and include enough
context in `content` for the model to respond appropriately.

When a matching running result exists, the completion replaces that result in
event history. Its text is submitted to the agent as a new prompt, which starts a
separate turn. Without a matching running result, the final result is appended as
a new event and still starts a turn.

## Update live tool UI

Live streams receive an ephemeral `session.tool_updated` event:

```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
event: session.tool_updated
data: {"type":"session.tool_updated","payload":{"type":"session.tool_updated","tool_use_id":"custom_toolu_123","event_id":"evt_running_result","payload":{"type":"user.custom_tool_result","tool_use_id":"custom_toolu_123","content":[{"type":"text","text":"Render completed."}],"status":"completed","is_error":false}},...}
```

Patch the existing event identified by `payload.event_id` with
`payload.payload`. The patch event itself is not retained. The client posting the
completion should also apply the updated event returned in the `202` response in
case its stream misses the ephemeral patch.

## Retry safety

Final-result submissions are not idempotent. Retrying a completion after it
succeeded can start another agent turn. Record successful completion responses
and only retry when you know the first request was not accepted.

An immediate or `running` result can return `409 no_active_turn` after its event
was accepted if the session stopped waiting for it. Reconcile event history
before retrying to avoid duplicate results.

## Permission policies

Custom tools use the same `always_allow`, `always_ask`, and `always_deny`
permission policies as other tools. With `always_ask`, handle the preceding
`agent.approval_request` before expecting `agent.custom_tool_use`. Your
application remains responsible for authorization, input validation, retries,
idempotency, and side-effect safety.
