マキモノ
Google Workspace無料✅ 公式検証済みv1.0.0 / 更新

GASの夜間バッチを必ず完走させる設計(6分制限・再開・作り置き)

実行時間上限のある環境で長時間バッチが永久に完走しない3つの原因(リスト後方の飢餓・入力1件の変更で全チャンク破棄・自分の生成物を再入力)と、台帳による優先順/チャンク単位フィンガープリント/生成物除外での直し方。部分確定と失敗カウンタ期限切れ、検証手順つき。

出品者: kim@orgiast.jp2 DL📖 読込 約3,323トークン (約5円)💰 コスパ 26
トークン節約メーター71%節約
ゼロからAIに作らせた場合12万トークン
このMDを読ませた場合3.5万トークン

8.5万トークンの節約 (API料金換算で約130円分)。 要件定義・技術調査・試行錯誤ぶんのトークンがまるごと不要になります。※ 出品者申告とレビューに基づく推定値。モデル・タスク内容により変動します。

この巻物について

GASの夜間バッチを必ず完走させる設計(6分制限・再開・作り置き)」は、Google WorkspaceカテゴリのAI指示書(MDファイル)です。実行時間上限のある環境で長時間バッチが永久に完走しない3つの原因(リスト後方の飢餓・入力1件の変更で全チャンク破棄・自分の生成物を再入力)と、台帳による優先順/チャンク単位フィンガープリント/生成物除外での直し方。部分確定と失敗カウンタ期限切れ、検証手順つき。この巻物をAIに読み込ませると、ゼロから設計・調査する場合に比べて 約8.5万トークン(API料金換算で約130円)・71%のトークンを節約できます。

カテゴリ
Google Workspace
対応AI
claude-code、cursor、codex-cli
ライセンス
商用利用可 (再販不可)
価格
無料
ゼロから開発時
約12万トークン
この巻物使用時
約3.5万トークン
節約量
約8.5万トークン (約130円)
更新日
2026-08-26

使い方 (AIに渡す3つの方法)

いちばん簡単なのはワンライナー。Claude Code のターミナルに貼るだけです。

⬇ .md をダウンロード
claude "https://makimono-md.vercel.app/api/v1/files/gas-6/raw を読み込んで、この指示書どおりに実装して"
claude-codecursorcodex-cliライセンス: 商用利用可 (再販不可)

中身

GAS の夜間バッチを「必ず完走」させる設計(6分制限・再開・作り置き)

Google Apps Script のトリガーで「全レコードを LLM に読ませて要約を作り置きする」型のバッチは、 素直に書くと 永久に完走しない。実運用で5晩回しても 1/5 の進捗のまま止まった実例をもとに、 原因3つと、それぞれの直し方をコードの形で示す。GAS 以外(Lambda/Cloud Functions など 実行時間に上限があり、状態を外に持つしかない環境)でもそのまま通じる。

前提とする処理の形

夜間トリガー(例: 01:00〜04:00 を30分刻みで7回)
  → 対象レコード一覧を取得
  → 各レコードについて「添付資料を全部 LLM に投げて要約」
  → 結果をシート/DBに作り置き
  → 昼間の生成処理は作り置きを読むだけ(= 表示が速く、課金も1回で済む)

1回の実行は 6分(GAS の上限)で強制終了される。1レコードの処理が数十秒〜2分かかるなら、 1回の実行で進むのは1〜2件。ここから先が本題。


原因1: リストの後方が永久に飢える

// ❌ 毎回リストの先頭から走査する
for (var i = 0; i < records.length; i++) {
  if (Date.now() - started > START_CUTOFF_MS) break;   // 新規着手の締切
  var materials = collectMaterials(records[i]);         // ここが数秒かかる
  if (isUpToDate(records[i], materials)) continue;      // スキップ判定のために毎回走査している
  process(records[i]);
}

締切に達すると break するので、リスト後方のレコードには毎晩到達しない。 しかも「スキップ判定のため」に、更新の必要がないレコードまで毎回重いスキャンを払っている。

直し方: 順序決定を「安いデータ」だけで行う

進捗の台帳を Script Properties(= 外部ストア)に1件だけ持ち、 並べ替えの段階では外部 API を一切呼ばない。重いスキャンは「今回着手すると決めた分」にだけ払う。

var LEDGER_KEY = 'BATCH_LEDGER';
var RECHECK_MS = 20 * 60 * 60 * 1000;   // 完了済みの再確認は1日1回で十分

