Receive Slack webhooks in Flyte: Events API callbacks, interactivity payloads (Block Kit actions, shortcuts, modals), and slash commands — one route serves all three.
pip install "flyteplugins-slack[app]"Hand a SlackProvider() to a WebhookAppEnvironment and register handlers with the
typed constants in events:
import flyte
from flyte.extras.webhooks import WebhookAppEnvironment, run_once
from flyteplugins.slack import SlackProvider, events
# SlackProvider.default_secret_env is mounted for you.
app_env = WebhookAppEnvironment(name="slack-webhooks", providers=[SlackProvider()])
@app_env.on_event(events.AppMention.ANY)
async def handle(event):
import flyte.remote as remote
task = remote.Task.get(name="my-env.my_task", auto_version="latest")
result = await run_once.aio(task, key=event.dedupe_key(), resource=event.resource_id)
if not result.created:
return {"skipped": result.run.name, "url": result.run.url}
return {"run": result.run.name}
flyte.serve(app_env)Handlers must await run_once.aio(...). The blocking form stalls the
app's event loop, and Slack times deliveries out in seconds.
One app can serve several products at once — hand it one provider per product.
Two examples, each runnable two ways — --local needs no Slack account:
python examples/slack_webhooks.py --local # Events API: replay a real sample delivery
python examples/slack_interactions.py --local # buttons + slash commands, signed and replayed
python examples/slack_webhooks.py # deploy the receiver to Flyte--local posts signed deliveries through the app with FastAPI's test client,
so you see each one verified, normalized, and dispatched — plus an unsigned one
refused with a 401. slack_webhooks.py covers the Events API and stable dedupe
keys; slack_interactions.py covers a Block Kit button (block_actions.<action_id>),
a slash command (command.<name>), and the ssl_check probe.
- Store the secret and mount it on the app:
flyte create secret SLACK_SIGNING_SECRET --value <secret>
- Point Slack at
<app-url>/webhook/slack— the same URL in every place your app uses, at api.slack.com/apps:- Event Subscriptions → Request URL, then subscribe to bot events;
- Interactivity & Shortcuts → Request URL, for Block Kit buttons, shortcuts, and modals;
- Slash Commands → each command's Request URL.
Slack POSTs a url_verification challenge before events flow and an ssl_check
probe to interactivity and slash-command URLs; both are answered automatically,
so the Request URL fields verify themselves.
Verification: HMAC-SHA256 over v0:{timestamp}:{body}, with a five-minute replay window (X-Slack-Signature). The same scheme signs all three delivery shapes.
Messages are keyed per message, so each one launches its own run. To collapse a whole thread onto one run, pass event.payload["event"]["thread_ts"] as your own key.
An interaction's action is its action_id (or callback_id), and a slash
command's is its name, so one button or one command registers as a raw string:
@app_env.on_event("block_actions.approve_reply")
async def approve(event):
# event.payload is Slack's full JSON: actions, container, message, response_url.
channel, ts = event.payload["container"]["channel_id"], event.payload["container"]["message_ts"]
...
@app_env.on_event("command.deploy") # /deploy
async def deploy(event):
text = event.payload["text"]
...events.Interaction.BLOCK_ACTIONS and events.Command.ANY match whole
categories. Slack shows the user an error unless the delivery is answered
within 3 seconds, so handlers for these must do nothing slower than
run_once.aio — post progress back via slack_sdk from the launched task.
events spells every event this plugin can dispatch, as str enums grouped by
event type, so a typo fails at import rather than by silently never matching.
Raw strings still work, for events the constants do not cover yet.
Call the Slack API. Use slack_sdk directly from your tasks — see
examples/external_saas_integrations. This plugin owns only the part that is
Flyte's: authenticating an inbound delivery and turning it into a run.