mcpField
G
google-ads-scripts

google-ads-scripts

github.com/cognyaiマーケティング・グロース11149 回インストール2 週間前に更新
リポジトリ

google-ads-scripts について

google-ads-scriptsは、Google Ads Scriptsを作成する人向けのJavaScriptリファレンススキルです。AdsApp、GAQL、自動化パターン、Google Sheets連携を扱い、スクリプトの作成、デバッグ、改善方法を説明します。

主な機能

  1. 01AdsAppのオブジェクトモデルとセレクターの解説
  2. 02GAQLクエリパターンの提示
  3. 03アカウントとMCCの自動化を網羅
  4. 04Google Sheetsとメール連携の例
  5. 05エラー処理と安全な運用方法

活用シーン

  1. 01予算ペース監視スクリプトの作成
  2. 02検索語句から除外キーワードを抽出
  3. 03Sheetsで品質スコアを記録
  4. 04スクリプトのデバッグや実行改善

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/cognyai/claude-code-marketing-skills --skill google-ads-scripts

インストール後、Claude Code で/google-ads-scriptsを入力して読み込まれたか確認します。

詳細

Google Ads Scripts リファレンス

Google Ads Scripts の完全なリファレンスです。Google Ads 内での JavaScript による自動化、AdsApp オブジェクトモデル、セレクターとイテレーター、GAQL クエリ、実行可能なコードを含む一般的なスクリプトパターン、Google Sheets との連携、MCC の並列処理、ベストプラクティスを解説します。

ドキュメント全文:https://cogny.com/docs/google-ads-scripts

使用方法

/google-ads-scripts                          # Full overview
/google-ads-scripts budget pacing            # Budget pacing monitor script
/google-ads-scripts negative keywords        # Search term negative keyword miner
/google-ads-scripts quality score tracker    # Quality score logging to Sheets
/google-ads-scripts MCC parallel             # MCC parallel processing pattern
/google-ads-scripts selectors                # Selector and iterator patterns
/google-ads-scripts GAQL                     # GAQL queries in scripts
/google-ads-scripts anomaly detection        # Anomaly alerting script

指示

あなたは Google Ads Scripts のエキスパートです。このリファレンスを使って、ユーザーが Google Ads Scripts を作成、デバッグ、最適化できるよう支援してください。適切なエラー処理とベストプラクティスを備えた、完全で実行可能なコードを提示してください。

ユーザーから質問されたら、以下の該当するセクションを見つけ、すぐに使える JavaScript コードとともに、正確で実用的な回答を提示してください。

ユーザーが引数として特定のトピックを指定した場合は、その分野に焦点を当ててください。それ以外の場合は、機能と一般的なパターンの概要を提示してください。

重要な原則:

  • アカウントを変更するスクリプトには、必ず DRY_RUN フラグを含めます
  • 変更を加える前に、変更内容をログに記録します
  • エラー処理には try/catch を使用します
  • 複雑なクエリには GAQL(AdsApp.search())を優先します
  • パフォーマンス向上のため、Google Sheets への書き込みはまとめて行います
  • 実行時間の上限が30分(MCCでは60分)であることをユーザーに伝えます

概要

Google Ads Scripts を使うと、JavaScript で Google Ads をプログラムから操作できます。スクリプトは Google Ads のウェブインターフェース内で直接実行されるため、外部サーバー、API キー、OAuth は不要です。

主な機能:

  • キャンペーン、広告グループ、広告、キーワード、アセットを読み取り、変更する
  • GAQL(Google Ads Query Language)を使用してパフォーマンスデータを取得する
  • ダッシュボードやログ記録のために Google Sheets と連携する
  • MailApp でメール通知を送信する
  • UrlFetchApp で HTTP リクエストを行う
  • スケジュールに沿って実行する(毎時、毎日、毎週、毎月)

単一アカウントのスクリプトと MCC スクリプト:

機能単一アカウントMCC スクリプト
対象範囲1つのアカウントMCC 配下のすべてのアカウント
エントリーポイントmain()AdsManagerApp を使用する main()
実行時間の上限30分60分
並列実行いいえはい、executeInParallel()

言語: JavaScript ES5(一部 ES6)。let、const、アロー関数、テンプレートリテラルは使用できます。async/await、import/export、class は使用できません。

AdsApp オブジェクトモデル

AdsApp オブジェクトは、単一アカウントのすべての操作のルートです。

エンティティ階層
AdsApp
  +-- campaigns()
  |     +-- adGroups()
  |     |     +-- ads()
  |     |     +-- keywords()
  |     |     +-- audiences()
  |     +-- extensions()
  +-- adGroups()        (account-level shortcut)
  +-- ads()             (account-level shortcut)
  +-- keywords()        (account-level shortcut)
  +-- negativeKeywords()
  +-- shoppingCampaigns()
  +-- videoCampaigns()
  +-- labels()
  +-- budgets()
  +-- biddingStrategies()
よく使うエンティティメソッド
// Status
entity.isEnabled()
entity.isPaused()
entity.isRemoved()
entity.enable()
entity.pause()
entity.remove()

// Naming
entity.getName()
entity.setName('New Name')

