9
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Workspace Studio × GAS】AIが生成した複数行データをスプレッドシートへ安全に一括展開&承認フローを自動化するアーキテクチャ

9
Last updated at Posted at 2026-10-23

【Workspace Studio × GAS】AIが生成した複数行データをスプレッドシートへ安全に一括展開&承認フローを自動化するアーキテクチャ

1. はじめに

Google Workspace Studio(旧AppSheet/Duet AI連携フロー等)の生成AIアクションを活用すると、外部データの自動要約や抽出ワークフローをノーコードで簡単に構築できます。

しかし、実務で運用しようとすると以下の大きな壁に直面します。

  • ノーコード側の制約: AIが一度に生成した「複数件の配列データ(JSON Array)」を、スプレッドシートの Add Row(1行追加)アクションでそのまま分解・一括展開できない。
  • 承認ステータスの管理: 生成されたデータを人間が確認し、チェックを入れたら本番シートへ安全に自動移送したい。

本記事では、この課題を**「受信用バッファシート + Google Apps Script(GAS)」**の組み合わせで解決し、AIによるデータ抽出から人間による承認、チャット通知までを完全に自動連携させたアーキテクチャを解説します。


2. 想定されるユースケース(Human-in-the-Loopの実現)

本アーキテクチャは、「AIに候補を複数ピックアップさせ、最終判断(承認)だけを人間がチェックボックスで行う」 という Human-in-the-Loop(人間参加型) の業務プロセス全般に適用できます。

  • CS・サポート管理: 日々の問い合わせログから「改善タスク候補」を抽出し、リーダーが承認した項目だけを開発バックログへ登録
  • PM・プロジェクト進行: 議事録テキストから「ToDo・ネクストアクション」を抽出し、担当者が確定したものだけ本番タスクシートへ反映
  • マーケ・リサーチ: 業界ニュースやフィードから「社内共有すべきトピック」をAIが要約し、担当者チェックで全社ポータルへ自動配信
  • 総務・調達管理: 備品リクエストから「購入候補」を洗い出し、決裁者のチェックで正式な発注手配リストへ転記

3. アーキテクチャの全体像

[Workspace Studio: 定期実行]
  │ ① 既存シートのデータを読み込み(重複除外のプロンプトへ注入)
  │ ② Gemini が新規データをJSON配列形式で抽出
  │ ③「受信用バッファ」シートのA列へJSONテキストを一発書き込み(Add Row)
  ▼
[スプレッドシート(GAS: 変更時トリガー)]
  │ ④ バッファ行を検知し、JSONをパース
  │ ⑤「要確認_提案リスト」へ複数行を一括展開 + A列に動的チェックボックスを付与
  │ ⑥ 用済みのバッファ行を削除
  ▼
[Google Chat 通知]
  │ ⑦ 担当者専用スペースへ通知(疑似DM)
  ▼
[管理者による確認(GAS: onEdit)]
  │ ⑧ A列のチェックボックスを [✓] に変更
  │ ⑨ 行が自動で「本番リスト」へ転記され、提案リストからは削除

4. Workspace Studio (Gemini) のプロンプト設定例

バッファシートを経由してGASに正しくデータをパースさせるためには、AIからの出力形式を厳密にコントロールする必要があります。
以下は、議事録からのタスク抽出や、新規アイデアの自動生成など、どんな業務にも使い回せる汎用的なプロンプトのテンプレートです。

【役割と目的】
あなたは優秀なアシスタントです。
提供されたテキストから、必要なタスク(または抽出対象のデータ)をピックアップしてください。

【出力ルール】
対象となるデータを1〜3件抽出し、以下のJSON配列形式のみで出力してください。
該当データがない場合は、無理に生成せず空の配列 [] を返してください。

[
  {
    "title": "タスク名やタイトル",
    "category": "カテゴリ(例: 営業, 開発, 事務など)",
    "description": "概要や詳細な説明",
    "notes": "特記事項や備考"
  }
]

