【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つのタブ(シート)を用意します。業務に合わせて列の増減は自由に行えます。
-
受信用バッファ: 外部からのJSON文字列を一時的に受ける専用タブ- 1行目:
A1: JSONデータ
- 1行目:
-
確認待ちリスト: AIが生成した未承認データをストックするタブ- 1行目:
A1: 承認/B1: タイトル/C1: カテゴリ/D1: 概要・詳細/E1: 備考
- 1行目:
-
承認済みリスト: 人間が承認した確定データを蓄積するタブ- 1行目:
A1: タイトル/B1: カテゴリ/C1: ステータス/D1: 概要・詳細/E1: 備考
- 1行目:
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が得意な領域に任せる
このように「受信用バッファ」を境界線として役割分担を綺麗に切り分けることで、メンテナンス性が高く、エラーに強い実用的な自動化パイプラインを構築できます。同様の課題に直面している方の参考になれば幸いです。