GASで日報をAIで自動レビューし
フィードバックを返す方法
スプレッドシートに集まった日報をClaude API(Anthropic社の対話型AIをプログラムから呼び出す仕組み)に渡し、良かった点・改善提案・次アクションを自動でレビューして本人に返す仕組みを、動くコードで解説します。
Table of Contents
なぜ日報レビューをAIで自動化するのか
日報は「書いてもらう」より「返す」ほうが続きません。人数が増えるほど全員分に目を通してコメントする負担が重くなり、フィードバックが滞ってしまいます。AIに一次レビューを任せると、返信のスピードと粒度をそろえられます。
返信の遅れを防ぐ
毎日決まった時刻にAIが全員分をレビューし、翌朝までにコメントが届く
粒度をそろえる
レビュー観点をコードで固定するので、レビュー担当者による当たり外れが減る
上司の負担を減らす
形式面の指摘はAIに任せ、人は評価や相談対応など判断の要る部分に集中できる
本人の気づきを促す
「次アクションが書かれていない」など、書き手が見落としがちな点を毎日指摘できる
ただしAIのコメントは万能ではありません。評価・人事に関わる判断は人が行い、AIは「下書き」「気づきのきっかけ」として使うのが安全です。この前提を押さえたうえで、実装を見ていきます。
全体の処理の流れ
今回作るのは、次の流れで動く仕組みです。日報はGoogleフォームやスプレッドシートに直接書き込む前提とします。
日報シートから、まだレビューしていない行だけを取り出す
レビュー観点を固定したプロンプトを組み立て、Claude APIに1件ずつ渡す
「良かった点・改善提案・次アクション・スコア」をJSONで受け取る
結果をシートに書き戻し、処理済みフラグを立てて二重レビューを防ぐ
毎日決まった時刻にトリガーで実行し、当日分を本人にメール通知する
前提として、Anthropicの管理画面で取得したAPIキーを、スクリプトプロパティ(コードとは別に保管する設定領域)のANTHROPIC_API_KEYに登録しておきます。キーをコードに直書きすると、共有時に漏洩する危険があるため避けます。
未レビューの日報だけを読み込む
まず、シート全体をgetDataRange().getValues()で一括取得し、本文があってまだレビューされていない行だけを対象にします。処理済みフラグ列(F列)を見てスキップすることで、トリガーが何度動いても同じ日報を二重にレビューしません。
// 日報シートのレイアウト例
// A: 日付 / B: 氏名 / C: メール / D: 日報本文 / E: レビュー / F: 処理フラグ
const SHEET_NAME = "日報";
const COL = { date: 0, name: 1, email: 2, body: 3, review: 4, done: 5 };
function getUnreviewedRows() {
const sheet = SpreadsheetApp.getActive().getSheetByName(SHEET_NAME);
const values = sheet.getDataRange().getValues();
const targets = [];
// 1行目はヘッダーなので i=1 から
for (let i = 1; i < values.length; i++) {
const row = values[i];
const body = String(row[COL.body]).trim();
const done = row[COL.done];
// 本文があり、まだレビューされていない行だけを対象にする
if (body && !done) {
targets.push({ rowNumber: i + 1, name: row[COL.name], body: body });
}
}
return { sheet, targets };
}1セルずつ読むと遅くなるため、読み込みは必ず一括にします。列の位置はCOLオブジェクトにまとめ、シートのレイアウトが変わっても1か所の修正で済むようにしています。
レビュー観点を固定するプロンプト設計
AIレビューの品質は、プロンプト(AIへの指示文)でほぼ決まります。ポイントは2つです。1つ目は、レビュー観点をコード側で固定して担当者ごとのばらつきをなくすこと。2つ目は、あとで機械的に処理できるようJSON形式で返すよう明確に指示することです。
function buildReviewPrompt(name, body) {
// レビュー観点をコード側で固定し、担当者による粒度のばらつきを防ぐ
return [
"あなたは日報をレビューする上司です。以下の日報を読み、建設的にフィードバックしてください。",
"",
"# レビュー観点",
"1. 事実(やったこと)と所感(考えたこと)が分けて書けているか",
"2. 成果や進捗が具体的な数字・状態で書けているか",
"3. 困りごと・相談したいことが書けているか",
"4. 明日やる次のアクションが明確か",
"",
"# 出力形式(必ずこのJSONだけを返す。前後に説明文を付けない)",
'{"good":"良かった点を1〜2文","improve":"改善提案を1〜2文","nextAction":"次アクションの助言を1文","score":1〜5の整数}',
"",
"# 対象の日報",
"投稿者: " + name,
"本文:",
body,
].join("\n");
}「前後に説明文を付けない」と念押ししても、モデルがはい、承知しましたなどを付けることがあります。次のセクションで、そうした場合でも壊れないJSONの取り出し方を用意します。レビュー観点の文面は、自社の日報フォーマットに合わせて自由に書き換えてください。
JSONでフィードバックを受け取り書き戻す
Claude APIを呼び出し、返ってきた本文からJSONを取り出します。応答の本文はcontent[0].textに入ります。前後に余計な文字が混じっても壊れないよう、最初の{から最後の}までを切り出してからパースします。
const API_KEY = PropertiesService
.getScriptProperties()
.getProperty("ANTHROPIC_API_KEY"); // キーはコードに直書きしない
function reviewOneReport(name, body) {
const payload = {
model: "claude-sonnet-4-5",
max_tokens: 400,
messages: [{ role: "user", content: buildReviewPrompt(name, body) }],
};
const res = fetchWithRetry("https://api.anthropic.com/v1/messages", {
method: "post",
contentType: "application/json",
headers: {
"x-api-key": API_KEY,
"anthropic-version": "2023-06-01",
},
payload: JSON.stringify(payload),
muteHttpExceptions: true,
});
const json = JSON.parse(res.getContentText());
const text = json.content[0].text; // モデルが返した本文
return parseReview(text);
}
function parseReview(text) {
// 前後に説明文が混じっても壊れないよう、最初の{ 〜 最後の} を抜き出す
const start = text.indexOf("{");
const end = text.lastIndexOf("}");
if (start === -1 || end === -1) {
throw new Error("JSONが見つかりません: " + text);
}
return JSON.parse(text.slice(start, end + 1));
}受け取ったレビューをシートに書き戻します。1件が失敗しても全体を止めないようtry/catchで囲み、失敗時は処理済みフラグを立てずに次回へ回します。1回の実行では件数を絞り、GASの6分実行制限に収めます。
function runDailyReview() {
const { sheet, targets } = getUnreviewedRows();
if (targets.length === 0) return;
// 1回の実行では最大20件までに絞り、実行時間制限に配慮する
const batch = targets.slice(0, 20);
batch.forEach((t) => {
try {
const r = reviewOneReport(t.name, t.body);
const comment =
"【良かった点】" + r.good + "\n" +
"【改善提案】" + r.improve + "\n" +
"【次アクション】" + r.nextAction + "\n" +
"【スコア】" + r.score + "/5";
sheet.getRange(t.rowNumber, COL.review + 1).setValue(comment);
sheet.getRange(t.rowNumber, COL.done + 1).setValue("done");
} catch (err) {
// 1件失敗しても全体を止めない。フラグは立てず次回に再挑戦
sheet.getRange(t.rowNumber, COL.review + 1).setValue("レビュー失敗: " + err.message);
}
});
}429エラーに備えるリトライ
日報を連続で送ると、レート制限を示す429エラーや一時的なサーバーエラー(5xx)が返ることがあります。少し待って再送するだけで多くは成功するため、指数バックオフ(待ち時間を1→2→4→8秒と倍にする方式)でリトライします。muteHttpExceptions: trueを付けると、エラー時も例外で止まらずステータスコードを自分で判定できます。
function fetchWithRetry(url, params, maxRetries) {
const retries = maxRetries || 4;
for (let attempt = 0; attempt <= retries; attempt++) {
const res = UrlFetchApp.fetch(url, params);
const code = res.getResponseCode();
if (code === 200) return res;
// 429(レート制限)と5xxのときだけ待って再送する
const retriable = code === 429 || code >= 500;
if (!retriable || attempt === retries) {
throw new Error("APIエラー code=" + code + " body=" + res.getContentText());
}
// 指数バックオフ + ジッター(1,2,4,8秒 + ランダム)
const wait = Math.pow(2, attempt) * 1000 + Math.floor(Math.random() * 500);
Utilities.sleep(wait);
}
}ランダムなジッター(ばらつき)を足しているのは、複数の再送が同じタイミングに集中するのを避けるためです。リトライしても直らない400番台のエラー(キーの誤りなど)は、そのままエラーを投げて気づけるようにしています。
トリガーで実行し本人に通知する
最後に、毎日決まった時刻に自動でレビューを走らせます。時間主導トリガーでrunDailyReviewを実行し、当日分のフィードバックを投稿者本人にメールで届けます。通知先をSlackに変えたい場合は、Incoming Webhookへの送信に置き換えるだけです。
// 毎日20時にレビューを実行する時間主導トリガー
function createReviewTrigger() {
ScriptApp.newTrigger("runDailyReview")
.timeBased()
.everyDays(1)
.atHour(20)
.create();
}
// レビュー結果を投稿者本人にメールで届ける
function notifyReviewees() {
const sheet = SpreadsheetApp.getActive().getSheetByName(SHEET_NAME);
const values = sheet.getDataRange().getValues();
const today = Utilities.formatDate(new Date(), "Asia/Tokyo", "yyyy/MM/dd");
for (let i = 1; i < values.length; i++) {
const row = values[i];
const date = Utilities.formatDate(new Date(row[COL.date]), "Asia/Tokyo", "yyyy/MM/dd");
const review = String(row[COL.review]).trim();
const email = String(row[COL.email]).trim();
if (date === today && review && email && row[COL.done] === "done") {
GmailApp.sendEmail(email, "本日の日報フィードバック", review);
}
}
}レビュー実行(20時)と通知をまとめて1つのトリガーにしてもかまいませんが、レビューが失敗した行に通知が飛ばないよう、通知はdoneフラグが立った行だけに絞っています。
実務での注意点
1. 評価には使わず「支援」に徹する
AIのスコアやコメントを人事評価にそのまま使うと、書き手がAI受けする書き方に寄ってしまいます。スコアは本人が振り返るための目安にとどめ、評価判断は人が行いましょう。
2. 送ってよい情報かを確認する
日報には取引先名や未公開の数字が含まれがちです。社外秘や個人情報を扱う場合は社内規程を確認し、必要に応じて固有名詞をマスキングしてからAPIに渡してください。
3. モデルとトークン上限を用途に合わせる
短いフィードバックならmax_tokensは控えめで十分です。モデル名や単価は変わることがあるため、最新の仕様はAnthropicの公式ドキュメントで確認してから設定してください。
4. 失敗ログを残して取りこぼしを防ぐ
本記事では失敗時にレビュー列へエラー内容を書き込み、処理済みフラグは立てない設計にしています。こうしておくと、次回のトリガーで自動的に再挑戦され、レビュー漏れを防げます。
まとめ
GASとClaude APIを組み合わせると、日報の一次レビューを毎日自動で回せます。ポイントは、レビュー観点をプロンプトで固定して粒度をそろえること、JSONで確実に受け取って書き戻すこと、429エラーにリトライで備えること、そして処理済みフラグで二重レビューを防ぐことです。AIは評価者ではなく支援役と位置づけ、人の判断と組み合わせて運用すれば、フィードバックの遅れを解消しながら現場の負担を減らせます。
よくある質問
置き換えではなく下書き・一次レビューとして使うのがおすすめです。AIは「事実の記録が薄い」「次のアクションが書かれていない」といった形式面の指摘や、見落としがちな論点の洗い出しが得意です。評価や人事に関わる最終判断は必ず人が行い、AIのコメントはあくまで参考として本人に返す運用にすると安全です。
可能です。プロンプト内のレビュー観点(例:「事実と所感が分けて書けているか」「数字で成果が書けているか」)を自社の日報フォーマットに合わせて書き換えるだけで調整できます。観点をコード側で固定しておくと、担当者によってフィードバックの粒度がばらつかなくなります。
大量に連続で呼び出すと429(レート制限)が返ることがあります。本記事では指数バックオフ付きのリトライを入れ、失敗しても数秒空けて再送する設計にしています。さらに1回のトリガーで処理する件数を絞り、レビュー済みの行はスキップすることで、GASの6分実行制限にも収まりやすくなります。
Claude APIはAnthropicのサーバーに本文を送信して処理します。社外秘や個人情報を含む日報を扱う場合は、送信してよい情報かを社内規程で確認し、必要なら氏名や取引先名をマスキングしてから渡す運用にしてください。APIキーはスクリプトプロパティに保管し、コードに直書きしないことも重要です。
できます。レビュー結果をシートに書き戻すだけでなく、日報の投稿者メールアドレス宛にGmailで送ったり、Slackにまとめて通知したりできます。本記事ではトリガーでレビューを実行し、当日分のフィードバックを本人にメールで届ける流れまで紹介しています。
AI×GASで業務を
自動化する。
日報レビュー以外にも、問い合わせ分類・議事録要約・メール自動化など、Claude APIとGASを組み合わせた業務自動化をご相談いただけます。