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

外部アプリがフォーム連携シートに行を足すと通知が飛ばず埋もれる問題を潰す(GAS)

フォームの onFormSubmit は直接 appendRow では発火せず、挿入行は回答追加のたびに押し下げられ、担当者名も空になる。3症状を「フォーム外の書き込み」という一因で捉え、列マップ確定・通知・名寄せまでアプリ側で完結させる実装指示書。

出品者: kim@orgiast.jp📖 読込 約3,926トークン (約6円)💰 コスパ 19
トークン節約メーター84%節約
ゼロからAIに作らせた場合9万トークン
このMDを読ませた場合1.4万トークン

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

この巻物について

外部アプリがフォーム連携シートに行を足すと通知が飛ばず埋もれる問題を潰す(GAS)」は、Google WorkspaceカテゴリのAI指示書(MDファイル)です。フォームの onFormSubmit は直接 appendRow では発火せず、挿入行は回答追加のたびに押し下げられ、担当者名も空になる。3症状を「フォーム外の書き込み」という一因で捉え、列マップ確定・通知・名寄せまでアプリ側で完結させる実装指示書。この巻物をAIに読み込ませると、ゼロから設計・調査する場合に比べて 約7.6万トークン(API料金換算で約110円)・84%のトークンを節約できます。

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

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

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

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

中身

外部アプリがGoogleフォーム連携シートに行を足すと「通知されない・埋もれる・記入者が空」になる問題を潰す(GAS)

前提と症状

Googleフォームの回答シート(<回答シートID> / シート名は任意にリネーム可)を、現場が「依頼台帳」としてそのまま運用しているケースは多い。 そこへフォームを通さず外部アプリ(GAS等)が appendRow で行を足すと、必ず次の3つが同時に起きる。

  1. 通知が飛ばない — Discord/Slack通知はフォームの onFormSubmit から出ているので、シートへの直接書き込みでは発火しない(onEdit/onChangeスクリプトによる変更では発火しないので、トリガーで拾おうとしても解決しない)。
  2. 行が埋もれる — フォーム回答はフォームが管理する回答ブロックの末尾に行を挿入して入るため、後から appendRow した行は回答が増えるたびに下へ押し下げられ、担当者の視界から消える。
  3. 記入者名が空/ラベルだけ — 担当者名を別台帳(例: 進捗管理表の 担当P:氏名 セル)から取ってくる実装だと、そこが空欄の案件で 担当P: というラベルだけが書かれる。

この3つは別々の不具合に見えるが、**「フォーム外の書き込みはフォームの仕組みの外にいる」**という一つの原因から出ている。

直し方(アプリ側で完結させる。フォーム側スクリプトは触らない)

フォーム側の onFormSubmit に相乗りしようとしないこと。フォーム側は別プロジェクト(フォームにバインドされたスクリプト)であることが多く、 権限も改修責任も別。書いた本人(アプリ)が通知まで責任を持つのが最小構成。

1. 列マップを実データから確定させる

推測で書かない。ヘッダー行を読み出して列番号を固定する。フォーム連携シートは質問の追加で列が増えるので、 「A=タイムスタンプ, B=依頼者名, C=種別 …」のような先頭の共通列だけを定数化し、残りは用途別ブロックとして扱う。

var SHEET_ID = '<回答シートID>';
var SHEET_NAME = '<シート名>';
var SHEET_GID = <gid>;            // 行リンクに使う
var ROLE_ID_DEFAULT = '<ロールID>'; // 通知でメンションするロール

シートの gid は「wrong gid で読んでシート一覧をエラーに含めて返す」デバッグ関数を1つ用意しておくと、1往復で判る。

function debug_readExternalSheet(ssId, gid, a1Range) {
  var ss = SpreadsheetApp.openById(ssId), target = null;
  ss.getSheets().forEach(function (s) { if (s.getSheetId() === gid) target = s; });
  if (!target) return { error: 'gid not found', sheets: ss.getSheets().map(function (s) { return { name: s.getName(), gid: s.getSheetId() }; }) };
  var r = target.getRange(a1Range);
  return { sheetName: target.getName(), lastRow: target.getLastRow(), lastCol: target.getLastColumn(), values: r.getValues() };
}

2. 常に「いちばん上」に入れる(appendRow をやめる)