// Stats (requires date range)
entity.getStatsFor('LAST_30_DAYS')
entity.getStatsFor('20250101', '20250131')

// Stats object methods
var stats = entity.getStatsFor('LAST_30_DAYS');
stats.getImpressions()
stats.getClicks()
stats.getCtr()
stats.getAverageCpc()
stats.getCost()
stats.getConversions()
stats.getConversionRate()

セレクターとイテレーター

すべてのエンティティコレクションは、セレクターとイテレーターのパターンを使用します。これは、あらゆるスクリプトにおける基本的なデータアクセスパターンです。

セレクターメソッド
var keywords = AdsApp.keywords()
  .withCondition('Status = ENABLED')
  .withCondition('CampaignStatus = ENABLED')
  .withCondition('AdGroupStatus = ENABLED')
  .withCondition('Ctr < 0.01')
  .forDateRange('LAST_30_DAYS')
  .orderBy('Impressions DESC')
  .withLimit(100)
  .get();

.withCondition(condition) — エンティティを絞り込みます。演算子:=、!=、>、<、>=、<=、CONTAINS、DOES_NOT_CONTAIN、STARTS_WITH、CONTAINS_IGNORE_CASE、REGEXP_MATCH、IN []。

.withCondition('Name CONTAINS "brand"')
.withCondition('Name REGEXP_MATCH "^(buy|shop|order).*"')
.withCondition('QualityScore > 5')
.withCondition('LabelNames CONTAINS_ANY ["Priority", "Monitor"]')
.withCondition('CampaignName IN ["Search - Brand", "Search - Generic"]')

.forDateRange(dateRange) — 指標に基づく条件では必須です。事前定義された期間:TODAY、YESTERDAY、LAST_7_DAYS、THIS_WEEK_SUN_TODAY、THIS_WEEK_MON_TODAY、LAST_WEEK、LAST_14_DAYS、LAST_30_DAYS、LAST_BUSINESS_WEEK、LAST_WEEK_SUN_SAT、THIS_MONTH、LAST_MONTH、ALL_TIME。

// Custom date range
.forDateRange('20250101', '20250131')

.orderBy(orderSpec) — 結果を並べ替えます(ASC/DESC)。

.withLimit(limit) — 結果数に上限を設定します。

イテレーターパターン
var iterator = AdsApp.keywords()
  .withCondition('Status = ENABLED')
  .forDateRange('LAST_7_DAYS')
  .withCondition('Impressions > 100')
  .get();

while (iterator.hasNext()) {
  var keyword = iterator.next();
  var stats = keyword.getStatsFor('LAST_7_DAYS');
  Logger.log(keyword.getText() + ': ' + stats.getClicks() + ' clicks');
}

スクリプト内の GAQL

Google Ads Query Language を使用すると、スクリプト内から Google Ads API 全体に SQL のような方法でアクセスできます。

AdsApp.search(query)

JavaScript オブジェクトとして表された結果行のイテレーターを返します。

function main() {
  var query = "SELECT " +
    "campaign.name, " +
    "campaign.status, " +
    "metrics.impressions, " +
    "metrics.clicks, " +
    "metrics.cost_micros, " +
    "metrics.conversions " +
    "FROM campaign " +
    "WHERE campaign.status = 'ENABLED' " +
    "AND segments.date DURING LAST_30_DAYS " +
    "ORDER BY metrics.cost_micros DESC";

  var results = AdsApp.search(query);

  while (results.hasNext()) {
    var row = results.next();
    Logger.log(row.campaign.name +
      ' | Cost: $' + (row.metrics.costMicros / 1000000).toFixed(2) +
      ' | Conversions: ' + row.metrics.conversions);
  }
}
AdsApp.report(query)

エクスポート機能を備えたレポートオブジェクトを返します。大量のデータや Sheets との連携に最適です。

var report = AdsApp.report(
  "SELECT " +
  "ad_group_criterion.keyword.text, " +
  "ad_group_criterion.quality_info.quality_score, " +
  "metrics.impressions, " +
  "metrics.clicks, " +
  "metrics.conversions " +
  "FROM keyword_view " +
  "WHERE campaign.status = 'ENABLED' " +
  "AND segments.date DURING LAST_30_DAYS"
);

// Export directly to Google Sheets
var sheet = SpreadsheetApp.openByUrl('YOUR_SHEET_URL').getActiveSheet();
report.exportToSheet(sheet);
Search と Report の比較
機能AdsApp.search()AdsApp.report()
戻り値の型行イテレーターレポートオブジェクト
フィールドへのアクセスrow.campaign.name行イテレーターまたは exportToSheet()
Sheets へのエクスポート手動組み込みの exportToSheet()
最適な用途行ごとの処理一括エクスポート、ダッシュボード

よく使うスクリプトパターン

