Polling
When you call sendToHuman() without a client webhook, the SDK does not return as soon as the task is created. Instead it polls the RobotRock API until someone handles the task in the inbox, then returns the chosen action and form data in the same call.
Use polling for scripts, CLIs, and small services where blocking until approval is acceptable. For long-running or serverless workflows, prefer Webhooks, Vercel Workflow, or Trigger.dev.
Webhook vs polling
You cannot set webhook and polling on the same createClient() call—the TypeScript types enforce one mode or the other.
| Client config | sendToHuman() behavior |
|---|---|
webhook set | Returns immediately with mode: "created". Your app receives the result via HTTP callback. |
No webhook | Blocks and polls until the task is handled, then returns mode: "handled" with a typed action result. |
The SDK attaches webhook handlers to each action only when webhook is set on createClient. If those handlers are present, polling is skipped.
// lib/robotrock.ts — polling mode (optional tuning)
import { createClient } from "robotrock";
export const robotrock = createClient({
app: "my-service",
polling: {
intervalMs: 2000,
timeoutMs: 10 * 60 * 1000,
},
});import { robotrock } from "@/lib/robotrock";
const result = await robotrock.sendToHuman({
type: "approval",
name: "Deploy to production",
actions: [
{ id: "approve", title: "Approve" },
{ id: "reject", title: "Reject" },
],
});
if (result.mode === "handled" && result.actionId === "approve") {
console.log("Approved");
}Omit polling on the client to use defaults (intervalMs 2s, timeoutMs 24h).
Polling settings
Configure polling on createClient(), not on individual sendToHuman() calls:
export const robotrock = createClient({
app: "my-service",
polling: {
intervalMs: 2000,
timeoutMs: 10 * 60 * 1000,
},
});| Option | Description | Default |
|---|---|---|
intervalMs | How long to wait between getTask checks (milliseconds). | 2000 (2 seconds) |
timeoutMs | Maximum time for the SDK to keep polling (milliseconds). Stops earlier if the task hits validUntil. | 86400000 (24 hours) |
Task deadline (validUntil)
Each task can set a deadline with validUntil on sendToHuman() (Date or ISO 8601 string). The SDK sends an ISO string to the API. After that time the task is no longer actionable in the inbox.
When polling, the SDK stops at the earlier of:
polling.timeoutMson the client, and- the task's
validUntil
If validUntil passes first, sendToHuman() throws TaskExpiredError. If the client timeout passes first while the task is still open, it throws TaskTimeoutError.
const result = await robotrock.sendToHuman({
type: "approval",
name: "Approve before standup",
validUntil: new Date(Date.now() + 2 * 60 * 60 * 1000), // 2 hours
actions: [
{ id: "approve", title: "Approve" },
{ id: "reject", title: "Reject" },
],
});Omit validUntil on the task to use the platform default (a long-lived deadline). You can still cap wait time with polling.timeoutMs on the client.
Errors
While polling, sendToHuman() can throw:
TaskExpiredError— the task'svalidUntilpassed (or the API reportedstatus: "expired") before anyone acted.TaskTimeoutError— polling hitpolling.timeoutMswhile the task was still open.
import { createClient, TaskExpiredError, TaskTimeoutError } from "robotrock";
const robotrock = createClient({
app: "my-service",
polling: { timeoutMs: 5 * 60 * 1000 },
});
try {
const result = await robotrock.sendToHuman({
type: "approval",
name: "Review change",
actions: [
{ id: "approve", title: "Approve" },
{ id: "reject", title: "Reject" },
],
});
} catch (error) {
if (error instanceof TaskExpiredError) {
// task expired in the inbox
} else if (error instanceof TaskTimeoutError) {
// exceeded client polling.timeoutMs
} else {
throw error;
}
}Typed results
With polling, the resolved result is discriminated by actionId, so TypeScript narrows data from each action’s JSON Schema (when you use as const on actions):
const actions = [
{
id: "approve",
title: "Approve",
schema: {
type: "object",
required: ["comment"],
properties: { comment: { type: "string" } },
},
},
{ id: "reject", title: "Reject" },
] as const;
const result = await robotrock.sendToHuman({
type: "approval",
name: "Review",
actions,
});
// With polling, result.mode is always "handled" when sendToHuman resolves.
// result.actionId is "approve" | "reject" — TypeScript narrows result.data per branch.
if (result.mode === "handled" && result.actionId === "approve") {
console.log(result.data.comment);
}Related
- Webhooks — receive handled tasks over HTTP instead of blocking
- Send to human — full task payload reference
- Task lifecycle —
getTaskandcancelTaskwhen you manage status yourself