Human in the loop AI fails when an Approve button is treated as the control. The button is only an input. If the system cannot prove which paused tool call it belongs to, who was authorized to decide, whether the proposed arguments changed, and whether execution happened once, an approval workflow can create more risk than it removes.
This guide gives IT and digital-workplace admins a production contract for consequential Slack agent actions. It covers durable pauses, reviewer identity, current permissions, constrained edits, expiry, replay protection, execution receipts, and failure tests. The design cuts review time without turning a Slack click into ambient authority.
Decide which actions require human approval
Do not route every agent step through a person. That recreates the manual queue and teaches reviewers to approve without reading. Classify actions by their effect and reversibility.
Start with three policy classes:
- Read automatically: search permitted knowledge, inspect a repository, fetch analytics, or summarize opened evidence when the operation does not change an external system.
- Prepare, then review: draft a knowledge update, support response, pull request, report, or customer communication. The agent may assemble evidence, but a person decides whether to continue.
- Block or require elevated approval: destructive code changes, permission changes, customer-visible sends, financial commitments, bulk edits, or actions whose destination cannot be verified.
Kipwise Agent provides the product context for this boundary. Work continues in a Slack thread, connected sources can be searched, knowledge can be written, GitHub can be accessed with safeguards, and consequential actions stay explicit. The approval policy should apply to the proposed effect, not merely to the name of the tool.
For example, reading a GitHub file can be automatic. Preparing a patch may be reviewable. Merging it or changing repository permissions needs a stricter rule. The same connector can therefore contain actions from all three classes.
Assign every rule an owner, reviewer group, maximum age, editable fields, and fallback behavior. If the policy engine cannot classify a proposed action, fail closed and show the missing classification to the admin.
Treat approval as a durable transaction
A production approval can outlive the worker that requested it. It may wait through a deployment, a reviewer handoff, or a Slack retry. In-memory state is not enough.
The OpenAI Agents SDK human-in-the-loop guide documents a useful execution model: a run pauses before a sensitive tool call, its state can be serialized, a person approves or rejects, and the original run resumes. The LangChain human-in-the-loop guide similarly uses persistent checkpointing and supports approve, edit, reject, and respond decisions.
Persist an approval record before posting the Slack card. One workable schema looks like this:
approval_id: apr_01HITL
run_id: run_01WORK
state: pending
requester:
slack_user_id: U_REQUESTER
workspace_id: T_WORKSPACE
thread:
channel_id: C_OPERATIONS
thread_ts: "1789412400.001200"
proposal:
tool: github.create_pull_request
destination: org/repository
arguments_digest: sha256:PROPOSAL_DIGEST
evidence_record_ids:
- ev_01SOURCE
policy:
risk_class: consequential-write
reviewer_group: engineering-maintainers
editable_fields:
- title
- body
expires_at: "2026-09-14T18:00:00Z"
decision:
status: pending
decided_by: null
decided_at: null
execution:
status: not_started
attempt_id: null
delivery:
status: not_attempted
slack_message_ts: nullThis is an implementation recommendation, not a vendor schema. Keep sensitive source material out of the record when identifiers and evidence links are enough. The record must survive independently of the Slack message and the process that created it.
Bind approval to a server-issued pause
Bind every approval to a server-issued paused tool call, not to action data reconstructed from a client message.
A current Pydantic AI issue about approval provenance describes the risk directly: a UI adapter may resume a run from client-supplied history without server-side proof that the approved call came from a real paused run. The author frames this as an optional provenance hardening issue within a documented trust boundary, not as a universal vulnerability.
For a Slack workflow, use this sequence:
- Create the exact proposed tool call on the server.
- Persist its run ID, tool name, normalized arguments, destination, policy version, and digest.
- Issue a random, single-purpose approval ID that refers to that record.
- Put only that opaque approval ID in the Slack interaction value.
- On interaction, load the server record and compare its stored digest.
- Reject the decision if the record is missing, expired, already consumed, or no longer matches the paused run.
Do not trust the Slack message body as the source of truth. People can edit messages, clients can replay payloads, and formatting can omit fields. The message explains the request. The server record defines it.
This point is easy to miss because most examples begin with a valid in-process tool call. Production adapters add a second boundary between the paused run and the reviewer interface. Admins should test that boundary explicitly.
Show enough context for a real decision
An approval card should let the reviewer decide without opening five systems, but it should not copy every sensitive input into Slack.
Show:
- the requesting person and originating thread;
- the proposed action and exact destination;
- a concise summary of arguments that will affect the result;
- links to permitted source evidence;
- the reason approval is required;
- fields the reviewer may edit;
- the expiry time and policy owner;
- Approve, Reject, and Request changes actions.
The Slack interactivity guide documents interaction payloads, response URLs, acknowledgements, and ways to publish responses back to Slack. Use those transport features to acknowledge the click quickly, then update the original thread after the server has validated and recorded the decision.
A button click should first move the record from pending to approved or rejected. It should not make the external write inside the acknowledgement handler. A separate worker can claim the approved record, recheck authorization, and perform the tool call. This separation keeps Slack retries from becoming action retries.
Allow edits only where the policy names them. If a support lead may edit response text but not the customer or channel, enforce that server-side. Recalculate the proposal digest after an allowed edit and preserve both versions. A change to the destination, repository, access scope, or action type should usually create a new approval.
Recheck authorization before execution
Recheck current authorization immediately before execution because an approval records intent, not permanent permission.
The requesting user or reviewer may lose access while a decision is waiting. A channel can become private, a repository role can change, a support ticket can move to another account, or the underlying object can be deleted. An approval made under yesterday's permissions must not bypass today's controls.
At execution time, verify:
- the requesting Slack identity still maps to the expected product identity;
- the reviewer still belongs to the required approval group;
- both identities belong to the expected workspace and tenant;
- the requester still has permission for the proposed action where applicable;
- the destination still exists and matches the stored identifier;
- the proposal has not expired or changed;
- the policy version still permits the action.
This is a first-principles authorization recommendation. It lets admins define approval as a time-bounded decision instead of granting a reusable capability. If a check fails, keep the proposal unexecuted and post a specific reason to the original thread.
Do not silently ask a different identity, such as a broad service account, to complete an action that the requester can no longer perform. If the product intentionally uses delegated service authority, show that fact in the approval card and policy record.
Consume each decision once and separate the outcomes
Track approval, execution, and delivery as separate states, and consume each approval decision only once.
A LangGraph production human-in-the-loop proposal reports practical gaps including in-memory approval state, missing Slack or email bridges, duplicate resume calls, audit trails, and multi-user routing. It is a practitioner proposal, not a guarantee about every deployment. Operations teams still need to test the reported failure: the same decision can arrive more than once.
Use an atomic state transition when a worker claims an approved action:
if approval.state != "approved":
stop("approval is not executable")
changed = compare_and_set(
approval_id,
from_state="approved",
to_state="executing",
attempt_id=new_attempt_id
)
if not changed:
stop("approval was already consumed")
recheck_authorization()
execute_bound_tool_call()
record_execution_result()
publish_thread_receipt()
record_delivery_result()The compare-and-set operation ensures that two workers cannot consume the same approval. The external tool should also receive an idempotency key where supported. If the worker crashes after sending the request but before recording the result, reconcile by that key or by querying the destination. Do not start another write merely because the local status is uncertain.
Keep these states distinct:
approval: pending | approved | rejected | expired | cancelled
execution: not_started | executing | succeeded | failed | unknown
receipt: not_attempted | posting | delivered | failed | unknownAn approved action has not necessarily executed. A successful execution has not necessarily produced a visible Slack receipt. This distinction gives admins an honest queue of uncertain outcomes instead of one green status that hides a failed write or failed notification.
Work through a code-change example
An engineer asks the agent in Slack to update a configuration file and open a pull request. The agent reads the permitted repository and relevant internal runbook, prepares a patch, and pauses before the GitHub write.
The approval card names the repository, base branch, files changed, patch digest, pull-request title, source links, requester, expiry, and required reviewer group. A maintainer approves in the same thread.
Before execution, the worker reloads the server-issued proposal. It confirms that the approval ID belongs to the paused run, the digest still matches, the maintainer remains authorized, the requester can still access the repository, and the base branch is current. It atomically changes the approval state to executing, then opens one pull request with the stored idempotency key.
If GitHub confirms the pull request but the Slack update times out, execution is succeeded and receipt delivery is unknown. The worker checks the thread before reposting. It does not open another pull request. The final receipt links to the pull request, records who approved it, and states that the proposed patch digest was the one executed.
If the base branch or destination changed, the old approval expires. The agent recalculates the patch and asks for a new decision. It does not treat approval of the earlier proposal as consent for a materially different action.
Handle rejection, expiry, and recovery
Rejection should be a terminal decision with an optional reason. Request changes should create a revised proposal and a new digest. Cancellation by the requester should invalidate any pending card.
Apply these recovery rules:
- If Slack sends the same interaction twice, return the recorded decision without consuming it again.
- If the worker restarts, reload pending and executing records from durable storage.
- If execution times out, mark it
unknownand reconcile the destination before retrying. - If authorization changes, leave the action unexecuted and explain the failed check.
- If the proposal expires, disable or replace the interactive controls and state that a new proposal is required.
- If receipt delivery fails, retry only the receipt, not the consequential action.
- If evidence becomes unavailable, stop and ask the requester to refresh the proposal.
Log policy decisions and state transitions, but redact source bodies, credentials, customer data, and unrestricted tool arguments. An audit trail should prove what was proposed and decided without becoming another sensitive data store.
Verify the human approval workflow
Run the following tests in a non-production workspace before enabling consequential writes:
- Approve a real paused call and confirm the stored run, proposal digest, and interaction ID match.
- Forge or alter the client-visible action data and confirm the server ignores it.
- Replay an approval payload and confirm execution happens once.
- Deliver two simultaneous approvals and confirm only one state transition wins.
- Restart the worker while approval is pending and confirm the run can resume.
- Remove the reviewer's role after approval and confirm execution is blocked.
- Remove the requester's destination permission and confirm service authority does not bypass policy.
- Edit an allowed field and confirm the revised digest and prior value are preserved.
- Change a protected field and confirm a new approval is required.
- Let the record expire and confirm the old button cannot resume the run.
- Time out the external write and confirm reconciliation precedes retry.
- Fail the Slack receipt and confirm the external action is not repeated.
- Confirm the final thread message distinguishes approved, executed, and delivered.
- Inspect logs and confirm they contain useful identifiers without sensitive payloads.
Track median approval latency, expired proposals, rejected actions, changed proposals, replay attempts blocked, permission changes caught, unknown execution outcomes, duplicate actions, and missing receipts. These measures expose control failures and review friction without treating approval volume as success.
Choose one reversible Slack agent write, define its server-side proposal record and state machine, and run the replay, permission-change, and uncertain-delivery tests before expanding the policy to customer-visible or destructive actions.
References
- Kipwise Agent supports the thread-native, permission-aware, source-citing product workflow and explicit action boundary used in this guide.
- OpenAI Agents SDK human-in-the-loop supports pausing sensitive tool calls, persisting run state, recording approval decisions, and resuming the run.
- LangChain human-in-the-loop supports persistent checkpointing and approve, edit, reject, or respond decisions around tool calls.
- Slack handling user interaction supports the interaction payload, acknowledgement, response, and thread-message transport used for the reviewer interface.
- Pydantic AI approval-provenance issue provides the author-reported risk of resuming from client-supplied approval state without proof of a server-issued pause.
- LangGraph production HITL patterns issue provides practitioner-reported demand for durable state, Slack or email bridges, idempotency, audit trails, and multi-user routing.


