GASからClaude APIをUrlFetchAppで呼ぶ — シートの行を1件ずつ分類
Google Apps ScriptからClaude APIをUrlFetchAppで呼び、スプレッドシートの行を1件ずつ分類します。キーの保管、必須ヘッダ、429の再試行、6分制限までの実装です。
Google Apps Script(GAS)からClaude APIを呼ぶには、UrlFetchApp.fetch()でhttps://api.anthropic.com/v1/messagesにJSONをPOSTします。APIキーはPropertiesServiceに置き、リクエストヘッダにx-api-keyとanthropic-versionを付けます。
ここでは、スプレッドシートのA列にある問い合わせ文を1行ずつ読み、B列にカテゴリを書き込むスクリプトを作ります。429エラーの再試行と、GASの実行時間制限への備えまで入れます。
全体の流れとシートの前提
作るのは、シートに紐づいたスクリプトです。メニューの「拡張機能」から開くApps Scriptエディタに、次の3つの関数を置きます。
callClaude_: 1件の文章をClaudeに送り、カテゴリ名を返すclassifyRows: 未分類の行を上から順に処理してB列へ書くsetApiKey: APIキーを保存する(最初に1回だけ実行)
シートは次の形を想定します。
| 列 | 中身 |
|---|---|
| A列 | 中身問い合わせ文(2行目から) |
| B列 | 中身分類結果(空欄なら未処理) |
カテゴリは「請求」「不具合」「機能要望」「その他」の4つにします。自由記述の分類を大量に回す設計は、自由記述の分類をバッチで回す記事にまとめています。ここは数百行までを手元のシートで済ませる前提です。
APIキーをPropertiesServiceに置く
キーをコードに直書きすると、スクリプトを共有したりコピーしたりするたびに文字列が付いて回ります。PropertiesService.getScriptProperties()に保存し、コードからは名前で引きます。
function setApiKey() {
// 実行後は、この行からキー文字列を消してください
PropertiesService.getScriptProperties()
.setProperty('ANTHROPIC_API_KEY', 'sk-ant-...');
}setProperty(key, value)は、現在のプロパティストアにキーと値の組を保存します。実行したらキー文字列を消してから保存し直してください。エディタ上の「プロジェクトの設定」にあるスクリプト プロパティから、手で登録しても同じです。
読み出しはgetProperty(key)で、キーが無ければnullが返ります。後の関数では、nullのときに分かりやすいエラーで止めます。
必須ヘッダとリクエスト本文
Claude APIのリクエストに必要なヘッダは、公式の一覧で次のとおりです。
| ヘッダ | 値 |
|---|---|
x-api-key | 値ConsoleのAPIキー |
anthropic-version | 値APIバージョン(例: 2026-10-21) |
content-type | 値application/json |
認証の第一候補はAuthorization: Bearer <キー>で、x-api-keyは旧来の方式として引き続きサポートされると書かれています。この記事はx-api-keyで書きますが、どちらもheadersに書くだけで差し替えられます。content-typeはUrlFetchAppのcontentTypeオプションに任せます。
anthropic-versionの最新は2026-10-21です。このバージョンでは、temperature・top_p・top_kが削除され、送ると全モデルで400になります。以前のサンプルコードを流用するときは、これらのパラメータを外してください。
本文のうち必須なのはmodel・max_tokens・messagesです。分類では、出力をラベル1語に絞りたいので、systemに指示を書き、max_tokensを小さくします。モデルIDは、公式のモデル一覧にあるclaude-haiku-5-5を使います。品質が足りなければclaude-sonnet-5-5などに差し替えるだけで済みます。
1件を分類する関数
const API_URL = 'https://api.anthropic.com/v1/messages';
const MODEL = 'claude-haiku-5-5';
const LABELS = ['請求', '不具合', '機能要望', 'その他'];
function callClaude_(text) {
const apiKey = PropertiesService.getScriptProperties()
.getProperty('ANTHROPIC_API_KEY');
if (!apiKey) throw new Error('ANTHROPIC_API_KEY が未設定です');
const options = {
method: 'post',
contentType: 'application/json',
muteHttpExceptions: true, // 4xx/5xxでも例外にせず、自分で判定する
headers: {
'x-api-key': apiKey,
'anthropic-version': '2026-10-21',
},
payload: JSON.stringify({
model: MODEL,
max_tokens: 20,
system: '問い合わせ文を次のいずれか1語で分類してください: '
+ LABELS.join('、') + '。ラベル以外は出力しないでください。',
messages: [{ role: 'user', content: text }],
}),
};
const res = fetchWithRetry_(options);
const body = JSON.parse(res.getContentText());
const label = (body.content[0].text || '').trim();
return LABELS.indexOf(label) >= 0 ? label : '要確認';
}ポイントは3つあります。
muteHttpExceptions: trueにすると、レスポンスコードが失敗を示していても例外を投げず、HTTPResponseが返ります。ステータスコードで再試行するかを決めるために必要です。- 応答の本文は
content配列にあり、テキストはtypeがtextのブロックのtextに入ります。 - モデルが想定外の語を返しても、
LABELSに含まれなければ「要確認」にします。シートの文面がそのままモデルに渡るため、出力の側で受け入れる値を固定しておく設計です。
厳密な形式で受け取りたいときは、output_config.formatにjson_schemaを指定する構造化出力もあります。ラベル1語なら上の検証で足ります。
429と5xxの再試行を入れる
UrlFetchAppには自動の再試行がありません。公式SDKは一時的な失敗を指数バックオフで再試行し、retry-afterヘッダがあればそれに従いますが、GASから直接呼ぶ場合は自分で書きます。
ステータスコードごとの扱いは次のとおりです。
| コード | 意味 | 扱い |
|---|---|---|
| 200 | 意味成功 | 扱い本文を解析 |
| 429 | 意味rate_limit_error | 扱い待って再試行 |
| 500 | 意味api_error | 扱い待って再試行 |
| 529 | 意味overloaded_error | 扱い待って再試行 |
| 400・401・413など | 意味リクエストや認証の誤り | 扱い再試行せず停止 |
429でretry-afterヘッダが付いていれば、その秒数だけ待ちます。早く再試行しても失敗する、と公式に書かれています。
function fetchWithRetry_(options) {
const maxAttempts = 5;
for (let attempt = 0; attempt < maxAttempts; attempt++) {
const res = UrlFetchApp.fetch(API_URL, options);
const code = res.getResponseCode();
if (code === 200) return res;
const retryable = code === 429 || code >= 500;
if (!retryable || attempt === maxAttempts - 1) {
throw new Error('Claude API ' + code + ': ' + res.getContentText());
}
const waitSec = getRetryAfterSec_(res) || Math.pow(2, attempt + 1);
Utilities.sleep(waitSec * 1000);
}
}
function getRetryAfterSec_(res) {
const headers = res.getHeaders();
for (const name in headers) {
if (name.toLowerCase() === 'retry-after') {
return Number(headers[name]) || 0;
}
}
return 0;
}Utilities.sleep(ms)は、指定したミリ秒だけスクリプトを止めます。ヘッダ名は大文字小文字を無視して探しているので、返ってくる表記に左右されません。
429には、再試行しても直らない種類もあります。利用ティアの月間支出上限に達した場合も同じrate_limit_errorの429ですが、retry-afterヘッダが付かず、アクセスが再開するまで失敗し続けると公式に書かれています。上のコードは5回で諦めて例外を投げるので、その場合はシートに書かずに止まり、本文のエラーメッセージで原因を追えます。
エラー本文にはrequest_idが入ります。サポートに問い合わせるときは、レスポンスヘッダのrequest-idと合わせて控えておくと調査が早くなります。
シートの行を1件ずつ処理する
function classifyRows() {
const startedAt = Date.now();
const sheet = SpreadsheetApp.getActiveSheet();
const lastRow = sheet.getLastRow();
if (lastRow < 2) return;
const values = sheet.getRange(2, 1, lastRow - 1, 2).getValues();
for (let i = 0; i < values.length; i++) {
const [text, current] = values[i];
if (!text || current) continue; // 空行と処理済みは飛ばす
if (Date.now() - startedAt > 4.5 * 60 * 1000) {
console.log('時間切れ前に中断。再実行で続きから処理します');
return;
}
const label = callClaude_(String(text));
sheet.getRange(i + 2, 2).setValue(label);
}
}B列が空の行だけを処理するので、途中で止まっても、もう一度実行すれば続きから再開します。1件ごとにsetValueで書き込むのは、失敗した時点までの結果をシートに残すためです。
落とし穴
6分を超えるとスクリプトが打ち切られる
Apps Scriptの実行時間は、1回あたり6分までです(個人アカウントもWorkspaceアカウントも同じ)。1件ずつ順番に呼ぶと、1件に数秒かかるだけで数百行が危うくなります。上のコードは4分30秒で自分から中断し、再実行で続きを処理します。残りの行が多いときは、時間主導型トリガーでclassifyRowsを定期実行すると、人手なしで最後まで進みます。
制限は他にもあります。UrlFetchの呼び出しは、個人アカウントで1日2万回、Workspaceアカウントで10万回までです。再試行も1回に数えるため、行数の上限は少し余裕を見て考えてください。スプレッドシートのカスタム関数は実行が30秒までなので、=CLASSIFY(A2)のような形で埋め込む用途には向きません。メニューやトリガーから動かす構成にします。
エラー本文を読んでシートに残す
APIはエラーを常にJSONで返します。トップレベルのerrorオブジェクトにtypeとmessageが入り、request_idも付きます。fetchWithRetry_が投げる例外メッセージには本文をそのまま入れているので、実行ログを開けばrate_limit_errorかinvalid_request_errorかを判別できます。
行単位の失敗をシートに残したいなら、callClaude_をtry/catchで包み、C列にエラーのtypeを書く形にします。B列を空のままにしておけば、原因を直したあとの再実行で、その行だけがやり直されます。
try {
sheet.getRange(i + 2, 2).setValue(callClaude_(String(text)));
} catch (e) {
sheet.getRange(i + 2, 3).setValue(String(e.message).slice(0, 200));
}ただし、キー未設定や400のように全行で同じ理由で失敗するエラーは、1行目で止めたほうが無駄な呼び出しが出ません。再試行の対象外のエラーは、catchせずに例外を伝えて実行ごと止める選び方もあります。
使用量を記録する
成功したレスポンスのusageには、input_tokensとoutput_tokensが入っています。分類のように1件が短い処理でも、数百行を回すと合計は無視できなくなります。
const usage = body.usage; // { input_tokens, output_tokens, ... }
console.log('tokens in=%s out=%s', usage.input_tokens, usage.output_tokens);実行ログに出しておけば、1回の実行で何トークン使ったかをあとから見積もれます。単価はモデルごとに違うので、料金は公式の料金ページで確認してください。
並列で呼ぶfetchAllは使うべきか
UrlFetchApp.fetchAll(requests)は、複数のURLへのリクエストをまとめて投げます。6分の枠に収まりやすくなりますが、レート制限の側は逆風です。
- レート制限はRPM(1分あたりのリクエスト数)、ITPM(入力トークン数)、OTPM(出力トークン数)で測られ、超えると429になります
- 容量は一定間隔でリセットされず、上限まで連続的に補充されるトークンバケット方式です。短時間に一気に投げると使い切って429が増えます
fetchAllでは、429が返った行だけを選んで再試行するコードが別に要ります
最初は1件ずつの逐次処理で動かし、6分に収まらず、429が出ない範囲に余裕があると分かってからfetchAllを足す順番が無難です。急な利用増は、加速度制限として429になる場合があるとも公式に書かれています。増やすなら少しずつです。
時間主導型トリガーで続きを自動実行する
6分の打ち切りに備えて中断する設計にしたので、未処理が残るあいだ定期的に再実行する仕組みを足します。トリガーは、エディタの「トリガー」画面から手で作るか、コードで作ります。
function createTrigger() {
ScriptApp.newTrigger('classifyRows')
.timeBased()
.everyMinutes(10)
.create();
}全行が終わったら、トリガーを削除する処理をclassifyRowsの末尾に入れておくと、空回りが続きません。トリガーの合計実行時間にも1日の上限があり、個人アカウントは90分、Workspaceアカウントは6時間です。10分おきの再実行で毎回4分半動くと、個人アカウントは1日の枠を使い切りやすくなります。
動かしてから確かめること
初回は2〜3行だけ入れて、次の順に確認します。
setApiKeyを実行し、承認画面で外部サービスへの接続を許可する- A列に3行ほど入れて
classifyRowsを実行する - B列にラベルが入るか、「要確認」が混じらないかを見る
- 実行ログ(エディタの「実行数」)で、エラーが出ていないかを見る
承認画面に出る権限は、UrlFetchAppがscript.external_requestのスコープを必要とするためです。初回の実行時に許可を求められます。
401が返るときは、キーの保存名やコピー時の空白を疑います。400が返るときは、リクエスト本文のパラメータを疑い、削除されたtemperatureなどが残っていないかを確かめます。
精度の確かめ方と、送る内容の線引き
分類の精度は、スクリプトが動いたかどうかでは測れません。人が付けた正解ラベルが20〜30件あるなら、別の列に置いて、Claudeの結果との一致を数式で数えます。食い違った行を読めば、カテゴリの定義が曖昧なのか、systemの指示が足りないのかが分かります。直すのは、まずsystemのカテゴリ説明です。各カテゴリに1行ずつ「どんな文面が入るか」を書き足すと、境界の行が安定しやすくなります。
もう一点、UrlFetchAppで送った文面は、api.anthropic.comに渡ります。個人情報や社外秘が混じる列をそのまま送るのか、氏名や連絡先を伏せた列だけを送るのかは、スクリプトを書く前に決めておきます。伏せる処理は、callClaude_に渡す前の関数で挟めます。
次に広げるとき
行数が数千に増えるなら、1件ずつの同期呼び出しをやめ、Batches APIで非同期にまとめて処理する方向があります。分類の評価と人手検証の組み方は、コンテンツモデレーションの実装記事が参考になります。
スクリプトそのものをClaude Codeに書かせたい場合は、claspでローカルに置く手順をClaude CodeでGAS開発を進める手順で扱っています。シートの引き継ぎ資料をClaudeに作らせる使い方は別の記事にあります。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。
- platform.claude.com/docs/en/api/messages
- platform.claude.com/docs/en/api/overview
- platform.claude.com/docs/en/api/errors
- platform.claude.com/docs/en/api/rate-limits
- platform.claude.com/docs/en/api/versioning
- developers.google.com/apps-script/reference/url-fetch/url-fetch-app
- developers.google.com/apps-script/guides/services/quotas
- developers.google.com/apps-script/reference/properties/properties