1. パフォーマンスの低いキーワードを一時停止する
function main() {
  var CONFIG = {
    MIN_QUALITY_SCORE: 4,
    MIN_CTR: 0.005,
    MIN_IMPRESSIONS: 100,
    DATE_RANGE: 'LAST_30_DAYS',
    DRY_RUN: true
  };

  var query = "SELECT " +
    "ad_group_criterion.keyword.text, " +
    "ad_group_criterion.keyword.match_type, " +
    "ad_group_criterion.quality_info.quality_score, " +
    "ad_group.name, " +
    "campaign.name, " +
    "metrics.impressions, " +
    "metrics.clicks, " +
    "metrics.ctr, " +
    "metrics.cost_micros, " +
    "metrics.conversions " +
    "FROM keyword_view " +
    "WHERE campaign.status = 'ENABLED' " +
    "AND ad_group.status = 'ENABLED' " +
    "AND ad_group_criterion.status = 'ENABLED' " +
    "AND metrics.impressions > " + CONFIG.MIN_IMPRESSIONS + " " +
    "AND segments.date DURING " + CONFIG.DATE_RANGE;

  var results = AdsApp.search(query);
  var pauseCount = 0;

  while (results.hasNext()) {
    var row = results.next();
    var qs = row.adGroupCriterion.qualityInfo.qualityScore;
    var ctr = row.metrics.ctr;

    if (qs < CONFIG.MIN_QUALITY_SCORE || ctr < CONFIG.MIN_CTR) {
      Logger.log('PAUSE: "' + row.adGroupCriterion.keyword.text + '"' +
        ' | QS: ' + qs + ' | CTR: ' + (ctr * 100).toFixed(2) + '%' +
        ' | Campaign: ' + row.campaign.name);

      if (!CONFIG.DRY_RUN) {
        var keywords = AdsApp.keywords()
          .withCondition('Text = "' + row.adGroupCriterion.keyword.text + '"')
          .withCondition('AdGroupName = "' + row.adGroup.name + '"')
          .withCondition('CampaignName = "' + row.campaign.name + '"')
          .get();
        if (keywords.hasNext()) keywords.next().pause();
      }
      pauseCount++;
    }
  }

  Logger.log('Total keywords to pause: ' + pauseCount);
  if (CONFIG.DRY_RUN) Logger.log('DRY RUN — no changes applied.');
}
2. 予算ペース管理モニター
function main() {
  var CONFIG = {
    MONTHLY_BUDGET: 10000,
    ALERT_THRESHOLD_OVER: 0.15,
    ALERT_THRESHOLD_UNDER: 0.20,
    ALERT_EMAIL: '[email protected]',
    CAMPAIGN_LABEL: 'Budget-Monitor'
  };

  var today = new Date();
  var daysInMonth = new Date(today.getFullYear(), today.getMonth() + 1, 0).getDate();
  var dayOfMonth = today.getDate();
  var expectedSpend = (CONFIG.MONTHLY_BUDGET / daysInMonth) * dayOfMonth;

  var firstOfMonth = Utilities.formatDate(
    new Date(today.getFullYear(), today.getMonth(), 1), 'UTC', 'yyyyMMdd');
  var todayStr = Utilities.formatDate(today, 'UTC', 'yyyyMMdd');

  var campaigns = AdsApp.campaigns()
    .withCondition('Status = ENABLED')
    .withCondition('LabelNames CONTAINS_ANY ["' + CONFIG.CAMPAIGN_LABEL + '"]')
    .forDateRange(firstOfMonth, todayStr)
    .get();

  var totalSpend = 0;
  var campaignDetails = [];

  while (campaigns.hasNext()) {
    var campaign = campaigns.next();
    var stats = campaign.getStatsFor(firstOfMonth, todayStr);
    var spend = stats.getCost();
    totalSpend += spend;
    campaignDetails.push({
      name: campaign.getName(),
      spend: spend,
      conversions: stats.getConversions()
    });
  }

  var paceRatio = totalSpend / expectedSpend;
  var projectedSpend = (totalSpend / dayOfMonth) * daysInMonth;
  var status = 'ON PACE';

  if (paceRatio > (1 + CONFIG.ALERT_THRESHOLD_OVER)) status = 'OVERSPENDING';
  else if (paceRatio < (1 - CONFIG.ALERT_THRESHOLD_UNDER)) status = 'UNDERSPENDING';

  Logger.log('Day ' + dayOfMonth + '/' + daysInMonth +
    ' | Expected: $' + expectedSpend.toFixed(2) +
    ' | Actual: $' + totalSpend.toFixed(2) +
    ' | Projected: $' + projectedSpend.toFixed(2) +
    ' | Status: ' + status);

  if (status !== 'ON PACE') {
    var body = 'Budget Pacing Alert\n\nStatus: ' + status +
      '\nDay ' + dayOfMonth + ' of ' + daysInMonth +
      '\nMonthly budget: $' + CONFIG.MONTHLY_BUDGET.toFixed(2) +
      '\nExpected spend: $' + expectedSpend.toFixed(2) +
      '\nActual spend: $' + totalSpend.toFixed(2) +
      '\nProjected: $' + projectedSpend.toFixed(2);

    MailApp.sendEmail(CONFIG.ALERT_EMAIL, '[Google Ads] Budget Alert: ' + status, body);
  }
}
3. 検索語句からの除外キーワード抽出
function main() {
  var CONFIG = {
    MIN_COST: 50,
    MAX_CONVERSIONS: 0,
    MIN_CLICKS: 10,
    DATE_RANGE: 'LAST_30_DAYS',
    NEGATIVE_MATCH_TYPE: 'EXACT',
    DRY_RUN: true,
    LOG_SHEET_URL: ''
  };

  var query = "SELECT " +
    "search_term_view.search_term, " +
    "campaign.name, " +
    "ad_group.name, " +
    "metrics.impressions, " +
    "metrics.clicks, " +
    "metrics.cost_micros, " +
    "metrics.conversions " +
    "FROM search_term_view " +
    "WHERE campaign.status = 'ENABLED' " +
    "AND metrics.cost_micros > " + (CONFIG.MIN_COST * 1000000) + " " +
    "AND metrics.clicks > " + CONFIG.MIN_CLICKS + " " +
    "AND metrics.conversions <= " + CONFIG.MAX_CONVERSIONS + " " +
    "AND segments.date DURING " + CONFIG.DATE_RANGE;

  var results = AdsApp.search(query);
  var negatives = [];

  while (results.hasNext()) {
    var row = results.next();
    var term = row.searchTermView.searchTerm;
    var cost = row.metrics.costMicros / 1000000;

    negatives.push({ term: term, campaign: row.campaign.name, cost: cost });

    Logger.log('NEGATIVE: "' + term + '" | Cost: $' + cost.toFixed(2) +
      ' | Clicks: ' + row.metrics.clicks + ' | Campaign: ' + row.campaign.name);

    if (!CONFIG.DRY_RUN) {
      var campaigns = AdsApp.campaigns()
        .withCondition('Name = "' + row.campaign.name + '"').get();
      if (campaigns.hasNext()) {
        var campaign = campaigns.next();
        if (CONFIG.NEGATIVE_MATCH_TYPE === 'EXACT') {
          campaign.createNegativeKeyword('[' + term + ']');
        } else if (CONFIG.NEGATIVE_MATCH_TYPE === 'PHRASE') {
          campaign.createNegativeKeyword('"' + term + '"');
        } else {
          campaign.createNegativeKeyword(term);
        }
      }
    }
  }

  Logger.log('Total negative keyword candidates: ' + negatives.length);
}
4. 無効な URL のチェック
function main() {
  var CONFIG = {
    ALERT_EMAIL: '[email protected]',
    PAUSE_BROKEN: false,
    MIN_IMPRESSIONS: 10,
    DATE_RANGE: 'LAST_7_DAYS'
  };

  var brokenUrls = [];

  var ads = AdsApp.ads()
    .withCondition('Status = ENABLED')
    .withCondition('CampaignStatus = ENABLED')
    .withCondition('AdGroupStatus = ENABLED')
    .forDateRange(CONFIG.DATE_RANGE)
    .withCondition('Impressions > ' + CONFIG.MIN_IMPRESSIONS)
    .get();

  while (ads.hasNext()) {
    var ad = ads.next();
    var url = ad.urls().getFinalUrl();
    if (url) {
      var status = checkUrl(url);
      if (status !== 200) {
        brokenUrls.push({
          campaign: ad.getCampaign().getName(),
          adGroup: ad.getAdGroup().getName(),
          url: url,
          httpStatus: status
        });
        Logger.log('BROKEN [' + status + ']: ' + url);
        if (CONFIG.PAUSE_BROKEN) ad.pause();
      }
    }
  }

  if (brokenUrls.length > 0 && CONFIG.ALERT_EMAIL) {
    var body = 'Broken URL Report\n\n';
    for (var i = 0; i < brokenUrls.length; i++) {
      body += 'HTTP ' + brokenUrls[i].httpStatus + ' | ' +
        brokenUrls[i].campaign + ' | ' + brokenUrls[i].url + '\n';
    }
    MailApp.sendEmail(CONFIG.ALERT_EMAIL,
      '[Google Ads] ' + brokenUrls.length + ' Broken URLs', body);
  }
}

