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

# Serve tools from your app

> Declare customer-executed tools, sync their draft schemas and approval ActionPolicies, and serve calls from the Session log.

<Warning>
  This guide is migration-fenced. `tools.sync` still targets the retired
  AgentDefinition draft workflow and is not compatible with a newly created
  CMA Agent. Custom-tool declarations belong directly in
  `checkfu.beta.agents.create` or `.update` until this serving guide is rebuilt
  on the CMA Session surface.
</Warning>

A Custom tool lets an Agent ask your existing app to do work without giving Checkfu custody of your code. Your app runs the handler. Checkfu records the call, parks the Run, enforces ActionPolicy and ActionApproval, and resumes after your app posts the result.

The TypeScript SDK implements this **ToolServing** contract over public API surfaces: `defineTool` declares one tool, `tools.sync` updates an AgentDefinition draft, and `tools.serve` follows explicitly named Sessions.

<Steps>
  <Step title="Define one source of truth" titleSize="h2">
    ```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
    import { Checkfu, defineTool } from "@checkfu/sdk"

    const checkfu = new Checkfu({
      apiKey: process.env.CHECKFU_API_KEY,
    })

    const lookupTicket = defineTool<{ readonly ticket_id: string }>({
      name: "lookup_ticket",
      description: "Read one support ticket by ID",
      schema: {
        type: "object",
        properties: {
          ticket_id: { type: "string" },
        },
        required: ["ticket_id"],
        additionalProperties: false,
      },
      handler: async (input, context) =>
        supportDesk.getTicket(input.ticket_id, { signal: context.signal }),
    })

    const closeTicket = defineTool<{
      readonly ticket_id: string
      readonly resolution: string
    }>({
      name: "close_ticket",
      description: "Close one support ticket after a human approves",
      schema: {
        type: "object",
        properties: {
          ticket_id: { type: "string" },
          resolution: { type: "string" },
        },
        required: ["ticket_id", "resolution"],
        additionalProperties: false,
      },
      needsActionApproval: true,
      handler: async (input) => supportDesk.closeTicket(input.ticket_id, input.resolution),
    })

    const tools = [lookupTicket, closeTicket]
    ```

    You can pass a Standard Schema V1 value instead, as long as it also exposes `jsonSchema` as an object or a zero-argument function. The SDK validates through `~standard.validate` and declares the rendered JSON Schema. No schema package becomes a runtime dependency of the SDK.

    For a raw JSON Schema object, the dependency-free fallback deliberately supports only `type`, `enum`, `const`, `required`, `properties`, `additionalProperties`, and `items`, plus annotation keywords such as `description`. An unsupported validation keyword is rejected when the tool is defined rather than silently ignored. Reach for a Standard Schema adapter when you need `pattern`, numeric ranges, unions, or references.

    New declarations must be object-root JSON Schemas and stay within 32 KiB, depth 32, and 2,000 JSON values. An invalid declaration throws before sync, and the API applies the same fence on create, patch, import, and publish. Published versions stay readable and immutable.
  </Step>

  <Step title="Sync the mutable draft" titleSize="h2">
    ```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
    await checkfu.tools.sync(process.env.CHECKFU_AGENT_ID!, tools)
    ```

    `sync` preserves catalog tools, replaces the draft's Custom-tool set, and stops there. It never publishes, so a running Session keeps using its admitted immutable AgentDefinition version. Review and publish the draft through the ordinary Agent workflow.

    For `needsActionApproval: true`, sync also creates or verifies a real `require_approval` ActionPolicy on the AgentDefinition permission `use_tool:<tool-name>`. It creates the replacement before deleting a stale SDK-managed rule. SDK ownership requires the current generated name grammar and a matching `custom-tool-<name>` rule identity, so a legacy or hand-named ActionPolicy is never deleted. The API key used for sync therefore needs authority to edit the AgentDefinition and manage ActionPolicies. This is platform enforcement, not a client-side prompt, and a definition-wide ActionPolicy on `use_tool` still applies to every Custom tool.

    ActionPolicy is live authority, separate from the immutable admitted definition. Changing `needsActionApproval` can govern a matching call in a Session that is already running, even though the draft declaration itself applies only after publication and later admission.
  </Step>

  <Step title="Serve the Sessions your app owns" titleSize="h2">
    ```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
    const controller = new AbortController()
    const serving = checkfu.tools.serve({
      sessions: [session.id],
      tools,
      signal: controller.signal,
    })

    try {
      await checkfu.sessions.events.send(session.id, {
        type: "user.message",
        payload: {
          content: "Look up INC-42 and close it if the fix is confirmed.",
          authored_by: principal.id,
          caused_by: { kind: "api" },
        },
      })

      await waitForTurnSettlement(session.id)
    } finally {
      controller.abort()
      await serving
    }
    ```

    `waitForTurnSettlement` is your own helper: watch the event log for the terminal event you care about, not a timer.

    The serving loop first rebuilds from the Session log, then tails with cursor resume. For each parked call it correlates `run.requires_action` or `run.action_authorized` with the preceding `agent.tool_use`, validates the recorded arguments, runs the matching handler, and posts the attributed `user.custom_tool_result` through the ordinary drive endpoint.

    Serving is persistent by default, whether or not you pass a cancellation signal. Set `persistent: false` only when you want each Session loop to stop after one clean stream pass.

    Handlers must return JSON-representable values. The SDK checks the result before posting and rejects `undefined`, functions, `BigInt`, cyclic objects, and anything JSON serialization would silently change.

    The handler context includes the serving group's `signal`. Pass it to `fetch` or your dependency client so caller cancellation can stop in-flight work before `serve()` settles. The context also carries `sessionId` and the replay-stable `toolUseId`.

    ActionApproval is a separate durable decision. A `needsActionApproval` call first parks on `action.kind: "action_approval"`, and your review UI answers it through the [ActionApproval API](/guides/handle-an-approval). ToolServing waits. Only `run.action_authorized` lets the handler run.

    <Warning>
      Pass only Sessions your app owns. One serving loop can safely own multiple Sessions: a failure in one Session does not cancel its siblings. This stream-based helper deliberately does not poll the Workspace for arbitrary waits. Keep each Session assigned to one live worker process or coordination boundary so two workers do not both invoke a non-idempotent handler for that same Session.
    </Warning>
  </Step>
