FDE PulseFDE jobs open 434New in the last 7 days 27
VI

The newspaper of the Forward Deployed Engineer

Guides

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.

In brief

  • Only ask for approval on actions with real consequences: payments, emails and data changes.
  • The approval gate must be durable: persist the pending state, set a timeout and record every decision.
  • With Slack, acknowledge the button click within 3 seconds and do the heavy work afterwards.
ShareLinkedInFacebookX

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:

# 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.

// 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.

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.

// 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.

// 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.

// 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.

4 sources
Read next on the roadmap · Stage 5: DeploymentWhen an agent must stop: four guardrails and the moment to hand over to a humanAn agent with no stop condition can run forever and burn money without throwing a single error. The person left explaining that to the client is the FDE.