複数行を1回の処理で書くときはまとめて挿入する。1行ずつ insertRowBefore(2) すると順序が逆転する。 挿入行はヘッダー行の書式を引き継いでしまうので、既存データ行から書式をコピーする。

function insertRowsTop_(rowsValues, sheetOverride) {
  if (!rowsValues || rowsValues.length === 0) return [];
  var sheet = sheetOverride || SpreadsheetApp.openById(SHEET_ID).getSheetByName(SHEET_NAME);
  var rowCount = rowsValues.length;
  var colCount = rowsValues.reduce(function (m, r) { return Math.max(m, r.length); }, 0);
  var padded = rowsValues.map(function (r) { var a = r.slice(); while (a.length < colCount) a.push(''); return a; });

  sheet.insertRowsBefore(2, rowCount);                 // 行1=ヘッダーは触らない
  var originalSecond = 2 + rowCount;                   // 挿入前の2行目
  if (sheet.getLastRow() >= originalSecond) {
    var w = sheet.getMaxColumns();
    sheet.getRange(originalSecond, 1, 1, w)
      .copyTo(sheet.getRange(2, 1, rowCount, w), SpreadsheetApp.CopyPasteType.PASTE_FORMAT, false);
  }
  sheet.getRange(2, 1, rowCount, colCount).setValues(padded);
  SpreadsheetApp.flush();

  return rowsValues.map(function (r, i) {              // read-back verify(silent ignore 対策)
    var row = 2 + i;
    if (String(sheet.getRange(row, 3).getDisplayValue()) !== String(r[2] == null ? '' : r[2])) {
      throw new Error('read-back verify に失敗: row ' + row);
    }
    return row;
  });
}

⚠️ 残リスクを必ず明記して運用に渡すこと: フォーム連携シートの回答ブロック先頭に行を挿し込む挙動は、 Google 側の仕様変更や運用によって影響が出ないと言い切れない。次のフォーム回答が入ったときに 「2行目のアプリ行が残っているか」「回答が正しい位置に入ったか」を1回だけ確認する運用チェックを入れる。 壊れた場合の退避策は「最下部 append に戻し、通知の行リンクで飛ばす」。

3. 記入者名は「ラベル剥がし+フォールバック+要確認」の3段で埋める

参照元が 担当P:氏名 のようなラベル付きテキストなら、全角/半角コロンと全角スペースの両方を潰す。 空だったときに黙って空欄を書かない。代理欄→自動名義の順に落とし、同時に「要確認」を人の見る場所へ積む。

function producerName_(value) {
  return String(value == null ? '' : value)
    .replace(/^[\s ]*担当[PD][\s ]*[::][\s ]*/, '')
    .replace(/^[\s ]+|[\s ]+$/g, '');
}
var name = producerName_(primaryRaw) || producerName_(deputyRaw);
var requesterName = name || '<アプリ名>(自動)';
if (!name) confirmations.push('依頼者名が未設定です。<参照元台帳> の担当欄を埋めてください');

同じ氏名を複数列に書く必要があることが多い(一覧用の「依頼者名」列と、種別ごとの「担当者名」列)。 片方だけ埋めると現場では「記載がない」と言われる。種別 → 書き込む列 の対応表を1箇所に持つ。

4. 通知は「1回の処理で1通」+行への直リンク

種別ごとに2通送らない(うるさい上に webhook のレート制限に当たる)。本文は既存のフォーム通知の書式に寄せると現場が読み替えなくて済む。

function buildNotifyText_(p) {
  var parts = [
    '<@&' + p.roleId + '>',
    '<台帳名>に登録がありました(<アプリ名>からの自動登録・フォーム外)。',
    '日時: ' + p.timestampText,                     // 純関数にする(中で new Date() を呼ばない=テスト可能)
    '----------', '【依頼者名】', p.requesterName, '',
    '【依頼内容】', p.requestTypes.join(' / '), '',
    '【案件】', p.clientName + ' / ' + p.caseName
  ];
  if (p.rows && p.rows.length) {
    parts.push('', '【登録行】');
    p.rows.forEach(function (it) {
      parts.push(it.label + ': ' + it.row + '行目 https://docs.google.com/spreadsheets/d/' + SHEET_ID +
        '/edit?gid=' + SHEET_GID + '&range=A' + it.row);
    });
    parts.push('※行番号は登録時点のものです(新しい行が上に入るとずれます)');
  }
  var text = parts.join('\n');
  return text.length <= 1900 ? text : text.substring(0, 1900 - 6) + '…(省略)';  // Discord は2000字上限
}