</Steps>

## Failure and replay behavior

Argument validation failure becomes an ordinary in-band tool result:

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "ok": false,
  "error": "$.ticket_id is required",
  "received": { "ticket": "INC-42" },
  "expected": {
    "type": "object",
    "properties": { "ticket_id": { "type": "string" } },
    "required": ["ticket_id"],
    "additionalProperties": false
  }
}
```

The handler is not called. The Run continues with the exact failure visible as the tool result, so the model can correct its arguments. There is no hidden success and no second error channel.

Failures that prevent a Session's serving loop from continuing are isolated to that Session. After all Session loops settle, `serve()` rejects with an `AggregateError`; each entry in its `errors` array is a `{ sessionId, cause }` record. With persistent serving, healthy sibling loops continue until they settle or the caller aborts the shared `signal`.

After a crash, restart the same serve call. ToolServing re-reads the log and does not invoke a handler whose result is already recorded. If a result post loses its response and a retry conflicts, the worker treats that as success only after a fresh log read finds a structurally identical result. A different recorded result stays a conflict.

A process can still crash after your handler performs its side effect but before the result reaches the Session log. Make mutating handlers idempotent on `context.toolUseId`, the second handler argument, or fence that identity in your own store before performing the side effect.

## Next steps

<CardGroup cols={2}>
  <Card title="Handle an approval" icon="user-check" href="/guides/handle-an-approval">
    Build the human decision surface for `needsActionApproval` calls.
  </Card>

  <Card title="Resume a stream" icon="arrows-rotate" href="/guides/resume-a-stream">
    Understand the cursor and replay rules ToolServing follows.
  </Card>
</CardGroup>
