AIに「シートを更新しておいて」と言える会社になるには?

ミドリ: タツヤさん、うちはGASで作った自動化スクリプトが20本くらいあるんですけど、AIエージェントの話が出るたびに「全部作り直しですか」と聞かれて困っています。

タツヤ(nodding): それ、答えははっきり「作り直さなくていい」だよ。むしろ既存のGASこそが、AIエージェントに渡せる一番いい道具なんだ。

ミドリ(confused): GASって、AIを呼ぶ側だと思っていました。

タツヤ: そこが今日の話。これまではGASからAIを呼ぶ構成が主流だった。これからは逆で、AIがGASを呼ぶ。この向きが変わったのが2026年のいちばん大きい変化かもしれない。

ミドリ: 向きが変わると、何が違うんですか?

タツヤ: 業務が「AIが文章を作る」で終わらなくなる。「AIが実際に処理を実行して終わる」ようになるんだ。

なぜGASが向いているのか

ミドリ: そもそも、なぜGASなんですか?

タツヤ: 理由は4つある。

ミドリ(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つある。ここを知らないとハマる。

ミドリ(surprised): 最後のやつ、怖いですね……。

タツヤ: いちばん怖い。AIエージェント専用のGoogleアカウントを作って、必要なシートだけ共有するのが本来の姿。個人アカウントで動かすと、そのアカウントが見えるものは全部触れてしまう。

脱エンジニアの視点で考えると、「この操作は取り消せるか」を業務側で仕分けできるかが分岐点になります。 取り消せる操作から順に渡していけば、AI連携は安全に広げられる。逆にここを飛ばして送信系から始めると、最初の1回で信頼を失うことになるよ。

段階的に広げる順番

ミドリ: 広げていく順番の目安はありますか?

タツヤ: この順で問題ない。

段階操作取り消し目安時期
1シートの読み取り初週
2シートへの行追加削除で戻せる1か月目
3下書きの作成(メール等)送らなければ無害2か月目
4既存行の更新履歴から復元3か月目
5送信・確定処理戻せない承認必須のまま

ミドリ: 段階5は、ずっと人間の承認を残すんですね。

タツヤ: うちの方針としてはそう。外に出る最後の1手だけは人間が押す。ここを自動化しても浮く時間はわずかなのに、事故った時の損失は桁違いだからね。

まとめ

タツヤ: 今日の要点を整理しよう。

ミドリ(smiling): 「AIのために新しいシステムを作る」んじゃなくて、「今あるGASに窓口を付ける」だけなんですね。

タツヤ: そこがいちばん伝えたかったところ。20本のGASは負債じゃなくて資産だよ。会社固有の業務知識が、すでにコードとして書かれているんだから。

次のアクション

ミドリ: 何から始めましょう。

タツヤ: 3ステップで進めてほしい。

  1. 既存のGAS関数を棚卸しし、「取り消せるか」で3分類する — 読み取り/戻せる書き込み/戻せない操作。1時間で終わる
  2. 戻せる書き込みを1本選び、doPostとトークン検証を付けて公開する — テスト用のシートで試す。本番シートは触らない
  3. ツール定義の説明文を書き、requestIdを必須にする — できないことを明記し、二重実行を弾く仕組みを最初から入れる

タツヤ: Shimanto AI Solutionsでは、この橋渡しを支援しているよ。

ミドリ: 今あるものを活かせるのが、いちばん嬉しいです。

タツヤ: そう。新しく作るより、今あるものにAIの手が届くようにするほうが速いし安全。そこから始めよう。


参考にした情報

本記事のコードは執筆時点(2026年8月)の仕様で動作を確認したものです。GASの制限値や挙動は変更されることがあります。

関連記事

シマント AI のサービス | 脱エンジニアの部屋 | AI戦略マップ 2025–2045