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

# Serve Tools from your app

> Declare Custom tools, govern them with ActionPolicy, and serve authorized calls without leaking application credentials or repeating remote effects.

A **Custom tool** lets an Agent ask your application to do work. Your
application executes the handler and owns its downstream authorization and
credentials. Checkfu records the call, evaluates ActionPolicy, waits for any
required ActionApproval, and matches the result to the waiting Run.

For example, a support Agent can ask your app to look up a ticket and close it.
Your app checks access to that ticket, performs the permitted action, and
returns a bounded result. Approval does not execute the handler. The result
continues the original Run.

```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
agent.tool_use
  → ActionPolicy allows, denies, or requires approval
  → authorized application handler
  → user.custom_tool_result with the same tool_use_id
  → original waiting Run continues
```

Custom-tool invocation has no Connection, ToolSource, or PermissionAssignment
check of its own. ActionPolicy governs the declared call; your application's
current tenant, account, and row-level checks still govern the downstream work.

<Note>
  Declare Custom tools in the Agent create or update request, or let the SDK's `checkfu.tools.sync(agentId, tools)` helper do it. The helper reads the current Agent, keeps its non-custom tools, replaces its Custom-tool declarations through the ordinary Agent update, and reconciles the ActionPolicies that `needsActionApproval` requires. If you also apply the Agent from a checked-in project with `checkfu agent apply`, keep the declarations in that project so the next apply does not remove them. A configuration-changing update creates the next AgentVersion. Use `tools.serve` only for Sessions admitted from a version that contains the same declarations.
</Note>

Create a Session with an explicit `acted_as` Principal when serving Custom tools. That Principal needs an effective `act_as` PermissionAssignment on the Agent at Session admission. This admission check is separate from ActionPolicy evaluation for each Tool call. The SDK derives `context.actedAs` from validated admission evidence for the served Session; an API key alone does not identify the acting Principal.

