惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

人人都是产品经理
人人都是产品经理
博客园_首页
IT之家
IT之家
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Vercel News
Vercel News
美团技术团队
D
Docker
WordPress大学
WordPress大学
T
Tailwind CSS Blog
酷 壳 – CoolShell
酷 壳 – CoolShell
The Cloudflare Blog
Y
Y Combinator Blog
F
Fortinet All Blogs
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
G
Google Developers Blog
爱范儿
爱范儿
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
月光博客
月光博客
MongoDB | Blog
MongoDB | Blog
S
SegmentFault 最新的问题
GbyAI
GbyAI
Hugging Face - Blog
Hugging Face - Blog
Microsoft Azure Blog
Microsoft Azure Blog
A
About on SuperTechFans

Workflow SDK Documentation

Patterns for Defining Tools Building Durable AI Agents Queueing User Messages Resumable Streams Sleep, Suspense, and Scheduling Streaming Updates from Tools API Reference Workflow Globals Changelog Resilient run start Cookbook Building a World Deploying Astro Express Fastify Hono Getting Started NestJS Next.js Nitro Nuxt Python SvelteKit Vite corrupted-event-log fetch-in-workflow hook-conflict Errors node-js-module-in-workflow
Human-in-the-Loop
2026-05-31 · via Workflow SDK Documentation

Wait for human input or external events before proceeding in your AI agent workflows.

A common pre-requisite for running AI agents in production is the ability to wait for human input or external events before proceeding.

Workflow SDK's webhook and hook primitives enable "human-in-the-loop" patterns where workflows pause until a human takes action, allowing smooth resumption of workflows even after days of inactivity, and provides stability across code deployments.

If you need to react to external events programmatically, see the hooks documentation for more information. This part of the guide will focus on the human-in-the-loop pattern, which is a subset of the more general hook pattern.

defineHook() creates a typed hook that can be awaited in a workflow. When the tool is called, it creates a hook instance using the tool call ID as the token.

The workflow pauses at await hook - no compute resources are consumed while waiting for the human to take action.

The UI displays the pending tool call with its input data (flight details, price, etc.) and renders approval controls.

The user submits their decision through an API endpoint, which resumes the hook with the approval data.

The workflow receives the approval data and resumes execution.

While this demo will use a client side button for human approval, you could just as easily create a webhook and send the approval link over email or slack to resume the agent.

Add a tool that allows the agent to deliberately pause execution until a human approves or rejects a flight booking:

Define the Hook

Create a typed hook with a Zod schema for validation:

import { defineHook } from "workflow";
import { z } from "zod";
// ... existing imports ...

export const bookingApprovalHook = defineHook({
  schema: z.object({
    approved: z.boolean(),
    comment: z.string().optional(),
  }),
});

// ... tool definitions ...

Implement the Tool

Create a tool that creates a hook instance using the tool call ID as the token. The UI will use this ID to submit the approval.

import { bookingApprovalHook } from "@/workflows/hooks/booking-approval"; 

// ...

async function executeBookingApproval( 
  { flightNumber, passengerName, price }: { flightNumber: string; passengerName: string; price: number }, 
  { toolCallId }: { toolCallId: string } 
) { 
  // Note: No "use step" here - hooks are workflow-level primitives

  // Use the toolCallId as the hook token so the UI can reference it
  const hook = bookingApprovalHook.create({ token: toolCallId }); 

  // Workflow pauses here until the hook is resolved
  const { approved, comment } = await hook; 

  if (!approved) {
    return `Booking rejected: ${comment || "No reason provided"}`;
  }

  return `Booking approved for ${passengerName} on flight ${flightNumber}${comment ? ` - Note: ${comment}` : ""}`;
}

// ...

// Adding the tool to the existing tool definitions
export const flightBookingTools = {
  // ... existing tool definitions ...
  bookingApproval: {
    description: "Request human approval before booking a flight",
    inputSchema: z.object({
      flightNumber: z.string().describe("Flight number to book"),
      passengerName: z.string().describe("Name of the passenger"),
      price: z.number().describe("Total price of the booking"),
    }),
    execute: executeBookingApproval,
  },
};

Note that the defineHook().create() function must be called from within a workflow context, not from within a step. This is why executeBookingApproval does not have "use step" - it runs in the workflow context where hooks are available.

Create the API Route

Create a new API endpoint that the UI will call to submit the approval decision:

import { bookingApprovalHook } from "@/workflows/hooks/booking-approval"; 

export async function POST(request: Request) {
  const { toolCallId, approved, comment } = await request.json();

  // Schema validation happens automatically
  // Can throw a zod schema validation error, or a
  await bookingApprovalHook.resume(toolCallId, { 
    approved,
    comment,
  });

  return Response.json({ success: true });
}

Create the Approval Component

