Webhook icon
Schedule icon
WorkingDirectory icon
Commands icon
Docker icon
Script icon
If icon
Loop icon
Request icon
Create icon
SlackIncomingWebhook icon
Log icon

Pause Runaway Ad Campaigns Before They Empty the Daily Budget

Pauses ad campaigns that breach the daily budget cap or spike past a sigma threshold, then escalates the pause through GitHub and Slack.

Categories
Business

How it works

  1. on_demand_check (io.kestra.plugin.core.trigger.Webhook, keyed with the ADS_WEBHOOK_KEY secret) runs the guard on demand, for example right after a flight launches or a budget is raised. daily_spend_watch (io.kestra.plugin.core.trigger.Schedule, cron 0 6 * * *) is the daily safety net and ships disabled: true until an operator turns it on.
  2. campaign_workspace (io.kestra.plugin.core.flow.WorkingDirectory) opens one shared filesystem so the fixture and the analysis see the same file.
  3. write_campaign_fixture (io.kestra.plugin.scripts.shell.Commands on the io.kestra.plugin.scripts.runner.docker.Docker runner, image alpine:3.20) writes campaigns.json with a heredoc. The fixture holds three campaigns, each with a seven day history and today's spend, one ordinary day, one sharp spike above the cap and one steady under-spender. entryPoint: [] clears the image entrypoint so the shell script runs instead of the default command.
  4. analyze_spend (io.kestra.plugin.scripts.python.Script on python:3.12-slim) reads the fixture, computes the mean and the sample standard deviation of each history, then the z-score of today's spend. A campaign breaches when today > daily_cap_usd or z > sigma_threshold; the worst offender is the breached campaign with the highest z-score. The verdict, the campaign, the spend, the deviation and the breached list are printed as a single ::{"outputs": ...}:: line.
  5. pause_gate (io.kestra.plugin.core.flow.If) branches on breached | length > 0. Above zero, pause_campaigns (io.kestra.plugin.core.flow.Loop, concurrencyLimit: 1) walks the breached list and pause_campaign (io.kestra.plugin.core.http.Request) POSTs https://ads.example.com/v1/campaigns/{id}/pause with a Bearer token from the ADS_API_TOKEN secret and retries transient failures. open_pause_issue (io.kestra.plugin.github.issues.Create) files a labeled issue with the paused list, and alert_pause posts the counts to Slack. At zero, log_within_budget records the quiet outcome.
  6. The flow level errors handler (io.kestra.plugin.slack.notifications.SlackIncomingWebhook) reports any failure with the execution id, because a guard that dies silently looks exactly like a quiet day.

What you get

  • A pause before the budget empties: breached campaigns are stopped through the ads API the moment they cross either threshold.
  • Two thresholds working together, a hard dollar cap and a statistical z-score, so a small budget that suddenly triples is caught even when the absolute number looks modest.
  • Gateable results: verdict, campaign, spend and deviation are flow outputs a downstream budget review or dashboard can branch on.
  • Escalation where the work happens, a labeled GitHub issue carrying the paused list plus a Slack ping with the counts, and a quiet log when everything is healthy.

Who it's for

  • Performance marketers who own paid search and paid social budgets and cannot watch the ad consoles all day.
  • Growth and finance partners who need a hard daily ceiling that nobody can silently overspend.
  • Marketing ops teams who want paused campaigns to arrive as tickets and channel messages instead of a surprise invoice.

Why orchestrate this with Kestra

An ad platform's own budget cap is a single number with no history behind it, and it tells you nothing about a campaign that usually spends forty dollars and suddenly spends five hundred. Kestra gives the check a cadence with a disabled daily schedule as the safety net, a keyed webhook for on demand runs, a fixture driven analysis that z-scores today against the campaign's own past week, a conditional branch that separates the pause from the all clear, a loop that calls the pause endpoint once per breached campaign, and routing into GitHub and Slack from one version controlled file. The same file carries the thresholds, the escalation policy and the execution history that proves the guard actually ran.

Prerequisites

  • A worker with Docker access, the fixture is written in alpine:3.20 and the analysis runs in python:3.12-slim.
  • An ads platform account whose pause endpoint matches https://ads.example.com/v1/campaigns/{id}/pause, or a stub of that API while you trial the flow.
  • A GitHub token that can open issues in the repository named on open_pause_issue (classic PAT scope repo, or fine-grained Issues: write).
  • A Slack incoming webhook for the pause alert and for failure alerts.

Secrets

  • ADS_WEBHOOK_KEY: key segment guarding the on demand webhook URL, anyone holding it can trigger a spend check.
  • ADS_API_TOKEN: bearer token used to call the ads platform pause endpoint.
  • GITHUB_TOKEN: token used to open the pause review issue.
  • SLACK_WEBHOOK_URL: Slack incoming webhook URL for pause and failure alerts.

Quick start

  1. Add the four secrets to your Kestra namespace.
  2. Run the flow once with the default inputs and confirm verdict reads PAUSE_REQUIRED, campaign reads cmp-summer-prospecting, spend reads 544.2 and deviation reads about 4.2.
  3. Point open_pause_issue.repository at a repository where issues are welcome, then re-run with sigma_threshold at 10 to watch the sigma branch go quiet while the cap still pauses the spike.
  4. Trigger on_demand_check at /api/v1/{tenant}/executions/webhook/{namespace}/ad-spend-anomaly-guard/{ADS_WEBHOOK_KEY}, or set disabled: false on daily_spend_watch once the token and webhook are in place.

How to extend

  • Replace the heredoc fixture with a io.kestra.plugin.core.http.Request that pulls yesterday's spend from the ads reporting API, keeping the fixture as the offline fallback.
  • Add a third threshold on week-to-date spend with another If inside pause_gate so long flights are caught before the daily cap trips.
  • Resume campaigns automatically with a second POST to the resume endpoint once tomorrow's spend normalizes.
  • Fan the breached list out to a subflow per campaign with io.kestra.plugin.core.flow.ForEachItem when pausing across several ad networks at once.

Links

See How

New to Kestra?

Use blueprints to kickstart your first workflows.