生成AIの試験運用では、要約や返信下書きが作成できた時点で「処理が完了した」と考えがちです。しかし実務で止まりやすいのは、AIの出力後に、誰が、いつ、何を確認するのかが記録されていないケースです。
この記事では、GoogleスプレッドシートとGoogle Apps Script(GAS)で、AIの最終判断を自動化せずに、次アクションの抜けだけを検査する台帳を作ります。問い合わせ本文や顧客情報をAIへ送る処理、メール送信、CRM更新は扱いません。
この記事で作るもの
- AI試験運用の1件を
next_actionsシートへ記録する - 判断理由と次アクションを別々の列で管理する
- 承認・修正・保留・停止の行に、担当者と期限があるか検査する
- 期限切れと未入力を一覧化し、人間が次の確認を行えるようにする
この実装はAIの判定を正しいとみなす仕組みではありません。人が判断した後に、引き継ぎに必要な記録が揃っているかを点検する小さな補助です。
1. 完了状態を先に決める
対象業務を「AIが下書きを作った」までで終わらせず、次のように定義します。
対象: FAQに基づく一般問い合わせの返信下書き
AIの担当: 伏せ字済み情報の要約、FAQ候補、返信下書きの作成
人の担当: 原文、宛先、根拠、例外、次アクションの確認
完了: 送信可否、担当者、期限、理由が記録されている
保留: 情報不足、根拠不明、マスク未確認
停止: 契約・返金・苦情・事故・権限外の依頼
「確認済み」だけを記録すると、次の担当者が動けません。判断理由(なぜそう決めたか)と、次アクション(次に何をするか)は別の項目にします。
2. シートの列を用意する
スプレッドシートに next_actions という名前のシートを作り、1行目へ次の見出しを入力します。
record_id,workflow,classification,decision,decision_reason,
owner,next_action,due_at,source_ref,reviewed_at
各列の役割は次のとおりです。
| 列 | 例 | 注意点 |
|---|---|---|
record_id |
INQ-2026-0813-004 |
本文の代わりに管理用IDを残す |
workflow |
faq_reply_draft |
対象業務を1つに絞る |
classification |
normal / boundary / stop
|
迷ったら boundary に戻す |
decision |
承認 / 修正 / 保留 / 停止
|
自由記述にしない |
decision_reason |
FAQ v3と一致 |
判断の根拠を書く |
owner |
support-lead |
次に動く役割または担当者 |
next_action |
宛先を確認して送信 |
「対応する」だけにしない |
due_at |
2026-08-13 17:00 |
期限を必須にする |
source_ref |
faq-v3#refund |
権限管理された参照IDだけにする |
reviewed_at |
2026-08-13 10:30 |
人が確認した時刻 |
顧客名、メールアドレス、電話番号、契約書の本文などをこの台帳へコピーしないでください。業務上必要な場合も、権限管理された正本へ戻れる参照IDを使います。
3. ヘッダー名で列を解決する
列番号を直接コードへ書くと、列を追加した時に別の値を読む危険があります。まず見出しから列番号を作ります。
function headerIndex(headers) {
return headers.reduce((index, value, position) => {
const key = String(value).trim();
if (key) index[key] = position;
return index;
}, {});
}
function requireHeaders(index, names) {
const missing = names.filter((name) => index[name] === undefined);
if (missing.length > 0) {
throw new Error(`必要な列がありません: ${missing.join(', ')}`);
}
}
headerIndex は1行目の値を { record_id: 0, workflow: 1, ... } のようなオブジェクトへ変換します。見出しが足りない場合は、検査を続けずにエラーにします。
4. 次アクションの必須項目を検査する
次の関数は、AIを呼び出さず、1行の値だけを検査します。
const DECISIONS = new Set(['承認', '修正', '保留', '停止']);
function validateNextActionRow(row, index) {
const recordId = String(row[index.record_id] ?? '').trim();
if (!recordId) return { recordId: '', valid: true, errors: [] };
const decision = String(row[index.decision] ?? '').trim();
const errors = [];
if (!DECISIONS.has(decision)) {
errors.push('decisionが未入力または許可されていません');
}
if (!String(row[index.decision_reason] ?? '').trim()) {
errors.push('decision_reasonが未入力です');
}
if (!String(row[index.owner] ?? '').trim()) {
errors.push('ownerが未入力です');
}
if (!String(row[index.next_action] ?? '').trim()) {
errors.push('next_actionが未入力です');
}
if (!row[index.due_at]) {
errors.push('due_atが未入力です');
}
return { recordId, valid: errors.length === 0, errors };
}
未入力を 承認 と解釈しないことが重要です。形式が不正な行は通常処理へ進めず、エラー一覧へ戻します。
5. 未完了の行を検査結果シートへ出す
検査結果を next_action_review シートへ出力します。元の台帳を書き換えず、担当者が確認しやすい別シートへ出す構成です。
function reviewNextActions() {
const book = SpreadsheetApp.getActiveSpreadsheet();
const source = book.getSheetByName('next_actions');
if (!source) throw new Error('next_actions シートがありません');
const values = source.getDataRange().getValues();
if (values.length < 2) throw new Error('検査対象の行がありません');
const index = headerIndex(values[0]);
requireHeaders(index, [
'record_id', 'decision', 'decision_reason',
'owner', 'next_action', 'due_at',
]);
const output = [[
'record_id', 'valid', 'errors', 'checked_at',
]];
const checkedAt = new Date();
values.slice(1).forEach((row) => {
const result = validateNextActionRow(row, index);
if (!result.recordId) return;
output.push([
result.recordId,
result.valid,
result.errors.join(' / '),
checkedAt,
]);
});
const review = book.getSheetByName('next_action_review')
|| book.insertSheet('next_action_review');
review.clearContents();
review.getRange(1, 1, output.length, output[0].length).setValues(output);
}
この処理は「有効」と判定された行を送信したり、CRMへ反映したりしません。確認結果を表にするだけなので、最初は手動で reviewNextActions を実行して運用を観察できます。
6. 期限切れを別の条件で見る
必須項目が揃っていても、期限を過ぎたまま放置されることがあります。期限切れは入力エラーとは別の運用課題として集計します。
function findOverdueNextActions() {
const book = SpreadsheetApp.getActiveSpreadsheet();
const source = book.getSheetByName('next_actions');
if (!source) throw new Error('next_actions シートがありません');
const values = source.getDataRange().getValues();
if (values.length < 2) return [];
const index = headerIndex(values[0]);
requireHeaders(index, ['record_id', 'decision', 'owner', 'next_action', 'due_at']);
const now = new Date();
return values.slice(1)
.filter((row) => row[index.record_id])
.filter((row) => row[index.decision] !== '停止')
.filter((row) => row[index.due_at] instanceof Date)
.filter((row) => row[index.due_at].getTime() < now.getTime())
.map((row) => ({
recordId: String(row[index.record_id]).trim(),
owner: String(row[index.owner] ?? '').trim(),
nextAction: String(row[index.next_action] ?? '').trim(),
dueAt: row[index.due_at],
}));
}
停止案件を通常の期限切れ一覧へ混ぜないようにしています。停止案件は契約、返金、苦情、事故、権限などを含む可能性があるため、権限のある担当者へ戻す別の扱いにします。
7. 最初の1日で確認すること
いきなり通知や自動送信を追加せず、まず1日分の記録で次を確認します。
- 対象業務が1つに絞られている
- AIが作るものと、人が確定するものが分かれている
- 通常・境界・停止のケースを用意している
- AIへ渡さない情報を決めている
-
decision_reasonとnext_actionが別の列になっている -
ownerとdue_atが必須になっている -
根拠を
source_refで追跡できる - 未完了の台帳を毎日誰が確認するか決めている
同じ理由が繰り返される場合は、AIのプロンプトを変更する前に、FAQの版番号、担当表、フォームの必須項目、CRMの状態定義などを見直します。未完了の台帳は失敗の記録ではなく、次に整える業務ルールの候補です。
AIに任せない範囲
この構成で自動化しているのは、記録の抜けを見つけることだけです。次の判断は人が行います。
- 個人情報や機密情報をAIへ渡してよいか
- 根拠の版と内容が一致しているか
- 契約、返金、苦情、事故、権限に関わらないか
- 宛先、期限、担当者が正しいか
- 送信、公開、外部更新を実行してよいか
AI API接続、メール送信、CRM更新、権限変更を追加する場合は、マスキング、アクセス権、保存期間、承認者を別途決めてから拡張してください。
まとめ
生成AIの試験運用を止めないために、処理件数だけでなく、1件ごとの次の一歩を残します。
-
decision_reasonに判断理由を残す -
owner、next_action、due_atを必須にする - GASで未入力と期限切れを検査する
- 繰り返す迷いをFAQ、フォーム、CRM、承認ルールへ戻す
まずは next_actions シートに10件だけ記録し、reviewNextActions を実行してください。下書き後に処理が止まる行が見つかったら、その行の next_action と担当者を決めることが、AIの利用件数を増やすより先の改善になります。
Miraigentでは、AI導入の前に対象業務、人間確認、記録の戻し先を確認する無料診断を案内しています。診断や外部公開へ進む場合も、最終判断と個人情報の扱いは人が確定します。
