Why your automation is stuck waiting for approval
An automation stalls at an approval step because three separate clocks run on every approval: the workflow's wait timeout, the lifetime of the callback behind the button you sent, and the reviewer's own response time. Setting only the wait timeout leaves you with an approve button that still looks live after the run behind it is gone, so the durable fix is to store the approval as a decision record the workflow reads, not as a pause it holds open.
An automation stuck waiting for approval is almost never a slow reviewer. It is a mismatch between three separate clocks: the wait timeout you set in the workflow, the lifetime of the callback behind the button you sent, and how long the human actually takes. Nearly every guide on this topic sets the first clock and ignores the other two. That leaves the failure operators keep hitting, which is an approve button that still looks live long after the run behind it is gone. The durable fix is to stop treating an approval as a pause inside a run and start treating it as a record your workflow reads.
Three clocks run on every approval, and most guides only set one
The first clock is the wait itself. n8n's Wait node has a Limit Wait Time option that resumes the execution automatically after an interval or at a wall-clock time. Zapier's Human in the Loop step asks you for a timeout value where the minimum is 1 and the unit can be minutes, hours, days, or weeks. This clock is easy to find and easy to set, which is why it gets all the attention.
The second clock belongs to whatever you used to ask the question. A Slack button, a modal, an emailed link, and a hosted form each have their own rules about how long the thing you handed the reviewer stays connected to the run that is waiting. Almost nobody sets this clock, because most people do not know it exists.
The third clock is the reviewer. Two hours on a Tuesday, four days over a holiday week, never if they changed roles in March.
Clock three beating clock one gives you a stuck run. Annoying, visible, fixable. Clock two beating clock three gives you something worse: a run that ends one way while the reviewer believes it ended the other way. Stuck is a nuisance. Silently wrong is an incident.
How long does the approve button actually keep working?
This table is the part missing from the platform documentation, because each row lives in a different vendor's docs and nobody puts them side by side.
| Approval mechanism | The real lifetime of the control | What breaks past it |
|---|---|---|
| Slack message button, using the response URL your app captured | Slack documents the response URL as usable up to 5 times within 30 minutes of the payload | Your run can no longer update or retract its own message, so a decided or expired request keeps sitting in the channel looking open |
| Slack modal opened from a button | The trigger ID expires in three seconds and can be used once, and the interaction itself must be acknowledged within three seconds | Do any work first, such as summarizing the item for the reviewer, and the dialog never opens at all. The reviewer clicks and nothing happens |
| Emailed link that approves on load | Effectively forever, in the original message and in every forwarded copy | Anyone holding the URL can approve, including automated systems that fetch links before a person reads the mail |
| n8n Wait node set to resume on webhook call | Until the execution resumes or Limit Wait Time fires | A wait under 65 seconds is not offloaded to the database, per n8n's own docs, so it is the least durable version of the pattern |
| Zapier Human in the Loop | Until the timeout you configured | Only one user can complete a review, and on the Professional plan you can only send requests to yourself, so there is no second pair of eyes to fall back on |
| Make scenario holding a pause | No durable pause exists, and a single scenario run has an outer wall clock of roughly 40 minutes | An approval that takes an afternoon cannot live inside one run, so it has to be split into two scenarios with a data store between them |
Two rows deserve attention. The Slack modal row produces the most confused bug reports, because the symptom is a button that does nothing at all and the cause is three seconds of work you added before opening the dialog. And the Slack button row is not saying the button stops working. A click always reaches your app. What dies is your ability to change the message you already sent, which is why a well-built approval stores the channel ID and the message timestamp at send time and edits that message later with a bot token, instead of depending on the response URL it captured.
Why an emailed approve link gets clicked before your reviewer sees it
Put a plain link in an email that approves when it loads and you have built an approval that machines can grant. This is not theoretical. Microsoft's Safe Links documentation for Defender for Office 365 states that URLs are scanned prior to message delivery, and that URLs without a valid reputation are detonated asynchronously in the background. The mail security layer follows your link. So do link preview fetchers and some mobile clients.
The email world already solved this and wrote the reasoning down. RFC 8058, the specification behind one-click unsubscribe, uses an HTTPS POST rather than a GET, and says plainly that anti-spam software often fetches all resources in mail header fields automatically, without any action by the user. It also notes that senders had been forced to add a confirmation landing page because a live user would recognize and act on that step and an automated system would not. Your approval link has the same problem and deserves the same treatment.
Four rules make an emailed approval safe. The link opens a page, and the decision is submitted by a deliberate action on that page, never by the page loading. The token in the link is single-use and tied to one request ID. The token expires, and its expiry is shorter than the workflow's timeout rather than longer. And the page identifies who is deciding, because a forwarded email otherwise turns a controlled approval into a bearer credential that anyone in the thread can spend.
What should happen when the timer runs out?
Zapier gives you two choices on timeout: skip and continue, or end the run. Most platforms offer some version of the same pair. Both are wrong for the case that actually needed a gate, and the reason is that neither one is failing closed.
Skip and continue is failing open. It is the right default only for internal, reversible writes where the review was a nicety, such as tagging a record or updating an internal note. Set it on an outbound customer message and you have built a system whose default behavior is to send unreviewed AI output whenever someone is on vacation.
Ending the run is not failing closed either. It is failing forgotten. The request disappears, nobody is told, and the item that needed a decision is now in no queue at all. Failing closed means the action does not happen and the item is still somewhere a human will find it.
So the exit you actually need is the third one, which no platform gives you: escalate. On expiry, notify a named second approver, re-arm the timer, and record that the escalation happened. If the second timer expires, park the item in a visible queue and alert an owner. Match the policy to the action class. Irreversible and external, meaning money moving, a message reaching a customer, a filing, or a deletion, escalates and never auto-proceeds. Reversible and external parks and stays retrievable. Internal and reversible can proceed with a log entry. If you have not yet sorted your actions into those classes, reversibility and blast radius is the grid that does it.
Make the approval a record, not a pause
Here is the change that makes the rest of these problems stop mattering. When the workflow reaches the gate, it writes a row: request ID, the action being approved, a hash of the payload, requested_at, expires_at, the assigned reviewer, and empty fields for decided_by, decided_at, and outcome. That row is the approval. The Slack message, the email, and the form link are all just pointers to it. The workflow resumes by reading the row, either on a resume webhook that looks it up or on a short polling loop.
Five things get better at once. A double click is harmless, because the second write finds a row that already carries an outcome. A platform restart during the wait cannot lose the decision, since the decision does not live inside a suspended execution. You can move the notification from Slack to email without rebuilding the gate. You can add a second reviewer, which Zapier's built-in step will not give you. And you get an audit trail with a real name and a timestamp on it, which is what you will want the first time somebody asks who approved a refund, and which follows the same discipline as what to log in every automation.
The payload hash earns one extra sentence. Store it, then compare it before acting on the approval. If the underlying record changed between the request and the decision, the approval no longer covers the action, and the right behavior is to ask again rather than execute a decision that was made about different facts.
What to do next
Pull four numbers on your existing approval steps this week. The age of the oldest pending request, which tells you immediately whether your timeout fires at all. The share of approvals resolved by the timer rather than by a person, where anything above one in five means the gate is decorative. The median time to decision, which is what the timeout should be set against, at roughly three times its value. And the approval rate: if 99 percent of requests get approved, you are paying somebody to click yes, and that work belongs in sampled review after the fact instead of a gate in front of every run.
Then fix one gate properly. Pick the approval with the highest consequence, give it a decision record, set its timeout from the median, and give it an escalation path with a named second approver. That one gate becomes the template you copy across the rest. If you would rather have the whole set reviewed at once, that is where our workflow automation work usually starts, and you can tell us which approvals keep stalling.
Frequently Asked Questions
SOURCES & CITATIONS
- Handling user interaction in your Slack apps — Slackhttps://docs.slack.dev/interactivity/handling-user-interaction/
- Wait node documentation — n8nhttps://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.wait/
- Request approval to keep your workflow running with Human in the Loop — Zapierhttps://help.zapier.com/hc/en-us/articles/38731463206029-Request-approval-to-keep-your-workflow-running-with-Human-in-the-Loop
- RFC 8058: Signaling One-Click Functionality for List Email Headers — IETFhttps://www.rfc-editor.org/rfc/rfc8058.html
About Alexey Yushkin
Alexey is the founder of GENERAL INFORMATICS LLC. He designs and ships AI and automation systems for businesses and operators across the US.
Related reading
Want this kind of system in your business?
We build practical AI and automation systems for operators. Send us your current workflow and we will show you what to automate first.