function checkUrl(url) {
  try {
    var response = UrlFetchApp.fetch(url, {
      muteHttpExceptions: true,
      followRedirects: true,
      validateHttpsCertificates: false
    });
    return response.getResponseCode();
  } catch (e) {
    return 0;
  }
}
5. 時間帯別の入札単価調整(デイパーティング)
function main() {
  var CONFIG = {
    DATE_RANGE: 'LAST_30_DAYS',
    MAX_BID_MODIFIER: 0.30,
    MIN_BID_MODIFIER: -0.50,
    MIN_CONVERSIONS: 5
  };

  var query = "SELECT " +
    "campaign.name, " +
    "segments.hour, " +
    "metrics.impressions, " +
    "metrics.clicks, " +
    "metrics.conversions, " +
    "metrics.cost_micros " +
    "FROM campaign " +
    "WHERE campaign.status = 'ENABLED' " +
    "AND segments.date DURING " + CONFIG.DATE_RANGE;

  var results = AdsApp.search(query);
  var hourlyData = {};

  while (results.hasNext()) {
    var row = results.next();
    var name = row.campaign.name;
    var hour = row.segments.hour;

    if (!hourlyData[name]) hourlyData[name] = {};
    if (!hourlyData[name][hour]) {
      hourlyData[name][hour] = { clicks: 0, conversions: 0, cost: 0 };
    }
    hourlyData[name][hour].clicks += row.metrics.clicks;
    hourlyData[name][hour].conversions += row.metrics.conversions;
    hourlyData[name][hour].cost += row.metrics.costMicros / 1000000;
  }

  for (var campaignName in hourlyData) {
    var hours = hourlyData[campaignName];
    var totalConv = 0, totalClicks = 0;
    for (var h in hours) {
      totalConv += hours[h].conversions;
      totalClicks += hours[h].clicks;
    }
    var avgCvr = totalClicks > 0 ? totalConv / totalClicks : 0;

    Logger.log('\n' + campaignName + ' | Avg CVR: ' + (avgCvr * 100).toFixed(2) + '%');

    for (var hour = 0; hour < 24; hour++) {
      var data = hours[hour] || { clicks: 0, conversions: 0 };
      var hourCvr = data.clicks > 0 ? data.conversions / data.clicks : 0;
      var modifier = 0;

      if (avgCvr > 0 && data.conversions >= CONFIG.MIN_CONVERSIONS) {
        modifier = (hourCvr / avgCvr) - 1;
        modifier = Math.max(CONFIG.MIN_BID_MODIFIER, Math.min(CONFIG.MAX_BID_MODIFIER, modifier));
      }

      Logger.log('  Hour ' + hour + ' | Conv: ' + data.conversions +
        ' | Modifier: ' + (modifier > 0 ? '+' : '') + (modifier * 100).toFixed(0) + '%');
    }
  }
}
6. 品質スコアの追跡

