Skip to main content
Private beta. This feature is only available to some organizations right now.Request access and we’ll turn webhooks on for your organization and send you the prerelease packages. Webhooks are open source, so they work self-hosted too. APIs may change before general availability.
An agent on a channel can pause a turn to get a human decision, then resume once someone answers in the thread. It posts controls into the conversation and picks up where it left off once someone clicks, with the decision merged into the turn. This is the channel counterpart of browser human-in-the-loop: the same no-execute tool, but the controls live in Slack (or your own surface) instead of a React component. The slack() connector ships Approve and Deny buttons out of the box.

How it works

The building block is a tool with no execute function. When the model calls it, the turn completes with the tool call still pending instead of resolving it. Over a channel the framework then takes over the round-trip: Because it is a no-execute pause, the run suspends while it waits rather than holding compute. A human can take minutes or days to decide without burning compute or hitting maxDuration. The mechanics are the same as the browser case, covered in human-in-the-loop.

Slack approvals

Out of the box, slack() posts Approve and Deny buttons for any pending no-execute tool. A click resolves to { approved: boolean } and the buttons collapse to the decision. You define the tool and list the channel on the agent:
trigger/support-agent.ts
Button clicks arrive through Slack’s interactivity, so the app needs Interactivity & Shortcuts turned on with the Request URL set to the same endpoint URL as events. The Slack agents manifest already does this. When the model calls requestApproval, the bot posts “Approval needed” with the action and Approve and Deny buttons, and the session’s status is suspended. Clicking Approve resolves the tool to { approved: true }, the buttons collapse to “Approved by @you”, and the agent continues and streams the outcome. Deny resolves { approved: false }. Pressing Slack’s stop button instead cancels the approval: the buttons are replaced with “Stopped” and a later click resumes nothing.

Customizing the controls

For chat.channels.custom, four hooks own the interaction round-trip:
(pending, ctx) => ChannelMessage | null
Map the pending tool call(s) to the controls you post. pending is a list of { toolCallId, toolName, input }, and ctx carries the event and deliveryId that started the turn (both absent on a bound turn, such as a chat.turn() action answering into the thread). Return null to skip posting controls.
(event) => { toolCallId, output } | null
Map a verified callback event to a resolution. The output is stitched onto the pending tool call matched by toolCallId and the run resumes. Return null to treat the event as a normal message instead (a new turn).
(event, resolution) => void
Collapse the controls after a decision so they cannot be clicked again. Best effort: a failure here is logged and the run still resumes.
(cancelled, ctx) => void
Take the controls down when a stop (onControl returning { kind: "stop", whenIdle: "drop" }) arrives while the session waits on them. Each cancelled call carries the ref its controls were posted with. The calls are already resolved as stopped. Best effort: a failure is logged.
Slack encodes the tool call id and the click’s output in each button’s value so onInteraction can resolve the exact call (see Approvals and buttons to customize the buttons or give a tool its own UI), and finalizeInteraction posts to the interaction’s response_url with replace_original to swap the buttons for the outcome. On a custom surface you choose the encoding and how you edit the controls away.
The resolved output is whatever your tool expects to receive. The Slack default resolves { approved: boolean }; if your tool needs a richer answer, supply your own onInteraction that returns the shape your tool reads.

The reply after a decision

When the turn resumes, the channel reply shows the agent’s answer, not the text it produced before pausing. The framework posts the assistant text generated after the tool call, so a preamble like “I’ll need approval first” is not concatenated onto the final “done” message. The approval prompt itself already lives in the collapsed controls.

Next steps

Channels

Point Slack or any surface at an agent so messages become turns.

Browser human-in-the-loop

The same no-execute pause, rendered in a React frontend.

Sessions

The durable per-conversation state a channel turn runs on.

Filters

Gate which verified deliveries reach the agent.