Skip to content
🎉 Launch sale:50% off everything over $22 — automatically applied at checkout· ends Aug 2Shop the sale →
DevOps AI ToolKit
Newsletter
All guides
AI for Slack By James Joyner IV · · 9 min read Last reviewed Jul 2026

Slack Error Guide: 'expired_trigger_id' — Open Modals in Time

Quick answer

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
Free toolkit

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.open returns expired_trigger_id after the handler did work first.
  • trigger_exchanged when 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.open as 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 uses view_id, not the trigger).
  • Never reuse a trigger_id — it is single-use; a second open yields trigger_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:

  1. Doing slow work before calling views.open.
  2. Serverless cold starts eating the ~3-second budget.
  3. Reusing a single-use trigger.
  4. Blocking the interaction ack behind slow logic.
  5. 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.

Free download · 368-page PDF

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?

Free download · 368-page PDF

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.