Guide for developers

Charts in Slack from n8n and AI agents

An image block takes the URL of a publicly hosted image S1. A ChartPNG/chart URL is that kind of URL, so a workflow or an agent can put a chart in a message without a Slack app that uploads files. This guide has the message to copy, the limits that matter, and the two ways to send it.

Sources read on 3 Nov 2026. Each fact about Slack links to its source (S1, S2...), listed at the end with a short quote.

The short version

The Block Kit message

This is a chart drawn by ChartPNG with the kpi preset. The image block has the chart URL and an alt text that says the finding. An image block needs type and alt_text, and one of image_url or slack_file S1. Slack documents that blocks can be sent with incoming webhooks S2, so you can paste this JSON as the body of a webhook call.

MRR is $9,840, up 8.1% on last week, its highest in eight weeks, after dips in weeks 3 and 7.
{
  "blocks": [
    {
      "type": "section",
      "text": {
        "type": "mrkdwn",
        "text": "*Weekly report*"
      }
    },
    {
      "type": "image",
      "image_url": "https://chartpng.dev/chart?c=%7B%22type%22%3A%22line%22%2C%22data%22%3A%7B%22labels%22%3A%5B%22W1%22%2C%22W2%22%2C%22W3%22%2C%22W4%22%2C%22W5%22%2C%22W6%22%2C%22W7%22%2C%22W8%22%5D%2C%22datasets%22%3A%5B%7B%22label%22%3A%22MRR%22%2C%22data%22%3A%5B8200%2C8650%2C8400%2C8900%2C9100%2C9350%2C9100%2C9840%5D%7D%5D%7D%2C%22options%22%3A%7B%22plugins%22%3A%7B%22title%22%3A%7B%22display%22%3Atrue%2C%22text%22%3A%22MRR%22%7D%2C%22subtitle%22%3A%7B%22display%22%3Atrue%2C%22text%22%3A%22Last%208%20weeks%22%7D%2C%22chartpng%22%3A%7B%22preset%22%3A%22kpi%22%2C%22previous%22%3A9100%2C%22prefix%22%3A%22%24%22%7D%7D%7D%7D&w=800&h=400&dpr=2",
      "alt_text": "MRR is $9,840, up 8.1% on last week, its highest in eight weeks, after dips in weeks 3 and 7."
    }
  ]
}

The URL is 631 characters long. The config behind it, which is the part you change:

{
  "type": "line",
  "data": {
    "labels": [
      "W1",
      "W2",
      "W3",
      "W4",
      "W5",
      "W6",
      "W7",
      "W8"
    ],
    "datasets": [
      {
        "label": "MRR",
        "data": [
          8200,
          8650,
          8400,
          8900,
          9100,
          9350,
          9100,
          9840
        ]
      }
    ]
  },
  "options": {
    "plugins": {
      "title": {
        "display": true,
        "text": "MRR"
      },
      "subtitle": {
        "display": true,
        "text": "Last 8 weeks"
      },
      "chartpng": {
        "preset": "kpi",
        "previous": 9100,
        "prefix": "$"
      }
    }
  }
}

The kpi preset draws a big number, the change against previous and a trend line. See KPI with a sparkline, andcomparison with the previous period for a second chart in the same message. The free plan adds a small watermark.

Limits that matter

When the URL does not fit

  1. Shorten the config. Fewer points, shorter labels, no options you do not need. The n8n KPI template stops with an error when a URL goes over 3000 characters instead of posting a broken message.
  2. POST the config and upload the PNG. POST /chart takes up to 16 KB of config and returns the PNG, so the config no longer has to fit in a URL.
curl -X POST https://chartpng.dev/chart \
  -H 'Content-Type: application/json' \
  -d '{"c":{"type":"bar","data":{"labels":["Mon","Tue","Wed"],"datasets":[{"data":[12,19,7]}]}},"w":800,"h":450,"dpr":2}' \
  -o chart.png

Then hand the PNG to Slack's files API. This is explained here, not built into ChartPNG: you need a Slack app and a token with the files:write scope S3. As Slack describes it, files.getUploadURLExternal returns a URL for the fileS3; you send the PNG bytes to that URL, and files.completeUploadExternalfinishes the upload. Without a channel the file is only hosted in Slack, so name the channel to share it thereS3. Slack scans uploaded files for malware before making them available S3.

n8n

Two importable workflows post to Slack. The n8n guide explains how to import and set them up.

AI agents

An agent has two ways to get a chart into the message above.

Language models often write a config that is almost JSON. ChartPNG repairs the usual slips (code fences, single quotes, unquoted keys, trailing commas, a config sent as an escaped string, a missing chart type) and removes functions written as text without running them. This always applies; there is no switch. When a repair was needed, the response carries the X-ChartPNG-Fixes header with the codes, separated by commas. When nothing was repaired the header is absent. An agent that calls the API can read it, tell the user, and fix its config. The full list of codes is in the API reference.

curl -si -X POST https://chartpng.dev/chart \
  -H 'Content-Type: application/json' \
  --data-binary @- -o /dev/null -D - <<'EOF'
{"c":"{type:'bar',data:{labels:['Mon','Tue'],datasets:[{data:[12,19,],},],},}"}
EOF
# HTTP/2 200
# content-type: image/png
# x-chartpng-fixes: trailing_comma,single_quotes,unquoted_keys

Slack's limit is on the URL you send S1, so an agent that builds URLs should check the length against 3000 before it posts, and switch to POST when it is over.

Checklist before you post

What we could not confirm

Sources

All pages read on 3 Nov 2026. Quotes are exact and short. Web pages change, so check the date.

  1. S1Slack: Image blockhttps://docs.slack.dev/reference/block-kit/blocks/image-block

    Quote: "Maximum length for this field is 3000 characters."

    Read 3 Nov 2026. The same page gives 2000 characters for alt_text, marks type and alt_text as required, and says: "You must provide either an image_url or slack_file."

  2. S2Slack: Sending messages using incoming webhookshttps://docs.slack.dev/messaging/sending-messages-using-incoming-webhooks

    Quote: "use all the usual formatting and layout blocks with incoming webhooks"

    Read 3 Nov 2026. The page does not name the image block.

  3. S3Slack: Working with fileshttps://docs.slack.dev/messaging/working-with-files

    Quote: "The response will contain a URL."

    Read 3 Nov 2026. About files.getUploadURLExternal. It also says: "If not specified, the file will only be uploaded, i.e., hosted in Slack." (files.completeUploadExternal without a channel) and "Slack will scan the file for malware before making it available." On scopes: "The files:write scope allows your app to upload files."