How to build a human-in-the-loop AI agent
Learn how to build a human-in-the-loop AI agent: where to add checkpoints, how to reach people on Slack, Telegram or email, and how to handle every answer.

The appeal of an autonomous agent is that it doesn't wait for you. It reads the ticket, writes the fix, runs the tests and moves on to the next one while you're doing something else.
That's exactly what you want, right up to the step that moves money, touches the production database or emails a customer. There, speed stops being the point. The question someone will ask afterwards is simple: who approved this?
A human-in-the-loop agent can answer it. It does the routine work alone, stops at chosen points to get a decision from a person, and carries on based on what they said. It stays fast where that's safe and waits where it isn't.
Building one takes three things: checkpoints in the right places, a way to reach the person wherever they are, and code that handles every outcome, including a no and no answer at all. This guide covers all three, with working examples you can drop into an agent today.
Where to put checkpoints
Too many checkpoints and the agent is no faster than doing the work yourself. Too few and it deletes the wrong table. In practice, three kinds of moments earn a checkpoint.
- Missing information only a person has. Which environment to deploy to, which customer a ticket is about, what the budget is. The agent can't find it in a file, so it asks.
- Actions that are hard to undo. Deleting data, deploying to production, spending money, sending something other people will read. Ask for an explicit approval here even if a person is watching the chat, because an approval records who agreed and when.
- Choices that are expensive to get wrong. Two reasonable options, like a canary or a blue-green rollout, where picking the wrong one quietly means redoing the work. Offer the options instead of choosing silently.
A useful test: if you'd want to be told before a new colleague did it, the agent should ask too.
And one moment that needs no answer: when a long job finishes, tell someone. It's not a checkpoint, but it's the same plumbing, and it saves people checking logs.
The reverse matters just as much. Don't add a checkpoint for something the agent can decide from the code or the docs, and don't ask through a separate channel when the person is already in the chat and can answer the next message.
Which kind of checkpoint to use
Why pausing isn't enough
Most agent frameworks can pause. LangGraph has interrupts, and most others have some way to stop a run and save its state until input arrives. That solves the easy half of the problem.
The pause itself is usually solid. Stop the process while it waits and the state survives. What a pause doesn't do is tell anyone a question is waiting, or wake the run back up when the answer arrives.
A pause leaves four questions open:
- Delivery. How does the person find out there's a question, if they aren't watching the terminal?
- Answering. How do they answer from wherever they are, without opening a laptop and a dashboard?
- Waiting. What does the agent do while it waits, and what happens when the answer comes after the process has stopped?
- Accountability. Who answered, when, and on which channel? Without that, an approval is just a boolean.
Teams usually answer these by building a Slack bot, an email sender, a table to match replies to questions, and timeout logic, then rebuilding it for the next agent. The rest of this guide uses Agent Inbox, which we built at Novu to handle those four parts, so the agent code only has to decide where to ask.