Build a new component that reacts to the tool call data, and allows the user to approve or reject the booking:

"use client";

import { useState } from "react";

interface BookingApprovalProps {
  toolCallId: string;
  input?: {
    flightNumber: string;
    passengerName: string;
    price: number;
  };
  output?: string;
}

export function BookingApproval({ toolCallId, input, output }: BookingApprovalProps) {
  const [comment, setComment] = useState("");
  const [isSubmitting, setIsSubmitting] = useState(false);

  // If we have output, the approval has been processed
  if (output) {
    return (
      <div className="border rounded-lg p-4">
        <p className="text-sm text-muted-foreground">{output}</p>
      </div>
    );
  }

  const handleSubmit = async (approved: boolean) => {
    setIsSubmitting(true);
    try {
      await fetch("/api/hooks/approval", { 
        method: "POST", 
        headers: { "Content-Type": "application/json" }, 
        body: JSON.stringify({ toolCallId, approved, comment }), 
      }); 
    } finally {
      setIsSubmitting(false);
    }
  };

  return (
    <div className="border rounded-lg p-4 space-y-4">
      <div className="space-y-2">
        <p className="font-medium">Approve this booking?</p>
        <div className="text-sm text-muted-foreground">
          {input && (
            <div className="space-y-2">
              <div>Flight: {input.flightNumber}</div>
              <div>Passenger: {input.passengerName}</div>
              <div>Price: ${input.price}</div>
            </div>
          )}
        </div>
      </div>

      <textarea
        value={comment}
        onChange={(e) => setComment(e.target.value)}
        placeholder="Add a comment (optional)..."
        className="w-full border rounded p-2 text-sm"
        rows={2}
      />

      <div className="flex gap-2">
        <button
          type="button"
          onClick={() => handleSubmit(true)}
          disabled={isSubmitting}
          className="px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700 disabled:opacity-50"
        >
          {isSubmitting ? "Submitting..." : "Approve"}
        </button>
        <button
          type="button"
          onClick={() => handleSubmit(false)}
          disabled={isSubmitting}
          className="px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700 disabled:opacity-50"
        >
          {isSubmitting ? "Submitting..." : "Reject"}
        </button>
      </div>
    </div>
  );
}

Show the Tool Status in the UI

Use the component we just created to render the tool call and approval controls in your chat interface:

// ... existing imports ...
import { BookingApproval } from "@/components/booking-approval";

export default function ChatPage() {

  // ...

  const { stop, messages, sendMessage, status, setMessages } =
    useChat<MyUIMessage>({
      // ... options
    });

  // ...

  return (
    <div className="flex flex-col w-full max-w-2xl pt-12 pb-24 mx-auto stretch">
      // ...

      <Conversation className="mb-10">
        <ConversationContent>
          {messages.map((message, index) => {
            const hasText = message.parts.some((part) => part.type === "text");

            return (
              <div key={message.id}>
                // ...
                <Message from={message.role}>
                  <MessageContent>
                    {message.parts.map((part, partIndex) => {

                      // ...

                      if (
                        part.type === "tool-searchFlights" ||
                        part.type === "tool-checkFlightStatus" ||
                        part.type === "tool-getAirportInfo" ||
                        part.type === "tool-bookFlight" ||
                        part.type === "tool-checkBaggageAllowance"
                      ) {
                        // ... render other tools
                      }
                      if (part.type === "tool-bookingApproval") { 
                        return ( 
                          <BookingApproval 
                            key={partIndex} 
                            toolCallId={part.toolCallId} 
                            input={part.input as any} 
                            output={part.output as any} 
                          />
                        ); 
                      } 
                      return null;
                    })}
                  </MessageContent>
                </Message>
              </div>
            );
          })}
        </ConversationContent>
        <ConversationScrollButton />
      </Conversation>

      // ...
    </div>
  );
}

For simpler cases where you don't need type-safe validation or programmatic resumption, you can use createWebhook() directly. This generates a unique URL that can be called to resume the workflow:

import { createWebhook } from "workflow";
import { z } from "zod";

async function executeBookingApproval(
  { flightNumber, passengerName, price }: { flightNumber: string; passengerName: string; price: number },
  { toolCallId }: { toolCallId: string }
) {
  const webhook = createWebhook(); 

  // The webhook URL could be logged, sent via email, or stored for later use
  console.log("Approval URL:", webhook.url);

  // Workflow pauses here until the webhook is called
  const request = await webhook; 
  const { approved, comment } = await request.json(); 

  if (!approved) {
    return `Booking rejected: ${comment || "No reason provided"}`;
  }

  return `Booking approved for ${passengerName} on flight ${flightNumber}`;
}

The webhook URL can be called directly with a POST request containing the approval data. This is useful for:

  • External systems that need to call back into your workflow
  • Payment provider callbacks
  • Email-based approval links