# Hands-on: build a Slack approval gate before your agent issues refunds or sends emails

> Six steps to make sure a person approves every refund or email your agent sends, without leaving customers waiting forever. Answer the button click within 3 seconds, wait as long as the decision takes, and reject automatically if nobody responds.

Bản gốc: https://fdetimes.net/en/guides/slack-approval-gate-for-ai-agents/

Slack gives your app exactly 3 seconds to respond when someone clicks a button. Meanwhile, the Cloudflare Agents documentation describes an approval gate that can wait for months, or even longer, without keeping the agent running.

A good approval gate has to do both: acknowledge the click almost instantly, yet wait patiently for the decision for as long as it takes without losing state.

Imagine your agent has just been given permission to issue refunds, email customers or edit CRM records. The first thing the client will want to know is: who presses the final button?

This guide shows how to build the answer: an agent that asks for approval in Slack before doing anything important, with a timeout, a fallback path and a log.

## What you will build, and what you need

The scenario: a customer support agent proposes a refund of 2 million dong (VND) on an order. Before calling the refund tool, it posts a message to the finance team's Slack channel stating the order number, the amount and the reason, with two buttons, Approve and Reject. The agent pauses. When someone clicks, it either carries on or cancels.

If nobody clicks, the request is sent again as a reminder and then rejected automatically.

You need a test Slack workspace where you can create an app with interactivity, and an agent runtime that supports stateful pausing. The two options covered here are Cloudflare Agents (with `waitForApproval()`, which runs on Cloudflare Workflows) and LangChain (with its human-in-the-loop middleware).

The code blocks below are **simplified sketches** to illustrate the flow; check the framework documentation for the exact function signatures.

## Step 1: Not every tool needs a human approver

The most common mistake is requiring approval for everything. Picture an approver receiving 40 messages a day for harmless lookups: they will learn to click Approve without reading, and the gate becomes a formality.

Cloudflare's guidance is explicit: only require confirmation for actions with significant consequences, such as payments, emails and data changes.

LangChain turns this principle into configuration. The HITL middleware takes an `interrupt_on` parameter that maps each tool to the decision types allowed for it. Write the policy down as data first:

```python
# Sketch: approval policy table
# The decision type names here are illustrative only;
# take the exact list from LangChain's HITL documentation.
interrupt_on = {
"issue_refund":   {"allowed_decisions": ["approve", "edit", "reject"]},
"send_email":     {"allowed_decisions": ["approve", "reject"]},
"update_crm":     {"allowed_decisions": ["approve", "reject"]},
"search_orders":  False,   # read-only, no approval needed
}
```

**Check:** print the table and go through it line by line with the process owner on the client side. If they cannot explain why a tool needs approval, it probably doesn't.

## Step 2: The waiting gate must survive dropped connections

An approval request may sit there overnight or over a weekend. If its state lives only in memory, a single redeploy wipes it out. LangChain requires you to configure a checkpointer to persist graph state across interrupts. Cloudflare recommends storing pending requests in the agent's state so they are not lost when a connection drops.

```ts
// Sketch on Cloudflare Agents
const decision = await this.waitForApproval({
action: "issue_refund",
args: { orderId: "DH-1042", amount: 2000000, reason: "wrong item delivered" },
});
```

The key point: according to the Cloudflare documentation, `waitForApproval()` creates a durable gate running on Workflows, so waiting does not tie up a running agent. If you use LangChain, choose a checkpointer that writes state to durable storage when you go to production, not one that keeps it only in memory.

**Check:** send an approval request, restart the service, then click Approve. The agent must resume exactly where it stopped, with the same parameters.

## Step 3: The Slack message must show exactly what will happen

Approvers can only decide well on the information they see. Cloudflare recommends showing exactly what the action will do, with all of its parameters. A message that says "The agent wants to issue a refund, approve?" is not enough. Spell it out: "refund 2,000,000 VND on order DH-1042, reason: wrong item delivered, to the customer's original payment account".

The same applies to email: put the exact subject line, recipients and body in the approval message, not a summary. What the approver reads must match precisely what the agent will send.

**Điểm mấu chốt:** Approvers cannot block what they cannot see: every parameter of the tool call must appear in the approval request.

## Step 4: Reply to Slack within 3 seconds, do the heavy work later