品質スコアの履歴を Google Sheets に記録します(Google Ads では品質スコアの履歴が標準で保持されません)。

function main() {
  var CONFIG = {
    SHEET_URL: 'YOUR_GOOGLE_SHEET_URL',
    SHEET_NAME: 'QS History',
    MIN_IMPRESSIONS: 50
  };

  var sheet = SpreadsheetApp.openByUrl(CONFIG.SHEET_URL)
    .getSheetByName(CONFIG.SHEET_NAME);

  if (sheet.getLastRow() === 0) {
    sheet.appendRow([
      'Date', 'Campaign', 'Ad Group', 'Keyword', 'Match Type',
      'Quality Score', 'Expected CTR', 'Ad Relevance', 'Landing Page Exp',
      'Impressions', 'Clicks', 'Cost', 'Conversions'
    ]);
  }

  var today = Utilities.formatDate(new Date(), 'UTC', 'yyyy-MM-dd');

  var query = "SELECT " +
    "campaign.name, ad_group.name, " +
    "ad_group_criterion.keyword.text, ad_group_criterion.keyword.match_type, " +
    "ad_group_criterion.quality_info.quality_score, " +
    "ad_group_criterion.quality_info.search_predicted_ctr, " +
    "ad_group_criterion.quality_info.creative_quality_score, " +
    "ad_group_criterion.quality_info.post_click_quality_score, " +
    "metrics.impressions, metrics.clicks, metrics.cost_micros, metrics.conversions " +
    "FROM keyword_view " +
    "WHERE campaign.status = 'ENABLED' " +
    "AND ad_group.status = 'ENABLED' " +
    "AND ad_group_criterion.status = 'ENABLED' " +
    "AND metrics.impressions > " + CONFIG.MIN_IMPRESSIONS + " " +
    "AND segments.date DURING LAST_7_DAYS";

  var results = AdsApp.search(query);
  var rows = [];

  while (results.hasNext()) {
    var row = results.next();
    var qi = row.adGroupCriterion.qualityInfo;
    rows.push([
      today, row.campaign.name, row.adGroup.name,
      row.adGroupCriterion.keyword.text, row.adGroupCriterion.keyword.matchType,
      qi.qualityScore || '', qi.searchPredictedCtr || '',
      qi.creativeQualityScore || '', qi.postClickQualityScore || '',
      row.metrics.impressions, row.metrics.clicks,
      (row.metrics.costMicros / 1000000).toFixed(2), row.metrics.conversions
    ]);
  }

  if (rows.length > 0) {
    sheet.getRange(sheet.getLastRow() + 1, 1, rows.length, rows[0].length).setValues(rows);
  }
  Logger.log('Logged ' + rows.length + ' keywords');
}
7. 広告文のテスト(RSA のパフォーマンス)
function main() {
  var CONFIG = {
    MIN_IMPRESSIONS: 500,
    DATE_RANGE: 'LAST_30_DAYS',
    WINNER_CTR_LIFT: 0.20,
    LOSER_CTR_DROP: -0.20,
    ALERT_EMAIL: '[email protected]'
  };

  var query = "SELECT " +
    "campaign.name, ad_group.name, ad_group_ad.ad.id, " +
    "ad_group_ad.ad_strength, " +
    "metrics.impressions, metrics.clicks, metrics.ctr, " +
    "metrics.conversions, metrics.cost_micros " +
    "FROM ad_group_ad " +
    "WHERE campaign.status = 'ENABLED' AND ad_group.status = 'ENABLED' " +
    "AND ad_group_ad.status = 'ENABLED' " +
    "AND ad_group_ad.ad.type = 'RESPONSIVE_SEARCH_AD' " +
    "AND metrics.impressions > " + CONFIG.MIN_IMPRESSIONS + " " +
    "AND segments.date DURING " + CONFIG.DATE_RANGE;

  var results = AdsApp.search(query);
  var adGroups = {};

  while (results.hasNext()) {
    var row = results.next();
    var agKey = row.campaign.name + ' > ' + row.adGroup.name;
    if (!adGroups[agKey]) adGroups[agKey] = [];
    adGroups[agKey].push({
      adId: row.adGroupAd.ad.id,
      strength: row.adGroupAd.adStrength,
      impressions: row.metrics.impressions,
      clicks: row.metrics.clicks,
      ctr: row.metrics.ctr,
      conversions: row.metrics.conversions
    });
  }

  var winners = [], losers = [];

  for (var agKey in adGroups) {
    var ads = adGroups[agKey];
    if (ads.length < 2) continue;

    var totalClicks = 0, totalImpr = 0;
    for (var i = 0; i < ads.length; i++) {
      totalClicks += ads[i].clicks;
      totalImpr += ads[i].impressions;
    }
    var avgCtr = totalImpr > 0 ? totalClicks / totalImpr : 0;

    for (var j = 0; j < ads.length; j++) {
      var ctrLift = avgCtr > 0 ? (ads[j].ctr - avgCtr) / avgCtr : 0;
      if (ctrLift >= CONFIG.WINNER_CTR_LIFT) winners.push({ adGroup: agKey, ad: ads[j], lift: ctrLift });
      else if (ctrLift <= CONFIG.LOSER_CTR_DROP) losers.push({ adGroup: agKey, ad: ads[j], lift: ctrLift });
    }
  }

  Logger.log('Winners: ' + winners.length + ' | Losers: ' + losers.length);
}
8. ROAS に基づく予算配分
function main() {
  var CONFIG = {
    TOTAL_DAILY_BUDGET: 500,
    MIN_BUDGET: 20,
    MAX_BUDGET: 200,
    DATE_RANGE: 'LAST_14_DAYS',
    CAMPAIGN_LABEL: 'Auto-Budget',
    MIN_CONVERSIONS: 3,
    DRY_RUN: true
  };

  var campaigns = AdsApp.campaigns()
    .withCondition('Status = ENABLED')
    .withCondition('LabelNames CONTAINS_ANY ["' + CONFIG.CAMPAIGN_LABEL + '"]')
    .forDateRange(CONFIG.DATE_RANGE)
    .get();

  var campaignData = [];
  while (campaigns.hasNext()) {
    var campaign = campaigns.next();
    var stats = campaign.getStatsFor(CONFIG.DATE_RANGE);
    var cost = stats.getCost();
    var convValue = stats.getConversionValue();

    campaignData.push({
      campaign: campaign,
      name: campaign.getName(),
      cost: cost,
      roas: cost > 0 ? convValue / cost : 0,
      conversions: stats.getConversions(),
      currentBudget: campaign.getBudget().getAmount()
    });
  }

  campaignData.sort(function(a, b) { return b.roas - a.roas; });

  var totalRoas = 0;
  for (var i = 0; i < campaignData.length; i++) {
    if (campaignData[i].conversions >= CONFIG.MIN_CONVERSIONS) totalRoas += campaignData[i].roas;
  }

  for (var j = 0; j < campaignData.length; j++) {
    var c = campaignData[j];
    var newBudget = (c.conversions >= CONFIG.MIN_CONVERSIONS && totalRoas > 0)
      ? (c.roas / totalRoas) * CONFIG.TOTAL_DAILY_BUDGET
      : CONFIG.MIN_BUDGET;

    newBudget = Math.max(CONFIG.MIN_BUDGET, Math.min(CONFIG.MAX_BUDGET, Math.round(newBudget * 100) / 100));

    Logger.log(c.name + ' | ROAS: ' + c.roas.toFixed(2) +
      ' | Current: $' + c.currentBudget.toFixed(2) + ' | New: $' + newBudget.toFixed(2));

    if (!CONFIG.DRY_RUN) c.campaign.getBudget().setAmount(newBudget);
  }

  if (CONFIG.DRY_RUN) Logger.log('DRY RUN — no budgets changed.');
}
9. 異常検知
function main() {
  var CONFIG = {
    LOOKBACK_DAYS: 14,
    SPIKE_THRESHOLD: 2.0,
    DROP_THRESHOLD: 0.5,
    ALERT_EMAIL: '[email protected]',
    METRICS: ['impressions', 'clicks', 'cost_micros', 'conversions']
  };

  var today = new Date();
  var yesterday = new Date(today.getTime() - 86400000);
  var lookbackStart = new Date(today.getTime() - (CONFIG.LOOKBACK_DAYS + 1) * 86400000);

  var yesterdayStr = Utilities.formatDate(yesterday, 'UTC', 'yyyyMMdd');
  var lookbackStr = Utilities.formatDate(lookbackStart, 'UTC', 'yyyyMMdd');

  var query = "SELECT campaign.name, " +
    "metrics.impressions, metrics.clicks, metrics.cost_micros, " +
    "metrics.conversions, segments.date " +
    "FROM campaign WHERE campaign.status = 'ENABLED' " +
    "AND segments.date BETWEEN '" +
      lookbackStr.substring(0, 4) + '-' + lookbackStr.substring(4, 6) + '-' + lookbackStr.substring(6, 8) +
    "' AND '" +
      yesterdayStr.substring(0, 4) + '-' + yesterdayStr.substring(4, 6) + '-' + yesterdayStr.substring(6, 8) + "'";

  var results = AdsApp.search(query);
  var campaignDays = {};

  while (results.hasNext()) {
    var row = results.next();
    var name = row.campaign.name;
    var date = row.segments.date;
    if (!campaignDays[name]) campaignDays[name] = {};
    campaignDays[name][date] = {
      impressions: row.metrics.impressions,
      clicks: row.metrics.clicks,
      cost_micros: row.metrics.costMicros,
      conversions: row.metrics.conversions
    };
  }

  var alerts = [];
  var yesterdayDate = Utilities.formatDate(yesterday, 'UTC', 'yyyy-MM-dd');

  for (var campaignName in campaignDays) {
    var days = campaignDays[campaignName];
    var yesterdayData = days[yesterdayDate];
    if (!yesterdayData) continue;

    for (var m = 0; m < CONFIG.METRICS.length; m++) {
      var metric = CONFIG.METRICS[m];
      var sum = 0, count = 0;
      for (var d in days) {
        if (d !== yesterdayDate) { sum += days[d][metric] || 0; count++; }
      }
      if (count === 0) continue;
      var avg = sum / count;
      var current = yesterdayData[metric] || 0;

      if (avg > 0) {
        var ratio = current / avg;
        if (ratio >= CONFIG.SPIKE_THRESHOLD || ratio <= CONFIG.DROP_THRESHOLD) {
          alerts.push({
            campaign: campaignName, metric: metric,
            type: ratio >= CONFIG.SPIKE_THRESHOLD ? 'SPIKE' : 'DROP',
            current: current, average: avg, ratio: ratio
          });
        }
      }
    }
  }

  if (alerts.length > 0) {
    var body = 'Anomaly Report — ' + yesterdayDate + '\n\n';
    for (var a = 0; a < alerts.length; a++) {
      body += alerts[a].type + ': ' + alerts[a].campaign +
        ' | ' + alerts[a].metric + ' | ' + alerts[a].ratio.toFixed(2) + 'x avg\n';
    }
    MailApp.sendEmail(CONFIG.ALERT_EMAIL, '[Google Ads] ' + alerts.length + ' Anomalies', body);
  }
}
10. ランディングページのパフォーマンス
function main() {
  var CONFIG = {
    DATE_RANGE: 'LAST_30_DAYS',
    MIN_CLICKS: 50,
    SHEET_URL: 'YOUR_GOOGLE_SHEET_URL',
    SHEET_NAME: 'Landing Pages'
  };

  var query = "SELECT " +
    "landing_page_view.unexpanded_final_url, " +
    "metrics.clicks, metrics.impressions, " +
    "metrics.cost_micros, metrics.conversions, " +
    "metrics.conversions_value " +
    "FROM landing_page_view " +
    "WHERE segments.date DURING " + CONFIG.DATE_RANGE + " " +
    "AND metrics.clicks > " + CONFIG.MIN_CLICKS + " " +
    "ORDER BY metrics.clicks DESC";

  var results = AdsApp.search(query);
  var pages = [];

  while (results.hasNext()) {
    var row = results.next();
    var cost = row.metrics.costMicros / 1000000;
    var cvr = row.metrics.clicks > 0 ? row.metrics.conversions / row.metrics.clicks : 0;
    var roas = cost > 0 ? row.metrics.conversionsValue / cost : 0;

    pages.push([
      row.landingPageView.unexpandedFinalUrl,
      row.metrics.clicks, row.metrics.impressions,
      cost.toFixed(2), row.metrics.conversions,
      row.metrics.conversionsValue.toFixed(2),
      (cvr * 100).toFixed(2) + '%', roas.toFixed(2)
    ]);
  }

  if (CONFIG.SHEET_URL && pages.length > 0) {
    var sheet = SpreadsheetApp.openByUrl(CONFIG.SHEET_URL)
      .getSheetByName(CONFIG.SHEET_NAME);
    sheet.clear();
    sheet.appendRow(['URL', 'Clicks', 'Impressions', 'Cost', 'Conversions',
      'Conv Value', 'CVR', 'ROAS']);
    sheet.getRange(2, 1, pages.length, pages[0].length).setValues(pages);
    Logger.log('Exported ' + pages.length + ' landing pages');
  }
}

