appscript.dev
Automation Beginner

Send rich notifications to Discord

Push Northwind deploy alerts and KPI updates to a Discord channel — embeds, not plain text.

Published Oct 15, 2025

Northwind runs its day-to-day operations in Discord — one channel for the engineering team, another for the wider company. When a deploy goes out or a KPI crosses a threshold, the team wants to hear about it there, not buried in an inbox nobody checks until lunchtime.

A plain-text webhook message works, but it scrolls past unnoticed. A Discord embed — a titled card with a coloured spine down the left edge — stands out in a busy channel and tells you the outcome at a glance: green for a clean deploy, red for a failed one. This script wraps that pattern in a small helper so any of your other scripts can post a proper notification in one line.

What you’ll need

  • A Discord channel you can post to, and permission to manage its integrations.
  • A webhook URL for that channel. In Discord, open Channel settings → Integrations → Webhooks → New Webhook, then copy the URL.
  • That URL saved as DISCORD_WEBHOOK in Script Properties — see Store API keys and secrets securely. A webhook URL is a secret: anyone who has it can post to your channel.

The script

// Brand colours for embed spines, as integers (Discord wants a decimal int,
// not a "#rrggbb" string — hex literals like 0x10b981 are the same thing).
const COLOUR_INFO = 0x2563eb;    // blue — neutral updates
const COLOUR_SUCCESS = 0x10b981; // green — something worked
const COLOUR_FAILURE = 0xef4444; // red — something broke

/**
 * Posts a single embed to the Northwind Discord channel.
 *
 * @param {string} title        The bold heading on the embed card.
 * @param {string} description  The body text under the title.
 * @param {number} colour       The spine colour, as a decimal integer.
 */
function discordSend(title, description, colour = COLOUR_INFO) {
  // The webhook URL is a secret — read it from Script Properties, never
  // hard-code it into the script.
  const hook = PropertiesService.getScriptProperties()
    .getProperty('DISCORD_WEBHOOK');
  if (!hook) {
    Logger.log('No DISCORD_WEBHOOK set — skipping notification.');
    return;
  }

  // An embed is just a JSON object inside an "embeds" array. Discord
  // renders the title in bold and draws the colour down the left edge.
  const response = UrlFetchApp.fetch(hook, {
    method: 'post',
    contentType: 'application/json',
    payload: JSON.stringify({
      embeds: [{ title, description, color: colour }],
    }),
    muteHttpExceptions: true,
  });

  // A successful webhook post returns 204 No Content. Anything else
  // (a 404 for a deleted webhook, a 429 for rate limiting) is worth logging.
  const code = response.getResponseCode();
  if (code !== 204) {
    Logger.log('Discord webhook returned ' + code + ': ' + response.getContentText());
  }
}

/**
 * Posts a deploy result to Discord — green for success, red for failure.
 *
 * @param {string}  version  The release version, e.g. "v2.4.0".
 * @param {boolean} success  Whether the deploy succeeded.
 */
function deployAlert(version, success) {
  discordSend(
    success ? 'Deploy succeeded' : 'Deploy failed',
    'Northwind ' + version,
    success ? COLOUR_SUCCESS : COLOUR_FAILURE
  );
}

How it works

  1. Three named constants hold the embed colours as decimal integers. Discord’s webhook API expects a number for color, not a CSS-style #rrggbb string — the 0x10b981 hex literals are just a readable way to write those numbers.
  2. discordSend reads the webhook URL from Script Properties. If it is missing, the script logs a note and stops rather than throwing — a missing secret should never crash the caller.
  3. It builds the payload: a single object inside an embeds array, carrying the title, description, and colour. Discord renders that as a card.
  4. It posts the payload with UrlFetchApp.fetch. A successful webhook call returns HTTP 204 No Content; any other code is logged so a deleted webhook or a rate-limit error does not pass silently.
  5. deployAlert is a thin wrapper that picks the title and colour from a boolean, so the rest of your code can announce a release in one line.

Example run

Calling the helpers from another script:

deployAlert('v2.4.0', true);
discordSend('Weekly MRR', 'Northwind MRR is £48,200 — up 3.1% on last week.');

The channel shows two embeds. The first is a green-spined card titled Deploy succeeded with the body Northwind v2.4.0. The second is a blue-spined card titled Weekly MRR showing the figure. Both are scannable at a glance — colour alone tells the team whether to relax or react.

Run it

discordSend is a building block, not a standalone job — call it from your existing scripts at the point where something noteworthy happens:

  • After a deploy step, call deployAlert(version, success).
  • After a metrics calculation, call discordSend('Weekly MRR', summary).

To test it in isolation, add a throwaway function and run it from the editor:

function testDiscord() {
  discordSend('Test', 'If you can read this in Discord, the webhook works.');
}

Watch out for

  • The webhook URL is a credential. Anyone who has it can post to your channel as the webhook. Keep it in Script Properties, and rotate it in Discord if it ever leaks.
  • A successful post returns 204, with an empty body — do not try to parse the response as JSON. The script checks the status code instead.
  • Discord rate-limits webhooks (roughly 30 messages per minute per channel). A burst of alerts can return 429; if you expect bursts, batch updates into one embed rather than firing one message per event.
  • Embed text has limits — a title caps at 256 characters and a description at 4,096. Long KPI dumps will be truncated, so summarise before you send.
  • A webhook can only post to the one channel it was created for. To notify several channels, create one webhook per channel and store each under its own property key.

Related