Web Inbox

The web inbox is always included in new Hitl(). No adapter package required. When a workflow calls waitForHuman, the server persists the request in state and records it for the inbox channel. Your app lists pending requests and resolves them via hitl.inbox.

Use the inbox for internal admin panels, custom REST APIs, or mobile apps. It is not mutually exclusive with the Chat SDK adapter: the same request can be visible in both if you configure delivery that way.

Deliver to the inbox from a workflow

By default, waitForHuman delivers to the inbox channel. Omit channel or pass "inbox" explicitly. The workflow only POSTs to your Hitl server; it does not render UI or call hitl.inbox directly.

In your workflow (after setting up a workflow client), request approval before sending email:

const emailDraft = { to: input.email, subject: input.subject, body: input.body };

const approval = await waitForHuman({
  message: `Send email to: ${input.email}?`,
  actions: actions().approve().deny().build(),
});

if (!isResolved(approval, "approve")) return;

await sendEmail(emailDraft);

Omit channel to use the inbox by default. Passing channel: "inbox" is equivalent.

List pending requests

On the server, hitl.inbox.list reads from state. Call it from route handlers, scripts, or background jobs that power your UI.

const pending = await hitl.inbox.list({ status: "pending" });
const all = await hitl.inbox.list();

Pass { status: "resolved" } to fetch completed requests. Omit status to return both.

Resolve a request

When a reviewer approves or denies, call hitl.inbox.resolve. Hitl updates state, notifies channel adapters, and calls your workflow engine's resolver to resume the suspended run.

await hitl.inbox.resolve(id, {
  actionId: "approve",
  by: { name: "you" },
  feedbacks: { subject: "Updated subject" }, // optional field edits
});

The id comes from hitl.inbox.list. The actionId must match an action you passed in actions() when creating the request.

Expose the inbox API

hitl.inbox is a programmatic API, not HTTP. Wrap it in your own routes so a browser or CLI can list and resolve requests. This step is required if you want humans to approve without building calls into server code directly.

Add handlers to your HTTP server (or extend server.ts from Host integration). Example using plain Node.js and JSON:

server.ts
import { createServer } from "node:http";
import { hitl } from "./lib/hitl";

createServer(async (req, res) => {
  const url = new URL(req.url ?? "/", "http://localhost");

  if (req.method === "GET" && url.pathname === "/api/inbox") {
    const status = url.searchParams.get("status");
    const requests = await hitl.inbox.list(
      status === "pending" || status === "resolved" ? { status } : undefined,
    );
    res.writeHead(200, { "content-type": "application/json" });
    res.end(JSON.stringify({ requests }));
    return;
  }

  if (req.method === "POST" && url.pathname === "/api/inbox") {
    const body = JSON.parse(await readBody(req));
    const result = await hitl.inbox.resolve(body.id, {
      actionId: body.actionId,
      feedbacks: body.feedbacks,
      by: body.by,
    });
    res.writeHead(200, { "content-type": "application/json" });
    res.end(JSON.stringify({ result }));
    return;
  }

  // ... mount /.well-known/hitl/v1 here too
}).listen(3000);

function readBody(req: import("node:http").IncomingMessage): Promise<string> {
  return new Promise((resolve, reject) => {
    const chunks: Buffer[] = [];
    req.on("data", (c) => chunks.push(c));
    req.on("end", () => resolve(Buffer.concat(chunks).toString()));
    req.on("error", reject);
  });
}

Build any UI on top of these endpoints: admin panels, internal tools, or mobile apps. The inbox channel stores the request; your handlers drive resolution.

Using Next.js, Express, Hono, or Fastify? See Host integration for mount patterns on each framework.

Batch requests

For multi-step approvals, use hitl.inbox.resolveBatch and batch listing APIs. See Human steps.