The examples use the [TypeScript SDK](/reference/typescript-sdk) in a Node.js
worker. Start with a Session created from the AgentVersion containing these
Tools and an explicit acting Principal. See [Start a Session](/concepts/sessions-and-runs#start-a-session).

`supportDesk`, `authorizeTicketAccess`, and `waitForTurnSettlement` below are
your application's adapters, not SDK methods. The authorization helper must
check current access for `context.actedAs` against the original tenant, provider
account, ticket, and requested action. It must refuse before provider contact
when access cannot be established. Keep that account binding stable on replay.

<Warning>
  The Checkfu CLI and TypeScript SDK are private-alpha artifacts with no self-service public installation channel.
  Part of this flow is behind a rollout fence; a deployment that has not enabled it refuses the request rather than running it.
  Confirm [capability status](/getting-started/status) before making availability part of your application's contract.
</Warning>

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { createHash } from "node:crypto"
import { Checkfu, defineTool, steerSession } from "@checkfu/sdk"

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

<Steps>
  <Step title="Define handlers in your application" titleSize="h2">
    Keep one source of truth for each logical name, JSON Schema, and handler.

    ```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
    type LookupTicketInput = {
      readonly ticket_id: string
    }

    type CloseTicketInput = {
      readonly ticket_id: string
      readonly resolution: string
    }

    const lookupTicket = defineTool({
      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 (raw, context) => {
        // defineTool validates the raw JSON against the declaration before
        // invoking this handler. The cast supplies the corresponding TS view.
        const input = raw as LookupTicketInput
        await authorizeTicketAccess(context.actedAs, input.ticket_id, "read", {
          signal: context.signal,
        })
        const ticket = await supportDesk.getTicket(input.ticket_id, {
          signal: context.signal,
        })
        await authorizeTicketAccess(context.actedAs, input.ticket_id, "read", {
          signal: context.signal,
        })
        return { ticket_id: ticket.id, status: ticket.status }
      },
    })

    const closeTicket = defineTool({
      name: "close_ticket",
      description: "Close one support ticket",
      schema: {
        type: "object",
        properties: {
          ticket_id: { type: "string" },
          resolution: { type: "string" },
        },
        required: ["ticket_id", "resolution"],
        additionalProperties: false,
      },
      handler: async (raw, context) => {
        const input = raw as CloseTicketInput
        const effectKey = createHash("sha256")
          .update(JSON.stringify([
            context.sessionId,
            context.runId,
            context.toolUseId,
          ]))
          .digest("hex")
        await authorizeTicketAccess(context.actedAs, input.ticket_id, "close", {
          resolution: input.resolution,
          signal: context.signal,
        })
        const ticket = await supportDesk.closeTicket(
          input.ticket_id,
          input.resolution,
          {
            // This must be a provider-supported idempotency facility, not
            // merely a local correlation field.
            idempotencyKey: `checkfu:${effectKey}`,
            signal: context.signal,
          },
        )
        await authorizeTicketAccess(context.actedAs, input.ticket_id, "read", {
          signal: context.signal,
        })
        return { ticket_id: ticket.id, status: ticket.status }
      },
    })

    const tools = [lookupTicket, closeTicket]
    ```

    `defineTool` validates the recorded arguments before calling the handler and requires a JSON-representable result. New declarations use an object-root JSON Schema and stay within the SDK's published size, depth, and value-count bounds. A Standard Schema V1 adapter can replace the explicit TypeScript cast when it also exposes the JSON Schema Checkfu should advertise.

    A `toolUseId` is unique only within its Run. The provider key therefore hashes
    the original Session, Run, and Tool-use IDs. Reuse that key, account, and
    exact arguments for the same effect. Never mint a fresh identity to work
    around an uncertain result or refusal. This example requires a provider
    whose idempotency facility prevents duplicate effects and lets you recover
    or query the first result; a correlation field alone is insufficient.
  </Step>

  <Step title="Declare the Tools on the Agent" titleSize="h2">
    Put the same logical names, descriptions, and schemas in the current Agent create or update request. Review the configuration-changing update that creates the next immutable AgentVersion.

    A live Session keeps the AgentVersion admitted at creation. Editing the mutable Agent later cannot add, remove, or change a Tool in that Session.
  </Step>

  <Step title="Configure ActionPolicy" titleSize="h2">
    Custom-tool invocation is **ActionPolicy-only**:

    * no restricting policy means the call proceeds to its application-result wait;
    * `deny` means the handler must not run; and
    * `require_approval` creates an ActionApproval before customer execution.

    Configure a narrow policy for mutating Tools such as `close_ticket`. Do not create a PermissionAssignment and describe it as part of this Tool-call path; there is no Connection or ToolSource to grant.

    Your application must still enforce its own tenant, account, and row-level authorization immediately before the handler effect. Checkfu policy does not replace your product's access checks.
  </Step>

  <Step title="Serve only the Sessions your worker owns" titleSize="h2">
    `tools.serve` rebuilds each named Session from its durable log and then tails from the cursor.

    ```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 steerSession(checkfu, {
        sessionId: session.id,
        content: "The fix for INC-42 is confirmed. Look up the ticket and close it.",
        actor: {
          requestId: "request-inc-42",
        },
        idempotencyKey: "request-inc-42",
      })

      await Promise.race([
        serving,
        waitForTurnSettlement(session.id, "request-inc-42"),
      ])
    } finally {
      controller.abort()
      await serving
    }
    ```

    Authenticated server context supplies durable actor attribution. The client supplies the permitted event content, and `caused_by` request correlation through the helper; it does not supply `authored_by`.

    `waitForTurnSettlement` is your own event-driven helper. Correlate the
    request to its Run and wait for that Run's terminal outcome. A Tool wait or
    approval is not completion. The race also surfaces a serving-worker failure.
    Retain `request-inc-42` for retries of this request; use a new request identity
    only for new work.
  </Step>

  <Step title="Execute only after the durable authorization fact" titleSize="h2">
    For a call that ActionPolicy allowed directly, `run.requires_action` carries `action.kind: "custom_tool"`; the application can serve the recorded call.

    For a call that required a person, ToolServing waits through the ActionApproval. It invokes the handler only after `run.action_authorized` correlates to the exact preceding `agent.tool_use`.

    Approval covers the recorded call, not a different account, ticket, or set
    of arguments. It does not bypass current policy or your application's
    authorization. A historical authorization event is not a permanent grant.

    After the handler returns, `tools.serve` posts `user.custom_tool_result`
    with the original `tool_use_id` and JSON result. Checkfu matches it to the
    call in the Session log. An accepted matching result closes that Tool wait
    and lets the original Run continue under current authority with its retained
    inputs. It does not create a new Run or resolve fresh `latest` context.
    ActionApproval neither executes the handler nor manufactures its result.

    If you post results yourself, use the served
    [Custom-tool result shape](/reference/events#return-a-custom-tool-result) and retain the exact
    payload and idempotency key for retries. Do not send a new user message to
    stand in for the Tool result.
  </Step>
</Steps>

## Keep credentials out of Tool data

Custodied Connection Credentials are stored separately from Tool declarations and arguments. For credentialed execution, the trusted adapter attaches them to authorized provider requests.

Custody forwarding returns bounded provider response bodies, which can reflect values sent to the provider. Discovery and credential-validation HTTP requests suppress request tracing and trace propagation, and credential-validation diagnostics omit provider response bodies. Credential exclusion from Tool results, errors, logs, and traces requires qualification of the specific execution path.

Your application can still leak its own secrets if it places them in arbitrary data. Never put application database passwords, provider tokens, API keys, session cookies, or private signing material into:

* a Tool declaration or Agent instruction;
* Tool arguments;
* `user.custom_tool_result`;
* Session events or logs;
* Files, Git content, or Memory; or
* a sandbox environment variable.

Keep them inside the trusted handler runtime and return only the bounded business result the Agent needs.

## Remote effects are not local transactions

A database transaction around this sequence is **not atomic**:

```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
remote provider call
  -> local receipt insert
```

The provider is outside your database transaction. A crash after provider acceptance but before the receipt insert leaves no local proof and can repeat the remote effect on restart.

Prefer one of these patterns:

1. **Provider idempotency.** Pass an exact provider-supported idempotency key derived from the original Session, Run, and Tool-use IDs, as the `close_ticket` handler does above. Exact replay must return or reconcile the first operation rather than perform it twice.
2. **Durable reservation.** Commit an effect intent before provider contact, move it to `dispatched`, and reconcile `dispatched` records after a crash before deciding whether another request is safe.
3. **Outbox or effect ledger.** Store pre-effect intent and post-effect settlement under one local owner, and let a reconciler—not the Session-serving loop—resolve ambiguous provider outcomes.

A timeout, connection loss, or process crash after dispatch is **indeterminate**, not failed. Mark it for reconciliation and do not automatically retry until the provider's state or idempotency record establishes what happened.

## Result posting and replay

`tools.serve` skips a handler when the Session log already records its result.
It correlates the Tool-use ID with the Run that opened the call. Within a live
serving loop, a prepared result is retained across supported retryable posting
failures, so the handler need not run again just to resend it.

If a result POST loses its response, retry the same payload with the same
idempotency key. The first POST may already have succeeded. For a conflict or
rejected response, the SDK accepts reconciliation only when the log contains
the exact same result for that call; a different or missing result stays
unresolved. Do not invent a new Tool-use ID or rerun the handler to settle a
result-delivery conflict.

Prepared results in worker memory do not survive a process crash. Before a
restarted worker repeats a call whose result is absent, your handler must
reconcile the original provider key or durable effect record. An absent
Session result is not proof that the provider did nothing.

Result delivery records an outcome; it neither undoes a completed external
effect nor grants permission to release its data. The example's final access
check applies at handler return only. `tools.serve` can retain those result bytes
and retry the POST without calling the handler or an application authorization
callback. Revocation after handler return is not covered by that check.

If your application requires fresh downstream authorization at delivery or
retry, own the result-posting boundary and recheck release access there. Retain
the original effect identity, Tool-use ID, result payload, and posting idempotency
key; do not rerun the effect to authorize delivery. This application check is
separate from Checkfu's current policy and Session acceptance checks. Neither
makes revocation atomic with a network POST already in flight.

If an effect completed but its result cannot be released, retain the effect's
actual status in your authorized ledger; do not turn it into a failed effect
or repeat it.

## Worker ownership and shutdown

Pass only Sessions your application owns. Keep one live worker or coordination lease per Session so two processes cannot both perform the same non-idempotent action. A failure in one Session is isolated from healthy sibling loops; the serving group reports per-Session causes when it settles.

Propagate `context.signal` to downstream clients. Cancellation can stop work before dispatch, but it cannot undo an effect the provider already accepted.

<CardGroup cols={2}>
  <Card title="Handle an approval" icon="user-check" href="/guides/handle-an-approval">
    Build the review path for Custom tools that require a person.
  </Card>

  <Card title="Tools" icon="wrench" href="/concepts/tools">
    Compare Custom tools with built-in and connected execution paths.
  </Card>
</CardGroup>


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