Generate Charts for Slack, Discord, and Teams Bots

5 min readChart-Output team

"Post our daily metrics to #growth every morning" is a common ask, and it comes up across every chat platform, not just Slack — Discord community bots and Teams alert integrations hit the same wall. None of these platforms render charts natively; they all expect you to hand them a finished image or an image URL. The bot integration itself is usually simple. Generating a real chart to attach is the actual work.

That means the pattern is the same regardless of platform: turn your data into a chart image, get a URL for it, and pass that URL to whichever chat API you're posting to. Here's Slack in detail, then the same pattern adapted for Discord and Teams.

Step 1: Render the chart and get a URL

Slack's image block supports two ways to reference an image: an image_url field pointing to an HTTPS URL Slack can fetch, or a slack_file reference for a file you've uploaded directly to Slack. The URL approach is the simpler integration and what we'll use here — it's exactly what returnUrl: true gives you:

javascript
const chartResponse = await fetch("https://www.chart-output.com/api/v1/render", { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${process.env.CHART_OUTPUT_API_KEY}` }, body: JSON.stringify({ type: "line", data: { labels: ["Mon", "Tue", "Wed", "Thu", "Fri"], datasets: [{ label: "Signups", data: [34, 41, 29, 52, 48] }] }, format: "png", returnUrl: true }) }); const { url: chartUrl } = await chartResponse.json();

Step 2: Post it to Slack

javascript
await fetch("https://slack.com/api/chat.postMessage", { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${process.env.SLACK_BOT_TOKEN}` }, body: JSON.stringify({ channel: "#growth", text: "Daily signups report", blocks: [ { type: "image", image_url: chartUrl, alt_text: "Daily signups chart" } ] }) });

That's the whole integration. No headless browser step, no image-hosting bucket to manage yourself — the CDN URL from the render call is already publicly accessible and ready for Slack to fetch.

The same pattern for Discord

Discord bots post images via webhooks or the bot API, and — like Slack — they accept either a direct image URL or an attached file. With returnUrl: true already giving you a CDN URL, posting to a Discord webhook is close to identical:

javascript
await fetch(process.env.DISCORD_WEBHOOK_URL, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ content: "Daily signups report", embeds: [ { title: "Signups this week", image: { url: chartUrl } } ] }) });

Wrapping the chart in an embed (rather than posting the raw image URL as message content) gives you a title and description alongside the chart, similar to Slack's Block Kit image block.

The same pattern for Microsoft Teams — with one important caveat

If you've built a Teams integration before, you may be used to the old "Office 365 Connector" incoming webhook. That mechanism has been retired by Microsoft — connector-based webhooks were disabled in 2026, with Workflows (built on Power Automate) as the replacement. If you're setting this up fresh, or migrating an old integration, use a Workflows webhook rather than the legacy connector URL.

To get a webhook URL: in the target Teams channel, open Workflows, choose the "Post to a channel when a webhook request is received" template (naming may vary slightly by Teams version), and copy the generated webhook URL.

The chart URL slots into an Image element inside an Adaptive Card body, posted to that webhook:

javascript
await fetch(process.env.TEAMS_WORKFLOW_WEBHOOK_URL, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ type: "message", attachments: [ { contentType: "application/vnd.microsoft.card.adaptive", content: { type: "AdaptiveCard", version: "1.4", body: [ { type: "TextBlock", text: "Daily signups report", weight: "bolder" }, { type: "Image", url: chartUrl } ] } } ] }) });

One behavior change worth knowing: messages posted via Workflows webhooks show up under the default Workflows bot identity rather than a custom name/icon — that customization isn't currently available for Adaptive Card payloads sent this way. Given how actively this part of the Teams platform has been changing, check Microsoft's current webhook documentation before shipping, rather than relying solely on this post.

Across all three platforms, the chart-generation half of the integration doesn't change — only the shape of the message payload and, in Teams' case, how you obtain the webhook URL in the first place. That's the useful property of rendering to a URL once: you can fan the same rendered chart out to multiple platforms simultaneously if your alerting needs to reach more than one channel type.

Common use cases this pattern covers

  • Scheduled digests — a cron job that renders and posts a daily/weekly metrics chart
  • Threshold alerts — trigger a render + post only when a metric crosses a threshold (e.g. error rate spike), so the chart appears alongside the alert message for immediate context
  • On-demand via slash command — a /metrics Slack command that renders the current state of a dashboard on request

For the alert case specifically, pairing this with webhooks means you can trigger the render asynchronously from whatever system detects the threshold breach, and post to Slack once the webhook confirms the image is ready — rather than blocking your alerting pipeline on a synchronous render call.

Keeping charts on-brand in Slack

If multiple charts get posted regularly, visual consistency matters more than it seems — a chart that looks like a different tool's default theme every time reads as sloppy. Brand kits let you apply consistent colors, fonts, and even a logo across every render without re-specifying styling in each request, so your daily Slack chart looks like it belongs to your product, not a stock library default.

Chat clients also crop and scale aggressively, so compact charts survive better than wide dashboards. Open this doughnut chart in the playground and switch the preview to the Slack tab to see how a chart reads inside a message before you ship the bot.

Full Slack-specific setup details, including Block Kit card composition (header + chart + KPI strip as a single Slack-ready image), are in the Charts in Slack guide. Discord and Teams use the same render + returnUrl step — only the message payload shape differs, as shown above.

More from the blog