AIに「シートを更新しておいて」と言える会社になるには?
ミドリ: タツヤさん、うちはGASで作った自動化スクリプトが20本くらいあるんですけど、AIエージェントの話が出るたびに「全部作り直しですか」と聞かれて困っています。
タツヤ(nodding): それ、答えははっきり「作り直さなくていい」だよ。むしろ既存のGASこそが、AIエージェントに渡せる一番いい道具なんだ。
ミドリ(confused): GASって、AIを呼ぶ側だと思っていました。
タツヤ: そこが今日の話。これまではGASからAIを呼ぶ構成が主流だった。これからは逆で、AIがGASを呼ぶ。この向きが変わったのが2026年のいちばん大きい変化かもしれない。
ミドリ: 向きが変わると、何が違うんですか?
タツヤ: 業務が「AIが文章を作る」で終わらなくなる。「AIが実際に処理を実行して終わる」ようになるんだ。
なぜGASが向いているのか
ミドリ: そもそも、なぜGASなんですか?
タツヤ: 理由は4つある。
- すでに業務ロジックが入っている — 承認ルール、命名規則、部署コード。この会社固有の知識がコードとして存在している
- Googleサービスへの権限を既に持っている — スプレッドシート、Gmail、カレンダー、ドライブに標準でつながる
- サーバーを用意しなくていい — Web Appとして公開すれば、その日からエンドポイントになる
- 業務担当者が読める — 何をするコードなのか、書いた本人が説明できる
ミドリ(thinking): 4つ目、地味に大事ですね。
タツヤ: すごく大事だよ。AIに渡す道具は、渡す側が中身を説明できないと危険だからね。ブラックボックスを自律実行させるのは事故のもと。
全体の仕組み
ミドリ: どういう構成になるんですか?
タツヤ: シンプルにするとこう。
[AIエージェント]
↓ ①「請求データを登録して」と判断
↓ ② ツール定義を見て呼ぶ関数を選ぶ
[GAS Web App (doPost)]
↓ ③ トークン検証 → 入力チェック
↓ ④ 業務処理を実行
[スプレッドシート / Gmail / カレンダー]
↓ ⑤ 実行結果 + 監査ログ
[AIエージェント] ← 結果を受け取り次の判断へ
ミドリ: GASが「窓口」になるんですね。
タツヤ(nodding): そう。AIから見ると関数が1つ増えただけ。中でスプレッドシートを触っているのか、Gmailを送っているのかは知らなくていい。
ステップ1:既存の関数を1つ選ぶ
ミドリ: どこから始めればいいですか?
タツヤ: いちばん地味で、失敗しても戻せるものを1つ選ぶ。おすすめは「シートに1行追加する」系の処理。
ミドリ: メール送信じゃないんですね。
タツヤ: 最初は外に出ない処理から。社外に出る操作は、AI連携の最後にやるのが鉄則。1行追加なら、間違っても消せばいい。
ステップ2:Web Appとして公開する
タツヤ: GASを外から呼べるようにするには、doPostを実装してWebアプリとしてデプロイする。
function doPost(e) {
try {
const req = JSON.parse(e.postData.contents);
// ① 認証
if (!verifyToken(req.token)) {
return json({ ok: false, error: "unauthorized" });
}
// ② 入力チェック
if (!req.customer || !req.amount) {
return json({ ok: false, error: "missing required field" });
}
// ③ 業務処理(既存関数をそのまま呼ぶ)
const result = addSalesRow(req.customer, req.amount, req.note || "");
// ④ 監査ログ
writeAuditLog(req, result);
return json({ ok: true, row: result.row });
} catch (err) {
return json({ ok: false, error: String(err) });
}
}
function json(obj) {
return ContentService
.createTextOutput(JSON.stringify(obj))
.setMimeType(ContentService.MimeType.JSON);
}
ミドリ: 既存のaddSalesRowはそのまま使えるんですね。
タツヤ: そう。新しく書くのは入口の30行だけ。業務ロジックは1行も触らない。
認証の注意点
ミドリ: トークンってどう持たせるんですか?
タツヤ: スクリプトプロパティに保存して、コードには絶対に書かない。
function verifyToken(token) {
const expected = PropertiesService
.getScriptProperties()
.getProperty("AGENT_TOKEN");
if (!expected || !token) return false;
if (token.length !== expected.length) return false;
let diff = 0;
for (let i = 0; i < token.length; i++) {
diff |= token.charCodeAt(i) ^ expected.charCodeAt(i);
}
return diff === 0;
}
ミドリ(thinking): この最後の比較、なぜ===じゃないんですか?
タツヤ: 応答時間の差から少しずつ答えを絞り込む攻撃を防ぐため。1文字ずつ比較して途中で抜けると、どこまで合っていたかが時間差に出るんだ。全部走査してから判定すればその情報が漏れない。
ミドリ: そこまで気にするんですね……。
タツヤ: 公開URLになる時点で、世界中から叩かれる前提だからね。GASのWeb Appは「リンクを知っている全員」に公開する設定を選ぶことが多いから、アプリ側の認証が最後の砦になる。
ステップ3:AIに渡す「道具の説明書」を書く
ミドリ: AI側では何を設定するんですか?
タツヤ: ツール定義を書く。要するに「この道具は何ができて、何を渡せばいいか」の説明書だね。
{
"name": "add_sales_row",
"description": "売上管理シートに1行追加する。登録のみで、既存行の編集・削除はできない。金額は税抜きで渡すこと。",
"parameters": {
"type": "object",
"properties": {
"customer": { "type": "string", "description": "取引先名。正式名称で指定する" },
"amount": { "type": "number", "description": "税抜金額(円)。1円単位の整数" },
"note": { "type": "string", "description": "備考。省略可" }
},
"required": ["customer", "amount"]
}
}
ミドリ: descriptionがやけに具体的ですね。
タツヤ(nodding): ここの品質が精度を決めるんだ。AIはこの説明文だけを読んで使うかどうかを判断する。「売上を追加する」だけだと、税込みを渡してきたり、修正のつもりで新規行を作ったりする。
ミドリ: 「できないこと」も書いてありますね。
タツヤ: それが大事。できることより、できないことを書くほうが事故が減る。「編集・削除はできない」と明示すると、AIは修正依頼が来た時に人間へ確認を返すようになる。
ステップ4:二重実行を防ぐ
ミドリ: AIって、うまくいかないと何度もやり直しますよね。同じ登録が2回入りませんか?
タツヤ(surprised): いい指摘だね。それ、実際にいちばん多い事故なんだ。
ミドリ: やっぱり起きるんですね。
タツヤ: 通信が切れた時、AI側は成功したかわからないから再試行する。でもGAS側では処理が完了していて、結果として同じ売上が2行入る。
対策:依頼IDで重複を弾く
function addSalesRowIdempotent(req) {
const cache = CacheService.getScriptCache();
const key = "req_" + req.requestId;
const done = cache.get(key);
if (done) {
return { ok: true, row: Number(done), duplicated: true };
}
const result = addSalesRow(req.customer, req.amount, req.note || "");
cache.put(key, String(result.row), 21600); // 6時間保持
return { ok: true, row: result.row, duplicated: false };
}
ミドリ: 依頼ごとにIDを付けてもらうんですね。
タツヤ: そう。ツール定義にrequestIdを必須で入れておく。同じIDが来たら処理せず、前回の結果を返す。これで再試行されても行は増えない。
ミドリ(thinking): AIが行儀よく振る舞うのを期待するんじゃなくて、こちらで受け止めるんですね。
タツヤ: そこが本質。相手が間違える前提で守るのが、エージェント連携の基本姿勢だよ。
ステップ5:監査ログを必ず残す
タツヤ: 最後に、誰が何をしたかをシートに残す。
function writeAuditLog(req, result) {
const sheet = SpreadsheetApp
.getActive()
.getSheetByName("_agent_log");
sheet.appendRow([
new Date(),
req.agentId || "unknown", // どのエージェントが
req.requestedBy || "", // 誰の依頼で
"add_sales_row", // 何をしたか
JSON.stringify({ customer: req.customer, amount: req.amount }),
result.row,
req.requestId || ""
]);
}
ミドリ: 「誰の依頼で」まで残すんですね。
タツヤ: これが無いと、後から追えなくなる。エージェントが実行したことは事実として残るけど、その起点が人間の誰なのかは記録しないと消えるんだ。
ミドリ: 責任の所在ですね。
タツヤ(nodding): そう。しかもログがシートに溜まっていくと、AIがどの業務を何回やっているかが自然に可視化される。効果測定の材料にもなるよ。
GAS特有の落とし穴
ミドリ: GASならではの注意点はありますか?
タツヤ: 4つある。ここを知らないとハマる。
- デプロイし直さないと反映されない — コードを保存しただけではWeb Appは変わらない。「新しいバージョンとしてデプロイ」が必要。修正したのに直らないの原因はほぼこれ
- 実行時間の上限がある — 長い処理は途中で打ち切られる。重い処理は分割するか、非同期で受け付けて結果は別途返す設計にする
- 同時実行で書き込みが競合する — 複数の依頼が同時に来るとシートの行がずれることがある。
LockServiceで排他制御を入れる - 実行アカウントの権限で動く — 「自分として実行」設定だと、AIがあなたの権限で全部できてしまう。専用アカウントを用意するのが安全
ミドリ(surprised): 最後のやつ、怖いですね……。
タツヤ: いちばん怖い。AIエージェント専用のGoogleアカウントを作って、必要なシートだけ共有するのが本来の姿。個人アカウントで動かすと、そのアカウントが見えるものは全部触れてしまう。
脱エンジニアの視点で考えると、「この操作は取り消せるか」を業務側で仕分けできるかが分岐点になります。 取り消せる操作から順に渡していけば、AI連携は安全に広げられる。逆にここを飛ばして送信系から始めると、最初の1回で信頼を失うことになるよ。
段階的に広げる順番
ミドリ: 広げていく順番の目安はありますか?
タツヤ: この順で問題ない。
| 段階 | 操作 | 取り消し | 目安時期 |
|---|---|---|---|
| 1 | シートの読み取り | — | 初週 |
| 2 | シートへの行追加 | 削除で戻せる | 1か月目 |
| 3 | 下書きの作成(メール等) | 送らなければ無害 | 2か月目 |
| 4 | 既存行の更新 | 履歴から復元 | 3か月目 |
| 5 | 送信・確定処理 | 戻せない | 承認必須のまま |
ミドリ: 段階5は、ずっと人間の承認を残すんですね。
タツヤ: うちの方針としてはそう。外に出る最後の1手だけは人間が押す。ここを自動化しても浮く時間はわずかなのに、事故った時の損失は桁違いだからね。
まとめ
タツヤ: 今日の要点を整理しよう。
- GASを作り直す必要はない。既存の業務関数に
doPostの入口を30行足すだけでAIの道具になる - GASが向くのは業務ロジック・Google権限・サーバー不要・担当者が読めるの4点が揃うから
- ツール定義の説明文が精度を決める。できることよりできないことを書く
- 二重実行は必ず起きる。
requestIdによる冪等化で受け止める - 監査ログには「誰の依頼で」まで残す。責任の所在と効果測定の両方に効く
- GAS特有の罠は再デプロイ必須・実行時間上限・同時実行の競合・実行アカウントの権限
- 広げる順番は読み取り → 行追加 → 下書き → 更新 → 送信。送信は承認を残す
ミドリ(smiling): 「AIのために新しいシステムを作る」んじゃなくて、「今あるGASに窓口を付ける」だけなんですね。
タツヤ: そこがいちばん伝えたかったところ。20本のGASは負債じゃなくて資産だよ。会社固有の業務知識が、すでにコードとして書かれているんだから。
次のアクション
ミドリ: 何から始めましょう。
タツヤ: 3ステップで進めてほしい。
- 既存のGAS関数を棚卸しし、「取り消せるか」で3分類する — 読み取り/戻せる書き込み/戻せない操作。1時間で終わる
- 戻せる書き込みを1本選び、
doPostとトークン検証を付けて公開する — テスト用のシートで試す。本番シートは触らない - ツール定義の説明文を書き、
requestIdを必須にする — できないことを明記し、二重実行を弾く仕組みを最初から入れる
タツヤ: Shimanto AI Solutionsでは、この橋渡しを支援しているよ。
- 既存GAS資産の棚卸しとAI連携の適性評価 — どの関数から公開すべきかを、取り消し可能性の観点で一緒に仕分ける
- 安全な公開の実装支援 — 認証・冪等化・監査ログ・専用アカウント設計まで含めたAPI化を伴走
- 段階的な権限拡大の設計 — 読み取りから送信まで、事故を起こさずに広げるロードマップを策定
ミドリ: 今あるものを活かせるのが、いちばん嬉しいです。
タツヤ: そう。新しく作るより、今あるものにAIの手が届くようにするほうが速いし安全。そこから始めよう。
参考にした情報
本記事のコードは執筆時点(2026年8月)の仕様で動作を確認したものです。GASの制限値や挙動は変更されることがあります。
- Google Apps Script 公式ドキュメント — Web App のデプロイ、実行時間の上限、
LockService/CacheService/PropertiesServiceの一次情報