Google Sheets との連携

スプレッドシートを開く
var ss = SpreadsheetApp.openByUrl('https://docs.google.com/spreadsheets/d/SHEET_ID/edit');
var ss = SpreadsheetApp.openById('SHEET_ID');
var sheet = ss.getSheetByName('Data');
データの読み取り
var values = sheet.getRange('A1:D10').getValues();  // 2D array
var allData = sheet.getDataRange().getValues();
var value = sheet.getRange('B2').getValue();         // Single cell
データの書き込み
sheet.getRange('A1').setValue('Campaign Name');
sheet.appendRow(['2025-02-11', 'Brand', 150, 12]);

// Batch write (much faster than appendRow in a loop)
var data = [['Campaign', 'Clicks'], ['Brand', 150], ['Generic', 300]];
sheet.getRange(1, 1, data.length, data[0].length).setValues(data);

メールと通知

// Simple email
MailApp.sendEmail('[email protected]', 'Subject', 'Body text');

// HTML email
MailApp.sendEmail({
  to: '[email protected]',
  subject: '[Google Ads] Weekly Report',
  htmlBody: '<h1>Report</h1><p>Details here.</p>'
});

スケジュール設定

ツールと設定 > 一括操作 > スクリプトで設定します。利用可能な頻度は、毎時、毎日、毎週、毎月です。

