<@ULVA73B9P> I already know how to send text messa...
# ask-marvin
m
@Marvin I already know how to send text messages to slack from my flows, is there a way to send a plot?
m
thought for 452.9 seconds
Yes. In Prefect 3.x you can send plots to Slack a couple of ways: Option A — Post an image URL in a Slack message (webhook or bot) - Works if your plot is hosted at an accessible URL (e.g., S3/GCS presigned URL). - Use the prefect-slack integration to send a message with an Image block. Example (incoming webhook):
Copy code
from prefect import flow
from prefect_slack.credentials import SlackWebhook
from prefect_slack.messages import send_incoming_webhook_message

@flow
def send_plot_via_webhook(image_url: str):
    webhook = SlackWebhook.load("my-slack-webhook")  # Prefect block you created
    blocks = [
        {"type": "section", "text": {"type": "mrkdwn", "text": "*Latest plot*"}},
        {"type": "image", "image_url": image_url, "alt_text": "plot"}
    ]
    send_incoming_webhook_message(slack_webhook=webhook, slack_blocks=blocks)
Notes: - If you’re currently using the built-in
prefect.blocks.notifications.SlackWebhook
block, that path is text-only. Switch to the
prefect-slack
integration to send blocks/attachments. - Slack Block Kit image docs: Image block docs - Prefect Slack integration docs: prefect-slack docs Option B — Upload the plot directly to Slack (no external hosting) - Generate your plot in-memory and upload it with Slack’s Files API. - This requires a bot token with the
files:write
scope. You can store the token in a Prefect block (SlackCredentials) or an environment variable and use the Slack SDK. Example (using slack_sdk directly):
Copy code
from prefect import flow
import io
import matplotlib.pyplot as plt
import os
from slack_sdk import WebClient

@flow
def upload_plot_to_slack(channel="#alerts"):
    # 1) Create a plot in memory
    fig, ax = plt.subplots()
    ax.plot([1, 2, 3], [4, 1, 2])
    ax.set_title("Latest metrics")
    buf = io.BytesIO()
    fig.savefig(buf, format="png", bbox_inches="tight")
    buf.seek(0)

    # 2) Upload to Slack
    client = WebClient(token=os.environ["SLACK_BOT_TOKEN"])
    client.files_upload_v2(
        channels=channel,
        filename="latest-plot.png",
        file=buf,
        title="Latest plot",
        initial_comment="Here’s the latest plot"
    )
Notes: - Files API docs: Slack files_upload_v2 - If you prefer to use a Prefect credentials block, you can store the token in a
SlackCredentials
block and read it in your flow, then pass it to
WebClient
. Which path are you using today to send text (incoming webhook vs. bot token)? If you tell me that preference, I can tailor the example to your setup (including how to generate a presigned URL if you want to host the image).
m
@Marvin I currently use a bot token and load the credentials with SlackCredentials.load
m
thought for 274.4 seconds
Great — since you’re already using a bot token via
SlackCredentials.load
, you’ve got two solid options: A) Upload the plot directly to Slack (no external hosting) - Uses Slack’s Files API (
files_upload_v2
) - Requires your bot to have the
files:write
(and typically
chat:write
) scope
Copy code
from prefect import flow
from prefect_slack.credentials import SlackCredentials
from slack_sdk import WebClient
import io
import matplotlib.pyplot as plt

@flow
def upload_plot_to_slack(channel="#alerts"):
    # 1) Load your Prefect block
    creds = SlackCredentials.load("my-slack-bot")  # replace with your block name

    # 2) Get the raw token from the block (field name may vary by version)
    token_field = getattr(creds, "token", None) or getattr(creds, "bot_token", None)
    token = token_field.get_secret_value() if hasattr(token_field, "get_secret_value") else token_field

    client = WebClient(token=token)

    # 3) Create a plot in memory
    fig, ax = plt.subplots()
    ax.plot([1, 2, 3], [4, 1, 2])
    ax.set_title("Latest metrics")
    buf = io.BytesIO()
    fig.savefig(buf, format="png", bbox_inches="tight")
    buf.seek(0)

    # 4) Upload to Slack
    client.files_upload_v2(
        channels=channel,               # use a channel ID like C12345 for reliability
        filename="latest-plot.png",
        file=buf,
        title="Latest plot",
        initial_comment="Here’s the latest plot"
    )
