Discount code removed alerts

When to use this workflow

Use this workflow when a discount code is removed and you need an immediate record of what was affected. It helps you capture the removed code, its usage count, the campaign details, and a summary you can send to finance or operations in Slack.

This is especially useful for leaked influencer codes, unplanned coupon exposure, or any case where you need a documented impact report after a code is removed.

This setup uses Flow Trigger Extensions for the Discount Code Removed trigger, Shopify Flow for orchestration, and a Slack workflow that Starts with a webhook.

What you will build

  • A Slack workflow that receives a text variable from a webhook and posts that same text into a Slack channel.

  • A Shopify Flow workflow triggered by Discount Code Removed.

  • A Get discount data step that looks up the discount using the ID from the trigger.

  • A Run code step that formats a removal report.

  • An HTTP Request step that sends the report to Slack.

Before you begin

  • Make sure the app is installed and visible in Shopify admin under Apps > Flow Trigger Extensions.

  • Make sure you have access to Shopify Flow.

  • Create or choose a Slack channel where alerts should appear.

  • Create a Slack workflow that starts with a webhook and accepts a text field.

Video Guide

You can see the full implementation video as well.

Create the Slack workflow first

In Slack, create a workflow with the trigger Starts with a webhook. Add a text variable named text, then add the message step that sends a message to your chosen channel using that same text variable.

Slack workflow that starts with a webhook and sends the text variable to a Slack channel

Name the Slack variable text to keep the Shopify Flow request body simple.

Grant Discount Access in Flow Trigger Extensions

Before the trigger appears in Shopify Flow, enable discount access in the app.

In Shopify admin, go to Apps > Flow Trigger Extensions.

On the Dashboard, look for the setup checklist.

Turn on Grant Discount Access. This enables discount-related triggers such as Discount Created, Discount Updated, Discount Deleted, Discount Code Added, Discount Code Removed, and Discount Expired.

Flow Trigger Extensions dashboard showing Grant Discount Access enabled

Build the Shopify Flow

Open Shopify Flow and create a new workflow.

Search for discount-related triggers and select Discount Code Removed from Flow Trigger Extensions.

Shopify Flow trigger picker with Discount Code Removed selected

Add the Shopify action Get discount data.

In Select a query to filter data, choose Advanced. In Edit query, use the discount ID from the trigger and remove the Shopify GID prefix:

id:"{{discount.id|remove:'gid://shopify/DiscountCodeNode/'}}"

Get discount data action using an advanced query with the cleaned discount ID

This step uses the discount identifier from the trigger so the workflow can fetch the matching discount campaign details.

Add a Run code step after Get discount data.

Use the following GraphQL query in Select inputs from previous steps:

query {
  redeemCode {
    code
    usageCount
    bulkRemoval
  }
  getDiscountData {
    discount {
      ... on DiscountCodeBasic {
        title
        startsAt
        endsAt
        asyncUsageCount
        codes {
          code
          asyncUsageCount
        }
        customerGets {
          value {
            ... on DiscountAmount {
              amount { amount currencyCode }
            }
            ... on DiscountPercentage {
              percentage
            }
          }
        }
      }
    }
  }
}

Define the outputs like this:

"Output of discount code removal report"
type Output {
  "Discount code that was removed"
  code: String!
  "Discount campaign title"
  title: String!
  "Campaign start date"
  startsAt: String!
  "Campaign end date"
  endsAt: String!
  "Number of times it was redeemed"
  usageCount: Int!
  "Total discount impact as a formatted string"
  totalImpact: String!
  "When it was removed"
  removedAt: String!
  "Full summary for alerts"
  summary: String!
}

Then paste this code into Write code:

