Slack
Receive Slack events, interactions, and slash commands in Flyte and turn them into runs. From tasks, the plugin can post and update messages, and pause a run until someone clicks an approval button.
Installation
pip install "flyteplugins-slack[app]"Requires Python 3.10 or later. The app extra adds fastapi and uvicorn for serving the receiver. The notify and approval modules need no extra.
Credentials
The plugin uses two Slack credentials. They aren’t interchangeable.
| Credential | Where to find it | Used by | Environment variable |
|---|---|---|---|
| Signing secret | Basic Information | The receiver, to verify deliveries | SLACK_SIGNING_SECRET |
Bot token (xoxb-…) |
OAuth & Permissions | notify and approval, to call the Web API |
SLACK_BOT_TOKEN |
The app mounts the signing secret automatically. Mount the bot token on the task environment. Posting requires the chat:write scope, and the bot must be invited to the channel with /invite.
The receiver
import flyte
from flyte.extras.webhooks import WebhookAppEnvironment, WebhookEvent, run_once
from flyteplugins.slack import SlackProvider, approval, events, notify
# SLACK_SIGNING_SECRET is mounted automatically. It's the signing secret from
# Basic Information, not the bot token, which goes on the task environment.
#
# `scopes` lists the channel IDs to act on: where the bot is mentioned, where
# /deploy is used, and where approvals are posted. Events from other channels
# are acknowledged but not dispatched.
app_env = WebhookAppEnvironment(
name="slack-webhooks",
providers=[SlackProvider()],
scopes=["C0DEPLOYS"],
image=flyte.Image.from_debian_base().with_pip_packages("flyteplugins-slack[app]"),
resources=flyte.Resources(cpu=1, memory="512Mi"),
)
# Adds a handler that resolves the condition behind each button posted by
# `approval.request`. Each button's value carries the run, action, and
# condition names, so no other configuration is needed.
approval.register(app_env)
approval.register(app_env) adds the handler that receives approval button clicks. See Approvals.
Delivery types
Slack sends three kinds of delivery to /webhook/slack. All three are signed the same way, and on_event tells them apart:
@app_env.on_event(events.AppMention.ANY)
async def on_mention(event: WebhookEvent) -> dict:
"""Launch a run for each @-mention.
The dedupe key identifies one message. For one run per thread, build a
key from `thread_ts` and pass it to `run_once` instead.
"""
import flyte.remote as remote
task = remote.Task.get(name="slack-bot.answer", auto_version="latest")
slack_event = event.payload["event"]
result = await run_once.aio(
task,
key=event.dedupe_key(),
channel=event.scope,
text=event.title or "",
thread_ts=slack_event.get("thread_ts") or slack_event["ts"],
)
return {"run": result.run.name, "created": result.created}
@app_env.on_event(events.Command, action="/deploy")
async def on_deploy_command(event: WebhookEvent) -> dict:
"""Acknowledge the /deploy slash command.
`respond` posts to the command's `response_url` and needs no bot token.
Slack expects a reply within three seconds, so acknowledge here and do
longer work in a launched run.
"""
await notify.respond(event.payload["response_url"], "Deploy queued.")
return {"ok": True}
| Delivery | Body | event_type |
action |
|---|---|---|---|
| Events API callback | JSON | The event’s type, such as message or app_mention |
The event’s subtype, if any |
| Interactivity (buttons, shortcuts, modals) | Form, with JSON in payload |
block_actions, view_submission, shortcut, … |
The action_id or callback_id |
| Slash command | Form | command |
The command name, without the leading / |
For interactions, the action is the action_id. A bare constant matches every button; add action= to match one:
# Every Block Kit button in the workspace
@app_env.on_event(events.Interaction.BLOCK_ACTIONS)
async def any_button(event): ...
# One button
@app_env.on_event(events.Interaction.BLOCK_ACTIONS, action="redeploy")
async def redeploy_button(event): ...For slash commands, you can write action="/deploy" or action="deploy". The leading / is dropped.
Set up the app in Slack
At api.slack.com/apps, paste the /webhook/slack URL from the dashboard into each of these that you use:
- Event Subscriptions → Request URL. Then subscribe to the bot events your handlers match.
- Interactivity & Shortcuts → Request URL, for buttons, shortcuts, modals, or approvals.
- Slash Commands → Request URL, for each command.
The provider answers Slack’s url_verification challenge and ssl_check probe, so each URL verifies as soon as you save it.
Required scopes: app_mentions:read to receive mentions, chat:write to post, commands for slash commands.
Events
Constants live in flyteplugins.slack.events.
| Class | Members |
|---|---|
Message |
ANY, CHANGED, DELETED, REPLIED, CHANNEL_JOIN, CHANNEL_LEAVE, BOT_MESSAGE, FILE_SHARE, THREAD_BROADCAST |
AppMention |
ANY |
Reaction |
ADDED, REMOVED |
Channel |
CREATED, DELETED, RENAME, ARCHIVE, UNARCHIVE |
Member |
JOINED_CHANNEL, LEFT_CHANNEL |
Team |
JOIN |
File |
CREATED, SHARED, DELETED |
Pin |
ADDED, REMOVED |
AppHome |
OPENED |
Interaction |
BLOCK_ACTIONS, VIEW_SUBMISSION, VIEW_CLOSED, SHORTCUT, MESSAGE_ACTION |
Command |
ANY |
Scope and deduplication
scope is the channel ID. resource_id is channel:ts, which identifies a single message. To launch one run per thread instead, build a key from thread_ts and pass it to run_once as key=.
For interactions, occurred_at is the click’s action_ts. Two clicks on one button launch two runs; a redelivery of either click doesn’t.
The provider rejects deliveries more than five minutes old (MAX_REQUEST_AGE_SECONDS). For this reason, SAMPLE_DELIVERY signs its payload when called rather than carrying a fixed signature.
Send messages from tasks
The notify module posts, edits, and deletes messages through the Slack Web API.
env = flyte.TaskEnvironment(
name="slack-bot",
image=flyte.Image.from_debian_base().with_pip_packages("flyteplugins-slack"),
# The bot token (xoxb-...) from OAuth & Permissions. Posting requires the
# `chat:write` scope, and the bot must be invited to the channel.
secrets=[flyte.Secret(key="SLACK_BOT_TOKEN", as_env_var="SLACK_BOT_TOKEN")],
resources=flyte.Resources(cpu=1, memory="512Mi"),
)
@env.task
async def answer(channel: str, text: str, thread_ts: str) -> str:
"""Post a threaded reply, then edit it when the work finishes.
`post` returns the message's `ts`, which `update` uses to edit it.
"""
ts = await notify.post(channel, f"Working on: {text}", thread_ts=thread_ts)
await notify.update(channel, ts, f"Done: {text}")
return ts
| Function | Description |
|---|---|
post(channel, text, *, blocks, thread_ts, token) |
Posts a message and returns its ts. Use the ts to reply in a thread or to update the message. |
update(channel, ts, text, *, blocks, token) |
Edits a posted message, for example to show progress or a final status |
delete(channel, ts, *, token) |
Deletes a message the bot posted |
respond(response_url, text, *, blocks, replace_original, response_type) |
Replies to an interaction or slash command. Needs no token. |
respond posts to the response_url that each interaction and slash command carries. The URL is valid for 30 minutes and five uses, so a task launched by a click can reply to that click without a bot token.
Failed calls raise SlackApiError with Slack’s error code. not_in_channel means the bot needs to be invited to the channel. missing_scope names the OAuth scope to add.
To keep the bot token in one place, deploy notify.env. It provides a send task that holds the token, and other runs post through flyte.run(notify.send, ...) without mounting the secret themselves.
Approvals
approval.request posts a message with buttons and pauses the run until someone clicks one.
@env.task
async def deploy_with_approval(release: str, channel: str = "C0DEPLOYS") -> str:
"""Post approval buttons and wait for a click.
`approval.request` posts Block Kit buttons and waits on a
`flyte.new_condition`. The handler added by `approval.register` resolves
the condition when someone clicks. The condition can also be resolved
from the Flyte UI.
"""
decision = await approval.request.aio(
channel,
f"Deploy `{release}` to prod?",
options=["approve", "reject"],
timeout=3600,
)
if decision != "approve":
return f"{release}: not deployed ({decision})"
return f"{release}: deployed"
The task posts a Block Kit message and waits on a flyte.new_condition. When someone clicks a button, the handler added by approval.register(app_env) resolves the condition and replaces the buttons with a line showing who decided. Each button carries the run, action, and condition names, so the handler needs no other configuration.
request accepts:
options: the button labels. Defaults to("approve", "reject").thread_ts: posts into an existing thread.timeout: defaults to one hour.nameandtoken.
Call request.aio(...) from async tasks and request(...) from sync tasks. It works only inside a task, because the condition belongs to the running action.
To embed the buttons in your own message layout, build them with approval.blocks(...). The registered handler answers them the same way.
The condition can also be resolved from the Flyte UI, which shows the same prompt.
Test without a Slack workspace
# A separate environment with no secrets, so the replay runs before any
# secret is created.
replay_env = flyte.TaskEnvironment(
name="slack-replay",
image=flyte.Image.from_debian_base().with_pip_packages("flyteplugins-slack"),
resources=flyte.Resources(cpu=1, memory="512Mi"),
)
@replay_env.task
async def replay_sample_delivery() -> dict[str, str]:
"""Verify and parse the sample delivery bundled with the plugin.
The provider rejects deliveries more than five minutes old, so
`SAMPLE_DELIVERY` signs the payload with the current time when called.
"""
import flyteplugins.slack as plugin
secret = "a-test-signing-secret"
sign, body = plugin.SAMPLE_DELIVERY
headers = sign(body, secret)
assert plugin.verify(body, headers, secret), "a correctly signed delivery must verify"
assert not plugin.verify(body, headers, "wrong-secret"), "a bad signature must not"
event = plugin.parse(headers, body)
return {
"qualified_type": event.qualified_type,
"scope": event.scope or "",
"title": event.title or "",
"actor": event.actor or "",
"dedupe_key": event.dedupe_key(),
# Maximum delivery age, in seconds.
"max_request_age": str(plugin.MAX_REQUEST_AGE_SECONDS),
}
flyte run --local slack_tasks.py replay_sample_deliveryExamples
Both files are in v2/integrations/flyte-plugins/slack:
slack_webhooks.py: the receiver, a mention handler, a slash command, andapproval.register.slack_tasks.py: posting and updating withnotify, the approval gate, and the offline replay.
See also
- Software development tools for the event model,
run_once, and scopes. - Human gates and approvals for when to use a Slack button versus a Flyte condition on its own.
- Slack API reference.