Mehdi Lallouache
11/17/2025, 2:39 PMMarvin
11/17/2025, 2:39 PMMarvin
11/17/2025, 2:47 PMfrom 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):
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).Mehdi Lallouache
11/17/2025, 2:50 PMMarvin
11/17/2025, 2:50 PMMarvin
11/17/2025, 2:54 PMSlackCredentials.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
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`:
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.Mehdi Lallouache
11/17/2025, 4:04 PMMarvin
11/17/2025, 4:04 PMMarvin
11/17/2025, 4:05 PMSlackCredentials.load("..."))
4) Make sure the bot is in the target channel
- Invite it if needed: /invite @YourBot
5) Retry the upload code (unchanged)
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:
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.Mehdi Lallouache
11/17/2025, 4:12 PMMarvin
11/17/2025, 4:12 PMMarvin
11/17/2025, 4:13 PMfrom 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
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.Mehdi Lallouache
11/17/2025, 4:17 PMMarvin
11/17/2025, 4:17 PMMarvin
11/17/2025, 4:18 PMdf.plot() to create the figure, and uploads it to Slack with your bot token from `SlackCredentials.load`:
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.