【絶対遵守事項】
JSON以外の解説文や、マークダウンのコードブロック表記(```json など)は一切含めないでください。先頭の [ から末尾の ] まで、純粋なJSONテキストのみを出力してください。

💡 ポイント
抽出対象がない場合に [](空の配列)を返すよう指示することで、無駄なエラーや誤作動を防ぎます。また、マークダウン表記の禁止を【絶対遵守事項】として念押しすることが重要です。

5. スプレッドシートの設計

1つのスプレッドシート内に、役割の異なる3つのタブ(シート)を用意します。業務に合わせて列の増減は自由に行えます。

  1. 受信用バッファ: 外部からのJSON文字列を一時的に受ける専用タブ

    • 1行目: A1: JSONデータ
  2. 確認待ちリスト: AIが生成した未承認データをストックするタブ

    • 1行目: A1: 承認 / B1: タイトル / C1: カテゴリ / D1: 概要・詳細 / E1: 備考
  3. 承認済みリスト: 人間が承認した確定データを蓄積するタブ

    • 1行目: A1: タイトル / B1: カテゴリ / C1: ステータス / D1: 概要・詳細 / E1: 備考

6. GASの実装コード

スプレッドシートの「拡張機能」>「Apps Script」に以下のコードを配置します。

① バッファ展開処理(トリガー実行用)

受信用バッファに書き込まれたJSONを展開し、確認待ちリストへ一括追加するスクリプトです。
※イベントのソースを「スプレッドシートから」、イベントの種類を 「変更時」 に設定してトリガー登録します。

function processJsonBuffer(e) {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const bufferSheet = ss.getSheetByName("受信用バッファ");
  const targetSheet = ss.getSheetByName("確認待ちリスト");
  
  if (!bufferSheet || !targetSheet) return;
  
  const lastRow = bufferSheet.getLastRow();
  if (lastRow <= 1) return; // ヘッダーのみの場合は終了

  let jsonString = bufferSheet.getRange(lastRow, 1).getValue();
  if (!jsonString) return;

  // AIの出力ゆらぎ対策(Markdownのバックティックを除去)
  jsonString = String(jsonString).replace(/^```json/i, "").replace(/^```/, "").replace(/```$/, "").trim();

  try {
    const items = JSON.parse(jsonString);
    if (!Array.isArray(items) || items.length === 0) return;

    const newRows = [];
    items.forEach(item => {
      newRows.push([
        false,                   // A列: 承認 (初期値 FALSE)
        item.title || "",        // B列: タイトル
        item.category || "",     // C列: カテゴリ
        item.description || "",  // D列: 概要・詳細
        item.notes || ""         // E列: 備考
      ]);
    });

    // B列(データ列)を走査して本当の末尾行を特定
    const bValues = targetSheet.getRange("B:B").getValues();
    let insertRow = 1;
    for (let i = bValues.length - 1; i >= 0; i--) {
      if (bValues[i][0] !== "") {
        insertRow = i + 2;
        break;
      }
    }

    // 複数行を一括挿入
    targetSheet.getRange(insertRow, 1, newRows.length, 5).setValues(newRows);
    
    // 追加行のA列にのみ動的にチェックボックスを付与
    targetSheet.getRange(insertRow, 1, newRows.length, 1).insertCheckboxes();

    // 処理完了したバッファ行を削除
    bufferSheet.deleteRow(lastRow);

  } catch (err) {
    console.error("JSONパースエラー: " + err.toString());
  }
}

② 承認時の自動転記処理(シンプルトリガー)

人間がチェックボックスをONにした瞬間、本番シートへ転記し、元の行を削除します。

/**
 * チェックボックスがTRUEになった行を本番シートへ転記し、元行を削除する
 */
function onEdit(e) {
  if (!e || !e.range) return;

  const sheet = e.source.getActiveSheet();
  const range = e.range;

  // ガード条件: 確認待ちリスト かつ A列 かつ チェックON(TRUE)
  if (sheet.getName() !== "確認待ちリスト") return;
  if (range.getColumn() !== 1) return;
  if (range.getValue() !== true) return;

  const row = range.getRow();
  if (row === 1) return; // ヘッダー保護

  // B列〜E列(4列分)を取得
  const rowData = sheet.getRange(row, 2, 1, 4).getValues()[0];

  const ss = e.source;
  const prodSheet = ss.getSheetByName("承認済みリスト");
  if (!prodSheet) return;

  // 承認済みリスト末尾へ追記
  prodSheet.appendRow([
    rowData[0], // タイトル
    rowData[1], // カテゴリ
    "承認済",   // ステータス(固定値)
    rowData[2], // 概要・詳細
    rowData[3]  // 備考
  ]);

  // 元行を削除
  sheet.deleteRow(row);
}

7. 実装で詰まったポイントと解決策(トラブルシューティング)

開発・検証の過程で実際に直面した具体的なエラーと、その原因・対処法です。

① 「1行目から列名を取得できませんでした」エラー

  • 事象: Workspace Studioの「シートのコンテンツを取得」ステップなどでエラーが発生し、フローが停止する。

  • 原因: 読み込み対象のシートが完全に空(白紙)だったり、1行目にタイトル文章が入っていたり、セル結合が存在すると列名として認識されません。

  • 対策: データが0件の空シートであっても、1行目(A1, B1...)には必ず1行単位の「列名(ヘッダー)」を定義しておく必要があります。 セルの結合は解除し、タイトル等の余計な装飾は排除します。

