Skip to main content

What breaks when a Slack approval modal's trigger_id expires before approval?

Slack's trigger_id is valid for only three seconds after the interaction that generated it, and can be used once. If an AP approval workflow does anything asynchronous — a permissions check, a lookup — between the button click and calling views.open, the trigger_id can expire, returning an expired_trigger_id error instead of opening the modal. The click appears to do nothing.

Zuny FesterBy Zuny Fester, Head of Operations and Marketing
Reviewed by Zuny Fester
Published Last reviewed Editorial policy

Part of the finance integrations guide.

SymptomA user clicks an AP approval button in Slack and nothing visibly happens
Root causeThe trigger_id from the interaction expired before views.open was called
What Slack's own docs say"Triggers expire in three seconds"
Also trueA trigger_id can only be used once — a second attempt returns exchanged_trigger_id, not expired_trigger_id
Where this shows upAn expired_trigger_id or exchanged_trigger_id error in the response from views.open

Why Slack enforces such a short window

Loopfour's Slack integration is the only one of the three communication channels in this matrix (Slack, Outlook, Gmail) with an interactive modal action — openModal and pushModal both require a trigger_id, which Slack issues on every user interaction (a button click, a shortcut invocation) and treats as extremely short-lived by design. Slack's own documentation states plainly that triggers expire in three seconds, and that a trigger_id can be used for only one operation — a second attempt with the same trigger_id returns an exchanged_trigger_id error rather than working again. Both limits exist to keep interactivity synchronous and predictable: Slack wants the modal opened as close as possible to the moment the user clicked something, not after an arbitrary delay.

Where the three seconds actually go

An AP approval workflow rarely opens a modal with zero logic in between the click and the views.open call. A realistic sequence — verify the clicking user is actually an authorized approver, look up the invoice's current status in case it changed since the message was posted, build the modal's view payload with that data — can easily add up to more than three seconds, especially if any of those steps involves a network call to a database or another API. Every one of those steps happens after Slack has already started the clock on the trigger_id.

How to design around the window

Call views.open with the trigger_id as the very first action after receiving the interaction payload, before any lookup or validation — open a minimal or loading-state modal immediately, then use views.update (which isn't trigger_id-gated) to populate it once the slower checks finish. If a check needs to reject the interaction entirely (the user isn't an authorized approver), that's a case for opening an error-state modal immediately and updating it, rather than doing the check before opening anything. Because a trigger_id is single-use, retry logic also can't simply call views.open again after an expired_trigger_id error — the only fix is to wait for the next user interaction and get a fresh trigger_id.

Next step

Map the finance workflow with the most exposure and prove the automation path.

Bring the invoice, contract, payment reconciliation, or customer finance workflow you have to defend at audit. Loopfour can map the trigger, controls, integrations, and approval loop.

Book a workflow review

Worked example

A 4-second approval-permission check that breaks the modal

An approver clicks 'Review invoice' on a Slack message. The workflow receives the interaction payload with trigger_id=12345.6789 at T+0ms, then calls an internal permissions API to confirm the clicking user is an authorized AP approver for that vendor's spend threshold — a call that, under normal load, takes 400ms but occasionally takes 3.5 seconds under load. On a slow run, by the time the permissions check returns and the workflow calls views.open with trigger_id=12345.6789, Slack has already expired it and returns expired_trigger_id. The approver sees the button click do nothing and, with no error surfaced to them, often clicks it again — which either repeats the same race or, if the first request eventually did complete server-side, returns exchanged_trigger_id on the second attempt instead. The fix: open a placeholder modal immediately using the trigger_id, run the permissions check after, and use views.update to either populate the real approval form or replace it with an access-denied message.

Frequently Asked Questions

No — a trigger_id is only issued by Slack as part of a genuine user interaction. There's no way to mint a replacement; the workflow has to wait for the user to interact again (click the button again, for example).

No. views.update operates on an already-open view using a view_id or external_id, not a trigger_id, so it isn't subject to the three-second window — which is exactly why opening an immediate placeholder and updating it afterward is the standard workaround.

It can be, for a single fast, low-latency lookup — but any check that depends on an external API, a cold cache, or a network hop under variable load is a real risk. Treat three seconds as a hard ceiling to design under, not a target to optimize toward.

Sources

Related