// 台帳: { "<id>": { s: '部分'|'完了', d: 済チャンク, t: 全チャンク, c: 最終確認ms, p: 部分になったms } }
function orderRecords(records, ledger, nowMs) {
  return (records || []).map(function (r, index) { return { r: r, i: index, e: ledger[String(r.id)] }; })
    // 完了済みで、かつ最近確認したものは今回の候補から外す
    .filter(function (x) { return !x.e || x.e.s !== '完了' || nowMs - (Number(x.e.c) || 0) >= RECHECK_MS; })
    .sort(function (a, b) {
      // 未着手(0) → 部分(1) → 完了(2)
      var ap = !a.e ? 0 : a.e.s === '部分' ? 1 : 2;
      var bp = !b.e ? 0 : b.e.s === '部分' ? 1 : 2;
      if (ap !== bp) return ap - bp;
      if (ap === 1) return (Number(a.e.p) || 0) - (Number(b.e.p) || 0) || a.i - b.i;  // 古い未完から
      if (ap === 2) return (Number(a.e.c) || 0) - (Number(b.e.c) || 0) || a.i - b.i;  // 古い確認から
      return a.i - b.i;
    }).map(function (x) { return x.r; });
}

カーソル(最後に処理した位置)より台帳の方がよい。カーソルは「失敗したものを飛ばし続ける」か 「同じものを再試行し続ける」のどちらかに転びやすいが、台帳は未完を必ず先に持ってくる。 1レコード処理するたびに台帳を保存すること(実行が途中で殺されても進捗が残る)。

Script Properties は 1値あたりのサイズに上限があるので、キーは短縮名にし、 上限に近づいたら「完了済みで最終確認が古いもの」から間引く。


原因2: 入力が1件変わると全チャンクを捨てて最初からやり直す

大きなレコードは資料を複数チャンクに割って、1実行=1チャンクで部分保存し次回再開する。 このとき進捗の再利用条件を「レコード全体のフィンガープリント一致」にすると——

// ❌ 全体 fp が1文字でも変われば、済んでいた要約を全部捨てる
if (storedFingerprint === currentFingerprint && storedState === '部分') { reuse(); }

稼働中のレコードは資料が動き続けるので、完走する前に必ずやり直しになる。 5晩かけても 1/5 のまま、という症状はこれ。

直し方: フィンガープリントをチャンク単位で持つ

function chunkFingerprint(chunkItems) {           // 全体 fp と同じハッシュ実装を共有する
  return fingerprint(chunkItems);                 // 対象は「そのチャンクに入る id:更新日時」だけ
}

// 保存時: 「チャンクN要約」の見出し行の隣のセルに、そのチャンクの fp を書く
rows.push(['チャンク' + (i + 1) + '要約', chunkFingerprints[i], '']);

// 再開時: インデックス一致ではなく fp 一致で引き当てる
function parseProgress(rows, fingerprint, chunkTotal, chunkFingerprints) {
  var byFingerprint = {}, summaries = [];
  // …保存済みの (fp → 要約) 辞書を作る…
  if (chunkFingerprints && Object.keys(byFingerprint).length) {
    chunkFingerprints.forEach(function (fp, index) {
      if (byFingerprint[fp]) summaries[index] = byFingerprint[fp];   // 変わっていないチャンクは流用
    });
  }
  return { summaries: summaries, chunkTotal: chunkTotal };
}

これで「資料が1件増えて 5→6 チャンクになった」ときも、既存5本の要約はそのまま使い、 新しい1本だけ課金される。状態が『完了』の行の要約も再利用対象に含めるのを忘れないこと (完了後に資料が1件増えるケースが最も多い)。

fp 列が無い旧データは従来条件(全体fp一致 かつ 部分)でのみ再利用する、と分けておけば後方互換になる。

処理ループも「先頭から連続」ではなく穴埋めに変える(途中のチャンクだけ欠けた状態が普通になる):

for (var i = 0; i < total; i++) {
  if (summaries[i]) continue;                                  // 流用できたものは飛ばす
  if (processed >= maxChunksPerRun || now() - started >= budget) break;
  summaries[i] = processChunk(i);
  processed++;
  savePartial(summaries);                                      // 1チャンクごとに部分保存
}

原因3: 自分の生成物を入力として読み直している(ハルシネーションの自己強化)

作り置きの出力をレコードのフォルダに書いていると、次回はそれが入力に混ざる。 害は2つ。①過去の生成物の誤りが「資料に書かれた事実」として固定される ②一次資料(規定・実測・図面)と二次生成物が同じ重みで混ざる。 さらに、生成するたびに入力集合が変わるので原因2のやり直しを毎回引き起こす

