Timeouts & reminders

Control what happens while a human step is pending: auto-resolve after a deadline, nudge the reviewer on the same channel, or escalate to a fallback channel.

Timeouts

Set timeout on waitForHuman to auto-resolve when no reviewer acts in time:

const result = await waitForHuman({
  message: "Approve expense?",
  actions,
  timeout: "72h",
});

When the deadline passes, the workflow resumes with HumanResult.type === "TIMED_OUT". Only id and externalRef are set on the result. Use isResolved to distinguish timed-out from resolved outcomes.

The workflow client owns the durable timer via sleep(). The server marks the request timed out only if it is still pending when the timer fires.

Duration

Timeout and reminder timings use the Duration type:

FormExampleMeaning
ms number36000001 hour in milliseconds
"Ns""30s"seconds
"Nm""15m"minutes
"Nh""72h"hours
"Nd""2d"days
"Nw""1w"weeks

Reminders

Import from @hitl-sdk/hitl:

import { remind, escalate } from "@hitl-sdk/hitl";

Pass entries to waitForHuman's reminders option (or the wait phase after requestHuman). The workflow client owns the durable schedule via sleep(); the server only acts if the request is still pending.

remind

Same-channel thread reminders while an approval is pending.

All methods return a RemindEntry for the reminders array.

MethodTiming
remind.after(duration, { message? })Fixed delay from wait start
remind.at(clock, { message?, tz?, skip? })Wall-clock time today (e.g. "07:00")
remind.tomorrowAt(clock, opts?)Wall-clock time next day
remind.every(interval, { message?, at?, count?, for?, until?, tz?, skip? })Repeating interval
remind.dailyAt(clock, opts?)Every day at clock time
remind.weekdaysAt(clock, opts?)Weekdays at clock time (skips Sat/Sun by default)

Default message: "Reminder: approval still pending".

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

await waitForHuman(pending, {
  reminders: [
    remind.after("1h", { message: "Still waiting for approval" }),
    remind.weekdaysAt("07:00", { tz: "UTC", message: "Morning reminder" }),
  ],
});

escalate

Fallback channel notification or re-delivery while pending.

escalate.to(channel: string).after(duration, { message?, mode? })
escalate.to(channel).at(clock, opts?)
escalate.to(channel).tomorrowAt(clock, opts?)
escalate.to(channel).every(interval, opts)
escalate.to(channel).dailyAt(clock, opts?)
escalate.to(channel).weekdaysAt(clock, opts?)
OptionTypeDefaultDescription
channelstring(required)Target adapter id or adapter_id:destination
messagestring"Escalation: approval still pending"Escalation body
mode"notify" | "redeliver""notify"Notify on fallback channel, or re-send the full approval there

With mode: "redeliver", the server re-sends the full approval message to the escalation channel. With "notify" (default), it sends only the escalation message.

import { remind, escalate } from "@hitl-sdk/hitl";

await waitForHuman({
  message: "Approve expense report?",
  actions,
  timeout: "72h",
  reminders: [
    remind.after("1h", { message: "Still waiting for approval" }),
    escalate.to("oncall:slack:C999").after("4h", { mode: "redeliver" }),
  ],
});

Combined schedule

await waitForHuman(pending, {
  timeout: "72h",
  reminders: [
    remind.after("1h"),
    remind.after("4h", { message: "Urgent: approval needed" }),
    escalate.to("manager:slack:C123").after("8h"),
    escalate.to("oncall").after("24h", { mode: "redeliver" }),
  ],
});

Reminders fire in schedule order. If the human resolves before a reminder fires, subsequent reminders are skipped.

See also