appscript.dev
Automation Intermediate Gmail Sheets

Embed inline charts in a status email

Render a Sheets chart as an image inside the email body, not as an attachment.

Published May 12, 2026

A status email with the chart as an attachment is a status email half the recipients won’t read. The pipeline picture is the whole point of the message, but it’s hidden behind a click — and on a phone, that click opens a separate viewer and breaks the flow entirely.

Northwind’s Friday status mail solves this by putting the chart in the body. This script grabs a live chart straight from a Sheet, renders it as a PNG, and embeds it inline so partners see the pipeline the instant they open the email — no attachment, no click, no manual export. The chart is whatever you’ve already built in the spreadsheet, so it stays in sync with the data on its own.

What you’ll need

  • A Google Sheet with at least one chart on its first tab. The chart can be any type — the script renders whatever it finds.
  • The sheet’s ID, pasted into PIPELINE_SHEET in the config block.
  • The recipient address (a person or a group) for the status mail.
  • Edit access to the Sheet, so the script can read the chart object.

The script

// The spreadsheet whose chart gets embedded in the email.
const PIPELINE_SHEET = '1abcPipelineSheetId';

// Who receives the status mail, and the subject line.
const RECIPIENT = '[email protected]';
const SUBJECT = 'Pipeline — this Friday';

// The content ID that links the HTML <img> to the inline image.
// It just has to match in both places; the exact string is arbitrary.
const CHART_CID = 'pipelineChart';

/**
 * Reads the first chart from the pipeline sheet, renders it as a PNG,
 * and emails it embedded inline in the message body.
 */
function emailStatusWithChart() {
  // 1. Open the sheet and grab its first chart.
  const sheet = SpreadsheetApp.openById(PIPELINE_SHEET).getSheets()[0];
  const charts = sheet.getCharts();

  // 2. Bail out cleanly if there's no chart to send.
  if (charts.length === 0) {
    Logger.log('No chart on the sheet — nothing to send.');
    return;
  }

  // 3. Render the chart to a PNG blob. This is a live render of the
  //    chart as it currently looks — no manual export step.
  const blob = charts[0].getAs('image/png').setName('pipeline.png');

  // 4. Build the HTML body. The <img> pulls from the inline image
  //    via cid: plus the matching content ID.
  const html = `
    <p>Northwind pipeline snapshot:</p>
    <img src="cid:${CHART_CID}" alt="Pipeline">
    <p>— Awadesh</p>
  `;

  // 5. Send it. The plain-text body is empty; htmlBody is the real
  //    message, and inlineImages maps the CID to the blob.
  GmailApp.sendEmail(RECIPIENT, SUBJECT, '', {
    htmlBody: html,
    inlineImages: { [CHART_CID]: blob },
  });
  Logger.log('Status email sent to ' + RECIPIENT);
}

How it works

  1. emailStatusWithChart opens the pipeline sheet and calls getCharts() on its first tab, which returns every chart embedded on that sheet.
  2. If there are no charts, it logs a message and stops — no point sending an email with a broken image in it.
  3. getAs('image/png') renders the first chart to a PNG blob. This is a fresh render of the chart in its current state, so the email always reflects the latest data without anyone exporting anything by hand.
  4. The HTML body references the image with <img src="cid:pipelineChart">. The cid: scheme means “look this up in the inline images for this message” rather than fetching a URL.
  5. GmailApp.sendEmail is called with an empty plain-text body and an options object. htmlBody carries the real message; inlineImages is a map from content ID to blob. Because CHART_CID is used as a computed key, the map key and the cid: reference can never drift apart.
  6. Gmail attaches the blob as a hidden inline part and wires it to the <img>, so the chart appears in the body itself, not as a downloadable file.

Example run

Imagine the pipeline sheet holds a column chart of deal value by stage:

StageValue (£k)
Lead40
Proposal95
Negotiation60
Won120

After a run, [email protected] receives an email titled Pipeline — this Friday. The body reads “Northwind pipeline snapshot:”, followed by the column chart rendered directly in the message, then the sign-off ”— Awadesh”. On a phone or a laptop, the chart is visible immediately with no attachment to open.

Trigger it

This is a weekly report, so schedule it for the end of the working week:

  1. In the Apps Script editor, open Triggers (the clock icon).
  2. Click Add Trigger.
  3. Choose emailStatusWithChart, event source Time-driven, type Week timer, day Friday, and the 4pm–5pm slot.
  4. Save, and approve the authorisation prompt the first time.

Watch out for

  • getCharts() returns charts in no guaranteed order. The script takes charts[0], so if the sheet has several charts, make sure the one you want is the only chart on that tab — or move it to its own dedicated sheet.
  • Inline images need an HTML body. If you pass a plain-text body and an inlineImages map, the image silently won’t render — htmlBody is required.
  • The CID is internal plumbing, not a URL. It just has to be identical in the <img src="cid:..."> and the inlineImages key; using the CHART_CID constant for both keeps them locked together.
  • Chart rendering reflects the spreadsheet, including its theme and size. If the chart looks cramped in the email, resize it in the Sheet — there’s no way to rescale it from the script.
  • A small number of email clients block inline images by default, the same as they block remote images. Keep the alt text meaningful so the message still makes sense if the chart doesn’t load.

Related