How to add SMS, WhatsApp and phone approvals to CopilotKit and OpenDots agents

CopilotKit's useHumanInTheLoop works while the user is in your app. Here is how to reach them by Slack, SMS or a phone call when they are not, using redphonr.

Written by
Published
Reading time
4 min

CopilotKit makes in-app agents easy, and its useHumanInTheLoop hook is a good way to ask the user before the agent acts. It has one assumption built in: the user is looking at your app when the question comes up.

That holds for a copilot in a sidebar. It does not hold for an agent that runs overnight, works through a queue, or, like CopilotKit’s new OpenDots template, is meant to be an always-on coworker. This tutorial shows how to cover both cases: keep useHumanInTheLoop for the user in front of you, and use redphonr to reach everyone else.

Two kinds of human-in-the-loop

In the appOutside the app
WhenThe user is in your UI right nowThe agent runs in the background, or the approver is someone else
CopilotKit tooluseHumanInTheLoop renders an approval cardNot built in
How the person answersClick in your appSlack, SMS, WhatsApp, Telegram, Discord, email or a phone call
If they do not answerThe agent stays pausedredphonr escalates, then returns your default (deny)
Audit trailBuild your ownEvery decision logged

You need the left column for good in-app UX and the right column for anything that runs when no one is watching.

The in-app part: useHumanInTheLoop

For completeness, this is the CopilotKit side. The hook registers a tool that pauses the agent and renders your component; calling respond resumes it:

import { useHumanInTheLoop } from "@copilotkit/react-core/v2";
import { z } from "zod";

useHumanInTheLoop({
  name: "confirmDelete",
  description: "Ask the user in the app before deleting records",
  parameters: z.object({
    itemName: z.string().describe("What will be deleted"),
    itemCount: z.number().describe("How many items"),
  }),
  render: ({ args, respond }) => (
    <ApprovalCard
      text={`Delete ${args.itemCount} ${args.itemName}?`}
      onApprove={() => respond?.("approved")}
      onDeny={() => respond?.("denied")}
    />
  ),
});

If the tab is closed, nobody sees the card. That is the gap the next steps fill.

Step 1: get a redphonr API key

Create a free account at redphonr.com and copy an API key from the dashboard. It starts with rp_live_. Store it as REDPHONR_API_KEY in your server environment, never in client code.

Step 2: connect the redphonr MCP server

CopilotKit’s BuiltInAgent takes a list of MCP servers. Add redphonr over HTTP:

import { BuiltInAgent } from "@copilotkit/runtime/v2";

const agent = new BuiltInAgent({
  model: "openai:gpt-5.4-mini",
  mcpServers: [
    {
      type: "http",
      url: "https://mcp.redphonr.com/mcp",
      // HTTP options are passed to the MCP SDK's StreamableHTTPClientTransport.
      options: {
        requestInit: {
          headers: { Authorization: `Bearer ${process.env.REDPHONR_API_KEY}` },
        },
      },
    },
  ],
});

The agent now has request_approval, ask_human, collect_input, verify_output, handoff and alert_human alongside its other tools.

Step 3: tell the agent which one to use

Two approval tools only help if the agent knows when to pick each. Add this to the agent’s instructions:

Before any irreversible or costly action (deletes, payments, deploys, access changes, bulk email):
- If the user is in this conversation, call confirmDelete (or your in-app approval tool).
- If you are running in the background, or the action needs someone else's approval,
  call redphonr's request_approval with the full context and default "deny".
Never proceed if the answer is not "approved".

Step 4: decide how people are reached

In the redphonr dashboard, open Reach me and set the order, for example Slack immediately, SMS after five minutes, a phone call after fifteen. For a team, create an escalation flow that goes to whoever is on call this week, then their backup.

Step 5: test the “no” path

Trigger a gated action with no one in the app. You should get a Slack card or a text within seconds. Deny it, and check that the agent stops and reports what it did not do. Then let a request time out and check that it is treated as denied.

Where OpenDots fits

OpenDots is CopilotKit’s open-source template for always-on AI coworkers: each Dot has its own role, permissions and sandbox, and you work with it through a web editor, voice calls and Slack. Its approval model is reviewing a Dot’s work before it is saved.

Always-on is exactly when redphonr earns its place. A Dot working through the night will hit decisions nobody is online to review. Where the template configures a CopilotKit agent, add the redphonr server from step 2 and the instructions from step 3, and the Dot can reach a person by phone or SMS instead of waiting for someone to open the editor. The template is in alpha, so look for where it defines each Dot’s agent and tools in its repository.

Why not build this part yourself?

You can. A Slack bot and a Twilio number get you a first version. The parts that take longer are the ones you only need when something goes wrong: escalating when nobody answers, on-call rotation, closing the request on every channel once someone replies, a deny default on timeout, and a log of who approved what. The human-in-the-loop guide covers those design choices, and the comparison of human-in-the-loop tools lists other options.

Frequently asked questions

Does CopilotKit have human-in-the-loop?

Yes. The useHumanInTheLoop hook registers a tool that pauses the agent and renders your own UI, such as an approve and deny card, in the app. The agent stays paused until your component calls respond with the user's answer.

What happens if the user is not in the app when a CopilotKit agent needs approval?

useHumanInTheLoop renders inside your app, so someone has to be looking at it. For agents that run in the background, add a delivery layer such as redphonr, which reaches the person on Slack, SMS, WhatsApp or a phone call and returns their answer to the agent.

Can a CopilotKit agent use MCP servers?

Yes. CopilotKit's BuiltInAgent accepts an mcpServers list with HTTP or SSE transports, so you can connect the redphonr MCP server and the agent gets request_approval, ask_human and the other redphonr tools.

Is OpenDots a replacement for redphonr?

No. OpenDots is an open-source, self-hosted template for building always-on AI coworkers with web, voice and Slack interfaces. Its approval model is reviewing a Dot's work in its own interface. redphonr is a managed service any agent can call to reach a person anywhere, with escalation, on-call rotation and deny on timeout. The two work together.