この記事を読む前に
この記事でわかること
- Google広告スクリプトは実行時間30分の制限があり、大規模アカウントで途中終了するため、分割処理と冪等性が必須です。
- 処理済みラベルで進捗を記録し、何度実行しても結果が同じになる構造にすることで、タイムアウト対策ができます。
- 複数アカウント運用ではMCCの並列実行機能を使い、最大50アカウントを同時処理することで、運用の効率化が実現できます。
生成AIによる要約
目次開く閉じる
- 動いたスクリプトが運用で壊れる理由
- 実行制限を前提に設計する
- 実行時間の上限は30分
- イテレータとセレクタの件数上限
- ポリシー上の条件も確認する
- タイムアウトに強い書き方
- 経過時間を監視して安全に中断する
- 同じ処理を2回実行しても壊れない構造にする
- executeInParallelで複数アカウントを並列実行する
- 並列実行の基本形
- 並列実行には固有の制約がある
- レポート取得はAdsApp.reportとAdsApp.searchを使い分ける
- AdsApp.reportとAdsApp.searchの違い
- フィールド名の表記で起きる典型的なミス
- データ保持期間の変更に対応する
- 外部サービスと連携する
- エラーを検知して通知する
- 通知の設計で決めておくこと
- 運用に乗せるための体制づくり
- 承認と権限を把握しておく
- 変更履歴で追跡できるようにする
- 切り戻しの手順を決めておく
- スクリプトから移行を検討するタイミング
- まとめ
- 関連記事
動いたスクリプトが運用で壊れる理由
Google広告スクリプトは、書いて動かすところまでは比較的短時間でたどり着けます。むずかしいのはその先です。毎日決まった時刻に、担当者が見ていない状態で、意図した処理だけを実行し続ける状態をつくることが本題になります。
実務でスクリプトが期待どおりに動かなくなる原因は、おおむね3つに整理できます。1つ目は、アカウントの規模が大きくなり実行時間の上限に達するケース。2つ目は、参照しているデータの仕様変更を受けるケースです。そして3つ目が、エラーに誰も気づかないケースになります。
この記事では、入門段階では踏み込まない実装上の論点を扱います。スクリプトの基本的な書き方や導入手順はGoogle広告スクリプト入門|自動化できること・導入手順・実用テンプレートで解説しています。未読の方は、そちらを先にご覧ください。
| つまずく場面 | 典型的な症状 | 対処の方向性 |
|---|---|---|
| 実行時間の上限 | 大規模アカウントで途中終了する | 分割処理と冪等性の担保 |
| 複数アカウント運用 | 1アカウントずつ処理して間に合わない | MCCでの並列実行 |
| データ仕様の変更 | ある日から値が取れなくなる | 集計単位と保存先の見直し |
| エラーの見落とし | 数週間動いていなかったと後で判明 | 失敗時の通知設計 |
実行制限を前提に設計する
スクリプトの制限値は公式ドキュメントに明記されています。この数字を知らずに書くと、アカウントが成長した段階で突然動かなくなります。
実行時間の上限は30分
単一アカウントで実行するスクリプトの実行時間は、最大30分です。マネージャーアカウント(MCC)から実行する場合も通常は30分ですが、完了関数を指定した並列実行を使うと最大60分まで延長されます。
上限に達したスクリプトは、その時点で強制的に終了します。エラーとして記録されますが、それまでに適用された変更は取り消されません。
運用メモ 実行時間の上限に達したときの挙動は、処理の途中で電源が落ちるイメージに近いです。入札単価を書き換える処理であれば、一部のキーワードだけ新しい値、残りは古い値という状態で止まります。
イテレータとセレクタの件数上限
取得件数にも上限があります。イテレータで一度に取得できるのは50,000件までです。withLimit() で明示的に絞り込めますが、指定しない場合でもこの上限が適用されます。
セレクタでIDを指定して絞り込む場合は、10,000IDが上限です。これを超えるとランタイムエラーになります。
ログの出力量にも制限があり、100KBを超えた分は切り捨てられます。デバッグのつもりで全件をログに出すと、肝心な最後のエラーメッセージが消えることがあります。
| 項目 | 上限 | 超過時の挙動 |
|---|---|---|
| 実行時間(単一アカウント) | 30分 | 強制終了。変更は取り消されない |
| 実行時間(MCC並列実行) | 60分 | 強制終了 |
| イテレータの取得件数 | 50,000件 | 以降が取得されない |
| セレクタのID指定 | 10,000ID | ランタイムエラー |
| ログ出力 | 100KB | 超過分が切り捨て |
ポリシー上の条件も確認する
Google広告のポリシーには、スクリプトの利用に関する記載もあります。公式ドキュメントのベストプラクティスに従わないまま、1日あたり100万件を超える読み取りや10万件を超える編集を行うことは、ポリシー違反にあたります。
上限を回避する目的で処理を分散させる行為や、公式に用意されていない方法でのアクセスは禁止されています。ポリシー違反と判断された場合、予告なく利用を停止される可能性があります。
タイムアウトに強い書き方
実行時間の上限は、処理を分割することで乗り越えられます。ポイントは、途中で止まっても次回の実行で続きから再開できる構造にすることです。
経過時間を監視して安全に中断する
実行時間を計測し、上限に達する前に自ら処理を止めます。強制終了ではなく自主的に抜けることで、ログの書き出しや通知の送信まで確実に完了できます。
/**
* 処理済みをラベルで記録しながら、時間内に収まる分だけ処理する。
* 途中で終了しても、次回の実行が未処理分から再開する。
*/
function main() {
var LABEL_NAME = 'script-processed';
var TIME_LIMIT_MS = 25 * 60 * 1000; // 30分の上限に対し5分の余裕を残す
var startedAt = new Date().getTime();
ensureLabel(LABEL_NAME);
var keywords = AdsApp.keywords()
.withCondition('Status = ENABLED')
.forDateRange('LAST_30_DAYS')
.get();
var processed = 0;
var skipped = 0;
while (keywords.hasNext()) {
if (new Date().getTime() - startedAt > TIME_LIMIT_MS) {
Logger.log('時間上限が近いため中断します。処理: ' + processed + '件');
return;
}
var keyword = keywords.next();
if (hasLabel(keyword, LABEL_NAME)) {
skipped++;
continue;
}
// ここに実際の処理を記述する
keyword.applyLabel(LABEL_NAME);
processed++;
}
Logger.log('全件の処理が完了しました。処理: ' + processed + '件/スキップ: ' + skipped + '件');
}
/**
* 指定名のラベルが存在しなければ作成する。
* @param {string} name ラベル名
*/
function ensureLabel(name) {
var labels = AdsApp.labels().withCondition("Name = '" + name + "'").get();
if (!labels.hasNext()) {
AdsApp.createLabel(name);
}
}
/**
* エンティティに指定名のラベルが付いているかを判定する。
* @param {Object} entity キーワードや広告などのエンティティ
* @param {string} name ラベル名
* @return {boolean} 付いていれば true
*/
function hasLabel(entity, name) {
var labels = entity.labels().get();
while (labels.hasNext()) {
if (labels.next().getName() === name) {
return true;
}
}
return false;
}
すべての処理が終わったらラベルを外す運用にすれば、翌月また同じ仕組みで回せます。ラベルの一括削除は管理画面からも実行できます。
運用メモ 件数が多いアカウントでは、セレクタの段階で処理済みラベルを除外しないと問題が起きます。取得上限の50,000件を処理済み分が占めてしまい、未処理のエンティティに到達しないためです。速度だけでなく再開の確実性にも関わるため、
withCondition()でラベルを条件に含める書き方を優先してください。記述方法は公式リファレンスで最新の仕様を確認できます。
同じ処理を2回実行しても壊れない構造にする
分割処理では、同じエンティティが2回処理される可能性を常に考える必要があります。入札単価を10%引き上げる処理を2回実行すれば、21%上がってしまいます。
対策は2つあります。ひとつは処理済みの記録を先ほどのラベルで管理する方法、もうひとつは目標値そのものを計算して設定する方法です。
// 避けたい書き方:現在値を基準にするため、実行回数だけ変化が累積する
keyword.bidding().setCpc(keyword.bidding().getCpc() * 1.1);
// 推奨する書き方:あるべき値を計算して設定するため、何度実行しても結果が同じ
var targetCpc = calculateTargetCpc(keyword);
keyword.bidding().setCpc(targetCpc);
何度実行しても結果が変わらない性質を冪等性と呼びます。自動化の設計では、この性質を確保できているかどうかが安定性を大きく左右します。
executeInParallelで複数アカウントを並列実行する
管理するアカウントが10を超えるあたりから、1つずつスクリプトを設置する運用は負担が大きくなります。MCCから配下のアカウントを一括処理する仕組みを使いましょう。
並列実行の基本形
executeInParallel() は、選択したアカウントごとに処理関数を並列実行します。アカウント数が多いほど、順番に処理する場合との差は大きくなります。
/**
* MCC配下のアカウントを並列処理し、結果をまとめて集計する。
*/
function main() {
var accountSelector = AdsManagerApp.accounts()
.withCondition('Impressions > 0')
.forDateRange('YESTERDAY')
.withLimit(50); // 並列実行は最大50アカウント
accountSelector.executeInParallel('processAccount', 'summarize');
}
/**
* 各アカウントで実行される処理。戻り値は文字列に限られる。
* @return {string} JSON文字列に変換した集計結果
*/
function processAccount() {
var account = AdsApp.currentAccount();
var stats = account.getStatsFor('YESTERDAY');
return JSON.stringify({
name: account.getName(),
cost: stats.getCost(),
conversions: stats.getConversions()
});
}
/**
* すべてのアカウントの処理が終わった後に1回だけ実行される。
* @param {Array} results 各アカウントの実行結果
*/
function summarize(results) {
var rows = [];
var failed = [];
for (var i = 0; i < results.length; i++) {
if (results[i].getStatus() !== 'OK') {
failed.push(results[i].getCustomerId());
continue;
}
rows.push(JSON.parse(results[i].getReturnValue()));
}
Logger.log('集計できたアカウント: ' + rows.length + '件');
if (failed.length > 0) {
Logger.log('処理に失敗したアカウント: ' + failed.join(', '));
}
}
並列実行には固有の制約がある
便利な仕組みですが、制約を理解しておかないと想定外の動きをします。
並列実行できるのは最大50アカウントまでです。この数を超えるアカウントを選択するとエラーになります。50を超える規模では、条件やラベルでアカウントを分割し、複数のスクリプトに分けて設置します。
処理関数が返せるのは文字列だけで、サイズは10MBまでです。オブジェクトをそのまま返すことはできないため、JSON.stringify() で変換します。
| 項目 | 制約 | 実務上の対応 |
|---|---|---|
| 対象アカウント数 | 最大50 | 条件やラベルで分割し、複数スクリプトに配置 |
| 実行時間 | 最大60分 | 処理関数側でも軽量化を意識する |
| 戻り値の型 | 文字列のみ | JSON文字列に変換して受け渡す |
| 戻り値のサイズ | 10MBまで | 明細ではなく集計値を返す |
| 失敗時の扱い | アカウント単位で記録 | 完了関数で失敗分を検知して通知 |
運用メモ 一部のアカウントで失敗しても、他のアカウントの処理は継続します。完了関数で
getStatus()を確認しないと、失敗に気づかないまま「正常終了」として記録されます。
レポート取得はAdsApp.reportとAdsApp.searchを使い分ける
スクリプトからレポートを取得する方法は2種類あります。どちらも現行の方式で、内部的にはGoogle Ads APIのクエリ言語であるGAQLを使っています。以前のクエリ言語であるAWQLは使えなくなっており、AdsApp.report() もGAQLで記述します。
AdsApp.reportとAdsApp.searchの違い
AdsApp.report() は、行を平坦な辞書型で返します。列名をそのままキーとして参照でき、スプレッドシートへの書き出しに向いています。
AdsApp.search() は、階層構造を持つオブジェクトを返します。プログラムの中で値を加工する処理に向いています。
| 観点 | AdsApp.report() | AdsApp.search() |
|---|---|---|
| 戻り値の形 | 平坦な辞書型 | 階層を持つオブジェクト |
| 値の参照 | row["campaign.id"] | row.campaign.id |
| 向いている用途 | スプレッドシート出力 | プログラム内での加工 |
| 補助機能 | シートへの直接エクスポートが可能 | 逐次処理に向く |
フィールド名の表記で起きる典型的なミス
AdsApp.search() を使うときにつまずきやすいのが、フィールド名の表記です。GAQLのクエリではアンダースコア区切りで書きますが、返ってくるオブジェクトのキーはキャメルケースになります。
/**
* 直近30日のキャンペーン実績を、費用の多い順にスプレッドシートへ出力する。
*/
function main() {
var SPREADSHEET_URL = 'https://docs.google.com/spreadsheets/d/xxxxxxxxxx/edit';
var query =
'SELECT campaign.name, metrics.cost_micros, metrics.conversions ' +
'FROM campaign ' +
'WHERE segments.date DURING LAST_30_DAYS ' +
'AND campaign.status = "ENABLED" ' +
'ORDER BY metrics.cost_micros DESC';
var rows = [['キャンペーン名', '費用', 'コンバージョン数']];
var iterator = AdsApp.search(query);
while (iterator.hasNext()) {
var row = iterator.next();
// クエリでは cost_micros、戻り値では costMicros になる
rows.push([
row.campaign.name,
Number(row.metrics.costMicros) / 1000000,
Number(row.metrics.conversions)
]);
}
writeToSheet(SPREADSHEET_URL, rows);
Logger.log('出力した行数: ' + (rows.length - 1));
}
/**
* 二次元配列をスプレッドシートの1枚目のシートに書き出す。
* @param {string} url スプレッドシートのURL
* @param {Array} rows 書き出す二次元配列
*/
function writeToSheet(url, rows) {
var sheet = SpreadsheetApp.openByUrl(url).getSheets()[0];
sheet.clear();
sheet.getRange(1, 1, rows.length, rows[0].length).setValues(rows);
}
費用を表す cost_micros はマイクロ単位で返るため、100万で割って通貨単位に直します。この処理を忘れると、桁の大きな数字がそのまま並ぶことになります。
データ保持期間の変更に対応する
2026年6月1日から、Google広告のデータ保持期間が変更されました。時間別・日別・週別の詳細なデータは37か月、月別・四半期別・年別の集計データは11年で区切られます。
この変更は管理画面だけでなく、スクリプトやAPI経由の取得にも適用されます。3年以上前のデータを日別で取得していたスクリプトは、ある時点から値が返らなくなります。
長期の推移を追う用途では、クエリの単位を見直しましょう。segments.date で3年分を取得していた処理は、segments.month に切り替えることで11年分まで参照できます。
ただし、リーチと頻度の指標は3年で区切られます。この2つは集計単位を変えても長期の参照ができないため、外部への書き出しが必要です。
運用メモ 過去データを長期保管したい場合は、BigQueryなど外部の保存先に定期的に書き出す方法が現実的です。詳しくはGoogle広告のデータをBigQueryに自動転送する|Data Transfer Serviceの設定ガイドをご覧ください。
外部サービスと連携する
スクリプトの価値は、Google広告の外側とつながったときに大きくなります。スプレッドシート、メール、そして外部のWebサービスが主な連携先です。
エラーを検知して通知する
自動化で見落としの影響が大きいのは、止まっていることに気づかない状態です。実行に失敗したときだけ通知が飛ぶ仕組みを、最初に組み込んでおきます。
/**
* 処理本体を try/catch で囲み、失敗時に通知したうえでエラーを再送出する。
*/
function main() {
var SCRIPT_NAME = '予算監視スクリプト';
try {
checkBudgetProgress();
} catch (e) {
notifySlack('[失敗] ' + SCRIPT_NAME + '\n' + e.message);
throw e; // 実行履歴にもエラーとして残すため再送出する
}
}
/**
* 月間予算に対する進捗を確認し、閾値を超えたら通知する。
*/
function checkBudgetProgress() {
var MONTHLY_BUDGET = 3000000; // 月間の広告費上限
var THRESHOLD = 0.9;
var account = AdsApp.currentAccount();
var stats = account.getStatsFor('THIS_MONTH');
var ratio = stats.getCost() / MONTHLY_BUDGET;
Logger.log('今月の予算進捗率: ' + Math.round(ratio * 100) + '%');
if (ratio >= THRESHOLD) {
notifySlack(
'[警告] ' + account.getName() +
' の広告費が月間上限の' + Math.round(ratio * 100) + '%に達しました。'
);
}
}
/**
* Slackの Incoming Webhook にメッセージを送信する。
* @param {string} text 送信するテキスト
*/
function notifySlack(text) {
var WEBHOOK_URL = 'https://hooks.slack.com/services/xxxxx/xxxxx/xxxxx';
var response = UrlFetchApp.fetch(WEBHOOK_URL, {
method: 'post',
contentType: 'application/json',
payload: JSON.stringify({ text: text }),
muteHttpExceptions: true
});
if (response.getResponseCode() !== 200) {
Logger.log('通知に失敗しました: ' + response.getContentText());
}
}
muteHttpExceptions を有効にすると、通知先がエラーのステータスコードを返した場合でも例外にならず、処理を続けられます。ただし接続そのものに失敗したときは例外が発生するため、必要に応じてtry/catchで囲みます。
通知の設計で決めておくこと
通知は多すぎると読まれなくなります。何を通知し、何を通知しないかを事前に決めておきましょう。
実務で機能しやすいのは、失敗したときと閾値を超えたときだけ通知する設計です。正常終了の通知は、週次のまとめとして別の頻度で送るほうが読まれます。
| 通知する場面 | 頻度の目安 | 送信先の例 |
|---|---|---|
| スクリプトの実行失敗 | 発生時に即時 | 運用担当者のチャンネル |
| 閾値を超えた変化の検知 | 発生時に即時 | 運用担当者のチャンネル |
| 正常終了と処理件数 | 週次でまとめて | ログ用のチャンネル |
運用に乗せるための体制づくり
スクリプトは書いた人だけのものにしないことが大切です。担当者の異動や退職で動かなくなる仕組みは、資産ではなく負債になります。
承認と権限を把握しておく
スクリプトの実行には、Googleアカウントによる承認が必要です。承認した本人がアカウントから削除されると、そのスクリプトは実行されなくなります。
再開するには、別の管理者が改めて承認します。担当者の変更時には、設置しているスクリプトの承認状態を必ず確認しましょう。
承認の状態は、Googleアカウントのセキュリティ設定にあるサードパーティアクセスの管理画面からも確認と取り消しができます。
変更履歴で追跡できるようにする
スクリプトが適用した変更は、Google広告の変更履歴に記録されます。予期しない設定変更が見つかったとき、人の操作なのかスクリプトなのかを切り分ける手がかりになります。
スクリプト自体の実行結果は、管理画面のスクリプト一覧から確認できます。実行日時、ステータス、ログの3点です。
運用メモ 何をするスクリプトなのか、誰が承認しているのか、失敗したら誰に連絡が飛ぶのか。この3点をスプレッドシートで一覧管理しておくと、引き継ぎが格段に楽になります。
切り戻しの手順を決めておく
プレビュー機能の使い方と段階的な導入の進め方は入門編で解説しています。ここでは、実践編で押さえておきたい点を挙げます。
MCCからの並列実行では、プレビューの結果もアカウントごとに記録されます。全体の件数だけを見て判断せず、想定と違う挙動をしているアカウントがないかを確認しましょう。
そのうえで、切り戻しの手順を決めておきます。適用された変更は自動では元に戻せないため、変更履歴で影響範囲を特定し、元の値へ戻す流れになります。実行前の値をスプレッドシートへ書き出しておくと、この復旧が現実的になります。
スクリプトから移行を検討するタイミング
自動化手段そのものの比較は入門編で扱っています。ここでは、スクリプトから次の手段へ移す判断のトリガーを整理します。
移行を判断する基準は3つあります。実行時間30分の上限に収まらなくなったとき、他システムとの双方向の連携が必要になったとき、そして運用担当者向けの管理画面を用意したくなったときです。
このいずれかに当てはまるなら、Google Ads APIへの移行を検討する段階といえます。逆に1つも当てはまらないのであれば、スクリプトの中で分割や冪等性を工夫するほうが効果的です。
Google Ads APIを使った実装は開発の負担が大きくなりますが、生成AIを開発に組み込むことで着手のハードルは下がってきました。具体的な進め方はClaude CodeからGoogle広告をAPI入稿する実践ガイド|ステップ分割とガードレールの設計で解説しています。
なお、ショッピング広告のフィードを扱うスクリプトでは、Content API for Shoppingが2026年8月18日に提供を終了しました。移行が済んでいない処理は、Merchant APIへの切り替えが必要です。
まとめ
Google広告スクリプトを運用に乗せる段階では、書き方よりも設計の判断が成果を左右します。
実行時間30分という上限を前提に、途中で止まっても続きから再開できる構造にします。同じ処理を2回実行しても結果が変わらない冪等性も確保します。この2点を押さえておくと、スクリプトの安定性は高まります。
複数アカウントを扱うなら executeInParallel() で並列化し、50アカウントの上限と戻り値の制約を織り込みます。レポートは AdsApp.search() と AdsApp.report() を用途で使い分け、フィールド名の表記差に注意します。
そして特に効果が大きいのが、失敗に気づける仕組みです。エラー時の通知を最初に組み込んでおけば、止まっていることに数週間気づかない事態は避けられます。
まずは現在稼働しているスクリプトに、try/catchと通知を追加するところから始めてみてください。
関連記事
この記事をシェア
参照元
ryottaman
広告代理店で17年、現在は事業会社でインハウスマーケティングを担当しています。
SIGNALZの記事はAIを活用して作成し、10年以上の運用型広告の実務経験をもとに内容を確認・監修しています。制作方針の詳細はサイトについてをご覧ください。
Google検索の優先ソースにSIGNALZを追加
追加すると、Google検索のトップニュースでSIGNALZの記事が見つけやすくなります。AIモードとAIによる概要でも「優先」バッジが付きます。設定はいつでも解除できます。