function notify_(text) {
  var hook = PropertiesService.getScriptProperties().getProperty('<WEBHOOK_PROP名>');
  if (!hook) return { sent: false, reason: 'webhook 未設定' };
  var options = { method: 'post', contentType: 'application/json',
                  payload: JSON.stringify({ content: text }), muteHttpExceptions: true };
  try {
    var res = UrlFetchApp.fetch(hook, options), code = res.getResponseCode();
    if (code === 429) { Utilities.sleep(3000); res = UrlFetchApp.fetch(hook, options); code = res.getResponseCode(); }
    return (code >= 200 && code < 300) ? { sent: true, code: code, reason: '' }
                                       : { sent: false, code: code, reason: 'HTTP ' + code };
  } catch (e) { return { sent: false, reason: String(e) }; }  // 通知失敗で本処理を落とさない
}

戻り値に sentreason を必ず入れ、画面のステータス文にも出す。旧実装のように sent=false を握り潰すと、 「webhook 未設定のまま数か月通知ゼロ」に気づけない(実際にこれで放置された)。

5. webhook は「作る」から自動化できる

Bot トークン(ウェブフックの管理 権限)があれば、チャンネルIDから webhook を作ってURLを取得できる。 人にDiscordの設定画面を触らせない。

POST https://discord.com/api/v10/channels/<channelId>/webhooks
Authorization: Bot <token>     body: {"name":"<通知名>"}
→ レスポンスの id と token を `<Discord APIホスト>/api/webhooks/<id>/<token>` の形に組み立てたものが webhook URL

既存 webhook を再利用したいときは同URLに GET(token 付きで返る)。取得したURLは Script Properties へ入れる (コードにも台帳にも平文で置かない。設定用の管理コマンドを1つ用意して、そこ経由で入れる)。

検証(ここを省くと「直ったつもり」になる)

  1. 純関数の単体テスト: 名前の正規化(全角/半角コロン、全角スペース、null、既に素の氏名)、通知本文(メンション・行リンク・省略)を node --test で固める。
  2. 通知の実送信: メンションを含まない接続テスト用関数を1つ用意して実際に投げ、チャンネルに着弾したことを目視/API取得で確認する。「webhook を設定した」だけで完了報告しない。
  3. 挿入の実シート検証: ダミー行を先頭に挿入 → 読み戻し → 即削除する使い捨てコマンドで、本番シートに痕跡を残さず確認する。
  4. 既存の壊れた行の手当て: 過去に書かれた不完全な行は、「アプリが書いた行だけ」を判定する条件付きでまとめて直す。 例: B列が /^担当[PD]\s*[::]/ で始まる行だけ を対象にし、それ以外(人が入れた行・フォーム行)は必ずスキップして結果に理由を返す。 書いたら flush()getDisplayValue() で read-back verify する。

つまずきポイント

  • onEdit/onChangeスクリプトによる変更では発火しない。「トリガーで拾えばフォーム通知に相乗りできる」は成立しない。
  • フォーム連携シートはシート名をリネームできるgetSheetByName('<元の名前>') を信じていると、ある日 silent に落ちる。gid を控えておく。
  • setValue は結合セル・保護範囲・入力規則違反で黙って無視される。書いたら必ず読み戻す。
  • 通知本文の行リンクは #gid= ではなく ?gid=<gid>&range=A<row> にすると行までジャンプする。
  • webhook のレート制限(HTTP 429 / Cloudflare の error code 1015)は実際に踏む。1処理1通にし、429 のときだけ数秒待って1回だけ再送する。
  • 「重複登録」に気づける仕組みを入れる。同一案件で数分差の同内容行が2組できるのは、再実行や修正リトライの跡。人が二重に手配してしまう。

よくある質問

「外部アプリがフォーム連携シートに行を足すと通知が飛ばず埋もれる問題を潰す(GAS)」とは何ですか?

フォームの onFormSubmit は直接 appendRow では発火せず、挿入行は回答追加のたびに押し下げられ、担当者名も空になる。3症状を「フォーム外の書き込み」という一因で捉え、列マップ確定・通知・名寄せまでアプリ側で完結させる実装指示書。

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

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

どうやって使いますか?

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

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

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

商用利用できますか?

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

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

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

AI代行堂を見る →

関連する巻物

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

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