Skip to main content
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.
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.
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.
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 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. 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.
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 before making availability part of your application’s contract.
1

Define handlers in your application

Keep one source of truth for each logical name, JSON Schema, and handler.
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.
2

Declare the Tools on the Agent

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.
3

Configure ActionPolicy

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.
4

Serve only the Sessions your worker owns

tools.serve rebuilds each named Session from its durable log and then tails from the cursor.
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.
5

Execute only after the durable authorization fact

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 and retain the exact payload and idempotency key for retries. Do not send a new user message to stand in for the Tool result.

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:
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.

Handle an approval

Build the review path for Custom tools that require a person.

Tools

Compare Custom tools with built-in and connected execution paths.