# How to Implement Human-in-the-Loop Action Approval in Agent-Native

> Learn to implement human-in-the-loop action approval in Agent-Native. Set needsApproval true to pause execution and require a human reviewer for action confirmation.

- Repository: [Builder.io/agent-native](https://github.com/BuilderIO/agent-native)
- Tags: how-to-guide
- Published: 2026-06-27

---

**Set `needsApproval: true` in your `defineAction` configuration to pause execution, emit an `approval_required` event to connected clients, and resume processing only after a human reviewer submits a valid `approvalKey` via the `/api/approval` endpoint.**

Agent-native provides a built-in **human-in-the-loop (HITL)** mechanism that gates sensitive operations behind explicit approval steps. By configuring the `needsApproval` flag in any action definition, you trigger a managed pause-resume cycle handled entirely by the core runtime—requiring no custom middleware for Slack, Telegram, or web UI integrations.

## How the Approval Flow Works

The human-in-the-loop implementation follows a five-step execution model managed by the core libraries:

1. **Declaration**: An action is marked with `needsApproval: true` in its metadata.
2. **Interception**: When invoked, the runtime emits an `approval_required` event instead of executing the `run` function.
3. **Propagation**: The event streams to clients via SSE, displaying an approval prompt in the active UI (Slack, Telegram, or web).
4. **Resolution**: A reviewer submits an `approvalKey` through the standard `/api/approval` endpoint.
5. **Completion**: The agent re-issues the original tool call with the key attached, and the action executes.

This flow is orchestrated across [`packages/core/src/action.ts`](https://github.com/BuilderIO/agent-native/blob/main/packages/core/src/action.ts) (lines 383–524), where the runtime detects the approval flag; [`packages/core/src/agent/production-agent.ts`](https://github.com/BuilderIO/agent-native/blob/main/packages/core/src/agent/production-agent.ts) (lines 582, 2484, 3150), where execution pauses; and [`packages/core/src/agent/types.ts`](https://github.com/BuilderIO/agent-native/blob/main/packages/core/src/agent/types.ts) (lines 200–230), where pending calls are stored.

## Defining Actions That Require Approval

To enable HITL for any action, import `defineAction` from `@agent-native/core` and set the `needsApproval` property:

```typescript
import { defineAction } from "@agent-native/core";
import { z } from "zod";

export default defineAction({
  needsApproval: true,  // Triggers the HITL flow
  schema: z.object({
    emailId: z.string(),
    body: z.string(),
  }),
  async run({ emailId, body }) {
    // Executes only after human approval
    await db.insert(replies).values({ emailId, body });
  },
});

```

When `needsApproval` is detected in [`action.ts`](https://github.com/BuilderIO/agent-native/blob/main/action.ts) (line 383), the runtime aborts immediate execution and generates an `approval_required` event containing an `approvalKey` and action summary. The actual `run` function remains uninvoked until the approval arrives.

### Customizing Approval Policies

For role-based or contextual gating, attach an `approvalPolicy` object to restrict who may approve the action:

```typescript
export default defineAction({
  needsApproval: true,
  approvalPolicy: {
    requiredRoles: ["admin", "security"],
    prompt: "Deploy to production – approve?",
  },
  schema: z.object({ /* ... */ }),
  run: async (input) => { /* ... */ },
});

```

The policy is enforced server-side in [`packages/dispatch/src/server/plugins/auth.ts`](https://github.com/BuilderIO/agent-native/blob/main/packages/dispatch/src/server/plugins/auth.ts) (line 42) before the approval UI allows submission.

## Runtime Event Handling

When the agent encounters an action requiring approval, the runtime performs three critical operations:

First, it emits a structured event defined in [`packages/core/src/agent/types.ts`](https://github.com/BuilderIO/agent-native/blob/main/packages/core/src/agent/types.ts) (lines 200–230) with the shape `{ type: "approval_required", approvalKey: string, summary: string }`.

Second, it stores the pending tool call in `AgentChatRequest.approvedToolCalls` so the original context is preserved for resumption.

Third, the main execution loop in [`packages/core/src/agent/production-agent.ts`](https://github.com/BuilderIO/agent-native/blob/main/packages/core/src/agent/production-agent.ts) (line 582) pauses the current turn, yielding control back to the client layer until the `approvalKey` is received.

## Client-Side Integration

The SSE client layer handles approval events transparently across all front-end implementations. In [`packages/core/src/client/sse-event-processor.ts`](https://github.com/BuilderIO/agent-native/blob/main/packages/core/src/client/sse-event-processor.ts) (lines 25, 51, 461), incoming events are parsed:

```typescript
if (ev.type === "approval_required") {
  // Forward to UI layer with approvalKey attached
}

```

The chat runtime in [`packages/core/src/client/chat/runtime.ts`](https://github.com/BuilderIO/agent-native/blob/main/packages/core/src/client/chat/runtime.ts) (line 1304) routes this event to render an approval card. Once a reviewer clicks **Approve**, the client POSTs the `approvalKey` to `/api/approval` (defined in [`templates/dispatch/shared/api.ts`](https://github.com/BuilderIO/agent-native/blob/main/templates/dispatch/shared/api.ts)), triggering the agent to re-issue the original tool call with the key included in the payload.

## Practical Example: Gated Email Sending

Here is a complete implementation for an email action that requires security team approval:

```typescript
// src/actions/send-email.ts
import { defineAction } from "@agent-native/core";
import { z } from "zod";

export default defineAction({
  needsApproval: true,
  approvalPolicy: {
    requiredRoles: ["security", "admin"],
    prompt: "Approve external email transmission?"
  },
  schema: z.object({
    to: z.string().email(),
    subject: z.string(),
    body: z.string(),
  }),
  async run({ to, subject, body }) {
    // Only executes post-approval
    await sendMail({ to, subject, body });
  },
});

```

When invoked through any interface—CLI, web UI, or A2A protocol—the action automatically surfaces an approval request in the connected client. After a valid reviewer submits the approval through the generic dispatch API, the mail function executes with the original arguments preserved.

## Summary

- **Declarative Configuration**: Adding `needsApproval: true` to `defineAction` automatically enables HITL without custom middleware.
- **Managed State**: The runtime stores pending calls in `AgentChatRequest.approvedToolCalls` and pauses execution via [`production-agent.ts`](https://github.com/BuilderIO/agent-native/blob/main/production-agent.ts) until resolution.
- **Universal Transport**: Approval events flow through SSE to all connected clients, with UI rendering handled by [`sse-event-processor.ts`](https://github.com/BuilderIO/agent-native/blob/main/sse-event-processor.ts) and [`chat/runtime.ts`](https://github.com/BuilderIO/agent-native/blob/main/chat/runtime.ts).
- **Policy Enforcement**: Role-based restrictions are validated server-side before approval acceptance.
- **Seamless Resumption**: After the `approvalKey` is submitted to `/api/approval`, the original tool call re-executes transparently with full context preserved.

## Frequently Asked Questions

### How does the agent resume execution after receiving an approval?

When a reviewer submits the `approvalKey` to the `/api/approval` endpoint, the agent runtime retrieves the stored pending call from `AgentChatRequest.approvedToolCalls` (as defined in [`packages/core/src/agent/types.ts`](https://github.com/BuilderIO/agent-native/blob/main/packages/core/src/agent/types.ts)) and re-issues the original tool call with the key attached. The `run` function then executes normally, returning results to the original caller without requiring client-side state management.

### Can I restrict which users are allowed to approve specific actions?

Yes, by attaching an `approvalPolicy` object to your action definition with a `requiredRoles` array. The dispatch server validates these roles in [`packages/dispatch/src/server/plugins/auth.ts`](https://github.com/BuilderIO/agent-native/blob/main/packages/dispatch/src/server/plugins/auth.ts) (line 42) before processing the approval submission. Only users with matching role assignments will see active approval buttons in the UI.

### What information is included in the `approval_required` event?

The event object includes an `approvalKey` (a unique identifier for the pending operation), a `summary` string describing the action, and the full context needed to resume execution. These fields are defined in [`packages/core/src/agent/types.ts`](https://github.com/BuilderIO/agent-native/blob/main/packages/core/src/agent/types.ts) (lines 200–230) and consumed by the SSE processor in [`packages/core/src/client/sse-event-processor.ts`](https://github.com/BuilderIO/agent-native/blob/main/packages/core/src/client/sse-event-processor.ts).

### Is human-in-the-loop supported across all dispatch templates?

Yes, the HITL mechanism is implemented in the core libraries used by all dispatch templates. The generic API in [`templates/dispatch/shared/api.ts`](https://github.com/BuilderIO/agent-native/blob/main/templates/dispatch/shared/api.ts) includes the `/api/approval` endpoint, and the event processing logic in `packages/core/src/client/` is shared across Slack, Telegram, and web UI runtimes, ensuring consistent behavior regardless of front-end implementation.