Next.js

09 Human in the Loop

Add approvals and human feedback to Next.js Anvia routes.

Human-in-the-loop work can live in Studio during development or in your own Next.js routes when production users need to approve actions.

1. Use Studio For Local Approval UI

Add approval metadata to side-effect tools:

const refundOrder = createTool({
  name: "refund_order",
  description: "Issue a refund.",
  input: z.object({
    orderId: z.string(),
    amount: z.number().positive(),
  }),
  approval: {
    when: ({ args }) => args.amount > 100,
    reason: ({ args }) => `Review refund of $${args.amount} for ${args.orderId}.`,
    rejectMessage: "Refund was not approved.",
  },
  async execute({ orderId, amount }) {
    return refunds.create({ orderId, amount });
  },
});

Run the same built agent in Studio from a server-only script:

import { Studio } from "@anvia/studio";
import { supportAgent } from "@/app/ai/support-agent";

new Studio([supportAgent]).start({ port: 4021 });

Studio reads the approval metadata and waits for a reviewer before running protected tools.

2. Use A Request Hook For Product Approval

Use a hook when your app owns the approval table, reviewer UI, notification, or timeout.

import { createHook } from "@anvia/core";
import { approvalRuntime } from "@/app/approvals/runtime";

function createApprovalHook(input: { userId: string; conversationId: string }) {
  return createHook({
    async onToolCall({ toolName, args, tool }) {
      if (toolName !== "refund_order") {
        return tool.run();
      }

      const approved = await approvalRuntime.waitForDecision({
        userId: input.userId,
        conversationId: input.conversationId,
        toolName,
        args,
      });

      return approved ? tool.run() : tool.skip("Refund was not approved.");
    },
  });
}

approvalRuntime is your own module. It is not imported from @anvia/core or any Anvia package.

Attach the hook in the route:

const response = await supportAgent
  .prompt(message)
  .requestHook(createApprovalHook({ userId, conversationId }))
  .send();

3. Create The Approval Runtime

approvalRuntime is application code, not an Anvia export. It is the small runtime that creates a pending approval record, notifies reviewers, waits until one of your routes or jobs resolves it, then returns the decision to the hook.

type ApprovalRequest = {
  userId: string;
  conversationId: string;
  toolName: string;
  args: string;
};

type ApprovalDecision = {
  approved: boolean;
  reason?: string;
};

export function createApprovalRuntime() {
  const waiters = new Map<string, (decision: ApprovalDecision) => void>();

  return {
    async waitForDecision(request: ApprovalRequest): Promise<boolean> {
      const approval = await db.approval.create({
        data: {
          userId: request.userId,
          conversationId: request.conversationId,
          toolName: request.toolName,
          args: request.args,
          status: "pending",
        },
      });

      await notifyReviewers({ approvalId: approval.id });

      const decision = await new Promise<ApprovalDecision>((resolve) => {
        waiters.set(approval.id, resolve);
      });

      waiters.delete(approval.id);
      return decision.approved;
    },

    async decide(input: {
      approvalId: string;
      reviewerId: string;
      approved: boolean;
      reason?: string;
    }): Promise<void> {
      await db.approval.update({
        where: { id: input.approvalId },
        data: {
          status: input.approved ? "approved" : "rejected",
          reviewerId: input.reviewerId,
          decisionReason: input.reason,
          resolvedAt: new Date(),
        },
      });

      waiters.get(input.approvalId)?.({
        approved: input.approved,
        reason: input.reason,
      });
    },
  };
}

export const approvalRuntime = createApprovalRuntime();

The Map is only the waiting mechanism for a single Node process. In production, keep approval records in durable storage and use your app's realtime channel, queue, pub/sub system, or polling worker to resolve waiters.

4. Keep Approval State In Your App

StateOwner
Pending approval recordYour database
Reviewer authorizationYour app
Notification or websocketYour app
Tool execution after approvalAnvia via tool.run()
Rejection message to modelAnvia via tool.skip(...)

Next

Add tests in Setup Tests. Core concepts: Human in the Loop, Approval by Hooks, Approval Runtimes, and Studio Tool Approvals.