Slack Error Guide: 'expired_trigger_id' — Open Modals in Time
Fix the Slack expired_trigger_id error: call views.open within 3 seconds, open a skeleton modal first, then populate it with views.update off the trigger path.
- #slack
- #api
- #troubleshooting
- #errors
Stuck on this Slack error? Get the free incident triage checklist
A one-page PDF — the exact steps to isolate, fix, and verify a production error like this one. No spam, unsubscribe anytime.
Overview
The expired_trigger_id error (and its sibling trigger_exchanged) is returned by views.open and views.push when the trigger_id you supplied is no longer valid. A trigger_id is a short-lived, single-use token Slack issues with every interactive payload (slash command, button click, shortcut, message action). You have roughly 3 seconds to exchange it for an open modal, and each trigger can be used only once. Do any slow work — a database read, an external API call, a cold serverless start — before calling views.open, and the trigger expires. Reuse it for a second modal, and you get trigger_exchanged.
Slack returns HTTP 200 with ok:false:
{
"ok": false,
"error": "expired_trigger_id"
}
It occurs when views.open runs too late after the interaction, or with an already-used trigger.
Symptoms
- Modals open reliably in dev but fail under load or on cold starts.
views.openreturnsexpired_trigger_idafter the handler did work first.trigger_exchangedwhen a single handler tries to open two modals from one trigger.- Intermittent “nothing happens when I click” reports from users.
- Failures correlate with slow downstream calls inside the interaction handler.
Common Root Causes
1. Doing work before opening the modal
Fetching data, calling an API, or running business logic before views.open burns the ~3-second budget and the trigger expires.
2. Serverless cold starts
A Lambda/Cloud Function cold start can consume most of the budget before your code even runs.
3. Reusing a trigger_id
Each trigger_id is single-use. Opening a second modal (or retrying with the same trigger) yields trigger_exchanged.
4. Not acknowledging the interaction fast enough
If the framework blocks the HTTP ack behind slow work, the trigger goes stale.
5. Passing a stale trigger from a queue
Enqueuing the interaction and processing it later means the trigger is long expired by the time views.open runs.
Diagnostic Workflow
Step 1: Capture the trigger_id from the interaction payload
Interactive requests arrive form-encoded with a payload field; the trigger is inside it:
# Example decoded interaction payload
echo "$PAYLOAD" | jq '{type, trigger_id}'
{
"type": "shortcut",
"trigger_id": "13345224609.8534564800.6f8ab1f53e13d0cd15f96a0d9a2b1927"
}
Step 2: Reproduce by opening late
# Simulate slow work before opening
sleep 4
curl -s -X POST https://slack.com/api/views.open \
-H "Authorization: Bearer $SLACK_BOT_TOKEN" -H "Content-Type: application/json" \
-d "{\"trigger_id\":\"$TRIGGER_ID\",\"view\":{\"type\":\"modal\",\"title\":{\"type\":\"plain_text\",\"text\":\"Deploy\"},\"blocks\":[]}}" \
| jq .error
"expired_trigger_id"
Step 3: Open FIRST, then do the work
# Open a lightweight modal immediately, within the budget
curl -s -X POST https://slack.com/api/views.open \
-H "Authorization: Bearer $SLACK_BOT_TOKEN" -H "Content-Type: application/json" \
-d "{\"trigger_id\":\"$TRIGGER_ID\",\"view\":{\"type\":\"modal\",\"callback_id\":\"deploy\",\"title\":{\"type\":\"plain_text\",\"text\":\"Deploy\"},\"blocks\":[{\"type\":\"section\",\"text\":{\"type\":\"mrkdwn\",\"text\":\"Loading…\"}}]}}" \
| jq '{ok, view_id: .view.id}'
{
"ok": true,
"view_id": "V08MODAL001"
}
Step 4: Fill the modal asynchronously with views.update
Once the modal is open you have the view.id and no longer need the trigger:
curl -s -X POST https://slack.com/api/views.update \
-H "Authorization: Bearer $SLACK_BOT_TOKEN" -H "Content-Type: application/json" \
-d '{"view_id":"V08MODAL001","view":{"type":"modal","callback_id":"deploy","title":{"type":"plain_text","text":"Deploy"},"blocks":[{"type":"section","text":{"type":"mrkdwn","text":"Ready — pick a service"}}]}}' \
| jq .ok
true
Example Root Cause Analysis
A ChatOps app opens a “Run deploy” modal from a global shortcut. The handler first queries the deploy service for the list of environments, then opens the modal. Under load the query takes ~2.5s and the open fails:
curl -s -X POST https://slack.com/api/views.open \
-H "Authorization: Bearer $SLACK_BOT_TOKEN" -H "Content-Type: application/json" \
-d "{\"trigger_id\":\"$TRIGGER_ID\",\"view\":$FULL_VIEW}" | jq .error
"expired_trigger_id"
The environment query consumes most of the 3-second budget before views.open is even called, so the trigger has expired. The fix is to open a skeleton modal immediately using the trigger, then load environments and views.update the modal in place:
# 1) Immediate skeleton (uses trigger_id)
VIEW_ID=$(curl -s -X POST https://slack.com/api/views.open \
-H "Authorization: Bearer $SLACK_BOT_TOKEN" -H "Content-Type: application/json" \
-d "{\"trigger_id\":\"$TRIGGER_ID\",\"view\":$SKELETON_VIEW}" | jq -r '.view.id')
# 2) Slow query happens here, then update (uses view_id, no trigger needed)
curl -s -X POST https://slack.com/api/views.update \
-H "Authorization: Bearer $SLACK_BOT_TOKEN" -H "Content-Type: application/json" \
-d "{\"view_id\":\"$VIEW_ID\",\"view\":$POPULATED_VIEW}" | jq .ok
true
Opening first and populating with views.update removes the trigger from the critical path.
Prevention Best Practices
- Call
views.openas the very first thing in the handler; do zero blocking work before it. - Open a lightweight skeleton/loading modal, then fill it asynchronously with
views.update(which usesview_id, not the trigger). - Never reuse a
trigger_id— it is single-use; a second open yieldstrigger_exchanged. - Eliminate cold starts on interaction handlers (provisioned concurrency, warm pools) so the budget isn’t eaten before your code runs.
- Never enqueue an interaction and open the modal later; the trigger will be long expired.
- For ad-hoc triage, the free incident assistant can flag slow work sitting in front of
views.open. See more in Slack guides.
Quick Command Reference
# Open a skeleton modal immediately (uses trigger_id)
curl -s -X POST https://slack.com/api/views.open \
-H "Authorization: Bearer $SLACK_BOT_TOKEN" -H "Content-Type: application/json" \
-d '{"trigger_id":"TRIGGER","view":{"type":"modal","title":{"type":"plain_text","text":"…"},"blocks":[]}}' \
| jq '.view.id'
# Populate later without a trigger (uses view_id)
curl -s -X POST https://slack.com/api/views.update \
-H "Authorization: Bearer $SLACK_BOT_TOKEN" -H "Content-Type: application/json" \
-d '{"view_id":"VIEW_ID","view":{"type":"modal","title":{"type":"plain_text","text":"Ready"},"blocks":[]}}' | jq .ok
Conclusion
expired_trigger_id / trigger_exchanged means the trigger_id was used too late or more than once. The usual root causes:
- Doing slow work before calling
views.open. - Serverless cold starts eating the ~3-second budget.
- Reusing a single-use trigger.
- Blocking the interaction ack behind slow logic.
- Processing the interaction from a queue after the trigger expired.
Open a skeleton modal immediately with the fresh trigger, then populate it asynchronously with views.update — keep everything slow off the trigger’s critical path.
Fixed it? Get 500 Slack & DevOps AI prompts — free
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.
Did this fix your issue?
Get 500 Battle-Tested DevOps AI Prompts — Free
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.