When the approver clicks a button, Slack sends a payload to your endpoint and expects a response within 3 seconds. If your handler writes to the database, wakes the agent and calls the refund API before replying, it can easily exceed that limit.

```ts
// Sketch of an interactivity handler
async function onSlackAction(payload) {
enqueue({ approvalId: payload.actionValue, user: payload.user,
decision: payload.actionId, responseUrl: payload.responseUrl });
return new Response("", { status: 200 }); // reply immediately
}
```

The real work runs afterwards in a background worker. When it finishes, you update the original message via `response_url`. Slack lets you post to `response_url` up to 5 times within 30 minutes of receiving the payload, which is enough to say "Processing" and then "Refund issued".

**Check:** log timestamps at the start and end of the handler. The figure must be under 3 seconds even on the first run, when everything is still cold.

## Step 5: What if nobody clicks?

This is where many demos cut corners. The approver is on leave, the Slack channel is muted, and the request waits indefinitely while the customer is still waiting for their money. Cloudflare recommends using `schedule()` to escalate or auto-reject after a reasonable period.

```ts
// Sketch: remind after 4 hours, auto-reject after 24 hours
await this.schedule(4 * 3600, "escalateApproval", { approvalId });
await this.schedule(24 * 3600, "autoRejectApproval", { approvalId });
```

The 4-hour and 24-hour figures are only examples. Agree the real thresholds with the client: a small refund can wait a day, but an apology email to a VIP customer may need escalating after an hour. Once a decision is made, remember to cancel the remaining scheduled jobs so you do not auto-reject a request that has already been approved.

## Step 6: Rejection needs a fallback, and every decision must be recorded

In LangChain, a `reject` decision declines the tool call and sends feedback to the agent instead of executing it. The agent receives the reason, such as "the customer has already been sent a replacement, no refund", and can draft a different reply to the customer. Cloudflare also recommends always having a fallback ready for when an action is rejected.

An agent that is rejected and then goes silent is a product bug, not a safety feature.

Finally, the audit trail. Cloudflare suggests using `this.sql` to record every approval decision for compliance.

```ts
// Sketch of an audit table
this.sql`INSERT INTO approvals
(approval_id, action, args_json, decided_by, decision, decided_at)
VALUES (${id}, ${action}, ${JSON.stringify(args)}, ${user}, ${decision}, ${now})`;
```

**Check:** run three scenarios: approve, reject and timeout. Each must leave exactly one row in the table, with the person who decided (or "system" for an automatic rejection).

## Common mistakes

The first is letting the Slack handler do everything synchronously, which easily breaches the 3-second limit Slack sets for responding to a payload. The second is an approval message that summarises instead of listing the parameters. The third is keeping pending requests in memory, then losing all of them on the first deploy.

The fourth is harder to spot: not checking whether the person who clicked is actually authorised to approve. Compare the `user` in the payload against that tool's list of approvers before accepting the decision.

## What this skill looks like on a client site

You usually won't be able to install the app yourself. According to Slack, workspace owners and admins control which agent apps can be added. So the first meeting should include both the Slack admin and the business process owner, and the policy table from Step 1 becomes the document both sides sign off on.

If you are aiming for an FDE role, put this on your CV in concrete terms: "designed an approval gate for payment and email tools, with auto-reject timeouts and an audit trail". In job descriptions, phrases such as "human-in-the-loop", "approval workflow" or "compliance" signal that the company needs exactly this skill.

A trustworthy agent is not one that never makes mistakes, but one that knows where to stop and leaves a trace every time it does.

**Thử ngay tuần này:**

- List every tool in an agent you are working on, mark which ones handle payments, send emails or write data, and turn the result into an approval policy table.
- Build a small Slack app with Approve/Reject buttons whose handler replies immediately and processes afterwards, and measure the response time to make sure it stays under 3 seconds.
- Add an audit table that records the approver, the time, the parameters and the decision, then test the no-click scenario to see whether the timeout rejects the request automatically.

## Nguồn

- [Human in the Loop · Cloudflare Agents](https://developers.cloudflare.com/agents/concepts/human-in-the-loop/)

- [Human-in-the-loop - LangChain docs](https://docs.langchain.com/oss/python/langchain/human-in-the-loop)

- [Handling user interaction - Slack Developer Docs](https://docs.slack.dev/interactivity/handling-user-interaction)

- [Slack AI Agents](https://slack.com/ai-agents)
