Receive Slack events, interactions, and slash commands as Flyte runs, and post messages and approval buttons from tasks.

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

slack_webhooks.py
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:

slack_webhooks.py
@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:

  1. Event Subscriptions → Request URL. Then subscribe to the bot events your handlers match.
  2. Interactivity & Shortcuts → Request URL, for buttons, shortcuts, modals, or approvals.
  3. 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.

slack_tasks.py
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.

slack_tasks.py
@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.
  • name and token.

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

slack_tasks.py
# 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_delivery

Examples

Both files are in v2/integrations/flyte-plugins/slack:

  • slack_webhooks.py: the receiver, a mention handler, a slash command, and approval.register.
  • slack_tasks.py: posting and updating with notify, the approval gate, and the offline replay.

See also