Automate Google Search Campaign Total Budgets Into Sheets (Template + Script)
Stop manually juggling campaign budgets — automate total budget tracking and get pacing alerts in Sheets
If you run Search or Shopping campaigns in Google Ads, you already know the new total campaign budget setting (rolled out to Search & Shopping in early 2026) can simplify short-term promos and launches. But “set it and forget it” isn’t governance. You still need reliable monitoring: is Google pacing spend to finish the campaign on budget, or are you under/overspending on day 3 of a 10-day promo?
This guide gives you a ready-made approach: a free Google Sheets template plus an Apps Script that calls the Google Ads API, pulls campaign budgets and daily spend, calculates pacing vs. an ideal curve, and warns you when pacing is off. Follow the step-by-step setup, copy the script, and connect it to Slack or Zapier alerts — all without rebuilding reports from scratch.
Why this matters in 2026: the rise of total budgets and why spreadsheets still win
Since Google expanded total campaign budgets beyond Performance Max in late 2025 and officially rolled it out to Search and Shopping in January 2026, more advertisers are using multi-day total budgets for promotions and product launches. The feature automates intra-campaign daily allocation, but three trends make an external monitor essential in 2026:
- Automation without explainability: Google optimizes spend automatically, but marketers need visibility into whether automated pacing is meeting business goals and ROAS guardrails. See broader coverage on programmatic tooling and privacy for ad managers.
- Short-window campaigns: Black Friday-style promos, flash sales, and product drops run on tight windows where small pacing errors matter — this is the same short-window thinking in modern drop playbooks.
- Integrations-first ops: Teams want spreadsheets that tie into other systems (CRM, inventory, Slack). Sheets + Apps Script remain the fastest way to combine API data, apply business logic, and trigger alerts.
What you’ll get
- A Google Sheets template layout for campaign metadata, total budgets, daily spend, and pacing.
- An Apps Script that queries the Google Ads API (GAQL) to pull budgets and segmented daily costs for a date range.
- Built-in pacing formulas and example alert rules (email + Slack webhook) you can tweak.
- Advanced tips for scaling across multiple accounts and avoiding API quota problems.
Quick architecture — how it works (inverted pyramid)
- Apps Script authenticates to Google Ads API and runs a GAQL query for campaign budgets and daily costs over a date range.
- Script writes rows to your Sheet: campaign id, name, start/end dates, total_budget, date, cost.
- Sheet calculates cumulative spend vs. expected linear pacing and flags campaigns outside a threshold.
- If a flag is set, Apps Script can send an email or post a message to Slack/Zapier for human review.
Before you start — prerequisites
- Google account with access to the Google Ads account(s) you want to monitor (customer IDs).
- A Google Ads developer token (manager or developer access) and an OAuth client configured for the Ads API (if you don’t have one, see the Google Ads developer center — this is standard in 2026 workflows).
- Google Sheets and Apps Script editor access.
- Optional: Slack webhook or Zapier webhook for alerting.
Sheet template: recommended layout
Create a Google Sheet with three tabs: Settings, RawData, and Dashboard.
Settings (one row per monitored campaign)
- Columns: customerId, campaignId, campaignName, startDate, endDate, totalBudget (currency), pacingLowThreshold (e.g. 0.85), pacingHighThreshold (e.g. 1.15), alertEmail, slackWebhookURL
RawData
- Columns the script will populate: timestampPulled, customerId, campaignId, campaignName, campaignStart, campaignEnd, totalBudget, date, dailyCost
Dashboard
- Use pivot tables or queries to sum daily costs into cumulative spend per campaign and calculate pacing.
- Key formulas (example):
// Example pacing calculation per campaign (in Dashboard):
// total_days = endDate - startDate + 1
// days_elapsed = today - startDate + 1
// expected_cumulative = totalBudget * (days_elapsed / total_days)
// actual_cumulative = SUM(dailyCost up to today)
// pacing_ratio = actual_cumulative / expected_cumulative
Flag if pacing_ratio < pacingLowThreshold or > pacingHighThreshold.
Apps Script: how the integration pulls data (how-to + code)
The script below shows a practical approach using the Ads API REST endpoint and Apps Script UrlFetchApp. It uses the OAuth2 library for Apps Script to get an access token for the Google Ads scope (https://www.googleapis.com/auth/adwords).
Notes for 2026: Google Ads API endpoints and GAQL remain the recommended approach for aggregated metric pulls. Replace vX with the current API version if needed. Also watch API quotas — when monitoring many campaigns or accounts, schedule incremental runs and use date windows.
/*
1) Add OAuth2 library to Apps Script: Script Editor -> Libraries -> Add by Script ID
Script ID for OAuth2 library: MswhXl8fVhTFUH_Q3UOJbXvxhMjh3Sh48
2) Create an Apps Script project attached to the Sheet.
3) Create OAuth client credentials in Google Cloud Console with Ads API enabled and set redirect URI to https://script.google.com/macros/d/{PROJECT_ID}/usercallback
*/
const ADS_API_BASE = 'https://googleads.googleapis.com/vX'; // replace vX with latest version
function getOAuthService() {
return OAuth2.createService('GoogleAds')
.setAuthorizationBaseUrl('https://accounts.google.com/o/oauth2/auth')
.setTokenUrl('https://oauth2.googleapis.com/token')
.setClientId('YOUR_OAUTH_CLIENT_ID')
.setClientSecret('YOUR_OAUTH_CLIENT_SECRET')
.setCallbackFunction('authCallback')
.setPropertyStore(PropertiesService.getUserProperties())
.setScope('https://www.googleapis.com/auth/adwords https://www.googleapis.com/auth.spreadsheets');
}
function authCallback(request) {
const service = getOAuthService();
const authorized = service.handleCallback(request);
return HtmlService.createHtmlOutput(authorized ? 'Success! Close this tab.' : 'Denied');
}
function ensureAuth() {
const service = getOAuthService();
if (!service.hasAccess()) {
const authUrl = service.getAuthorizationUrl();
Logger.log('Open the following URL and re-run the script: %s', authUrl);
throw new Error('Auth required. Open the URL logged and authorize the script.');
}
}
function fetchCampaignBudgetAndSpend() {
ensureAuth();
const service = getOAuthService();
const token = service.getAccessToken();
const ss = SpreadsheetApp.getActiveSpreadsheet();
const settings = ss.getSheetByName('Settings');
const raw = ss.getSheetByName('RawData');
// read settings rows (skip header)
const rows = settings.getDataRange().getValues();
const header = rows.shift();
rows.forEach((r) => {
const customerId = r[0];
const campaignId = r[1];
const campaignName = r[2];
const startDate = Utilities.formatDate(new Date(r[3]), ss.getSpreadsheetTimeZone(), 'yyyy-MM-dd');
const endDate = Utilities.formatDate(new Date(r[4]), ss.getSpreadsheetTimeZone(), 'yyyy-MM-dd');
const totalBudget = Number(r[5]);
// GAQL query: pull daily metrics for campaign from startDate to endDate
const gaql = `SELECT campaign.id, campaign.name, campaign.start_date, campaign.end_date, campaign_budget.amount_micros, segments.date, metrics.cost_micros ` +
`FROM campaign WHERE campaign.id = ${campaignId} AND segments.date BETWEEN '${startDate}' AND '${endDate}'`;
const endpoint = `${ADS_API_BASE}/customers/${customerId}/googleAds:searchStream`;
const payload = {query: gaql};
const options = {
method: 'post',
contentType: 'application/json',
payload: JSON.stringify(payload),
headers: {
Authorization: 'Bearer ' + token,
'developer-token': 'YOUR_DEVELOPER_TOKEN'
},
muteHttpExceptions: true
};
const response = UrlFetchApp.fetch(endpoint, options);
if (response.getResponseCode() !== 200) {
Logger.log('Failed for customer %s campaign %s: %s', customerId, campaignId, response.getContentText());
return;
}
const content = response.getContentText();
// searchStream returns newline-delimited JSON in many cases; handle conservatively
const lines = content.trim().split('\n').map(s => s.trim()).filter(Boolean);
lines.forEach((line) => {
try {
const obj = JSON.parse(line);
const results = obj.results || [];
results.forEach(rItem => {
const fields = rItem;
const date = fields.segments?.date || '';
const costMicros = fields.metrics?.costMicros || fields.metrics?.cost_micros || 0;
const dailyCost = Number(costMicros) / 1e6;
// Append to RawData: timestamp, customerId, campaignId, campaignName, start, end, totalBudget, date, dailyCost
raw.appendRow([new Date(), customerId, campaignId, campaignName, startDate, endDate, totalBudget, date, dailyCost]);
});
} catch (e) {
Logger.log('Parse error: ' + e + ' for line: ' + line);
}
});
// After writing rows you can also trigger a pacing check per campaign
checkPacingForCampaign(customerId, campaignId);
});
}
function checkPacingForCampaign(customerId, campaignId) {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const raw = ss.getSheetByName('RawData');
const settings = ss.getSheetByName('Settings');
const rawVals = raw.getDataRange().getValues();
const header = rawVals.shift();
// filter rows for this campaign and dates up to today
const today = Utilities.formatDate(new Date(), ss.getSpreadsheetTimeZone(), 'yyyy-MM-dd');
const campaignRows = rawVals.filter(r => String(r[1]) === String(customerId) && String(r[2]) === String(campaignId) && r[7] <= today);
if (campaignRows.length === 0) return;
const settingsVals = settings.getDataRange().getValues();
const sHeader = settingsVals.shift();
const sRow = settingsVals.find(r => String(r[0]) === String(customerId) && String(r[1]) === String(campaignId));
if (!sRow) return;
const startDate = new Date(sRow[3]);
const endDate = new Date(sRow[4]);
const totalBudget = Number(sRow[5]);
const lowThreshold = Number(sRow[6]) || 0.85;
const highThreshold = Number(sRow[7]) || 1.15;
const alertEmail = sRow[8];
const slackWebhook = sRow[9];
const totalDays = Math.floor((endDate - startDate) / (1000*60*60*24)) + 1;
const daysElapsed = Math.floor((new Date() - startDate) / (1000*60*60*24)) + 1;
const expectedCumulative = totalBudget * Math.max(0, Math.min(1, daysElapsed / totalDays));
// sum actual costs up to today
const actualCumulative = campaignRows.reduce((sum, r) => sum + Number(r[8] || 0), 0);
const pacingRatio = expectedCumulative === 0 ? 0 : actualCumulative / expectedCumulative;
// write status to a Dashboard sheet (simple append)
const dashboard = ss.getSheetByName('Dashboard');
dashboard.appendRow([new Date(), customerId, campaignId, actualCumulative, expectedCumulative, pacingRatio]);
// Alert logic
if (pacingRatio < lowThreshold || pacingRatio > highThreshold) {
const message = `PACING ALERT: campaign ${campaignId} (customer ${customerId}) has pacing ratio ${pacingRatio.toFixed(2)} (actual ${actualCumulative} vs expected ${expectedCumulative}).`;
if (alertEmail) {
MailApp.sendEmail(alertEmail, 'Campaign Pacing Alert', message);
}
if (slackWebhook) {
try {
UrlFetchApp.fetch(slackWebhook, {
method: 'post',
contentType: 'application/json',
payload: JSON.stringify({text: message})
});
} catch (e) {
Logger.log('Slack post failed: ' + e);
}
}
}
}
How the pacing math works (practical example)
Example: 10-day campaign, totalBudget = $10,000
- Day 3 elapsed → expected cumulative spend = 10,000 * (3/10) = $3,000
- If actual cumulative = $1,800 → pacingRatio = 0.6 → below 0.85 threshold → flag for under-delivery
- If actual cumulative = $3,400 → pacingRatio = 1.133 → below 1.15 threshold (ok) but close; tune thresholds per campaign ROAS needs
Alerting best practices (2026)
- Use multi-channel alerts: email for owners, Slack/Teams for operations, and a Zapier webhook for cross-system actions (e.g., auto-pausing an inventory-sensitive campaign). If you’re tying spend to inventory or CRM, see approaches for live-commerce integrations.
- Rate-limit alerts: don’t spam. Use a caching field in settings like lastAlertAt and only send once per X hours.
- Use dynamic thresholds for ROAS-sensitive campaigns — e.g., if ROAS is above target, allow a looser overpacing threshold because high-value conversions justify faster spend.
Advanced strategies
1) Weighted pacing
Not all campaigns should run linear pacing. Use event-weighted pacing for promos that should front-load or back-load spend. Replace the linear expectedCumulative with a weighting vector (e.g., heavier in first 2 days) — similar to the timing strategies used in drop playbooks.
2) Multi-account management
Monitor many customerIds by batching queries per account and staggering runs to stay under Ads API quotas. Store the last fetched date per campaign in a Properties store to incrementalize pulls; small micro-app patterns help here.
3) Cross-check Google’s Total Budget setting
Pull the campaign_budget.amount_micros field and compare it with the totalBudget value you store in Settings. If they differ, surface a mismatch warning — this often happens when a campaign is transitioned to a total budget in the Ads UI.
4) Tie to inventory or CRM
If inventory is low or CRM lead targets are met, automatically reduce spend by notifying operations or calling an internal endpoint (via webhook) to pause the campaign. Use Zapier or a serverless function to map alert to action securely. For teams running live commerce or micro-revenue pop-ups, these hooks are essential.
Troubleshooting & common gotchas
- Auth Failures: double-check OAuth redirect URIs and that the Ads API scope is granted. In 2026, Google tightened consent screens for Ads scopes — ensure your app verification is complete if sharing with many users. See guidance on secure agent design and OAuth best practices.
- Developer token restrictions: sandbox tokens won’t return real cost data. Use production tokens and ensure your account access is permitted.
- API Quotas: for large-scale monitoring, paginate or segment date ranges. Use searchStream carefully — it can return NDJSON; parse robustly. Consider batching and serverless patterns to avoid hitting quotas.
- Currency micros: Ads API reports costs in micros. Always divide by 1e6 for human-readable dollars.
Real-world example (mini case study)
UK retailer --------------------------------
Related Reading
- Build a Micro-App in 7 Days: A Student Project Blueprint
- Serverless Edge for Tiny Multiplayer: Compliance, Latency, and Developer Tooling in 2026
- Programmatic with Privacy: Advanced Strategies for 2026 Ad Managers
- News & Analysis: Low‑Latency Tooling for Live Problem‑Solving Sessions — What Organizers Must Know in 2026
- Traveling with Pets to the Coast in 2026 — Carriers, Rules, and Comfort Tips
- How to vet new social platforms for safe esports communities (Bluesky, Digg and beyond)
- Scaling Production: Procurement and Financing Lessons from a Craft Syrup Maker
- Incident response for hotels: Playbook for last-mile failures (payment gateway, CDN, PMS)
- Smart Plugs vs. Smart Appliances: When to Automate Your Coffee Setup
Related Topics
spreadsheet
Contributor
Senior editor and content strategist. Writing about technology, design, and the future of digital media. Follow along for deep dives into the industry's moving parts.
Up Next
More stories handpicked for you