---
title: Tool approvals
description: Require a trusted decision before a tool performs a sensitive action.
pagination: false
sidebar:
  icon: shield-check
---

Require approval before a tool performs an action that needs the user's decision, such as cancelling an order. The model may propose the action; it must not approve itself.

Native LangChain middleware and tools may interrupt execution, but they do not replace RELKIT's tool
policy. Use the policies and trusted invocation decisions below for function-backed tools.

> **Before you begin**
>
> Start with an existing [function-backed tool](/docs/modules/ai/tools). These approval policies do
> not require a model SDK; native agent execution and its [test
> helpers](/guides/ai/build-an-offline-agent#test-without-a-model-key) require the explicit
> `@relkit/langchain` installation.

## Choose a policy

`defineTool.approval` and `.asTool({ approval })` accept exactly three policy strings:

The examples below are alternatives: choose the policy that fits each tool.

| `approval` | Behavior                                                                       |
| ---------- | ------------------------------------------------------------------------------ |
| `never`    | No separate approval decision is required.                                     |
| `on-write` | Require approval for `write` and `external` actions, but not `none` or `read`. |
| `always`   | Require approval for every call, including reads.                              |

Declare `sideEffect` accurately. A write incorrectly marked as a read can skip an intended `on-write` approval.

### No approval for a read

Use `never` when the operation may run without a separate confirmation:

```ts title="src/services/hello/tools/lookup.tool.ts"
/** Exposes the hello function as a read-only greeting tool with its existing two-second timeout. */
import hello from "@app/services/hello/functions/hello.function.js";

export default hello.asTool({
  id: "hello.lookup",
  description: "Read a greeting for a supplied name",

  sideEffect: "read",
  approval: "never",
  timeoutMs: 2_000,
});
```

### Approval for writes

This `defineTool` example requires confirmation because its side effect is `write`:

```ts title="src/services/orders/tools/cancel-order.tool.ts"
import { defineTool } from "@relkit/app/tools";
import deleteOrder from "../../src/services/orders/functions/delete-order.function.js";

export const cancelOrder = defineTool({
  id: "orders.cancel-order-on-write",
  target: deleteOrder,
  description: "Cancel an order after confirmation",

  sideEffect: "write",
  approval: "on-write",
  timeoutMs: 2_000,
  mcp: false,
});
```

It uses the existing order-deletion function. `mcp: false` keeps this approval-required tool out of MCP.

### Approval on every call

This order-cancellation tool requires approval on every call:

```ts title="src/services/orders/tools/cancel-order.tool.ts"
/** Exposes the order deletion function as a write tool requiring explicit approval before invocation. */
import deleteOrder from "../functions/delete-order.function.js";

const cancelOrder = deleteOrder.asTool({
  id: "orders.cancel-order",
  description: "Cancel one order after a human approves the write",
  sideEffect: "write",
  approval: "always",
  timeoutMs: 2_000,
});

export default cancelOrder;
```

The function must still check whether the caller may cancel that order. Approval does not replace authorization.

## Use an approval callback

The tool's `approval` policy is not a function. The callback goes in the second argument of **`tool.invoke()`**, where your application supplies the decision for that call.

The callback may return `true` or `"approved"` to allow execution, and `false` or `"denied"` to stop it. It may also return a Promise of any of those values. It receives `{ toolId, sideEffect, policy }`; capture the specific action's arguments from your calling code.

### Wait for a trusted confirmation

For the `on-write` tool above, call your own confirmation flow before cancelling:

```ts title="Calling cancelOrder with an asynchronous decision"
/** Invokes cancellation only after the trusted confirmation flow approves the exact order.
 * @param orderId - Order identity shown to confirmation and passed to the tool.
 * @param confirm - Trusted asynchronous decision callback.
 * @returns The native checked deletion result when approval succeeds.
 * @throws The original invocation failure when confirmation denies or fails. */
export async function cancelOrderWithApproval(
  orderId: string,
  confirm: (message: string) => Promise<boolean>,
): Promise<{ orderId: string; deleted: boolean }> {
  return cancelOrder.invoke(
    { orderId },
    {
      // Ask your trusted confirmation flow about this specific order.
      approval: async () => {
        const confirmed = await confirm(`Cancel order ${orderId}?`);
        return confirmed ? "approved" : "denied";
      },
    },
  );
}
```

Your app supplies `confirm` from a trusted UI or approval service. Bind the decision to the authenticated user and this specific order; do not take approval from model-generated arguments or an unchecked request field. Setting the tool policy alone does not create a confirmation screen.

### Test synchronous decisions

These unit-test examples show every supported decision value. The unconditional callbacks are test stubs, not a production approval flow:

```ts title="tests/tool-approvals.test.ts — decision values"
await expect(
  cancelOrder.invoke({ orderId: "order-123" }, { approval: () => true }),
).resolves.toEqual({ orderId: "order-123", deleted: true });

await expect(
  cancelOrder.invoke({ orderId: "order-123" }, { approval: () => "approved" }),
).resolves.toEqual({ orderId: "order-123", deleted: true });

await expect(
  cancelOrder.invoke({ orderId: "order-123" }, { approval: () => false }),
).rejects.toMatchObject({ code: "RELKIT_APPROVAL_DENIED" });

await expect(
  cancelOrder.invoke({ orderId: "order-123" }, { approval: () => "denied" }),
).rejects.toMatchObject({ code: "RELKIT_APPROVAL_DENIED" });
```

A missing callback on an approval-required call fails with `RELKIT_APPROVAL_REQUIRED`; a denied decision fails with `RELKIT_APPROVAL_DENIED`. A callback that throws also stops execution. A callback is not called when the policy does not require approval.

## Agent and MCP behavior

A client-exposed agent that declares the `approve` control publishes approval waiting state through its
durable thread. The typed hook exposes `approve(approvalId, { threadId })` and
`deny(approvalId, { threadId })`; the caller must use the same authorized business thread. The
[agent test helper](/docs/api/testing) can supply scripted approvals for deterministic tests.

Over [MCP](/docs/modules/ai/mcp#protect-access), approval-required calls are currently rejected; there is no MCP approval-and-resume flow. Keep these tools out of MCP with `mcp: false` if clients cannot use them.
