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:
| Form | Example | Meaning |
|---|---|---|
| ms number | 3600000 | 1 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.
| Method | Timing |
|---|---|
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?)| Option | Type | Default | Description |
|---|---|---|---|
channel | string | (required) | Target adapter id or adapter_id:destination |
message | string | "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
- Human steps:
timeoutandremindersoptions onwaitForHuman - Actions and fields:
HumanResultandisResolved - Foundations overview: API map