AIのプロンプト、参照FAQ、フォーム項目、確認者を変更した時、関係者全員へ同じ通知を送っていないでしょうか。
全員通知は重要な変更を埋もれさせ、通知しない運用は古い条件のまま返信や確認を続ける原因になります。必要なのは通知量を増やすことではなく、変更の影響に応じて、止める・当日確認する・履歴へ残すを分けることです。
この記事では、GoogleスプレッドシートとGoogle Apps Script(GAS)で、AIを使った問い合わせ対応や社内業務の変更を3段階に分類する最小構成を作ります。外部サービスへの自動通知、AI API接続、AIによる再開承認は扱いません。変更を記録し、人間が確認するキューを作るところまでが対象です。
この記事で作るもの
-
AI_CHANGE_IMPACT_LOG: 変更・影響・通知先・確認状態を1行で管理するシート -
AI_CHANGE_REASON_RULES: 理由コードと影響区分の対応表 -
STOP_REVIEW/SAME_DAY_NOTIFY/ROUTINE_LOGの3段階判定 - 同じ変更を二重登録しないGAS関数
- 未確認の変更を抽出する確認キュー
3段階の意味を先に固定する
変更内容の大きさではなく、変更後に誰の判断やどの業務が変わるかで分類します。
| 区分 | 使う場面 | 最初にすること | 完了条件 |
|---|---|---|---|
STOP_REVIEW |
契約・料金・個人情報・認証・外部送信・判定条件へ影響する | 対象処理を止め、人間確認または元手順へ戻す | 責任者が差分と再開条件を確認 |
SAME_DAY_NOTIFY |
FAQの正本、フォーム項目、担当者、確認期限、出力形式が変わる | 担当者・確認者へ当日確認キューを出す | 既存案件への影響を確認し記録 |
ROUTINE_LOG |
表記や説明順など、判断条件を変えない | 変更履歴へ記録する | 次回定例で影響なしを確認 |
迷う場合は上位の区分へ寄せます。理由コードが未登録なら、ROUTINE_LOG として処理せず REVIEW_REQUIRED にして人へ戻します。
SAME_DAY_NOTIFY は通知済みを意味しません。通知先へ送った後も、古いFAQを参照する下書きが残っていないか、フォーム変更が既存案件へ影響しないかを確認して初めて完了です。
シートを2枚作る
AI_CHANGE_IMPACT_LOG
1行を1変更として、本文をコピーせず、正本へ戻る参照IDだけを保存します。
| 列 | 例 | 用途 |
|---|---|---|
change_id |
CHG-20260910-001 |
変更の一意なID |
changed_at |
2026-09-10 09:00 |
変更日時 |
subject |
faq_source |
変更対象 |
summary |
FAQの解約条件を更新 | 人が読む要約 |
before_version |
faq-2026-09-01 |
変更前の版 |
after_version |
faq-2026-09-10 |
変更後の版 |
impacted_workflows |
inquiry_reply_draft |
影響する業務 |
reason_codes |
contract_or_money |
判定理由コード |
impact |
STOP_REVIEW |
GASの候補判定 |
notify_roles |
support_owner,final_decider |
通知先の役割 |
verification_owner |
operations_owner |
再開前の確認役割 |
status |
OPEN |
OPEN / VERIFIED / CLOSED
|
verified_at |
空欄 | 人が確認した日時 |
evidence_ref |
FAQ-20260910-01 |
正本・差分の参照ID |
next_review_at |
2026-09-10 15:00 |
次回確認期限 |
AI_CHANGE_REASON_RULES
理由コードと影響区分を管理します。コードを直接GASへ散らさず、シートでレビューできるようにします。
| 列 | 例 | 初期区分 |
|---|---|---|
reason_code |
contract_or_money |
STOP_REVIEW |
label |
契約・料金・返金に関係 | 表示用の説明 |
impact |
STOP_REVIEW |
影響区分 |
enabled |
TRUE |
現在使うか |
owner_role |
final_decider |
判断する役割 |
契約、料金、返金、苦情、重大な個人情報、認証情報、外部送信ルール、判定条件の変更は、通常の通知だけで閉じない区分にします。
GASで変更の影響を判定する
まず、判定値と理由コードを固定します。GASは通知を送らず、候補区分と人間確認が必要かどうかを返します。
const IMPACTS = ['STOP_REVIEW', 'SAME_DAY_NOTIFY', 'ROUTINE_LOG'];
const STOP_REASONS = new Set([
'contract_or_money',
'high_impact_personal_data',
'credential_or_access',
'external_send_rule',
'decision_condition_changed',
]);
const SAME_DAY_REASONS = new Set([
'canonical_source_changed',
'input_field_changed',
'reviewer_changed',
'due_date_changed',
'output_format_changed',
]);
function suggestImpact(reasonCodes) {
const codes = new Set(reasonCodes.filter(Boolean));
if ([...codes].some((code) => STOP_REASONS.has(code))) {
return { impact: 'STOP_REVIEW', requiresHumanGate: true };
}
if ([...codes].some((code) => SAME_DAY_REASONS.has(code))) {
return { impact: 'SAME_DAY_NOTIFY', requiresHumanGate: true };
}
return { impact: 'ROUTINE_LOG', requiresHumanGate: true };
}
すべての区分で requiresHumanGate を true にしています。理由コードの分類ができても、実際の影響範囲、正本、未反映の媒体、顧客への影響は人が確定します。
未知の理由コードを黙って ROUTINE_LOG に落とさないため、登録済みコードか確認する関数も用意します。
function normalizeReasonCodes(value) {
if (Array.isArray(value)) return value.map(String).map((v) => v.trim()).filter(Boolean);
return String(value || '')
.split(',')
.map((v) => v.trim())
.filter(Boolean);
}
function validateReasonCodes(reasonCodes, enabledCodes) {
const unknown = reasonCodes.filter((code) => !enabledCodes.has(code));
if (unknown.length > 0) {
return { valid: false, unknown };
}
return { valid: true, unknown: [] };
}
validateReasonCodes() が失敗した行は、区分を確定せず REVIEW_REQUIRED として確認キューへ送ります。未知の変更を安全な変更とみなさないことが重要です。
ヘッダー名で列を読む
シートの列順をGASへ埋め込むと、列の追加や移動で別の値を読み違える可能性があります。ヘッダー名から位置を作ります。
function headerIndex(headers) {
const index = {};
headers.forEach((value, position) => {
const name = String(value).trim();
if (name) index[name] = position;
});
return index;
}
function requireHeaders(index, required) {
const missing = required.filter((name) => index[name] === undefined);
if (missing.length > 0) {
throw new Error('Missing headers: ' + missing.join(','));
}
}
function rowToChange(row, index) {
return {
changeId: String(row[index.change_id] || '').trim(),
subject: String(row[index.subject] || '').trim(),
summary: String(row[index.summary] || '').trim(),
reasonCodes: normalizeReasonCodes(row[index.reason_codes]),
status: String(row[index.status] || 'OPEN').trim(),
};
}
change_id、subject、summary、reason_codes、status がない場合は処理全体を失敗させます。入力不備を成功扱いにしないためです。
重複を防いで影響区分を書き込む
変更を再確認するために時間主導トリガーを使う場合でも、同じ変更へ何度も履歴行を追加しないようにします。既存行の change_id をキーにして、未確認の行だけ候補区分を更新します。
function refreshImpactSuggestions() {
const book = SpreadsheetApp.getActiveSpreadsheet();
const sheet = book.getSheetByName('AI_CHANGE_IMPACT_LOG');
const rules = book.getSheetByName('AI_CHANGE_REASON_RULES');
if (!sheet || !rules) throw new Error('Required sheet is missing');
const values = sheet.getDataRange().getValues();
if (values.length < 2) return { updated: 0, reviewRequired: 0 };
const index = headerIndex(values[0]);
requireHeaders(index, [
'change_id', 'subject', 'summary', 'reason_codes',
'impact', 'status', 'evidence_ref',
]);
const ruleValues = rules.getDataRange().getValues();
const ruleIndex = headerIndex(ruleValues[0] || []);
requireHeaders(ruleIndex, ['reason_code', 'enabled']);
const enabledCodes = new Set(
ruleValues.slice(1)
.filter((row) => String(row[ruleIndex.enabled]).toUpperCase() === 'TRUE')
.map((row) => String(row[ruleIndex.reason_code]).trim())
.filter(Boolean),
);
let updated = 0;
let reviewRequired = 0;
values.slice(1).forEach((row, offset) => {
const change = rowToChange(row, index);
if (!change.changeId || change.status !== 'OPEN') return;
const validation = validateReasonCodes(change.reasonCodes, enabledCodes);
const suggestion = validation.valid
? suggestImpact(change.reasonCodes)
: { impact: 'REVIEW_REQUIRED', requiresHumanGate: true };
sheet.getRange(offset + 2, index.impact + 1).setValue(suggestion.impact);
updated += 1;
if (!validation.valid) reviewRequired += 1;
});
return { updated, reviewRequired };
}
この関数は通知送信やAI処理の再開を行いません。impact は確認候補であり、VERIFIED や CLOSED へ自動変更もしません。
未確認の変更を確認キューへ出す
次に、OPEN のまま残っている行を確認用シートへ出します。個人名や顧客本文を複製せず、変更ID・要約・役割・参照IDだけを使います。
function buildHumanReviewQueue() {
const book = SpreadsheetApp.getActiveSpreadsheet();
const source = book.getSheetByName('AI_CHANGE_IMPACT_LOG');
const queue = book.getSheetByName('AI_CHANGE_REVIEW_QUEUE')
|| book.insertSheet('AI_CHANGE_REVIEW_QUEUE');
if (!source) throw new Error('AI_CHANGE_IMPACT_LOG is missing');
const values = source.getDataRange().getValues();
if (values.length < 2) return 0;
const index = headerIndex(values[0]);
requireHeaders(index, [
'change_id', 'summary', 'impact', 'notify_roles',
'verification_owner', 'status', 'evidence_ref',
]);
const rows = [['change_id', 'summary', 'impact', 'notify_roles',
'verification_owner', 'evidence_ref', 'queue_status']];
values.slice(1).forEach((row) => {
if (String(row[index.status]).trim() !== 'OPEN') return;
rows.push([
row[index.change_id], row[index.summary], row[index.impact],
row[index.notify_roles], row[index.verification_owner],
row[index.evidence_ref], 'WAITING_FOR_HUMAN',
]);
});
queue.clearContents();
queue.getRange(1, 1, rows.length, rows[0].length).setValues(rows);
return rows.length - 1;
}
clearContents() を使う対象は、この関数が管理する確認キューだけに限定してください。顧客対応の正本ログや公開用シートへ向けて実行しないでください。
変更後の状態遷移
運用上は、通知を出したことと、変更を確認したことを分離します。
変更を1件登録
↓
影響先と理由コードを確認
├─ STOP_REVIEW → 対象処理を止め、人間確認
├─ SAME_DAY_NOTIFY → 当日確認キューへ
└─ ROUTINE_LOG → 定例レビューへ記録
↓
差分・未反映媒体・再開条件を確認
├─ 不足あり → OPEN / REVIEW_REQUIRED のまま
└─ そろった → 責任者が VERIFIED へ変更
確認者が VERIFIED へ変更する際は、少なくとも次を確認します。
- 変更前後の版が正本と一致している
- 影響するFAQ、フォーム、CRM、公開ページを洗い出した
- 既存の返信下書きや確認待ち案件への影響を見た
- 契約、料金、返金、苦情、個人情報、認証、外部送信を確認した
- 再開または継続する条件と、戻し先を記録した
テスト用のケースを作る
実データをコピーせず、理由コードだけで判定をテストします。
| ケース | 理由コード | 期待区分 | 人が確認すること |
|---|---|---|---|
| FAQの表記だけ修正 | なし | ROUTINE_LOG |
判断条件を変えていないか |
| FAQの正本を更新 | canonical_source_changed |
SAME_DAY_NOTIFY |
既存下書きへの影響 |
| 確認者を変更 | reviewer_changed |
SAME_DAY_NOTIFY |
新しい役割と期限 |
| 返金条件を変更 | contract_or_money |
STOP_REVIEW |
正本・責任者・再開条件 |
| 外部送信ルールを変更 | external_send_rule |
STOP_REVIEW |
送信前確認の再実施 |
| 未登録の理由コード | new_reason |
REVIEW_REQUIRED |
ルール追加の要否 |
最低限、次の性質を確認してください。
-
STOP_REVIEWがSAME_DAY_NOTIFYへ弱められない - 未知の理由コードが
ROUTINE_LOGにならない -
OPENの行だけが確認キューへ出る -
VERIFIEDの行を再実行しても重複しない - いずれの区分でも通知・公開・外部送信が自動実行されない
AIと人間の境界
AIに任せられるのは、匿名化した変更記録から影響する業務の候補を整理すること、変更前後の設定差分を要約すること、通知先の役割が空欄であることを指摘することです。
人が確定する範囲は次の通りです。
- 顧客、契約、料金、返金、苦情、重要な個人情報への影響
- AI処理を止めるか、元手順へ戻すか
- 正本と変更後の設定が一致しているか
- 未反映のFAQ、フォーム、CRM、公開ページがあるか
- 停止を解除してよいか、誰が判断したか
変更通知の文面をAIが作れても、通知済みを承認済み・再開可能とは扱いません。顧客本文、氏名、メールアドレス、認証情報、契約・決済情報はログへコピーせず、権限管理された正本の参照IDだけを保存します。
導入前チェックリスト
- 直近の変更を3件だけ選び、1件1行に分けた
- 変更前後の版または参照IDを記録した
- 影響する業務と影響しない業務を分けた
- 3段階の意味と上位区分へ寄せるルールを共有した
- 通知先を個人名ではなく役割で記録した
- 未知の理由コードを確認キューへ戻した
- 通知済み、確認済み、再開可能を別の状態にした
- 顧客本文や認証情報を変更ログへ複製していない
- 次回確認日と未完了の対応を残した
まとめ
AI運用の変更管理では、全員へ同じ通知を送るより、影響に応じて扱いを分ける方が確認漏れを見つけやすくなります。
- 契約・料金・個人情報・送信・判定条件に関わる変更は
STOP_REVIEW - 正本・入力・担当・期限・出力形式の変更は
SAME_DAY_NOTIFY - 判断条件を変えない変更は
ROUTINE_LOG - 未知の理由や不足情報は
REVIEW_REQUIREDとして人へ戻す - GASは候補整理までにし、通知・承認・再開は人が確定する
Miraigentの業務診断でも、AIを導入するかどうかだけでなく、変更が起きた時に誰が止め、何を照合し、どこへ戻すかを確認します。まずは直近の変更を1件選び、impact、notify_roles、evidence_ref の3列を埋めるところから始めてください。
