Human steps

A human step has two phases: create (deliver the approval to a channel) and wait (suspend until a reviewer resolves). waitForHuman({ … }) runs both in one call. Split them with requestHuman when you need logic or notify between create and wait.

Import

import { requestHuman, waitForHuman } from "@/lib/hitl-client";

Import from your engine-bound client module, not from @hitl-sdk/hitl in workflow functions.

One-shot: waitForHuman

The simplest pattern. Create, deliver, and suspend in one call.

Signatures

// Create and wait in one call
waitForHuman(opts: WaitForHumanOptions<Actions>): Promise<HumanResult<Actions>>

// Batch: create and wait for multiple items
waitForHuman(opts: WaitForHumanOptions<Actions> & { items: HumanItem[] }): Promise<HumanResult<Actions>[]>

// Wait on an existing pending handle
waitForHuman(pending: HumanPending<Actions>, opts?: HumanWaitOptions): Promise<HumanResult<Actions>>

// Batch pending handle
waitForHuman(pending: HumanBatchPending<Actions>, opts?: HumanWaitOptions): Promise<HumanResult<Actions>[]>

Example

import { actions, field, isResolved } from "@hitl-sdk/hitl";
import { waitForHuman } from "@/lib/hitl-client";

const approval = await waitForHuman({
  message: "Send this email?",
  actions: actions()
    .approve({
      label: "Send",
      fields: {
        subject: field.textField({ label: "Subject", default: draft.subject }),
        body: field.textArea({ label: "Body", default: draft.body }),
      },
    })
    .deny({
      label: "Reject",
      fields: { reason: field.textArea({ label: "Reason" }) },
    })
    .build(),
  timeout: "72h",
});

if (!isResolved(approval, "approve")) {
  return; // denied, timed out, or another action
}

const { subject, body } = approval.feedbacks;

Create + wait: requestHuman

The create phase delivers the approval and returns a pending handle without suspending yet. Pass the handle to waitForHuman when you are ready to suspend.

Signatures

// Single request
requestHuman(opts: RequestHumanOptions<Actions>): Promise<HumanPending<Actions>>

// Batch request
requestHuman(opts: RequestHumanOptions<Actions> & { items: HumanItem[] }): Promise<HumanBatchPending<Actions>>

Returns

TypeDescription
HumanPending<Actions>Single request; pass to waitForHuman or notify
HumanBatchPending<Actions>Batch request; id is the batch id

Both implement TimelineAnchor (id, externalRef) for thread chaining.

Example

import { actions, field } from "@hitl-sdk/hitl";
import { requestHuman, waitForHuman, notify } from "@/lib/hitl-client";

const pending = await requestHuman({
  message: "Approve deployment to production?",
  actions: actions()
    .approve({ label: "Ship it" })
    .deny({ fields: { reason: field.textArea({ label: "Reason" }) } })
    .build(),
});

await notify({
  after: pending,
  message: "Diff: https://github.com/org/repo/compare/main...release",
  detail: { commit: "abc123" },
});

const result = await waitForHuman(pending, { timeout: "24h" });

When you need to notify or run logic between creation and waiting:

import { remind } from "@hitl-sdk/hitl";
import { requestHuman, waitForHuman, notify } from "@/lib/hitl-client";

const pending = await requestHuman({ message: "Approve expense?", actions });

await notify({ after: pending, message: "Receipt attached in thread" });

const result = await waitForHuman(pending, {
  timeout: "72h",
  reminders: [remind.after("1h", { message: "Still waiting" })],
});

Shared options

Both requestHuman and waitForHuman({ … }) accept these create options:

OptionTypeRequiredDescription
messagestringYes*Prompt shown to the reviewer
actionsHumanActionsYesBuilt with actions()
contextRecord<string, unknown>NoOpaque metadata stored with the request
channelstringNoAdapter id or adapter_id:destination; defaults to first configured adapter
afterHumanResult | TimelineAnchorNoPost under the same chat thread as a prior step
itemsHumanItem[]NoBatch mode: one reviewer decision per item
defaultsActionIdstringNoBatch defaults target when no approve action exists

*Required when items is absent.

HumanItem

FieldTypeDescription
messagestringPer-item prompt in batch mode
defaultsPartial<FeedbackValues>Override submit field defaults for this item

Wait-only options

Used when calling waitForHuman(pending, opts) after requestHuman, or included in waitForHuman({ … }) for one-shot calls.

OptionTypeDescription
timeoutDurationAuto-resolve as timed out after this duration
remindersReminderEntry[]Scheduled remind / escalate entries

See Timeouts & reminders for Duration formats, reminder schedules, and escalation channels.

Returns

HumanResult<Actions> is a discriminated union:

typeMeaning
"RESOLVED"Reviewer chose an action; actionId and typed feedbacks are set
"TIMED_OUT"No decision before timeout

Use isResolved to narrow to a specific action and get typed feedback values.

Batch mode

Pass items to create multiple reviewer decisions in one delivery:

const batch = await requestHuman({
  actions,
  items: [
    { message: "Approve line item 1" },
    { message: "Approve line item 2", defaults: { amount: "500" } },
  ],
});

const results = await waitForHuman(batch, { timeout: "48h" });

Or create and wait in one call:

const results = await waitForHuman({
  actions,
  items: [
    { message: "Review item A" },
    { message: "Review item B", defaults: { amount: "1200" } },
  ],
});

Batch items require an approve action (or explicit defaultsActionId) for per-item defaults. For programmatic resolution, see hitl.inbox.resolveBatch on the server side.

When to use which

PatternUse when
waitForHuman({ … }) aloneSimple approve/deny with no intermediate steps
requestHumanwaitForHumanYou need to notify or run logic between create and wait
requestHuman with afterMulti-step approvals in the same channel thread

See also