npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

robotrock

v1.8.0

Published

Human-in-the-loop approval workflows for AI agents

Readme

robotrock

Human-in-the-loop approval workflows for AI agents.

Installation

npm install robotrock
# or
bun add robotrock

Agent skill (Cursor, Claude Code, etc.)

Install the RobotRock agent skill from skills.sh to teach coding agents how to integrate the SDK:

npx skills add robotrock-io/robotrock-skills --skill robotrock

For Trigger.dev, also install @trigger.dev/sdk.

Quick Start

Create a shared client in its own module (for example lib/robotrock.ts), then import it wherever you create tasks.

// lib/robotrock.ts
import { createClient } from "robotrock";

export const robotrock = createClient({
  app: "budgeting-service",
  webhook: {
    url: "https://your-app.com/api/robotrock/webhook",
    headers: {
      // place your headers here
    },
  },
});
import { robotrock } from "./lib/robotrock";

const response = await robotrock.sendToHuman({
  type: "budget-approval",
  name: "Q1 Budget Approval",
  description: "Please review and approve the Q1 budget",
  actions: [
    { id: "approve", title: "Approve" },
    { id: "reject", title: "Reject" },
  ],
  idempotencyKey: "budget-q1-approval-v1",
});

console.log("Task created:", response.task.taskId);

Set ROBOTROCK_API_KEY in your environment (create a named key in the RobotRock dashboard under Settings → API Keys). The client reads it when you omit apiKey on createClient.

Inbox routing

Set app on createClient to control dashboard inbox grouping for every task from that client.

When app is omitted on the client, the API uses your API key name as the inbox bucket.

Client webhook

Configure a single webhook on the client. It is applied to every action when you call sendToHuman:

export const robotrock = createClient({
  webhook: {
    url: "https://your-app.com/api/robotrock/webhook",
    headers: {
      // place your headers here
    }, // optional, defaults to {}
  },
});

Automatic fallback polling

When the client has no webhook, sendToHuman polls until a human handles the task or throws TaskExpiredError / TaskTimeoutError. Configure polling on createClient (not on sendToHuman). Polling stops at the earlier of polling.timeoutMs and the task's validUntil. You cannot set both webhook and polling on the same client.

import { createClient, TaskExpiredError, TaskTimeoutError } from "robotrock";

const robotrock = createClient({
  app: "my-service",
  polling: {
    intervalMs: 2_000,
    timeoutMs: 5 * 60 * 1_000,
  },
});

const actions = [
  {
    id: "approve",
    title: "Approve",
    schema: {
      type: "object",
      required: ["ticket"],
      properties: {
        ticket: { type: "string" },
      },
    },
  },
  {
    id: "reject",
    title: "Reject",
    schema: {
      type: "object",
      required: ["reason"],
      properties: {
        reason: { type: "string" },
      },
    },
  },
] as const;

const result = await robotrock.sendToHuman({
  type: "approval",
  name: "Review change",
  actions,
});

if (result.mode === "created") {
  console.log("Task created:", result.task.taskId);
} else if (result.actionId === "approve") {
  console.log("Approved with ticket", result.data.ticket);
} else {
  console.log("Rejected because", result.data.reason);
}

Thread updates

Send short status updates (1-2 sentences) to a thread. The newest update shows in a status bar at the top of the inbox task detail, with an icon and color from an optional status; every update is logged in an expandable history. Updates are scoped to a threadId.

// Standalone update against an existing thread
const update = await robotrock.sendUpdate({
  threadId, // from response.task.threadId
  message: "Deployment started, running smoke tests.",
  status: "running", // optional, defaults to "info"
});

// Or log an initial update when creating the task
await robotrock.sendToHuman({
  type: "deploy-approval",
  name: "Approve production rollout",
  threadId: `deploy_${deploymentId}`,
  update: { message: "Build finished, awaiting approval.", status: "waiting" },
  actions: [{ id: "approve", title: "Approve" }],
});

status is optional and one of: info | queued | running | waiting | succeeded | failed | cancelled. The update field is top-level (like threadId and assignTo), not inside context.

Updates are fire-and-forget, so the integration packages expose them too:

// Vercel Workflow: durable step, returns immediately (no suspend)
import { sendUpdateInWorkflow } from "robotrock/workflow";
await sendUpdateInWorkflow({ threadId, message: "Build started.", status: "running" });

// Vercel AI SDK: let an agent report progress on its thread
import { createSendUpdateTool } from "robotrock/ai";
const sendUpdate = createSendUpdateTool(robotrock, { threadId });

// Trigger.dev: no wrapper needed — call the SDK directly inside a task
await robotrock.sendUpdate({ threadId, message: "Job running.", status: "running" });

Other client methods

await robotrock.getTask("task_...");
await robotrock.cancelTask("task_...");

Webhook verification helper

import {
  verifyRobotRockWebhook,
  RobotRockWebhookError,
  isPlatformTerminalAction,
  isPlatformRejectRequestAction,
  parsePlatformRejectRequestData,
  type RobotRockWebhookPayload,
} from "robotrock";