推奨スケジュール:

  • 予算ペース管理:毎日または毎時
  • 除外キーワードの抽出:毎週
  • 品質スコアの追跡:毎日
  • 異常検知:毎日(朝)
  • 無効な URL のチェック:毎週
  • ダッシュボード:毎日

エラー処理

function main() {
  try {
    processKeywords();
  } catch (e) {
    Logger.log('ERROR: ' + e.message + '\nStack: ' + e.stack);
    MailApp.sendEmail('[email protected]',
      '[Google Ads Script] Error', 'Error: ' + e.message);
  }
}

実行時間の制限: 30 分(単一アカウント)、60 分(MCC)。適切に処理します。

var startTime = new Date().getTime();
var MAX_RUNTIME = 25 * 60 * 1000;  // 25 min buffer

while (iterator.hasNext()) {
  if (new Date().getTime() - startTime > MAX_RUNTIME) {
    Logger.log('Approaching time limit. Stopping.');
    break;
  }
  // Process...
}

MCC スクリプト

基本的な MCC スクリプト
function main() {
  var accounts = AdsManagerApp.accounts()
    .withCondition('Impressions > 0')
    .forDateRange('LAST_7_DAYS')
    .withLimit(50)
    .get();

  while (accounts.hasNext()) {
    var account = accounts.next();
    AdsManagerApp.select(account);
    Logger.log('Processing: ' + account.getName());
    // Now AdsApp refers to this account
  }
}
executeInParallel() による並列処理
function main() {
  var accounts = AdsManagerApp.accounts()
    .withCondition('Impressions > 0')
    .forDateRange('LAST_7_DAYS')
    .get();

  accounts.executeInParallel('processAccount', 'aggregateResults');
}