export default function main(input) {
  // ── Trigger: removed code ──
  const removedCode  = input?.redeemCode?.code         ?? "Unknown";
  const usageCount   = input?.redeemCode?.usageCount   ?? null; // null = bulk removal or unknown
  const bulkRemoval  = input?.redeemCode?.bulkRemoval  ?? false;

  const currency = input?.shop?.currencyCode ?? "";
  const shopName = input?.shop?.name         ?? "(not bound)";
  const removedAt = new Date().toISOString();

  // ── Find matching discount ──
  const dataItems = input?.getDiscountData ?? [];
  let matchedDiscount = null;
  for (const item of dataItems) {
    const d = item?.discount?.DiscountCodeBasic ?? item?.discount;
    if (!d) continue;
    const codesArray = Array.isArray(d.codes)
      ? d.codes.map(c => (typeof c === "string" ? c : c?.code ?? ""))
      : [];
    if (codesArray.includes(removedCode)) {
      matchedDiscount = d;
      break;
    }
  }
  // fallback to first entry if removed code already gone from list
  if (!matchedDiscount && dataItems.length > 0) {
    matchedDiscount =
      dataItems[0]?.discount?.DiscountCodeBasic ?? dataItems[0]?.discount ?? null;
  }

  const title              = matchedDiscount?.title            ?? "Untitled";
  const startsAt           = matchedDiscount?.startsAt         ?? "N/A";
  const endsAt             = matchedDiscount?.endsAt           ?? "None (no expiry)";
  const totalUsageCount    = matchedDiscount?.asyncUsageCount  ?? null;

  // ── Discount value ──
  const valueWrapper = matchedDiscount?.customerGets?.value ?? {};
  const pctObj       = valueWrapper?.DiscountPercentage ?? null;
  const amtObj       = valueWrapper?.DiscountAmount ?? valueWrapper?.amount ?? null;

  let discountLabel = "Unknown";
  if (pctObj?.percentage != null) {
    discountLabel = `${(pctObj.percentage * 100).toFixed(0)}% off`;
  } else if (amtObj?.amount != null) {
    const cur = amtObj.currencyCode || currency;
    discountLabel = `${cur} ${parseFloat(amtObj.amount).toFixed(2)} off`;
  }

  // ── This code's impact ──
  let thisCodeImpact;
  if (bulkRemoval) {
    thisCodeImpact = `Not available — removed as part of bulk deletion`;
  } else if (usageCount === null) {
    thisCodeImpact = `Could not be determined`;
  } else if (pctObj?.percentage != null) {
    thisCodeImpact = `${discountLabel} × ${usageCount} use(s)`;
  } else if (amtObj?.amount != null) {
    const perUse = parseFloat(amtObj.amount);
    const cur    = amtObj.currencyCode || currency;
    thisCodeImpact = `${cur} ${(perUse * usageCount).toFixed(2)} (${cur} ${perUse.toFixed(2)} × ${usageCount} uses)`;
  } else {
    thisCodeImpact = "Unknown";
  }

  // ── Campaign-wide impact using asyncUsageCount on the discount ──
  let campaignImpact;
  if (totalUsageCount === null) {
    campaignImpact = "Not available";
  } else if (pctObj?.percentage != null) {
    campaignImpact = `${discountLabel} × ${totalUsageCount} total use(s) across all codes`;
  } else if (amtObj?.amount != null) {
    const perUse = parseFloat(amtObj.amount);
    const cur    = amtObj.currencyCode || currency;
    campaignImpact = `${cur} ${(perUse * totalUsageCount).toFixed(2)} total (${cur} ${perUse.toFixed(2)} × ${totalUsageCount} uses)`;
  } else {
    campaignImpact = "Unknown";
  }

  // ── Per-code breakdown ──
  const codesNodes = Array.isArray(matchedDiscount?.codes)
    ? matchedDiscount.codes
    : [];

  const allCodes = codesNodes
    .map(c => (typeof c === "string" ? c : c?.code ?? ""))
    .filter(Boolean);

  const codesRoster = codesNodes.length
    ? codesNodes.map(c => {
        const code = typeof c === "string" ? c : c?.code ?? "";
        const used = c?.asyncUsageCount ?? "?";
        const flag = code === removedCode ? " ← REMOVED" : "";
        return `${code} (used ${used}×)${flag}`;
      }).join("\n                  ")
    : "N/A";

  const usageCountLabel = bulkRemoval
    ? "N/A (bulk removal)"
    : usageCount !== null
      ? String(usageCount)
      : "Unknown";

  const summary =
    `DISCOUNT CODE REMOVED\n` +
    `─────────────────────\n` +
    `Store:            ${shopName}\n` +
    `Code removed:     ${removedCode}\n` +
    `Bulk removal:     ${bulkRemoval ? "Yes" : "No"}\n` +
    `Campaign:         ${title}\n` +
    `Discount:         ${discountLabel}\n` +
    `Valid from:       ${startsAt}\n` +
    `Valid until:      ${endsAt}\n` +
    `\n` +
    `THIS CODE\n` +
    `  Times used:     ${usageCountLabel}\n` +
    `  Impact:         ${thisCodeImpact}\n` +
    `\n` +
    `CAMPAIGN TOTAL\n` +
    `  Total uses:     ${totalUsageCount ?? "Unknown"}\n` +
    `  Total impact:   ${campaignImpact}\n` +
    `\n` +
    `CODE BREAKDOWN\n` +
    `  ${codesRoster}\n` +
    `\n` +
    `Removed at:       ${removedAt}`;

const safeSummary = summary
  .replace(/\\/g, "\\\\")   // escape backslashes first
  .replace(/"/g, '\\"')     // escape quotes
  .replace(/\n/g, "\\n")    // escape newlines
  .replace(/\r/g, "\\r");

  return {
    code:           removedCode,
    bulkRemoval,
    title,
    discountLabel,
    startsAt,
    endsAt,
    usageCount:     usageCount ?? 0,
    usageCountLabel,
    thisCodeImpact,
    totalUsageCount: totalUsageCount ?? 0,
    campaignImpact,
    allCodes,
    codesRoster,
    removedAt,
    summary: safeSummary,
  };
}

Run code step in Shopify Flow with the query, outputs, and JavaScript report builder

Add the HTTP Request action. In the Variables or Body Override (JSON) field, send the summary returned by the Run code step:

{
  "body": {
    "text": "{{runCode.summary}}"
  }
}

This posts the generated summary to the Slack webhook workflow using the text variable.

HTTP Request action in Shopify Flow sending the runCode summary in the text field

If your Shopify setup allows direct HTTP requests in Shopify Flow, you can send the webhook directly. If not, you can use the Flow Transactional Email action HTTP Request method described in the guide at https://docs.flow-action-extensions.app/make-an-http-request.

Save the workflow after all steps are in place.

To test, delete a discount code from your discount campaign and confirm the Slack channel receives the summary message.

Slack message showing the discount code removed summary

What the Slack alert includes

  • The removed discount code

  • Whether it was a bulk removal

  • The campaign title

  • The discount value

  • Start and end dates

  • Times used for the removed code

  • Estimated impact for the removed code

  • Campaign-wide usage and impact

  • A code-by-code breakdown when available

  • The timestamp when the removal was processed

Troubleshooting

Go back to Apps > Flow Trigger Extensions and confirm Grant Discount Access is enabled. The discount triggers only appear after access is granted.

Double-check the advanced query in Edit query. The ID should be cleaned exactly as shown:

id:"{{discount.id|remove:'gid://shopify/DiscountCodeNode/'}}"

Make sure the Slack workflow starts with a webhook, the webhook URL is the one used by your HTTP request, and the request body includes the text field.

Some removals may be part of a bulk action or may not return complete usage details. In those cases, the summary will still send the best available information and clearly label missing values.

Best practices

  • Send these alerts to a dedicated finance or operations channel instead of a general support channel.

  • Keep the workflow name specific, such as Discount Code Removed, so it is easy to identify in Shopify Flow and Slack.

  • Test with a non-production discount code first to confirm the message format and webhook behavior.

Deleting a discount code is a real change in Shopify. Test with a disposable code rather than a live campaign code.