② AIの出力が配列型 [ {...} ] のとき、Add Row で値が入らない

  • 事象: AIからはJSONが返ってきているのに、スプレッドシートの行追加アクションでセルが空白(null)になる。

  • 原因: Workspace Studioの標準アクションは、AIの出力を「1つの巨大なテキスト」として認識しており、構造化された「配列データ」としてパースできていないため、キーの個別マッピングに失敗します。

  • 対策: ノーコード側で無理にループ処理(For Each)や型変換を組もうとせず、「プレーンテキストのままバッファシートにAdd Rowし、GAS側で JSON.parse() させる」 アーキテクチャに切り替えることで完全解決しました。

③ 事前にチェックボックスを配置すると遥か下行に挿入される「空白行判定の罠」

  • 事象: GASで appendRow や getLastRow を使うと、データが入っていないチェックボックス枠の「さらに下(1000行目など)」からデータが追記されてしまう。

  • 原因: スプレッドシートの仕様上、「チェックボックスがフォーマットされている=セルにデータが存在する」とみなされます。

  • 対策: あらかじめ空行にチェックボックスを敷き詰める運用を廃止。GAS内で「実データが入っている列(B列など)の末尾」を逆ループ走査して本当の最終行を特定し、データ挿入と同時に insertCheckboxes() メソッドで動的生成するロジック(Step 5のコード参照)に変更しました。

④ シート名取得時の TypeError: Cannot read properties of null

  • 事象: GAS側で Cannot read properties of null (reading 'getLastRow') エラーでスクリプトが落ちる。

  • 原因: getSheetByName("受信用バッファ") の指定において、シートタブ名に不要な末尾スペース(受信用バッファ )が紛れ込んでおり、取得失敗(null)となっていました。

  • 対策: シート名は完全一致で厳格に管理し、コード内には必ず if (!sheet) return; のガード節を配置します。また、インデックス指定(getSheets()[0]など)はタブの並び替え事故に弱いため、必ずシート名による明示指定に統一しました。

8. 運用上の懸念点とベストプラクティス

本番運用に向けて考慮しておくべき制限事項と安全対策です。

① AIの出力ゆらぎ(Markdownバックティック混入問題)

  • 懸念: プロンプトで厳しく指定しても、AIが気まぐれに ```json ... ``` のマークダウン形式で返してくるケースがあり、JSON.parse() で構文エラーを起こすリスクがあります。

  • 対策: プロンプト末尾に「純粋なJSONテキストのみ出力」を強調するとともに、GAS側で文字列の前処理(正規表現でバックティックを除去するサニタイズ処理)を入れて防御力を高めています(Step 5のコードに実装済み)。

② 短時間の同時実行による競合(排他制御)

  • 懸念: バッファシートへの書き込みが連続して発生した場合、GASの変更時トリガーが重複起動し、同じ行を2回処理したり行の削除が競合する可能性があります。

  • 対策: 処理頻度が高い運用の場合は、GASの LockService を導入して、スクリプト実行中に排他ロック(トランザクション制御)をかけるのが安全です。

③ シンプルトリガー(onEdit)の実行権限と制限

  • 懸念: 承認用に使っている onEdit はシンプルトリガーであるため、30秒以上の長時間処理や、権限を要する外部API呼び出しは実行できません。また、複数行を一括でチェック(コピペ等)した場合は1行目しかイベントが発生しません。

  • 対策: 今回の「1行読み込んで別シートへ転記し元行を削除する」処理は1秒未満で終わるため問題ありませんが、大量の一括承認を想定する場合は、チェックボックス監視ではなく「一括承認ボタン(カスタムメニューや図形にマクロ登録)」方式を検討推奨です。

④ Chat通知における「DM送信」の制約

  • 懸念: Workspace StudioからGoogle Chatへ直接ダイレクトメッセージ(DM)を送りたい場合、Incoming Webhookは仕様上「スペース」にしか紐付けできないため、個人宛のWebhook URLは発行できません。

  • 対策: 自分(管理者)だけが参加する専用の「個別受信用スペース」を1つ作成し、そこにWebhookを設定する「疑似DM方式」を採用することで、複雑なChat API認証を組むことなく見逃しのない通知基盤を構築できます。

9. 終わりに

ノーコードツール(Workspace Studio)とスクリプト(GAS)は、どちらか一方だけで完結させようとすると各ツールの仕様制限にぶつかりがちです。

  • 外部システム連携・定期起動・生成AI操作: ノーコードが得意な領域に任せる

  • データ構造の変換・複雑な配列展開・UI動的変更: GASが得意な領域に任せる

このように「受信用バッファ」を境界線として役割分担を綺麗に切り分けることで、メンテナンス性が高く、エラーに強い実用的な自動化パイプラインを構築できます。同様の課題に直面している方の参考になれば幸いです。

9
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?