function processAccount() {
  var account = AdsApp.currentAccount();
  var result = { name: account.getName(), id: account.getCustomerId(), campaigns: [] };

  var campaigns = AdsApp.campaigns()
    .withCondition('Status = ENABLED')
    .forDateRange('LAST_7_DAYS').get();

  while (campaigns.hasNext()) {
    var c = campaigns.next();
    var s = c.getStatsFor('LAST_7_DAYS');
    result.campaigns.push({
      name: c.getName(), clicks: s.getClicks(),
      cost: s.getCost(), conversions: s.getConversions()
    });
  }

  return JSON.stringify(result);  // Must return string
}

function aggregateResults(results) {
  for (var i = 0; i < results.length; i++) {
    if (results[i].getStatus() === 'OK') {
      var data = JSON.parse(results[i].getReturnValue());
      Logger.log(data.name + ': ' + data.campaigns.length + ' campaigns');
    } else {
      Logger.log('Error: ' + results[i].getError());
    }
  }
}

executeInParallel() のルール:

  • アカウントごとの関数は文字列(JSON.stringify())を返す必要があります
  • コールバックは ExecutionResult オブジェクト(.getStatus()、.getReturnValue()、.getError()、.getCustomerId())の配列を受け取ります
  • 1 回の呼び出しにつき最大 50 アカウント
  • 各アカウントの実行時間は最大 30 分

ベストプラクティス

  1. まずプレビューする — 実行前に必ず「プレビュー」をクリックします
  2. 変更前にログを記録する — keyword.pause() の前に Logger.log('Pausing: ' + keyword.getText()) を実行します
  3. DRY_RUN フラグ — 変更を行うスクリプトには、すべて設定に DRY_RUN: true を含めます
  4. 段階的に展開する — まず 1 つのキャンペーンで開始し、検証後に範囲を広げます
  5. エラー通知 — 失敗時にはメールを送信し、ログの確認だけに頼らないようにします
  6. ラベルを使用する — キャンペーン名をハードコードせず、ラベルを使ってスクリプトの対象を指定します
  7. Sheets への書き込みをまとめる — ループ内で appendRow() を使わず、2 次元配列で setValues() を使用します
  8. スクリプトを文書化する — 目的、スケジュール、作成者、設定の説明を含むコメントブロックを追加します

制限事項

制限事項詳細
実行時間30 分(単一アカウント)、60 分(MCC)
async/await は非対応同期実行のみ
ES6 のサポートは限定的import/export および class は使用できません
ディスプレイ/動画 API は非対応スクリプト経由のサポートは限定的
Performance Max は非対応読み取り専用(レポート機能は限定的)
URL フェッチ応答の上限は 50 MB、タイムアウトは 60 秒
MailApp1 日あたり 100 件の宛先
npm/モジュールは非対応外部ライブラリをインポートできません
executeInParallel()1 回の呼び出しにつき最大 50 アカウント

リソース

コメント