← Back to Blog

Human-in-the-Loop AI Tools in Next.js

Key Takeaways

  • A read-only AI assistant is safe but it's a demo — users want it to do things, not just describe them
  • Human-in-the-loop (HITL) is the named pattern: the agent pauses before a high-stakes action and waits for explicit human approval
  • In AI SDK v6, one flag — needsApproval: true — turns a tool into an inline Approve/Deny card
  • Every AI-driven action should write an audit row tagged triggered_by: 'ai', owned by the human who approved it
  • Gate writes, deletes, and outbound messages; never gate reads — and never expose write tools to members

A chatbot that can read your data is a weekend project. A chatbot that can invite a teammate, change a role, or remove an account is a product, and a liability. The gap between those two is the part nobody screenshots in the launch video: what happens the first time the model decides, on its own, to delete the wrong person.

The honest reason most AI features stay read-only is that giving an agent write access feels reckless, and it usually is. But there's a well-understood pattern that lets you hand an agent real mutating power without handing it autonomy: human-in-the-loop approval. This guide walks through it concretely, using VibeReady's v0.4.0 write-action tools built on AI SDK v6.

If you've read our piece on harness engineering, treat this as one specific guardrail in that harness: the one that sits between the model's intent and your database.

A Read-Only Agent Is a Demo

Read-only agents are safe precisely because they can't do anything. They summarize, search, and answer questions. The worst outcome is a wrong answer, which a user can ignore. That safety is also the ceiling: the assistant can tell you the contractor's account should be removed, but you still have to go to the settings page and remove it yourself. The AI saved you nothing but a sentence.

Users feel that gap immediately. They ask the assistant to "remove the contractor" and it responds with instructions instead of an action, which reads as a broken promise. The product looked agentic in the demo and turned out to be a search box with manners.

The reason teams stop at read-only is trust, and the data backs up the caution. In a survey of 603 business leaders fielded in July 2025 by Harvard Business Review Analytic Services and sponsored by Workato and AWS, only 6% of organizations said they fully trust AI agents to run core business processes. That number isn't an argument against shipping write-actions. It's an argument for shipping them with a person in the decision.

The trust gap

Capability is no longer the blocker; control is. Gartner predicts that over 40% of agentic AI projects will be canceled by the end of 2027, citing escalating costs, unclear value, and inadequate risk controls. An agent that can act but can't be supervised is exactly the kind of project that gets cut.

What Human-in-the-Loop Actually Is

Human-in-the-loop (HITL) is a named pattern, not a vibe. The agent reasons about a task and decides which action to take, but before any high-stakes action executes, the run pauses and waits for a person to approve or deny it. The model keeps its judgment; the human keeps the veto.

Cloudflare documents this directly in its Agents platform. Its Human-in-the-Loop concept page describes pausing an agent's workflow for human input or approval, holding the pending action in durable state, and resuming once a decision arrives — sometimes minutes later, sometimes days. The defining trait is the deliberate pause at a decision point, not a confidence threshold or a retry.

IBM frames the same idea as governance. Its agentic AI governance playbook positions human oversight and approval checkpoints as a control you design into autonomous systems, so accountability for consequential actions stays with a person. The pattern is becoming a default expectation rather than a nicety: Gartner predicts that by 2029, 70% of government agencies will require human-in-the-loop and explainability for automated decisions.

The practical version for a SaaS app is narrow and concrete: the assistant can propose to invite a member or change a role, but the invitation does not go out, and the role does not change, until someone with authority looks at the exact arguments and clicks Approve.

The Pattern in Code — AI SDK v6

Here's where HITL stops being a concept. In AI SDK v6, the entire pause-and-wait behavior is one flag on the tool definition: needsApproval: true. When the model calls a tool marked this way, the SDK emits an approval-request part instead of running execute(). Below is VibeReady's inviteTeamMember tool, which wraps the same service function the manual settings page calls:

import { tool } from 'ai';
import { z } from 'zod';
import { inviteMember } from '@/lib/services/team';

export const inviteTeamMember = tool({
  description: 'Invite a person to the current team by email',
  inputSchema: z.object({
    email: z.string().email(),
    role: z.enum(['admin', 'member']),
  }),
  // The one flag that turns this into a human-in-the-loop tool.
  // The SDK pauses and emits an approval request instead of executing.
  needsApproval: true,
  execute: async ({ email, role }, ctx) =>
    inviteMember({
      orgId: ctx.orgId,
      email,
      role,
      // Provenance travels with the action, set when the human approves.
      auditContext: {
        triggeredBy: 'ai',
        approvedByUserId: ctx.userId,
      },
    }),
});

Two things are worth calling out. First, execute() never touches the database directly — it calls inviteMember(), the same service function your normal routes use, so validation, row-level security, and audit logging stay in one place. Second, needsApproval can be an async function instead of a literal true, which lets you gate conditionally. The AI SDK cookbook shows a payment tool that only requires approval when the amount exceeds a threshold (needsApproval: async ({ amount }) => amount > 1000).

The Inline Approve/Deny Card

On the client, the paused tool call arrives as a message part with state: 'approval-requested'. You render it as a card that shows the exact arguments — who, what role — and two buttons. Tapping a button calls addToolApprovalResponse({ id, approved }) with the approval id and the user's decision. For a destructive action like removeTeamMember, render the Approve button in a destructive variant so a removal never looks like a routine confirm.

Auto-Resume on Approval

The piece that makes it feel smooth instead of clunky is auto-resume. You configure useChat with sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithApprovalResponses. Once the last assistant message has a response for every pending approval, the turn is sent back automatically, the approved tools execute, and the assistant reports the result. The user never re-types anything. This helper ships in the ai package and is shown in the AI SDK's chatbot tool-usage docs.