- Slack Files API reference: files_upload_v2 docs B) Post a message with an image URL (no file upload) - Host your image somewhere (e.g., presigned S3/GCS URL), then post a message with an Image block using `prefect-slack`:
Copy code
from prefect import flow
from prefect_slack.credentials import SlackCredentials
from prefect_slack.messages import send_chat_message

@flow
def send_plot_via_blocks(image_url: str, channel="#alerts"):
    creds = SlackCredentials.load("my-slack-bot")
    blocks = [
        {"type": "section", "text": {"type": "mrkdwn", "text": "*Latest plot*"}},
        {"type": "image", "image_url": image_url, "alt_text": "plot"}
    ]
    send_chat_message(channel=channel, slack_credentials=creds, slack_blocks=blocks)
- Prefect Slack integration: prefect-slack docs - Slack Block Kit image reference: Image block docs Notes: - The built-in Prefect notifications block (
prefect.blocks.notifications.SlackWebhook
) is text-only; use
prefect-slack
for images/blocks. - For direct uploads, Slack prefers channel IDs (e.g.,
C12345
) over
#channel-name
. Want me to tailor this to your exact setup? Share your SlackCredentials block name and whether you’d prefer direct uploads or hosting via URL.
m
@Marvin tried your snippet, got: slack_sdk.errors.SlackApiError: The request to the Slack API failed. (url: https://slack.com/api/files.getUploadURLExternal) The server responded with: {'ok': False, 'error': 'missing_scope', 'needed': 'files:write', 'provided': 'chatwrite,usersread,usergroupsread,channelsread,groupsread,mpimread,incoming-webhook'}
m
thought for 50.4 seconds
Thanks for the details — that error is exactly what Slack returns when the bot token is missing the files:write scope. Here’s how to fix it: What to do 1) Add the files:write scope to your Slack app (Bot Token Scopes) - Go to your app → OAuth & Permissions → Scopes → Bot Token Scopes → Add files:write - Docs: files:write scope and OAuth &amp; Permissions 2) Reinstall the app to your workspace - On the same page, click “Install to Workspace” (or “Reinstall”) - Reinstalling usually issues a new bot token 3) Update your Prefect SlackCredentials block with the new token - If the token changed, update your block (the one you load with
SlackCredentials.load("...")
) 4) Make sure the bot is in the target channel - Invite it if needed:
/invite @YourBot
5) Retry the upload code (unchanged)
Copy code
client.files_upload_v2(
    channels=channel,  # Prefer a channel ID like C12345
    filename="latest-plot.png",
    file=buf,
    title="Latest plot",
    initial_comment="Here’s the latest plot"
)
Alternatives (no new scope needed) - If you can’t add files:write, host the image (e.g., presigned S3/GCS URL) and send it as a Block:
Copy code
from prefect_slack.messages import send_chat_message
from prefect_slack.credentials import SlackCredentials

creds = SlackCredentials.load("my-slack-bot")
blocks = [
    {"type": "section", "text": {"type": "mrkdwn", "text": "*Latest plot*"}},
    {"type": "image", "image_url": image_url, "alt_text": "plot"}
]
send_chat_message(channel="#alerts", slack_credentials=creds, slack_blocks=blocks)
Helpful links - Slack files API (v2): files_upload_v2 docs - Prefect Slack integration: prefect-slack docs If you’d like, I can walk you through updating your SlackCredentials block or validating your token after the reinstall.
m
@Marvin I added the scope file:read and file:write, the error is now: "slack_sdk.errors.SlackApiError: The request to the Slack API failed. (url: https://slack.com/api/files.completeUploadExternal) The server responded with: {'ok': False, 'error': 'channel_not_found'}"
m
thought for 50.5 seconds
channel_not_found almost always means one of: - You passed a channel name like #alerts instead of a channel ID (C..., G..., D...) - The bot isn’t a member of that channel (especially for private channels) - The token and the channel belong to different workspaces Fix checklist 1) Invite the bot to the channel - In Slack, run: /invite @YourBot in the target channel 2) Use the channel ID, not the name - In Slack UI: open the channel → About → Copy Channel ID - Help: Find your channel ID 3) Ensure you’re using the same workspace’s token - If you have multiple workspaces, verify the app is installed to the one that contains the channel 4) Retry with the channel ID
Copy code
from prefect import flow
from prefect_slack.credentials import SlackCredentials
from slack_sdk import WebClient
import io
import matplotlib.pyplot as plt