// ✅ 命名規約で機械的に外す。語ではなく「接頭辞」で判定するのが安全
function isAppGenerated(label, recordId) {
  var name = String(label || '').normalize('NFC');
  var id = String(recordId || '').normalize('NFC');
  if (!id || name.indexOf('議事録') >= 0) return false;         // 一次情報は残す(例外を先に置く)
  return name.indexOf(id + '_') === 0 || name.indexOf('<出力プレフィックス>') === 0;
}
  • 除外理由は reason: 'app_generated' のようにラベル付けし、デバッグコマンドから見えるようにする。
  • 「初版」「スライド」「手順」のような一般語で除外してはいけない。顧客・取引先が同じ語を使う。
  • 生成物の命名を最初から <recordId>_… / 固定プレフィックスに寄せておくと、この判定が1行で済む。

仕上げ: 止まったまま放置されない仕掛け

部分のまま滞留したら確定させる

入力が動き続けるレコードは、理屈上いつまでも「部分」になれる。逃げ道を用意する。

function shouldFinalizePartial(v) {
  return Number(v.chunksDone) >= 1 && Number(v.chunkTotal) > Number(v.chunksDone)
    && Number(v.partialSinceMs) > 0 && Number(v.nowMs) - Number(v.partialSinceMs) >= 5 * 24 * 3600 * 1000;
}

確定させるときは、それが部分であることを本文に残す注記 行を書き、読み出し側が本文先頭に「一部のみ反映」と付ける)。黙って完成扱いにしない。

失敗カウンタは必ず期限切れにする

無人ループには失敗の打ち切りが必要(外部 API を叩き続けてアカウントが凍結された実例がある)。 ただし打ち切りが永久だと、一過性の障害で対象が静かに消える。

function shouldSkipByFailure(v) {   // 5回失敗しても、3日経てばまた試す
  return Number(v.n) >= 5 && Number(v.lastMs) > 0 && Number(v.nowMs) - Number(v.lastMs) < 3 * 24 * 3600 * 1000;
}

注意: カウンタを「着手前に +1、成功で削除」する実装だと、実行時間上限で殺されるたびに増える。 優先順を変えて重いレコードを先頭に持ってくると、この事故を踏みやすくなる(だから期限切れが要る)。


検証のしかた(ここを省くと直った気になるだけ)

  1. 純関数に切り出してローカルでテストする。並べ替え・進捗の引き当て・確定条件・失敗判定は すべて外部 API を呼ばない純関数にできる。module.exports に足せば GAS 外のテストランナーで回る。
  2. 本番で1件を完走させる。読み取り専用のデバッグコマンドで状態を read-back し、 state: 完了済/全チャンク・本文の文字数を数字で確認する(「動いたはず」で終わらせない)。
  3. バッチ全体を1回手で走らせる。「全件走査できたか」(残り0か)を戻り値に含めておくと、 飢餓が直ったことを1回の実行で確認できる。
  4. 仮説は実測で棄却する。上の実例では「失敗カウンタ5回で永久停止しているのでは」を最初に疑ったが、 カウンタ一覧を読むと空だった。推測のまま層を足すと、本当の漏れ口は開いたまま残る。

コストの目安

1チャンク(PDF・画像を数MB分まとめて長文脈モデルへ)で $0.02 前後、6チャンク+統合で $0.12 前後。 チャンク単位の再利用が効いていれば、2回目以降は変わったチャンクの分だけしか課金されない。 「入力が変わっていなければ 0 円」を成立させるのが、この設計の一番の効果。

よくある質問

「GASの夜間バッチを必ず完走させる設計(6分制限・再開・作り置き)」とは何ですか?

実行時間上限のある環境で長時間バッチが永久に完走しない3つの原因(リスト後方の飢餓・入力1件の変更で全チャンク破棄・自分の生成物を再入力)と、台帳による優先順/チャンク単位フィンガープリント/生成物除外での直し方。部分確定と失敗カウンタ期限切れ、検証手順つき。

どれくらいトークン(費用)を節約できますか?

ゼロから開発すると約12万トークンかかりますが、この巻物を使えば約3.5万トークンで済みます。差し引き約8.5万トークン(API料金換算で約130円)・71%の節約です。

どうやって使いますか?

無料です。MDファイルを Claude Code などのAIに読み込ませるだけ。ワンライナーをターミナルに貼れば実装が始まります。要件定義や技術調査を省いて実装だけにトークンを使えます。

どのAIツールに対応していますか?

claude-code、cursor、codex-cli に対応しています。

商用利用できますか?

ライセンスは「商用利用可 (再販不可)」です。

🤝 自分でAIを動かすのは、まだ不安…という方へ

この巻物の内容を、AIを使うプロに丸ごと任せることもできます。姉妹サービスAI代行堂なら「LINEで頼むだけで、仕事が完成」。

AI代行堂を見る →

関連する巻物

この巻物、誰かのトークンも救えます

𝕏 で節約レシートをシェア