export async function POST(request: Request) {
  try {
    const payload: RobotRockWebhookPayload = await verifyRobotRockWebhook(request);
    console.log(payload.action.id, payload.headers["x-request-id"]);

    // Always stop the agent for inbox platform actions (not your task's approve/reject ids)
    if (isPlatformRejectRequestAction(payload.action.id)) {
      const { feedback } = parsePlatformRejectRequestData(payload.action.data) ?? {
        feedback: "",
      };
      // log and abort — do not continue the workflow
      return Response.json({ ok: true, stopped: true, feedback });
    }
    if (isPlatformTerminalAction(payload.action.id)) {
      return Response.json({ ok: true, stopped: true });
    }

    return Response.json({ ok: true });
  } catch (error) {
    if (error instanceof RobotRockWebhookError) {
      return Response.json({ error: error.code }, { status: 401 });
    }
    return Response.json({ error: "Unknown error" }, { status: 500 });
  }
}

Platform terminal actions

Reviewers can close a task from the inbox without choosing one of your defined actions. These reserved action.id values arrive on webhooks, polling, and getTask()always stop your agent when you see them:

| Constant | Action ID | Meaning | |----------|-----------|---------| | PLATFORM_MARK_DONE_ACTION_ID | robotrock:mark-done | Closed manually; data is {} | | PLATFORM_REJECT_REQUEST_ACTION_ID | robotrock:reject-request | Bad request; data.feedback is required |

import {
  isPlatformTerminalAction,
  parseHandledOutcome,
  assertNotPlatformRejectRequest,
  shouldStopAgentForHandledAction,
} from "robotrock";

const result = await robotrock.sendToHuman({ ... });

if (result.mode === "handled") {
  assertNotPlatformRejectRequest(result.actionId, result.data); // throws PlatformRejectRequestError

  if (shouldStopAgentForHandledAction(result.actionId)) {
    return; // mark-done or reject-request — do not continue
  }

  const outcome = parseHandledOutcome({
    actionId: result.actionId,
    data: result.data,
    handledBy: result.handledBy,
    handledAt: result.handledAt,
  });
  if (outcome.source === "platform") {
    return;
  }
}

See Platform actions in the docs.

Trigger.dev

Install @trigger.dev/sdk, re-export SDK tasks from your trigger/ directory, and call them with triggerAndWait():

// trigger/robotrock.ts
export { sendToHumanTask, approveByHumanTask } from "robotrock/trigger";
import { task } from "@trigger.dev/sdk";
import { approveByHumanTask } from "./robotrock";

export const gate = task({
  id: "gate",
  run: async () => {
    const waitResult = await approveByHumanTask.triggerAndWait({
      type: "release-gate",
      name: "Ship this release?",
    });

    if (!waitResult.ok) {
      throw waitResult.error;
    }

    return waitResult.output.actionId === "approve";
  },
});

Set ROBOTROCK_API_KEY (and optionally ROBOTROCK_APP) in your Trigger worker environment.

Vercel AI SDK

Install ai and use the optional robotrock/ai entry:

npm install ai
import { generateText, stepCountIs } from "ai";
import { createClient } from "robotrock";
import { approveByHumanTool, createSendToHumanTool } from "robotrock/ai";

const robotrock = createClient({
  app: "my-agent",
  polling: { timeoutMs: 30 * 60_000 },
});

const result = await generateText({
  model: "anthropic/claude-sonnet-4",
  tools: {
    approveByHuman: approveByHumanTool(robotrock),
  },
  stopWhen: stepCountIs(10),
  prompt: "Plan a release; get human approval before finalizing.",
});

For AI SDK tool execution approval (approve deleteFile before it runs), use createRobotRockToolApproval, resolveToolApprovalsViaRobotRock, and runWithRobotRockApprovals from robotrock/ai. See the Vercel AI integration docs.

Run long polls inside Trigger.dev or a worker — not short serverless HTTP handlers.

Exports

| Import | Description | |--------|-------------| | robotrock | createClient, RobotRock, env helpers, types, schemas | | robotrock/trigger | sendToHumanTask, approveByHumanTask for Trigger.dev | | robotrock/workflow | sendToHumanInWorkflow, approveByHumanInWorkflow, sendUpdateInWorkflow for Vercel Workflow | | robotrock/ai | Vercel AI SDK tools and toolApproval bridge (peer: ai) | | robotrock/ai/trigger | Same API, documented for mode: "trigger" in Trigger.dev workers | | robotrock/ai/workflow | Same API, documented for mode: "workflow" in Vercel Workflow | | robotrock/eve | Eve bridge helpers, tool result formatters (formatTool* + replyGuidance) |

Eve agents — tool result UI (OpenUI)

RobotRock dashboard chat renders rich tool results with OpenUI (OpenUI Lang + OpenUI’s shadcn library).

Contract for tool authors: return plain domain JSON + optional replyGuidance via formatTool*. Tools do not return layouts. The Eve agent must emit OpenUI Lang in a ```openui fence for any user-visible structured result; the dashboard <Renderer> paints it. Without OpenUI, tools stay as compact activity only (Working / Used N tools) — no JSON result bars. Spec / component signatures: skill references tool-result-display.md and openui-lang.md.

import { formatToolListResult, formatToolObjectResult } from "robotrock/eve";

return formatToolListResult("members", rows, {
  replyGuidance: "Do not restate list rows.",
});

return formatToolObjectResult({ city, temperatureF }, {
  replyGuidance: "Do not repeat weather fields.",
});

Linkable list items: include url on each item when you know a navigable link. Use attachments / files / documents for file downloads.

return formatToolListResult("items", [
  { name: "Wireless Keyboard", price: 79.99, url: "https://shop.example.com/sku-1001" },
], { replyGuidance: "Do not restate product rows." });

Install the RobotRock skill for Eve + OpenUI patterns.

License

MIT