Skillsの生成ファイルをダウンロードする4ステップ
Skillsがコード実行環境で作ったExcel・PowerPoint・PDFは、応答に埋め込まれずfile_idだけが返ります。Files APIで実体を取り出すまでの4ステップを扱います。
Messages APIのSkillsにExcelファイルの作成を頼んでも、応答にファイルの中身は入っていません。返るのはfile_idという識別子だけです。実体のバイト列を手に入れるには、Files APIへ別リクエストを送る必要があります。この記事では、file_idを取り出してから実ファイルを保存するまでの4ステップと、途中で踏みやすい落とし穴を扱います。
なぜ応答にファイルがそのまま入らないのか
Skillsはコード実行ツールの上で動きます。Excel・PowerPoint・PDF・Wordの各Skillは、コンテナ内のコードを実行してファイルを作り、コマンドが終わった時点で$OUTPUT_DIR直下に残っているファイルだけを拾い上げます。拾い上げたファイルはバイト列としてレスポンスに埋め込まれるのではなく、Files API側にアップロードされ、その参照であるfile_idだけが応答のcontentに載ります。Messages APIのレスポンス自体は元々テキストベースの構造なので、バイナリを直接返す代わりにこの間接参照方式を取っています。
前提はAPIキーと、コード実行ツール(code_execution_20250825)を有効にしたリクエストです。Skillはcontainer.skillsに指定して呼び出します。
{
"model": "claude-opus-5",
"max_tokens": 4096,
"container": {
"skills": [
{ "type": "anthropic", "skill_id": "xlsx", "version": "latest" }
]
},
"messages": [
{ "role": "user", "content": "Create an Excel file with a simple budget spreadsheet" }
],
"tools": [
{ "type": "code_execution_20250825", "name": "code_execution" }
]
}Anthropic製Skillとカスタムskillは、typeとskill_idの形式が異なります。上限や指定方法など、比較しておくと混同しにくい項目は次の通りです。
| 項目 | Anthropic製Skill | カスタムSkill |
|---|---|---|
type | Anthropic製Skill"anthropic" | カスタムSkill"custom" |
skill_idの形式 | Anthropic製Skillxlsx / pptx / docx / pdfのような短い名前 | カスタムSkillskill_01AbCdEfGhIjKlMnOpQrStUvのような生成ID |
version指定 | Anthropic製Skill"latest"などバージョン指定可能 | カスタムSkill同様にバージョン指定可能 |
| 1リクエストの上限 | Anthropic製Skillcontainer.skills配列全体で最大20個(共通) | カスタムSkill同左 |
つまりtype・skill_idの書式こそ違いますが、versionの指定方法と1リクエストあたりの上限はAnthropic製・カスタムで共通です。リクエストを組む際にどちらのSkillを使っているか迷ったら、skill_idの見た目がこの表のどちらに近いかで判別できます。
ステップ1〜2: file_idを応答から取り出す
Skillがファイルを作ると、応答のcontentにはbash_code_execution_tool_resultというブロックが含まれます。この中のbash_code_execution_result.content配列に、作成されたファイルごとのfile_idが入っています。
FILE_ID=$(echo "$RESPONSE" | jq -r '
.content[]
| select(.type=="bash_code_execution_tool_result")
| .content
| select(.type=="bash_code_execution_result")
| .content[]
| select(.file_id)
| .file_id
')Python SDKなら、レスポンスのcontentをループして同じ構造を辿ります。
def extract_file_ids(response):
file_ids = []
for item in response.content:
if item.type == "bash_code_execution_tool_result":
content_item = item.content
if content_item.type == "bash_code_execution_result":
for file in content_item.content:
file_ids.append(file.file_id)
return file_idsここまでの4ステップは、どれも単体で見れば短いコードですが、実際に組んでみると症状だけが見えて原因が分かりにくい詰まり方をします。以下は、file_idの抽出からダウンロードまでの間で実際に踏みやすい4つの落とし穴を、症状から逆引きできるように整理したものです。
| 症状 | 原因 | 切り分け方 |
|---|---|---|
file_idが0件しか返らない | 原因コードが$OUTPUT_DIRの外に書き込んでいる | 切り分け方コンテナ内の保存先パスを確認し、$OUTPUT_DIR直下に書き直す |
メタデータは読めるのにcontent取得が404になる | 原因ファイルの有効期限(expires_at)が過ぎている | 切り分け方GET /v1/files/{file_id}はメタデータのみ期限切れから30日間は読み取れるため、expires_atの値で期限切れかどうかを確認する |
| アップロードやダウンロードがエラーになる | 原因1ファイル500MB・組織1TBの上限に当たっている | 切り分け方ファイルサイズと組織全体のストレージ使用量を確認し、不要なファイルをdeleteしてから再試行する |
ダウンロードしたfile_idが意図と違うファイルを指す | 原因出力用のfile_idと、入力用に渡したcontainer_uploadのfile_idを取り違えている | 切り分け方応答から抽出したfile_idと、リクエストに含めたcontainer_uploadのfile_idを別の変数で管理し、混同していないか見直す |
ステップ3〜4: メタデータ取得とFiles APIダウンロード
file_idが手に入ったら、まずファイル名などのメタデータを取得し、それから実体をダウンロードします。ファイル名を先に取っておくのは、拡張子込みの保存名をコード側で決め打ちしないためです。
# ステップ3: メタデータからファイル名を取得
FILENAME=$(curl "https://api.anthropic.com/v1/files/$FILE_ID" \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" | jq -r '.filename')
# ステップ4: Files APIで実体をダウンロード
curl "https://api.anthropic.com/v1/files/$FILE_ID/content" \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
--output "$FILENAME"Python SDKではretrieve_metadataとdownloadという2つのメソッドに対応します。
for file_id in extract_file_ids(response):
file_metadata = client.files.retrieve_metadata(file_id=file_id)
file_content = client.files.download(file_id=file_id)
file_content.write_to_file(file_metadata.filename)TypeScriptでは、response.contentをループしてブロックの型を判定する書き方になります。ステップ2〜4を1つのループにまとめられます。
import { writeFile } from "node:fs/promises";
// ステップ2: 応答からfile_idを抽出
const fileIds: string[] = [];
for (const block of response.content) {
if (
block.type === "bash_code_execution_tool_result" &&
block.content.type === "bash_code_execution_result"
) {
for (const outputBlock of block.content.content) {
fileIds.push(outputBlock.file_id);
}
}
}
// ステップ3〜4: メタデータ取得とダウンロードを1件ずつ実行
for (const fileId of fileIds) {
const fileMetadata = await client.files.retrieveMetadata(fileId);
const fileResponse = await client.files.download(fileId);
await writeFile(fileMetadata.filename, Buffer.from(await fileResponse.arrayBuffer()));
}以上が4ステップです。①Skillでファイルを作る、②応答からfile_idを取り出す、③メタデータでファイル名を確認する、④Files APIでダウンロードして保存する、という順序は言語やSDKが変わっても同じです。Files APIには他にretrieve_metadata(単体のメタデータ)・list(全ファイル一覧)・delete(削除)のメソッドもあり、Files API全体の使い方はClaude Files APIでPDFを処理する方法で扱っています。
ダウンロードした画像・動画はコンテナ内のものと一致しない
Skillが画像や動画(PNG・JPEG・GIF・WebP・TIFF・HEIC・AVIF・SVG・MP4・MOV)を作った場合、Files API経由でダウンロードしたファイルには、コンテンツの来歴を記録する業界標準規格C2PA(Coalition for Content Provenance and Authenticity)に基づくContent Credentialsが自動で埋め込まれます。Anthropicを発行者として記録する署名付きマニフェストで、ファイルの見た目やピクセル自体は変わりませんが、メタデータが数キロバイト分増えます。その結果、ダウンロードしたファイルのサイズとチェックサムは、コンテナ内にあった元のファイルと一致しません。生成直後のファイルをアプリ側でハッシュ突合するような検証を組んでいる場合、この差分を織り込む必要があります。
この署名は画像・動画にだけ付き、テキストファイル・PDF・Officeドキュメント(Excel・PowerPoint・Word)は対応フォーマット外のため署名されません。本記事の例で使ったExcelファイルはこの対象外です。リクエスト・レスポンスの扱い方を変える必要はなく、署名の有無自体はダウンロード手順そのものには影響しません。
Skillへ入力ファイルを渡す場合
逆方向、つまりSkillに処理させたいファイル(元データのExcelなど)を渡す場合は、先にFiles APIへアップロードしてから、そのIDをリクエストに含めます。
{
"container": {
"skills": [{ "type": "anthropic", "skill_id": "xlsx", "version": "latest" }]
},
"messages": [
{
"role": "user",
"content": [
{ "type": "text", "text": "このシートを集計して" },
{ "type": "container_upload", "file_id": "file_011CNha8iCJcU1wXNR6q4V8w" }
]
}
]
}container_uploadブロックのfile_idには、事前にFiles APIへアップロードして得たIDを指定します。出力の取得と入力の受け渡しは、どちらも同じFiles APIを介する点で対称的です。
この対称性ゆえに、実装上は「出力として受け取ったfile_id」と「入力として渡すfile_id」を、うっかり同じ変数名で扱ってしまいやすい点に注意が必要です。前段の落とし穴の表で挙げた取り違えは、この対称な設計そのものが原因で起きます。1つのリクエストの中でSkillの出力を次のリクエストの入力に連鎖させるようなパイプラインを組む場合は、どちらの向きのfile_idなのかをコード上で明示的に区別しておくと、後から読み返したときにも追いやすくなります。
ダウンロードした後、生成ファイルはいつまで残るか
Skillsが作ったファイルも、内部的にはFiles API上の1オブジェクトです。アップロード時と同じライフサイクルルールに従い、明示的な有効期限(expires_at)を設定しない限りはnullのまま、つまり自分で削除するまでずっと残り続けます。組織あたりのストレージ上限は1TB、1ファイルあたりの上限は500MBです。
有効期限が来た(またはdeleteした)ファイルは、GET /v1/files/{file_id}/contentが404エラーを返すようになります。メタデータ(GET /v1/files/{file_id})だけは、期限切れから30日間はexpires_atが過去日時のまま読み取れます。
ダウンロードが終わったファイルを放置すると、組織のストレージ上限を静かに圧迫していきます。棚卸しと回収は次の2手順です。
listで棚卸しする:client.files.list()で組織内の全ファイルを列挙し、created_atやfilenameから不要なものを洗い出します。ダウンロード済みでAnthropic側に残す理由がないファイルは、この時点で対象にします。deleteで回収する: 対象になったファイルをclient.files.delete(file_id=file_id)で1件ずつ削除します。手元に保存できているファイルであれば、Anthropic側に残しておく必要はありません。
Skillsのように実行のたびにファイルを作る使い方では、この棚卸しと回収を定期的に回さないと1ファイル500MB・組織1TBという上限に想定より早く当たります。特に、検証やデモのために同じSkillを何度も試すような使い方をしていると、生成されたファイルが有効期限なしのままFiles API側に積み上がっていきます。手元のダウンロード先ディレクトリだけを見ていると気づきにくいため、listによる定期的な棚卸しを実行フローの一部として組み込んでおくのが安全です。
1リクエストに含められるSkillsの数
container.skills配列には、1リクエストあたり最大20個までSkillを含められます。Anthropic製・カスタムを問わず、この上限は共通です。多数のSkillを一度に指定すると、使われないSkillがあってもコンテキストを消費するため、公式ドキュメントは「タスクに必要なSkillだけを含める」ことを推奨しています。データ分析(Excel)とプレゼン作成(PowerPoint)のように複数の文書種別をまたぐタスクでは複数Skillの併用が有効ですが、使わないSkillを含め続けるのは避けます。
まとめ
Skillsが作ったファイルは応答に直接埋め込まれず、file_idという参照だけが返ります。実体を手に入れるには、①作成 → ②file_id抽出 → ③メタデータ取得 → ④Files APIダウンロードという4ステップを踏みます。ファイルは$OUTPUT_DIR直下に書かれたものだけが対象になるため、この条件を外れるとファイルが消えたように見えます。入力ファイルを渡す場合は逆方向に同じFiles APIを使い、container_uploadブロックでIDを渡します。1リクエストに含められるSkillsは最大20個です。Skills自体の全体像はAgent Skillsとは何か、コード実行環境の制約はコード実行ツールが無料になる条件にまとめています。