0
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?

AI返信下書きの承認期限切れをSheets+GASで安全に停止する

0
Last updated at Posted at 2026-07-22

生成AIで問い合わせ返信の下書きを作っても、人間の承認待ちが放置されれば返信漏れは減りません。本記事ではGoogle SheetsとGoogle Apps Script(GAS)を使い、期限を過ぎた下書きを自動送信せず EXPIRED に止める最小構成を実装します。

AI APIやメール送信処理は扱いません。対象は、承認期限の検出、状態遷移、監査ログ、再実行しても壊れないテストです。

この記事で作るもの

  • DRAFTED / REVIEWING の承認待ちキュー
  • 期限超過時に EXPIRED へ変えるGAS
  • 承認者・停止理由・引き継ぎ先を残す列
  • 同じ処理を再実行しても二重更新しないテスト

問い合わせ受付から人間承認までのレビューゲート

完成状態は「期限切れなら送らない」「誰が次に判断するか分かる」です。

シートを準備する

ReplyQueue シートの1行目に次の列を置きます。

ticket_id,received_at,category,draft,source_refs,status,reviewer,review_deadline,approved_at,sent_at,stop_reason,handoff_to,expired_at

状態は次のように限定します。

  • DRAFTED: AI下書き作成済み
  • REVIEWING: 人間が確認中
  • APPROVED: 人間が承認済み
  • EXPIRED: 期限切れで停止
  • SENT: 人間が送信済み

APPROVEDSENT は分けます。承認済みでも、宛先・添付・送信時刻の最終確認は残すためです。

期限切れを検出する

列番号のずれを避けるため、ヘッダー名から位置を解決します。

const SHEET_NAME = 'ReplyQueue';
const WAITING = new Set(['DRAFTED', 'REVIEWING']);

function getColumnMap(headers) {
  return headers.reduce((map, name, index) => {
    map[String(name).trim()] = index;
    return map;
  }, {});
}

function markExpiredDrafts() {
  const sheet = SpreadsheetApp.getActive().getSheetByName(SHEET_NAME);
  if (!sheet) throw new Error(`Sheet not found: ${SHEET_NAME}`);

  const range = sheet.getDataRange();
  const values = range.getValues();
  if (values.length < 2) return { checked: 0, expired: 0 };

  const column = getColumnMap(values[0]);
  const required = ['ticket_id', 'status', 'review_deadline', 'stop_reason', 'expired_at'];
  required.forEach((name) => {
    if (column[name] === undefined) throw new Error(`Missing column: ${name}`);
  });

  const now = new Date();
  let expired = 0;

  for (let index = 1; index < values.length; index += 1) {
    const row = values[index];
    const status = String(row[column.status] || '').trim();
    const deadline = row[column.review_deadline];
    const isValidDate = deadline instanceof Date && !Number.isNaN(deadline.getTime());

    if (!WAITING.has(status) || !isValidDate || deadline >= now) continue;

    row[column.status] = 'EXPIRED';
    row[column.stop_reason] = '承認期限超過';
    row[column.expired_at] = now;
    expired += 1;
  }

  if (expired > 0) {
    range.setValues(values);
  }
  return { checked: values.length - 1, expired };
}

EXPIRED は待機状態の集合に含まれないため、時間主導トリガーが同じ行を再度処理しません。通知を追加する場合は notified_at を持たせ、本文やメールアドレスではなくチケットIDと件数だけを社内へ送ります。

期限内だけ承認できるようにする

シートを直接書き換えるだけでは停止ルールを回避できます。承認操作にも状態と期限の検証を入れます。

function approveDraft(rowNumber, reviewerId) {
  if (!reviewerId) throw new Error('確認者を指定してください');

  const sheet = SpreadsheetApp.getActive().getSheetByName(SHEET_NAME);
  const values = sheet.getDataRange().getValues();
  const column = getColumnMap(values[0]);
  const row = values[rowNumber - 1];
  const now = new Date();
  const status = String(row[column.status] || '').trim();
  const deadline = row[column.review_deadline];

  if (!WAITING.has(status)) throw new Error(`承認できない状態です: ${status}`);
  if (!(deadline instanceof Date) || deadline < now) {
    throw new Error('承認期限を過ぎています。引き継ぎ後に再判定してください');
  }

  sheet.getRange(rowNumber, column.status + 1).setValue('APPROVED');
  sheet.getRange(rowNumber, column.reviewer + 1).setValue(reviewerId);
  sheet.getRange(rowNumber, column.approved_at + 1).setValue(now);
}

本番では対象列を保護し、担当者がメニューや権限を限定したWebアプリから操作する方が安全です。

テストケース

本番データではなく複製したシートで確認します。

入力 期待結果
期限前の DRAFTED 変化なし
期限後の DRAFTED EXPIRED
期限後の REVIEWING EXPIRED
期限後の APPROVED 変化なし
期限が空欄・文字列 変化なし、データ不備として確認
同じ関数を2回実行 2回目の更新件数は0
EXPIRED を承認 エラーで停止

実行ログでは checkedexpired を確認します。期限のタイムゾーンはスプレッドシートとApps Scriptプロジェクトで揃えてください。

運用チェックリスト

  • 承認期限と一次担当を決めた
  • 一次担当不在時の handoff_to を決めた
  • 苦情、契約、返金、個人情報を含む案件は最初から人間へ送る
  • AIが参照したFAQ・規程の現行版を人が確認する
  • シートの共有範囲と編集権限を限定する
  • トリガー失敗通知の受信先を決める
  • 期限切れを自動送信しない

GASの時間主導トリガーには実行時刻の揺れがあります。秒単位のSLAには向きません。短い締切が必要なら専用のジョブキューと監視基盤を検討してください。

自動化しない範囲

  • AIによる最終承認
  • 期限切れ下書きの自動送信
  • 苦情、契約、返金、法務・税務判断を含む返信
  • 根拠が古い、または確認できない返信
  • 引き継ぎ先がない状態での再開

まず過去1週間の承認待ちを確認し、「何時間で止めるか」「誰へ引き継ぐか」「最初からAI対象外にする分類」の3点を決めると、小さく安全に始められます。

0
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
0
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?