@flow
def upload_plot_to_slack(channel_id="C0123456789"):
    creds = SlackCredentials.load("my-slack-bot")

    # Extract token from the block
    token_field = getattr(creds, "token", None) or getattr(creds, "bot_token", None)
    token = token_field.get_secret_value() if hasattr(token_field, "get_secret_value") else token_field

    client = WebClient(token=token)

    # Quick connectivity test (optional)
    client.chat_postMessage(channel=channel_id, text="Bot connectivity test")

    # Create plot
    fig, ax = plt.subplots()
    ax.plot([1, 2, 3], [4, 1, 2])
    ax.set_title("Latest metrics")
    buf = io.BytesIO()
    fig.savefig(buf, format="png", bbox_inches="tight")
    buf.seek(0)

    # Upload file to the channel ID
    client.files_upload_v2(
        channels=channel_id,              # e.g., "C0123456789" or a list of IDs
        filename="latest-plot.png",
        file=buf,
        title="Latest plot",
        initial_comment="Here’s the latest plot"
    )
Optional: programmatically look up a channel ID by name - Requires channels:read for public and groups:read for private
Copy code
def get_channel_id_by_name(client: WebClient, name: str) -> str | None:
    cursor = None
    while True:
        resp = client.conversations_list(
            limit=1000,
            cursor=cursor,
            types="public_channel,private_channel",
            exclude_archived=True,
        )
        for ch in resp["channels"]:
            if ch["name"] == name.lstrip("#"):
                return ch["id"]
        cursor = resp.get("response_metadata", {}).get("next_cursor") or None
        if not cursor:
            break
    return None
If you still see channel_not_found: - Double-check the ID prefix: C… (public), G… (private), D… (DM) - Confirm the bot is actually in that channel (run /who or look at members) - Make sure the app is installed to the same workspace as the channel If adding the bot to the channel isn’t possible, you can fall back to posting an image URL via a Block Kit message instead of uploading a file.
m
@Marvin I managed to successfully send the snippet, can you update the snippet to send a plot coming from a pandas dataframe with the command df.plot()
m
thought for 40.4 seconds
Absolutely — here’s a flow that takes a pandas DataFrame, uses
df.plot()
to create the figure, and uploads it to Slack with your bot token from `SlackCredentials.load`:
Copy code
from prefect import flow
from prefect_slack.credentials import SlackCredentials
from slack_sdk import WebClient

import io
import pandas as pd
import matplotlib
matplotlib.use("Agg")  # safe for headless environments
import matplotlib.pyplot as plt


@flow
def upload_df_plot_to_slack(
    block_name: str,
    channel_id: str,            # e.g., "C0123456789" (channel ID, not #name)
    df: pd.DataFrame,
    title: str = "DataFrame Plot",
    filename: str = "df-plot.png"
):
    # 1) Load your Prefect block and extract the token
    creds = SlackCredentials.load(block_name)
    token_field = getattr(creds, "token", None) or getattr(creds, "bot_token", None)
    token = token_field.get_secret_value() if hasattr(token_field, "get_secret_value") else token_field
    client = WebClient(token=token)

    # 2) Create the plot with pandas (returns an Axes)
    ax = df.plot(figsize=(10, 6))
    ax.set_title(title)

    # 3) Save to an in-memory buffer
    fig = ax.get_figure()
    fig.tight_layout()
    buf = io.BytesIO()
    fig.savefig(buf, format="png", bbox_inches="tight", dpi=150)
    buf.seek(0)
    plt.close(fig)  # free memory

    # 4) Upload to Slack
    client.files_upload_v2(
        channels=channel_id,          # use a channel ID (C..., G...) and ensure bot is a member
        filename=filename,
        file=buf,
        title=title,
        initial_comment=f"Here’s the plot: {title}"
    )


if __name__ == "__main__":
    # Example usage:
    sample_df = pd.DataFrame(
        {"A": [1, 3, 2, 4], "B": [4, 1, 3, 2]},
        index=pd.date_range("2024-01-01", periods=4, freq="D"),
    )
    upload_df_plot_to_slack(
        block_name="my-slack-bot",
        channel_id="C0123456789",
        df=sample_df,
        title="Sample df.plot()"
    )
Notes: - Use a channel ID (C…/G…) and make sure the bot is in the channel. - Your app needs
files:write
(and typically
chat:write
) scopes.