Managing unstructured documents is a common challenge in enterprise workflows. Invoices arrive from different vendors, in different formats, and end up in a single folder with no consistent organization. Manually sorting and categorizing them is tedious and error-prone.
In this post, we’ll walk through building a lightweight application that uses Box AI Extract to classify invoices by expense category and Files SDK to automatically organize them into folders — all in under 130 lines of TypeScript.
What is Files SDK?
Files SDK is an open-source, community-built unified storage API for JavaScript/TypeScript. It provides a single interface (upload, download, move, list, and more) across Box, object stores, local storage, and other file backends.
For this app, we’ll use the Box adapter, which wraps the Box TypeScript SDK under the hood. That means everyday file operations can use Files SDK’s consistent API, while the files.raw escape hatch still gives you direct access to Box-specific capabilities like Box AI.
That unified API becomes useful when a workflow doesn’t start and end in one storage system. Invoices might arrive from an object store, a local directory, or another file source before they need to land in Box for AI, governance, metadata, and collaboration. Files SDK keeps the generic file operations consistent across those sources, while files.raw lets you use the full power of Box when the workflow calls for it.
Setting up the Box adapter
The whole connection is one object:
import { Files } from "files-sdk";
import { box } from "files-sdk/box";
const files = new Files({
adapter: box({
ccg: {
clientId: process.env.BOX_CLIENT_ID!,
clientSecret: process.env.BOX_CLIENT_SECRET!,
enterpriseId: process.env.BOX_ENTERPRISE_ID!,
},
rootFolderId: process.env.BOX_FOLDER_ID ?? "0",
}),
hooks: { onAction: (e) => console.log(e) },
});CCG (Client Credentials Grant) auth, a root folder, and a lifecycle hook that logs every SDK operation. That’s it! You're connected to Box.
Listing files
To discover what’s in the inbox:
for await (const f of files.listAll()) {
console.log(f.key, f.size);
}listAllreturns an async iterable that handles pagination transparently. No cursors, no offset / limit loops — just iterate.
Classifying with Box AI Extract
This is where Box does the heavy lifting. Files SDK doesn't try to abstract AI features — that’s provider-specific territory. Instead, you drop into the native Box client withfiles.raw:
const res = await files.raw.ai.createAiExtractStructured({
items: [{ id: fileId, type: "file" }],
fields: [
{
key: "expense_category",
type: "enum",
prompt: "What expense category does this invoice belong to?",
options: CATEGORIES.map((key) => ({ key })),
},
{ key: "vendor_name", type: "string", prompt: "What is the name of the vendor or merchant?" },
{ key: "total_amount", type: "float", prompt: "What is the total amount charged?" },
],
});You define the schema — field names, types, prompts — and Box AI reads the document server-side. The file never leaves Box. You get back structured data like:
{
"expense_category": "Meals",
"vendor_name": "Starbucks",
"total_amount": 7.45
}Theenum type constrains the response to your predefined categories, so you always get a clean value you can use as a folder name.
Organizing the files
Once we have the category, filing is one line:
await files.move(key, `${category}/${key}`);That movesstarbucks_receipt.pdf intoMeals/starbucks_receipt.pdf. Files SDK auto-creates theMeals/folder if it doesn’t exist. With the Box SDK directly, this would be: resolve the file ID by name, check if the destination folder exists, create it if not, get the new folder’s ID, copy the file into it, then delete the original.files.movehandles all of that.
To avoid conflicts on re-runs, we check first:
if (await files.exists(dest)) continue;And at the end, we upload a summary:
await files.upload(
"classification-report.json",
JSON.stringify(report, null, 2)
);The full pipeline
Here’s the complete organize function. Five Files SDK methods, one Box AI escape hatch:
async function organize() {
const fileKeys: string[] = [];
for await (const f of files.listAll()) fileKeys.push(f.key);
// ... resolve Box file IDs for AI call ...
const report = [];
for (const key of fileKeys) {
// Box AI Extract — classify the invoice
const res = await files.raw.ai.createAiExtractStructured({ ... });
const category = res.rawData?.answer?.expense_category ?? "Other";
const dest = `${category}/${key}`;
if (await files.exists(dest)) continue; // skip duplicates
await files.move(key, dest); // file it
report.push({ file: key, category, ... });
}
await files.upload("classification-report.json", JSON.stringify(report, null, 2));
}The full app, including the Bun HTTP server, SSE streaming for real-time progress in the browser, and a single-page UI, is under 130 lines. You can grab it here.

Where to take it from here
The demo covers the core flow, but there’s a lot of room to extend it:
- Ingest from other sources withtransfer()— Files SDK can stream files from S3, R2, GCS, or a local directory into Box with a single call. You could build an ingestion step that pulls invoices from wherever they originate, then classify and organize them in Box. That’s something you can’t do with just the Box API.
- Apply Box metadata templates — Instead of only filing into folders, write the extracted fields (vendor, amount, category) back to each file as Box metadata using files.raw.files.updateFileMetadataById(). This makes the invoices searchable and filterable in Box without opening them.
- Share with stakeholders — After organizing, usefiles.rawto create shared links or add collaborators on each category folder — automatically route Professional Services/to your accounting team, Travel/to your travel admin.
- Sync downstream — Use the classification report as a trigger. Push the extracted data to a spreadsheet, an ERP system, or a Slack channel. Since the report is just JSON, you can pipe it anywhere.
- Swap the backend — The whole point of a unified API. Replacebox()withs3()or r2()and the same listAll, move, exists, uploadcalls work unchanged. The Box AI Extract call is the only thing tied to Box.

