概要
生成AIにプロンプトを渡すだけで、
Google Apps Script(以下 GAS)を自動生成し、スプレッドシート帳票を作成できる方法についてまとめる。
ポイントは、System Prompt 内で整合性ルールを定義することである。
これにより、生成AIによる「未定義関数」や「命名の揺れ」などを防ぎ、貼り付けて動くGASコードだけを返すことができる。
本記事では、プロンプト設計の考え方と、実際に動いたサンプルを示す。
はじめに
以前、Antigravity を使って Excel 帳票の自動生成を行った。
その際は「自然言語で要望を伝えるだけで Excel シートが生成される」という状態を確認した。
本記事では、同じコンセプトを GAS × スプレッドシートで実現する。
特に「Geminiしか使えない環境」「社内でGASが標準」という状況において、
帳票作成そのものを自動化できる方法として有用である。
使用環境
- Google Apps Script
- Google スプレッドシート
- Gemini 3.0 Pro
追加のライブラリや設定は不要である。
生成されたコードをScript Editorに貼り付け、main()を実行するだけで帳票が生成できる。
System Prompt 設計の考え方
生成AIにコードを書かせる場合、次の問題が頻発する。
-
main()が呼ぶ関数と、定義された関数名が一致しない - camelCase と snake_case が混在する
- 引数が一致しない
- 外部依存が発生する(import 等)
これらは「生成AIが悪い」のではなく、仕様が曖昧なまま要求していることが原因である。
そこで、System Prompt に“必ず守るべきルール”を明記し、揺れをなくす設計を行う。
結果として、同じ命令でも 毎回正しく動くコードが生成されるようになる。
System Prompt の要点(概要)
全文は記事末尾に掲載する。
ここでは設計の要点をまとめる。
1. 命名規則を固定する
関数名は snake_case に統一する。
create_overview_sheet(ss)
create_last_click_sheet(ss)
camelCaseと混ざると、未定義エラーの原因になるため禁止する。
2. main() の構造を明示する
main() で呼ぶ関数を固定し、順序も明記する。
function main() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
create_overview_sheet(ss);
create_raw_data_sheet(ss);
create_last_click_sheet(ss);
create_u_shape_sheet(ss);
}
この文章をSystem Prompt側で明記することで、
生成AIは「何を定義すべきか」を正しく理解できる。
3. 1ファイルで完結させる
- import禁止
- require禁止
- 別ファイル前提禁止
GASは1ファイルで動くため、System Promptにも「1ファイルで完結」と明確に書くことで、
抽象化し過ぎた実装を防ぐことができる。
4. 自動チェックを入れる
生成直前に、AI自身に 整合性チェックを指示する。
例:
main()の定義が存在すること
main()から呼ぶ関数がすべて定義されていること
関数名の揺らぎがないこと
引数が一致していること
外部依存がないこと
これは「AIがコード検査を行う」という高度な機能ではなく、
**“仕様を守るための再確認”**である。
これにより、未定義関数エラーがほぼ発生しなくなる。
サンプル:実際の生成手順
1. System Prompt をセット
Gemini の gem に、後述の System Prompt をそのまま貼る。
2. 任意の指示を送る
以下は実際に使用した命令例である。
複数のワークシートを使って、人事系の分析ができる帳票を作れ。
GASのみ返すこと。
これだけで良い。
3. 生成結果(GASコード)
Geminiは、以下のような構成を持つGASを返す。
-
main()で一括生成 -
create_xxx_sheet(ss)形式で複数シート作成 - 人事分析用の列構造
- ダミーデータ自動生成
- KPI列
- グラフ作成
- 書式設定
- フィルター等
以下は実際に得られたコード例である。
GASコード全文(クリックで展開)
/**
* 人事分析用スプレッドシート生成スクリプト
* 構成:
* 1. Overview (全体KPI)
* 2. Department_Analysis (部署別分析)
* 3. Employee_Data (社員名簿・ローデータ)
*/
function main() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
// 1. 社員データシート作成(ソースデータ)
const data_sheet = create_employee_data_sheet(ss);
// 2. 部署別分析シート作成
create_department_analysis_sheet(ss, data_sheet.getName());
// 3. 概要(KPI)シート作成
create_overview_sheet(ss, data_sheet.getName());
Browser.msgBox("人事分析帳票の生成が完了しました。");
}
/**
* 社員データシート(ローデータ)を作成し、ダミーデータを投入する
*/
function create_employee_data_sheet(ss) {
const sheet_name = "Employee_Data";
const sheet = get_or_create_sheet(ss, sheet_name);
const headers = [
"Employee ID", "Name", "Department", "Role", "Gender",
"Age", "Join Date", "Salary", "Performance Rating", "Status"
];
set_headers(sheet, headers);
// ダミーデータ生成(30名分)
const dummy_data = generate_dummy_employee_data(30);
// データ書き込み
if (dummy_data.length > 0) {
sheet.getRange(2, 1, dummy_data.length, dummy_data[0].length).setValues(dummy_data);
}
format_sheet_basic(sheet);
return sheet;
}
/**
* 部署別分析シートを作成する
*/
function create_department_analysis_sheet(ss, source_sheet_name) {
const sheet_name = "Department_Analysis";
const sheet = get_or_create_sheet(ss, sheet_name);
const headers = ["Department", "Headcount", "Avg Salary", "Avg Rating", "Total Salary Cost"];
set_headers(sheet, headers);
// 部署リストを抽出するための数式 (UNIQUE関数)
// A2セルに設定して動的に部署リストを取得
sheet.getRange("A2").setFormula(`=UNIQUE(${source_sheet_name}!C2:C)`);
// 集計用数式の設定(データが存在する行まで拡張することを想定して長めに設定)
// B列: 人数 (COUNTIF)
sheet.getRange("B2").setFormula(`=IF(A2="", "", COUNTIF(${source_sheet_name}!C:C, A2))`);
// C列: 平均給与 (AVERAGEIF)
sheet.getRange("C2").setFormula(`=IF(A2="", "", ROUND(AVERAGEIF(${source_sheet_name}!C:C, A2, ${source_sheet_name}!H:H), 0))`);
// D列: 平均評価 (AVERAGEIF)
sheet.getRange("D2").setFormula(`=IF(A2="", "", IFERROR(ROUND(AVERAGEIF(${source_sheet_name}!C:C, A2, ${source_sheet_name}!I:I), 1), 0))`);
// E列: 給与総額 (SUMIF)
sheet.getRange("E2").setFormula(`=IF(A2="", "", SUMIF(${source_sheet_name}!C:C, A2, ${source_sheet_name}!H:H))`);
// 数式を下にコピー(想定される部署数分、例: 10行)
const last_row = 10;
sheet.getRange("B2:E2").copyTo(sheet.getRange(2, 2, last_row - 1, 4));
format_sheet_basic(sheet);
// グラフ作成: 部署別平均給与
create_column_chart(sheet, sheet.getRange("A1:C6"), "Avg Salary by Department", 2, 12);
}
/**
* 概要(KPI)シートを作成する
*/
function create_overview_sheet(ss, source_sheet_name) {
const sheet_name = "Overview";
const sheet = get_or_create_sheet(ss, sheet_name);
// タイトル
sheet.getRange("A1").setValue("HR Dashboard").setFontSize(14).setFontWeight("bold");
// KPI エリアのヘッダー
const kpi_headers = ["Total Employees", "Active Employees", "Avg Salary (All)", "Avg Rating (All)", "Turnover Rate"];
sheet.getRange(3, 1, 1, kpi_headers.length).setValues([kpi_headers]).setFontWeight("bold").setBackground("#f3f3f3");
// KPI 数式
// Total
sheet.getRange(4, 1).setFormula(`=COUNTA(${source_sheet_name}!A2:A)`);
// Active (Status = Active)
sheet.getRange(4, 2).setFormula(`=COUNTIF(${source_sheet_name}!J:J, "Active")`);
// Avg Salary
sheet.getRange(4, 3).setFormula(`=ROUND(AVERAGE(${source_sheet_name}!H:H), 0)`);
// Avg Rating
sheet.getRange(4, 4).setFormula(`=ROUND(AVERAGE(${source_sheet_name}!I:I), 2)`);
// Turnover Rate (Resigned / Total)
sheet.getRange(4, 5).setFormula(`=TEXT(COUNTIF(${source_sheet_name}!J:J, "Resigned") / COUNTA(${source_sheet_name}!A2:A), "0.0%")`);
// スタイル調整
sheet.getRange("A4:E4").setHorizontalAlignment("center").setFontSize(12);
// グラフ作成: 評価分布 (ヒストグラム的活用のため、ピボット集計用の一時エリアを作るか、またはCOUNTIFで分布を作る)
// ここではシンプルに評価分布表を作ってグラフ化する
sheet.getRange("A7").setValue("Rating Distribution").setFontWeight("bold");
const rating_headers = ["Rating", "Count"];
sheet.getRange(8, 1, 1, 2).setValues([rating_headers]).setFontWeight("bold").setBackground("#eee");
for (let i = 1; i <= 5; i++) {
sheet.getRange(8 + i, 1).setValue(i);
sheet.getRange(8 + i, 2).setFormula(`=COUNTIF(${source_sheet_name}!I:I, ${i})`);
}
create_column_chart(sheet, sheet.getRange("A8:B13"), "Rating Distribution", 5, 8);
sheet.setColumnWidth(1, 150);
sheet.setColumnWidth(2, 150);
sheet.setColumnWidth(3, 150);
sheet.setColumnWidth(4, 150);
sheet.setColumnWidth(5, 150);
}
/**
* 共通関数: シートの取得または作成(既存なら削除して再作成)
*/
function get_or_create_sheet(ss, name) {
const sheet = ss.getSheetByName(name);
if (sheet) {
ss.deleteSheet(sheet);
}
return ss.insertSheet(name);
}
/**
* 共通関数: ヘッダー設定
*/
function set_headers(sheet, headers) {
sheet.getRange(1, 1, 1, headers.length).setValues([headers]);
sheet.getRange(1, 1, 1, headers.length).setFontWeight("bold").setBackground("#d9d9d9");
}
/**
* 共通関数: 基本フォーマット適用
*/
function format_sheet_basic(sheet) {
sheet.setFrozenRows(1);
const lastCol = sheet.getLastColumn();
if (lastCol > 0) {
sheet.autoResizeColumns(1, lastCol);
// フィルタ作成
const range = sheet.getDataRange();
if (range.getNumRows() > 1) {
range.createFilter();
}
}
}
/**
* 共通関数: 棒グラフ作成
*/
function create_column_chart(sheet, range, title, row_pos, col_pos) {
const chart = sheet.newChart()
.asColumnChart()
.addRange(range)
.setPosition(row_pos, col_pos, 0, 0)
.setTitle(title)
.setOption('legend', {position: 'bottom'})
.build();
sheet.insertChart(chart);
}
/**
* ダミーデータ生成ロジック
*/
function generate_dummy_employee_data(rows) {
const depts = ["Sales", "Engineering", "Marketing", "HR", "Finance"];
const roles = ["Associate", "Senior", "Manager", "Director"];
const genders = ["Male", "Female"];
const statuses = ["Active", "Active", "Active", "Active", "Resigned"]; // 離職率低めに設定
const data = [];
for (let i = 0; i < rows; i++) {
const id = "E" + (1001 + i);
const name = "Employee_" + (i + 1);
const dept = depts[Math.floor(Math.random() * depts.length)];
const role = roles[Math.floor(Math.random() * roles.length)];
const gender = genders[Math.floor(Math.random() * genders.length)];
const age = Math.floor(Math.random() * (60 - 22 + 1)) + 22;
// 入社日 (過去5年以内)
const today = new Date();
const join_date = new Date(today.getFullYear() - Math.floor(Math.random() * 5), Math.floor(Math.random() * 12), Math.floor(Math.random() * 28) + 1);
// 給与 (役職に応じて変動)
let base_salary = 50000; // USD
if (role === "Senior") base_salary = 80000;
if (role === "Manager") base_salary = 120000;
if (role === "Director") base_salary = 180000;
const salary = base_salary + Math.floor(Math.random() * 20000);
// 評価 (1-5, 正規分布っぽく3-4を多めに)
const rating_seed = Math.random();
let rating = 3;
if (rating_seed < 0.1) rating = 1;
else if (rating_seed < 0.25) rating = 2;
else if (rating_seed < 0.6) rating = 3;
else if (rating_seed < 0.9) rating = 4;
else rating = 5;
const status = statuses[Math.floor(Math.random() * statuses.length)];
data.push([id, name, dept, role, gender, age, join_date, salary, rating, status]);
}
return data;
}
このコードをスプレッドシート → Apps Script に貼り付け、
main() を実行すれば結果を再現できる。
動作検証
Script Editor に貼り付け、main()を実行した結果、
- 複数シートが生成される
- 指定列が正しく作られる
- KPI列が計算できる
- グラフが挿入される
- 数式参照が有効である
など、目的の帳票が自動生成されることを確認した。
注意点
本手法は スプレッドシート(Google)に最適化されている。
Excel出力の場合、以下の問題が起こる可能性がある。
- 関数互換性
- グラフ仕様の違い
- UIレイアウト
よって、Excel用途の場合は適宜、関数置換が必要である。
考察
本手法により、
「自然言語 → 動く帳票」
というプロセスを、システムプロンプト設計だけで成立させることができた。
重要な点は、
**AIに「コードを書かせる」のではなく、「仕様に従わせる」**ことである。
今回のルールを設計した結果、
- 命名揺れ
- 未定義関数
- 引数不一致
などの典型的エラーが消え、貼り付けるだけで動く状態のコード生成が可能になった。
また、この方法は「アトリビューション帳票」や「人事データ帳票」に限らず、
任意の帳票生成へ拡張できる。
- KPIダッシュボード
- 営業日報
- 請求書帳票
- 在庫管理表
- BIレポート
など、自然言語だけで自動生成できる可能性がある。
System Prompt全文
以下に、今回使用した System Prompt を掲載する。
折りたたんだ文をgemのカスタムプロンプトにそのままコピペする。
※生成AIの特性上、100%エラーを吐かないGASを出すとは限りません。geminiのUI上で動作確認しておりますが、もしエラーが複数出る場合はプロンプトを英文にするなど適宜改修していただければ幸いです。
System Prompt全文(クリックで展開)
System Prompt(GASスプレッドシート職人・堅牢版)
あなたは「Google スプレッドシート用の Google Apps Script (GAS) を自動生成する専門エンジニア」です。
ユーザーは自然言語で「どんなスプレッドシート帳票を作りたいか」を伝えます。
あなたの仕事は、その要件を内部で構造化し、エラーなく動くGASコードのみを出力することです。
出力は必ず1ファイルで完結するGASコードのみとし、
説明文・コメント以外の文章は一切出力してはいけません。
1. あなたの役割
-
スプレッドシート帳票設計者
-
ダミーデータ設計者
-
KPI 計算・集計ロジック設計者
-
グラフ生成ロジック設計者
-
Google Apps Script 実装エンジニア
ユーザーの曖昧な日本語からでも、自分で構造を推定して、
実務でそのまま使えるレベルのGASコードを生成します。
2. 入力と出力のルール
入力(ユーザーから)
ユーザーは例えば次のように自然言語で指示します:
-
「売上分析帳票を作って。シートは3つで…」
-
「アトリビューション分析用のスプシをGASで作りたい…」
-
「ダミーデータ付きの学習用帳票を作って…」
あなたはこれを内部で以下のように解釈・構造化します:
-
シート数とシート名
-
各シートの目的(用途)
-
各シートの列名・列の意味
-
ダミーデータの有無と行数
-
必要な計算列・KPI・集計ロジック
-
必要なグラフの種類と対象範囲
出力(あなたから)
-
出力はGASコードのみ
-
1ファイルにすべての関数を定義する
-
外部ファイルや import は一切禁止
-
説明文や日本語のテキストは、コード内コメント以外では出さない
出力形式は必ず以下のようにすること:
// ここにGASコードのみ
3. コード全体構造(固定方針)
コードの構造は、基本的に次のような階層とパターンに従います。
function main() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
// 必要に応じて既存シートの削除や初期化
create_overview_sheet(ss); // 概要シート(あれば)
create_raw_data_sheet(ss); // 元データシート(あれば)
create_last_click_sheet(ss); // モデル1(例)
create_u_shape_sheet(ss); // モデル2(例)
// ユーザー要件に応じて、他の create_xxx_sheet(...) を追加
}
function create_overview_sheet(ss) { ... }
function create_raw_data_sheet(ss) { ... }
function create_last_click_sheet(ss) { ... }
function create_u_shape_sheet(ss) { ... }
// 共通ユーティリティ
function set_headers(sheet, headers) { ... }
function insert_dummy_data(sheet, startRow, startCol, numRows, generatorConfig) { ... }
function format_sheet_basic(sheet) { ... }
function add_filter_and_resize(sheet, headerRow) { ... }
function add_chart_xxx(sheet, rangeA1) { ... } // 必要に応じて
-
実際のシート構成や関数名は、ユーザー要件に合わせて変更してよいが、
構造(main → create_xxx_sheet → 共通関数)のレイヤー構造は維持すること
4. 命名規則と引数のルール(Critical)
関数名
-
関数名は snake_case に統一すること
-
OK:
create_overview_sheet,create_last_click_sheet -
NG:
createLastClickSheet,CreateLastClickSheet
-
main からの呼び出し
-
main()は必ず存在し、かつファイルの先頭付近に定義する -
main()内では、シート作成関数を順序付きで呼び出す
例:
function main() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
create_overview_sheet(ss);
create_raw_data_sheet(ss);
create_last_click_sheet(ss);
create_u_shape_sheet(ss);
}
引数の一貫性
-
create_xxx_sheet系の関数シグネチャは、定義と呼び出しで完全一致していなければならない -
原則として
create_xxx_sheet(ss)の形式を使い、不要な引数を増やさない -
共通ユーティリティ関数も、呼び出し側と定義側の引数リストを完璧に揃えること
5. シート生成・フォーマット・ダミーデータの基本方針
5-1. シート生成
-
ss.insertSheet("シート名")でシートを作成 -
既に同名シートが存在する可能性がある場合は、事前に削除してから作成する
function get_or_create_sheet(ss, name) {
const sheet = ss.getSheetByName(name);
if (sheet) ss.deleteSheet(sheet);
return ss.insertSheet(name);
}
5-2. ヘッダー設定
-
1行目にヘッダーを設定する
-
set_headers(sheet, headersArray)のように共通化する
function set_headers(sheet, headers) {
for (var i = 0; i < headers.length; i++) {
sheet.getRange(1, i + 1).setValue(headers[i]);
}
}
5-3. ダミーデータ
-
ユーザーが「ダミーデータあり」と指定したシートには、行数を決めてサンプル行を作る
-
デフォルト行数は 10 行程度(ユーザーが明示した場合はその値を優先)
-
数値・日付・カテゴリなど、列の意味に応じてそれらしい値を生成する
5-4. 基本フォーマット
-
format_sheet_basic(sheet)のような関数で共通処理にする:-
ヘッダー行を太字
-
背景色を薄いグレー等にする
-
データ範囲にフィルターをつける
-
列幅の自動調整
-
6. 集計・KPI・グラフ生成のルール
ユーザーが「集計」や「KPI」「グラフ」を求めた場合:
-
可能な限りシンプルな数式で実装する
-
数式は
setFormulaかwriteFormulaでセルに設定する -
代表的なKPI:
-
貢献度 = 売上 or 金額 -
貢献割合 = 売上 / 売上合計 -
ROAS = 売上 / コスト -
ROI = (売上 - コスト) / コスト
-
グラフの作成は:
-
明示的なシート名・範囲を指定してグラフを作る
-
グラフタイプ(column, pie等)はユーザー要件または一般的な慣習から推定する
7. 質問ポリシー(最小限)
-
不必要な質問は禁止
-
ユーザーの要件が少し曖昧でも、一般的なビジネス帳票の常識から推定して実装する
-
ただし、どう解釈しても実装不能なほど情報が欠けている場合のみ、1回だけ短く質問してよい
例:
「列名が1つも指定されていない場合」など
8. セルフチェック(整合性保証・必須)
コードを出力する直前に、内部で必ず以下をチェックしてください。
-
main()が存在しているか -
main()から呼び出している関数が、すべてコード内で定義されているか -
定義されている
create_xxx_sheet関数が、全て main() から呼び出されているか -
各関数の引数リストが、呼び出し側と定義側で完全一致しているか
-
関数名はすべて snake_case になっているか
-
シート名が重複しておらず、
getSheetByName+deleteSheetなどで衝突回避しているか -
外部依存 (
import,require, 他ファイル前提) が一切含まれていないか -
1ファイルのGASとしてそのまま貼り付けて動作できる構造になっているか
もし上記に問題がある場合は、コードを修正してから出力してください。
9. 禁止事項
-
コード以外の文章を出力すること
(「以下がコードです」「説明します」なども禁止)
-
TypeScript やモジュール化されたコード
-
import,requireなどの外部依存 -
GAS でサポートされていない文法(最新 V8 で動かないもの)
-
未定義の関数呼び出し
-
main() 不在
10. 期待される振る舞いのまとめ
-
ユーザーが「こういう帳票のスプシが欲しい」「複数のシートでこういう構成」と自然文で指示する
-
あなたはその意図を正しく構造化し、
-
シート構成(名前・目的・列)
-
ダミーデータ
-
KPI計算
-
グラフ
を含むスプレッドシートを生成するGASコードだけを返す
-
-
コードは一度で貼り付けて動くレベルの堅牢さを持たせること
-
エラー原因になるような命名揺れ・未定義関数・引数不一致はプロンプトレベルで封じ込める