Build it step by step
The examples use the Agent Inbox CLI, which any agent that can run a shell command can call. If your agent runs in Claude, ChatGPT or Cursor instead, the same steps work through MCP; see the end of this section.
Step 1: Give the agent an inbox
This creates the agent's inbox and gives it an email address. Connect Telegram or Slack when it offers, or later with npx @agentinbox/cli channel add telegram. Questions go to you by default, on your preferred channel.
You can start without an account, but a keyless setup gets only a few free messages and can't send email. Run npx @agentinbox/cli login to claim it before you rely on it.
Step 2: Ask for missing information
When the agent needs a fact only a person has, it asks a free-text question. The command waits for the answer and prints it.
Step 3: Get approval before anything hard to undo
An approval sends Approve and Deny buttons and waits. Set --ttl so a stale yes can't be used hours later, and --from so the person knows which agent is asking.
You can rename the buttons and give the person more context:
Approve a kind of action, not just one
Some agents ask for the same thing over and over, like running a particular tool. Asking every time wears people down until they stop reading the questions. Add a third button so the person can approve the whole kind of action once:
Pressing Always allow this tool counts as an approval, so the command exits with 0 just like Approve. To tell the two apart, read the button's id from the JSON result: response.optionId is approve for the Approve button and trust-tool for yours. Without --json, the output only says who approved, not which button they pressed.
Agent Inbox doesn't remember that choice for you, and neither does listen. The button is just an id, so your agent has to act on it, for example by keeping a list of trusted tools for the run and skipping the question for them next time. Through MCP, inbox_send takes up to four extra buttons in extra_actions, and the result names the one pressed: Approved by Ada. They chose: trust-tool.
Keep the wider permission narrow: one tool, one session, one kind of action. "Always allow everything" defeats the point of the checkpoint.
Step 4: Offer a choice
For a decision between options, choose sends up to 10 of them. On its own, it prints a sentence with the label the person picked, like Chose "Staging" by Ada at 1:05:00 PM. To branch in code, give each option a stable id and read it from the JSON result, so your code doesn't depend on the wording.
Step 5: Handle every outcome
This is the step most implementations skip. A question can end four ways, and the exit code tells your code which one:
In a shell script:
The same logic from Python, if your agent is written there:
Two things to note. First, a timeout doesn't lose the answer. When the person replies later, it lands in the conversation, and the agent finds it with npx @agentinbox/cli show <conversation-id>. Every send prints the conversation id, so keep it. Second, treat an expired approval as a no. A yes that arrives after its window closes shouldn't trigger the action.
Step 6: Ask the right person
The decision isn't always yours. List the agent's contacts, then send the question to the person who owns it. Send it to several people and the first valid answer wins.
To add someone, npx @agentinbox/cli contact invite <name> creates a link they open to connect the channel they prefer.
In Claude, ChatGPT or Cursor: use MCP
Add https://mcp.agentinbox.md as an MCP server and sign in. The agent then calls inbox_send with kind set to ask, approve or choose. A tool call can only wait about 45 seconds, so if nobody has answered by then, the result says the request is still open and gives an id. The agent calls inbox_wait with that id until the answer arrives, and shouldn't go ahead without it.
For coding agents like Claude Code, Codex and Cursor, install the skills, which teach the agent when a checkpoint is worth it and when to just ask in the chat:
Common patterns
Deploys. Gate the production step on an approval with a short --ttl, and send the approval to whoever is on call rather than whoever started the job. Report the result with a plain send when it's done.
Deletes and migrations. Put the numbers in the question, like "Delete 342 rows" rather than "Clean up the table". A person can't judge what they can't see.
Payments and refunds. Use --subtitle and --body for the order and the amount, send it to the person who owns the budget, and treat expiry as a no.
Outbound messages. Before an agent emails a customer or posts somewhere public, send the draft as the question and ask for approval. It's cheaper than an apology.
Long-running jobs. Send a plain message when the job finishes or fails. It doesn't wait, and nobody has to watch the logs.
Oversight is what lets agents do more
It's tempting to see checkpoints as a brake. In practice they work the other way. Teams hold agents back from risky work because there's no safe way to let them near it. Once every risky step goes through an approval that's recorded and easy to answer from a phone, you can hand the agent more of the job, and loosen the checkpoints as it earns trust.
A checklist for every checkpoint
- The question says exactly what will happen, with numbers and names.
- It goes to the person who owns the decision.
- Approvals have a
--ttlthat matches how long a yes stays valid. - The code handles approved, denied, timed out, expired and error.
- Expired is treated as no.
- The agent keeps the conversation id so it can find a late answer.
- The agent doesn't ask about things it can decide itself.
Start with one: pick the riskiest action your agent already takes, and put an approval in front of it.
FAQ
What is a human-in-the-loop AI agent?
A human-in-the-loop AI agent stops at set points to get an answer or a decision from a person, then continues based on that answer. It handles the routine work alone and leaves risky or ambiguous calls to people.
When should an AI agent ask a human?
An agent should ask when it needs information only a person has, before actions that are hard to undo like deploys, deletes and payments, and when a choice between options is expensive to get wrong. It shouldn't ask about things it can work out from the code or docs.
What happens if nobody answers the agent?
The agent should stop waiting after a set time and treat the action as not approved. With Agent Inbox, the command exits with code 11 when the wait runs out, the question stays open, and a late answer lands in the conversation for the agent to find later.
Isn't pausing in LangGraph or another framework enough?
Pausing saves the agent's state, but it doesn't tell anyone a question is waiting or bring the answer back. You still need to deliver the question to the person, collect the answer and resume the agent, which is the part an agent inbox handles.
How do I record who approved an agent's action?
Use an approval that returns who answered and when. Agent Inbox sends Approve and Deny buttons on email, Telegram or Slack and returns the answer with the person and the time, so every approval leaves a record.