The previous version of this report - an internal "Budget Pacer V.2" template from November 2020 - pulled data through an internal ScriptyApp/AWQL layer and reports like CAMPAIGN_PERFORMANCE_REPORT. It worked fine for years, until Google gradually phased out the legacy AWQL reports the whole thing was built on, and the template stopped reliably pulling data. The only supported path today is GAQL via AdsApp.search(), so this called for a rewrite from the ground up rather than a patch - and this script has no dependency on the old shim at all.

What the script does

Budget Pacer tracks daily budget spend at the campaign level and, hour by hour, builds a picture of who's "on pace" and who's hitting their cap before the day is over. Specifically, it writes to its own Google Sheets file:

  • Today / Yesterday - an hourly budget-depletion curve (cost only, against the campaign's current daily budget) with a color gradient showing how fast the budget is filling up.
  • Previous 7 Days / Previous 14 Days - a daily breakdown with Impression Share, Lost IS (budget), and selected KPIs, columns ordered newest-first, with a two-row header (day of week + the actual date).
  • Summary - two sections, "Capped by Budget" (campaigns that actually hit their cap in the latest window) and "Underspending" (campaigns not using up their budget), plus a Recommended Budget column and a mini export table ready to paste straight into Google Ads Editor, sorted by recommended increase.
  • Email alert - when budget pacing crosses a configured threshold and there are still enough hours left in the day to act on it, the script sends an HTML email listing the affected campaigns.

You don't maintain the spreadsheet by hand - on its first run the script builds its own complete spreadsheet with every tab, label, and default setting already in place, so there's nothing to prepare up front.

How to use the script

This is the single-account variant - it's created directly inside the Google Ads account it reports on, not at MCC level. (For reporting across multiple accounts from one place there's a separate MCC variant of the same script - two distinct files, not one toggled by a setting.)

  1. Add the script to the account. Tools & Settings → Bulk Actions → Scripts → "+". Paste in the full code below, confirm the permissions (Authorize), and save.
  2. Decide on the spreadsheet. Leave the SPREADSHEET_URL constant at the top of the file blank and the script will create a new spreadsheet on its first run and remember its address, or paste in an existing file's URL if you want to keep the report in one known place.
  3. Run it once manually. Preview, then main(). With a blank SPREADSHEET_URL, this first run creates the spreadsheet, fully labels the "Configure Report" tab with working defaults, and does the first data pull in the same run - the new file's address shows up in the run log.
  4. Set the frequency to Hourly. In the scripts list, via the pencil icon. That's the only scheduling Ads Scripts offers, and main() itself decides what actually needs to happen on each run (more on that in "How the script works internally").
  5. Fine-tune it in "Configure Report". Check which reports should run (Hourly / Previous 7 Days / Previous 14 Days), which KPI columns to show, the minimum daily budget for a campaign to be included, the window used for KPI calculations (1/7/14/30 days), and optionally turn on email alerts with a spend threshold and the latest hour it still makes sense to alert.
GOOGLE ADS BUDGET PACER

loading…

What needs adjusting

Unlike a typical script, there's almost nothing to change directly in the code. The one constant at the top of the file worth touching is:

  • SPREADSHEET_URL - optional. Leave it blank to have the script create its own spreadsheet, or fill it in only if you want the report tied to a file that already exists.

Everything else - which reports run, which KPIs show, the minimum-budget filter, the KPI window, email alerts and their recipients - is set in the "Configure Report" tab inside the generated spreadsheet itself, not in the code. The script fully labels that tab (headings, checkboxes, dropdowns) on its very first run, so it's immediately clear what each cell means.

How the script works internally

A few decisions that aren't obvious at a glance but are worth knowing before you touch the code:

  • No ScriptApp, no Session. Ads Scripts have neither a trigger API nor an active spreadsheet - the only scheduling is the UI frequency setting, and the script reads the time zone via AdsApp.currentAccount().getTimeZone(). Spreadsheet access always goes through SpreadsheetApp.openByUrl(), never getActiveSpreadsheet(), which would simply throw here.
  • Hourly frequency, but daily reports only once a day. main() is the one scheduled function, and it tracks via PropertiesService whether today's Previous 7/14 Days refresh has already happened. The gate is "the first run of the calendar day," not a fixed hour - so even a manual test run right after deployment still fills in every tab, instead of waiting for a specific time.
  • Budget is fetched separately. campaign_budget.amount_micros can't be combined in a single GAQL query or Report Editor report with search_impression_share / search_budget_lost_impression_share - Google Ads rejects that combination outright. So budget is pulled via its own unsegmented query and joined onto the performance rows in script, by campaign ID.
  • Impression Share only at the day level. Hour-segmented IS metrics are notoriously unreliable in the Ads API (often coming back as zero), so the hourly curve works with cost alone, and IS/Lost IS are only ever resolved at the day/window level.
  • Recommended Budget is a heuristic, not an API field. HasRecommendedBudget/RecommendedBudgetAmount don't exist in GAQL, so it's computed as current budget × (1 + Budget Lost IS) - roughly, "what budget would have captured the impressions this campaign lost to its cap yesterday."
  • No invented values for the unknown. When a campaign spent nothing in the given window, Search IS / Budget Lost IS - and the Recommended Budget derived from it - aren't shown as 0% or $0; they're unknown, so the report shows the literal text "--". Every other KPI (Conversions, CPA, ROAS, Ctr, Conv.Rate, Lost Conversions, Lost Revenue) still defaults to 0 when there's no data, same as before.
  • Hidden sheets are a raw dump, not a web of formulas. The script does the calculations; the visible sheets get plain written values. The hidden *BudgetData and *KPIData sheets exist purely as a flat data source for matching by campaign ID - no IFERROR(INDEX(MATCH(...))) chains that used to break the moment row order shifted.

If you're interested in more scripts and automations, browse every entry in the Scripts section.