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.
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 app | Outside the app | |
|---|---|---|
| When | The user is in your UI right now | The agent runs in the background, or the approver is someone else |
| CopilotKit tool | useHumanInTheLoop renders an approval card | Not built in |
| How the person answers | Click in your app | Slack, SMS, WhatsApp, Telegram, Discord, email or a phone call |
| If they do not answer | The agent stays paused | redphonr escalates, then returns your default (deny) |
| Audit trail | Build your own | Every 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.