The tool decides what to do and asks for approval; the service function decides how; the audit layer records who. Keep each layer thin and the HITL guarantee holds.

For the full file-by-file walkthrough — the tool registry, the card component, and the resume wiring — see the Write Actions & Human-in-the-Loop docs guide.

Provenance — Who Did What

An approval card stops a bad action in the moment. Provenance answers the question that comes later: three weeks from now, when someone asks why an admin was removed, can you tell whether a person did it or the assistant did? If your audit log can't distinguish the two, you've added a fast way to make changes you can't account for.

The fix is to make every AI-driven mutation write an audit row through the same service layer as a manual change, with extra metadata attached. VibeReady's write tools record this shape:

metadata: {
  triggered_by: 'ai',     // distinguishes assistant actions from manual ones
  conversation_id,        // which chat the action came from
  tool_call_id,           // which specific tool call
  approved_by_user_id,    // the human who tapped Approve
}

The detail that matters most: the audit row's userId is set to the human approver, not a bot identity. The person who clicked Approve owns the action, the same way they would if they'd made the change by hand. The triggered_by: 'ai' flag is metadata you filter on to find assistant-driven changes; it isn't a way to shift responsibility onto the model. This keeps accountability where IBM's governance guidance puts it, with a named person, while still letting you audit and review what the assistant has been doing across the team.

Because the tool reuses the existing service function, you get this for free: there's no second audit code path to keep in sync, and no scenario where an AI-triggered write skips a log entry that a manual write would have created.

Who Can Do What — Role Gating

Approval controls the moment of action. Role gating controls who is even offered the action. These are different defenses, and a serious implementation uses both. An approval card is no protection if a member can summon the remove-teammate tool and approve their own request.

VibeReady gates its four team-admin write tools to owners and admins. The tools are only registered in the model's tool list when the caller holds one of those roles. A member's assistant simply doesn't have removeTeamMember or changeMemberRole available, so the model can't propose them — it'll explain that it can't perform the action rather than rendering an approval card the member shouldn't see in the first place.

This ordering matters. Gate first, approve second. Decide which tools a given user's agent is allowed to hold based on their permissions, then apply needsApproval to the high-stakes ones among them. A member never reaches the approval step for an admin action because the tool was never in their agent's hands. The principle is the same one we apply to context: the agent should only ever see what this specific user is entitled to act on, a theme we go deeper on in context engineering.

When to Gate, and When Not To

Not every tool needs an approval card, and over-gating is its own failure. If the assistant asks you to confirm before every search, you'll start clicking Approve reflexively without reading — which trains the exact habit HITL exists to prevent. The skill is drawing the line in the right place.

Gate any action that changes state or leaves your system:

  • Writes and updates — creating records, changing roles, editing settings that affect other people.
  • Deletes and removals — anything destructive or hard to reverse, rendered with a destructive Approve button so it never looks routine.
  • External communications — invitations, emails, webhooks, anything that reaches a third party. Once a message is sent, no approval can recall it.
  • Money movement — payments, refunds, plan changes, often with a conditional needsApproval that triggers above a threshold.

Don't gate reads. Listing members, searching the knowledge base, fetching the current plan — these are reversible, low-stakes, and high-frequency. Forcing approval on them adds friction with no safety payoff. The honest test is the irreversibility question: if the action fired by accident, could you undo it cheaply? If yes, let it run. If no, put a person in front of it. To see where that line lands across support, sales, ops, and research agents, we mapped the gate in six real-world AI agent examples.

VibeReady ships HITL write-actions out of the box — approval cards and AI-provenance audit trails included. See editions from $149 →

For the broader picture of how tools, models, and memory fit together around this pattern, the AI agent starter kit maps the full stack, and the harness engineering guide shows where HITL sits among the other guardrails that keep an agent reliable.

Frequently Asked Questions

What is human-in-the-loop AI?

Human-in-the-loop (HITL) AI is a pattern where an autonomous agent pauses before a high-stakes action and waits for a person to approve or deny it. The model can still decide what to do, but it can't execute writes, deletes, or external communications on its own. Cloudflare and IBM both document HITL as a named governance pattern for agentic systems.

How do I add approval to an AI tool? (AI SDK v6 needsApproval)

In AI SDK v6, set needsApproval: true on the tool definition. The SDK then emits an approval-request part instead of running execute(). Your UI renders an Approve/Deny card, and once the user responds, useChat configured with sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithApprovalResponses resumes the turn so the approved tool actually executes. needsApproval can also be an async function for conditional gating, e.g. only above a dollar threshold.

Which AI actions should require approval?

Gate anything that mutates state or leaves your system: database writes, deletions, role changes, payments, and outbound messages like emails or invitations. Don't gate reads. Forcing approval on a list or search query adds friction with no safety benefit and trains users to click Approve without reading.

How do you audit AI-triggered actions?

Route every AI-driven mutation through the same service layer as manual changes, then write an audit row with metadata: triggered_by 'ai', the conversation_id, the tool_call_id, and approved_by_user_id. Set the row's userId to the human who tapped Approve, not a bot identity. The approver owns the action, and triggered_by === 'ai' is the flag you filter on to find assistant-driven changes.

Can members use AI write-tools?

In VibeReady's v0.4.0 implementation, no. The four team-admin write tools are registered only when the caller is an owner or admin. Members never see them in the tool list and the model never offers them, so a member's assistant can describe an action but can't propose executing it.

Have more questions? See our full FAQ →