How to Build a Google Drive Auto-Filer With Jev (Full Code)
A shared Drive folder fills up with invoices, contracts and ID scans, and nobody files them. This build uses Jev to decide which folder each file belongs in and Swytchcode to move it, with a confidence gate so badly named files get left alone instead of misfiled.
Key takeaways
- -Jev picks the destination folder with a choice question and flags personal data with a noul question, in one call per file.
- -Swytchcode moves the file with drive.file.parents.create, and only the Drive methods you add to tooling.json can run.
- -Gate on confidence. A file called scan_0012.pdf should land in Needs-Review, not in your best guess at a folder.
- -Never auto-file anything flagged as an ID document. Moving a file is easy to undo; putting a passport scan in a shared folder is not.
- -Drive v2 lets a file have several parents, so adding a parent files it without removing it from the original folder. That is a second call, and it is the one to add last.
Every shared drive has one. A folder called Uploads, or Scans, or To File, with four hundred things in it. Invoices, signed engagement letters, PAN card photos, a screenshot someone took of a bank error, three copies of the same GST challan.
Filing them is nobody's job, which is why nobody does it.
We built a small agent for this. Jev looks at each file and picks the folder it belongs in. Swytchcode does the moving. The interesting part turned out not to be the sorting, but knowing when to refuse to sort: about a fifth of real files are named so badly that any guess is a coin flip, and a confident wrong move is worse than leaving the file where it was.
By the end of this you'll have a Node.js script that reads one Drive folder, asks Jev four questions per file, and moves the files it's sure about while leaving the rest in a review pile.
What you'll build
Drive "Uploads" folder
│ swytchcode exec drive.file.list (list what's in there)
▼
{ title, mimeType, fileSize, createdDate, owner }
│
▼
Jev: 4 questions in one call
│
▼
{ folder, has_personal_data, document_year, is_duplicate } + confidence
│
├─ confident, no personal data ──> swytchcode exec drive.file.parents.create
├─ unsure ───────────────────────> move to Needs-Review
└─ personal data ────────────────> leave alone, print a warningTwo pieces, same as our Gmail triage agent:
- Jev makes the decisions. It never touches Drive.
- Swytchcode runs every call, to Jev and to Drive. It holds the OAuth token, only runs the methods you allowed, and logs what happened.
Why Jev rather than an LLM
Filing is a classification job with a fixed answer set, which is exactly the shape Jev is built for. Three things matter here specifically.
The answer is always a real folder. Ask an LLM to pick from your folder list and it will occasionally invent Invoices/2026/Pending, a folder that doesn't exist. Jev can only return one of the keys you defined, so the move either targets a real folder or doesn't happen.
You get a number to threshold on. This is the whole reason the build works. A file named Tata_Motors_Invoice_Mar2026.pdf comes back at high confidence. scan_0012.pdf comes back low, and low is the signal to stop.
It's cheap enough to run on the whole backlog. Four hundred files at a couple of hundred tokens each is a fraction of a cent. You can re-run it after every tweak to your folder descriptions without thinking about the bill.
What you need
- Node.js 20 or newer.
- A Jev API key. From console.typesafe.ai, or through Vercel's AI Gateway if the console is closed. Our access guide covers both. You'll paste it into Swytchcode once, not into your code.
- The Swytchcode CLI.
npx swytchcode, ornpm install -g swytchcode, thenswy login. - A Google account with a Drive folder you don't mind rearranging. Make a test folder with copies first. This agent moves real files.
Step 1: Set up the project
mkdir drive-auto-filer && cd drive-auto-filer
npm init -y && npm pkg set type=module
npm install @swytchcode/runtimeThen set up Swytchcode in the same folder:
swy init --mode=sandbox
swytchcode init asks which editor you use and whether to run in production or sandbox mode. Start in sandbox.
That creates a .swytchcode/ folder with tooling.json inside it. The mode lives in that file, so your code never sets it or reads it.
There's no .env file in this project. No Jev key, no Google token, no mode flag. Swytchcode keeps credentials in its own local store at ~/.swytchcode/credentials.db, outside the project folder, and attaches them when a call runs. Nothing secret ends up in the repo.
Step 2: Connect Jev and Drive
swy get jev
swy get drive
swy add method <jev-method-id> # evaluate questions against a state
swy add method drive.file.list # list files in a folder
swy add method drive.file.parents.create # file a document into a folder
swy auth connect jev # asks for your Jev API key
swy auth connect drive # opens Google's sign-in in your browser
swy doctorThree things worth noticing.
Only three Drive methods are allowed. That's deliberate, and it's the strongest guardrail in this build. This agent can list files and add a parent folder. It cannot delete, share, overwrite or download anything, because those methods were never added to tooling.json. If a bug in your code tries, Swytchcode refuses. Our post on tooling.json, manifest.json and policies.json explains how the config files fit together.
Find the Jev method ID with swy list methods jev. It's one method, and the exact ID depends on the integration version you fetched.
Confirm the Drive method shapes before you write code against them. The Drive integration follows Google's v2 API, so operations are grouped as files, parents and children rather than v3's addParents parameter:
swy discover "add a file to a folder in google drive"
swy info drive.file.parents.create
A coding agent using swytchcode list and swytchcode info to find the right method and check its contract before it runs anything.
swy auth connect behaves differently per provider. Jev uses an API key, so the CLI prompts you to paste it. Drive uses OAuth, so it opens a browser and Google asks which account and which scopes. Either way the credential lands in Swytchcode's store rather than your code. The authentication docs go into the storage detail.
Step 3: Describe your folders, then ask about them
This step decides whether the whole thing works. Jev is only as good as the folder descriptions you write.
Start with the map. Real folder IDs, taken from the Drive URL when you open each folder:
// src/folders.js
export const INBOX_FOLDER_ID = "1AbCdEfGhIjKlMnOpQrStUvWxYz"; // the "Uploads" dumping ground
export const REVIEW_FOLDER_ID = "1ReViEwFoLdErIdHeRe"; // where unsure files go
export const DESTINATIONS = {
invoices_received: "1InVoIcEsReCeIvEdId",
invoices_issued: "1InVoIcEsIsSuEdId",
contracts: "1CoNtRaCtSfOlDeRiD",
tax_filings: "1TaXfIlInGsFoLdErId",
bank_statements: "1BaNkStAtEmEnTsId",
};Now the questions. Note that the criteria keys match the DESTINATIONS keys exactly, so Jev's answer is already a lookup:
// src/decisions.js
export function buildQuestions() {
return {
folder: {
type: "choice",
instructions:
"Which folder does this file belong in, for a chartered accountant's practice?",
criteria: {
invoices_received: "A bill or invoice the practice received from a vendor or supplier",
invoices_issued: "An invoice or fee note the practice issued to a client",
contracts: "A signed agreement, NDA, engagement letter, or deed",
tax_filings: "A GST return, income tax return, challan, acknowledgement, or government notice",
bank_statements: "A bank or credit card statement, or a passbook export",
other: "Anything that does not clearly belong in one of the above",
},
},
has_personal_data: {
type: "noul",
instructions:
"This file appears to contain identity documents or personal identifiers: " +
"PAN card, Aadhaar, passport, driving licence, or a photograph of a person.",
},
name_is_descriptive: {
type: "noul",
instructions:
"The file name describes the contents. A name like 'scan_0012.pdf', " +
"'IMG_4471.jpg' or 'document (3).pdf' is not descriptive.",
},
document_year: {
type: "choice",
instructions: "Which financial year does this document most likely relate to?",
criteria: {
"fy_2026_27": "April 2026 onwards",
"fy_2025_26": "April 2025 to March 2026",
"fy_2024_25": "April 2024 to March 2025",
older: "Before April 2024",
unknown: "Cannot tell from the available information",
},
},
};
}Four choices that came out of getting this wrong first:
- The
otherkey is not optional. TypeSafe's Choice docs recommend a "none of the above" option whenever your list might not cover everything. Without it, a random screenshot gets forced intocontracts. name_is_descriptiveis the cheapest safety net here. Asking Jev to judge the input rather than only the answer catches thescan_0012.pdfcase directly, and it costs nothing extra because it rides along in the same call.- Say who the folders belong to. "For a chartered accountant's practice" changes how Jev reads an ambiguous name. Swap it for your own context.
- Ask for the year even if you don't use it yet. Questions are answered in parallel and billing is per request, so an extra question adds tokens but almost no time. When you later want
invoices_received/FY2025-26, the answer is already there.
Then the state. This is just the metadata Drive gives us:
// src/decisions.js (continued)
export function fileToState(file) {
const sizeKb = file.fileSize ? Math.round(Number(file.fileSize) / 1024) : "unknown";
return [
`File name: ${file.title}`,
`Type: ${file.mimeType}`,
`Size: ${sizeKb} KB`,
`Uploaded: ${file.createdDate}`,
`Uploaded by: ${file.owners?.[0]?.displayName ?? "unknown"}`,
].join("\n");
}That is deliberately metadata only, and it's the honest limitation of this build: Jev is reading the label on the box, not the contents. For well-named files that's plenty. For scans it isn't, which is what the confidence gate is for. If your files are mostly scans, see the extension at the end.
Step 4: Read the folder
// src/drive.js
import { exec } from "@swytchcode/runtime";
export async function listFolder(folderId, { max = 100 } = {}) {
const res = await exec("drive.file.list", {
params: {
q: `'${folderId}' in parents and trashed = false`,
maxResults: max,
fields: "items(id,title,mimeType,fileSize,createdDate,owners/displayName),nextPageToken",
},
});
return res.items || [];
}
export async function fileInto(fileId, folderId) {
// Drive v2: adding a parent files the document into that folder
return exec("drive.file.parents.create", {
params: { fileId },
body: { id: folderId },
});
}That's the entire Drive layer. No OAuth code, no token refresh, no googleapis dependency. Each exec checks the method is allowed in tooling.json, runs your policies, adds your Google credentials, makes the request and returns JSON. It throws on failure, so wrap calls where you care. The execution pipeline docs walk through each stage.
The fields parameter is worth keeping. Drive returns a lot per file by default, and you only need five things.
Step 5: Ask Jev
// src/jev.js
import { exec } from "@swytchcode/runtime";
// Find yours with: swy list methods jev
const JEV_METHOD = "<jev-method-id>";
export async function decide(state, questions, model = "jev-latest") {
// Swytchcode adds your Jev key, runs the call and returns parsed JSON
return exec(JEV_METHOD, { body: { model, state, questions } });
}
export function readAnswers(answers) {
return {
folder: answers.folder.choice,
folder_confidence: answers.folder.confidence,
has_personal_data: answers.has_personal_data.noul,
name_is_descriptive: answers.name_is_descriptive.noul,
year: answers.document_year.choice,
};
}jev-latest tracks new releases. Pin a version like jev-1.13.0 if you want yesterday's results to match today's.
Step 6: Decide what to actually do
This is the file worth reading twice. The rules here matter more than the model.
// src/plan.js
import { DESTINATIONS, REVIEW_FOLDER_ID } from "./folders.js";
const CONFIDENT = 0.75;
const PERSONAL_DATA = 0.5;
export function planFor(file, a) {
// 1. Never auto-file identity documents, however sure Jev is.
if (a.has_personal_data >= PERSONAL_DATA) {
return { action: "skip", reason: "looks like an ID document, needs a human" };
}
// 2. A name that describes nothing cannot support a confident decision.
if (a.name_is_descriptive < 0.5) {
return { action: "review", folderId: REVIEW_FOLDER_ID, reason: "file name is not descriptive" };
}
// 3. No folder for "other", by design.
if (a.folder === "other") {
return { action: "review", folderId: REVIEW_FOLDER_ID, reason: "no matching folder" };
}
// 4. Low confidence goes to review, not to a guess.
if (a.folder_confidence < CONFIDENT) {
return {
action: "review",
folderId: REVIEW_FOLDER_ID,
reason: `low confidence (${a.folder_confidence.toFixed(2)})`,
};
}
return { action: "file", folderId: DESTINATIONS[a.folder], reason: a.folder };
}Three bands, which is what TypeSafe's confidence guide recommends: act, check, don't act. The thresholds are not universal. Scale them to what a wrong move costs you. Misfiling an invoice costs a minute. Moving a client's passport scan into a folder shared with staff is a different kind of problem, which is why rule 1 ignores confidence entirely and just stops.
Step 7: Wire it up and run it
// index.js
import { listFolder, fileInto } from "./src/drive.js";
import { buildQuestions, fileToState } from "./src/decisions.js";
import { decide, readAnswers } from "./src/jev.js";
import { planFor } from "./src/plan.js";
import { INBOX_FOLDER_ID } from "./src/folders.js";
const apply = process.argv.includes("--apply");
const questions = buildQuestions();
const files = await listFolder(INBOX_FOLDER_ID, { max: 100 });
console.log(`${files.length} files in the inbox folder\n`);
let filed = 0, review = 0, skipped = 0, tokens = 0;
for (const file of files) {
const { answers, usage } = await decide(fileToState(file), questions);
const a = readAnswers(answers);
const plan = planFor(file, a);
tokens += usage?.input_tokens ?? 0;
const tag = plan.action.toUpperCase().padEnd(6);
console.log(`[${tag}] ${file.title}`);
console.log(` ${plan.reason}`);
if (plan.action === "file") filed++;
if (plan.action === "review") review++;
if (plan.action === "skip") skipped++;
if (apply && plan.folderId) {
await fileInto(file.id, plan.folderId);
}
}
console.log(
`\n--- ${filed} to file, ${review} to review, ${skipped} skipped. ` +
`~${tokens} Jev input tokens. ${apply ? "Moves applied." : "Dry run, nothing moved."} ---`
);Run it without moving anything first:
node index.jsOn our test folder of 60 files, copied from a real Uploads folder and renamed, that printed:
60 files in the inbox folder
[FILE ] Tata_Motors_Invoice_Mar2026.pdf
invoices_received
[FILE ] Engagement_Letter_Shah_Associates_signed.pdf
contracts
[REVIEW] scan_0012.pdf
file name is not descriptive
[SKIP ] PAN_card_front.jpg
looks like an ID document, needs a human
[REVIEW] GST_stuff.pdf
low confidence (0.61)
[FILE ] HDFC_Statement_Apr2026.pdf
bank_statements
--- 38 to file, 15 to review, 7 skipped. ~9240 Jev input tokens. Dry run, nothing moved. ---38 of 60 filed themselves, 15 went to a review pile, 7 identity documents were left alone. The 15 are the honest number: those files genuinely cannot be sorted from their names, and the point of the gate is that the agent says so instead of guessing.
About 9,240 input tokens for 60 files. At $0.042 per million, that is $0.00039.
Read the output against the real folder. When you're happy with it:
node index.js --applyMake it safer before you point it at a real drive
Run on copies first. Duplicate twenty real files into a test folder and point INBOX_FOLDER_ID at that. Moves are reversible in Drive, but explaining them to a colleague is not fun.
Stay in sandbox. The project runs in the mode you picked at swy init, stored in .swytchcode/tooling.json. Move to production only once you've read enough dry-run output to trust it.
Add a policy before adding any destructive method. If you later add a delete or share method, put a human approval policy in front of it first. Our guide to policy guardrails walks through the syntax.
Check the audit log. swy audit network lists every Drive call the agent made, and swy audit policy shows anything blocked or held. After an unattended run, that log is how you answer "what did it move last night?"
Keep the review folder shallow. If 40% of files land in review, the folder descriptions are the problem, not the model. Rewrite the criteria strings and run the dry pass again.
Things we got wrong the first time
We used folder names as the choice keys. Our first version had criteria keys like "Invoices (Received)" with spaces and brackets, then tried to match them back to the folder map with string cleanup. Keys that are plain identifiers and a separate DESTINATIONS lookup removed a whole class of bug.
We forgot a file can have two parents. In Drive v2, adding a parent doesn't remove the old one, so our "moved" files showed up in both the destination folder and Uploads. The folder looked untouched and we spent twenty minutes convinced the calls were failing. They weren't. Removing the original parent is a second call: run swy discover "remove a parent from a drive file" and swy info on what it returns, then add that method and call it after the add succeeds. We'd recommend adding the destination first and removing the source second, so a failure between the two leaves the file in both places rather than neither.
We trusted confidence on bad names. Early on we gated only on folder_confidence, and Jev was sometimes fairly confident about a file called invoice.pdf, reasonably enough, since the name does say invoice, but it can't tell received from issued. Adding the name_is_descriptive question caught these properly.
We sent too much state. The first version included the full MIME type list, parent IDs and modified dates. More tokens, no better answers. Five fields was as good.
We ran it on 400 files before reading any output. Everything worked, which was the problem: we had no idea whether the 400 decisions were good ones. Dry-run on 20, read all 20, then scale.
Where to go from here
- Read the file contents for scans. This is the big one if your folder is mostly images and PDFs. Run
swy discover "download or export a drive file"to find a content method for your integration version, add it, and pass the first page of text to Jev along with the metadata. Jev's limit is 64k tokens for state plus questions, so send the first page, not the whole document. - File by year as well as type.
document_yearis already in the answers. Add a second level toDESTINATIONSand build the path from both answers. - Flag duplicates. Add a
noulfor "this looks like a duplicate of something already filed" and pass a list of existing file names in the state. - Notify instead of moving. For a drive nobody wants an agent writing to, post the proposed filing to Slack with
slack.chat.postmessage.createand let a person click. - Run it on a schedule. Once dry runs look right for a week, run it nightly on whatever arrived that day.
For more patterns, the Jev guide covers eight use cases across Gmail, Calendar, Slack, GitHub, Stripe, Intercom and Salesforce, and the examples repo has working agents to copy.
FAQ
Can Jev read the contents of my Drive files?
Not by itself. Jev only evaluates the text you send it. In this build we send metadata only: file name, type, size, upload date and owner. If you want it reading contents, you fetch the text with Drive first and include it in the state.
Does this delete or overwrite anything?
No. The only methods added to tooling.json are drive.file.list and drive.file.parents.create. Nothing else can run, so the agent cannot delete, share or overwrite files even if the code asks it to.
What happens to files it can't classify?
They move to a review folder with a printed reason, or stay put if they look like identity documents. The agent is built to refuse rather than guess.
Which Drive methods do I need?
drive.file.list to read the folder and drive.file.parents.create to file a document. Add a parent-removal method if you want files to leave the source folder. Use swy discover and swy info to confirm names and shapes for your integration version.
Why do files appear in two folders after a move?
Because Drive v2 allows multiple parents, and adding one doesn't remove the others. Add the removal call as a second step.
Do I need Google Cloud credentials or an OAuth app?
Not with Swytchcode. swy auth connect drive runs the OAuth flow and stores the token in Swytchcode's credential store, outside your project.
How much does it cost to file a few hundred files?
Our 60-file dry run used about 9,240 input tokens, roughly $0.0004 at Jev's $0.042 per million. A thousand files is still under a cent.
Where does my Jev API key go?
Into Swytchcode, not your code. Run swy auth connect jev and paste it when asked. There's no .env file in this project.
Can I write this in Python?
Yes. Swytchcode has a Python runtime with the same model, so the same Jev and Drive calls work from Python.
Wrapping up
The whole agent is about 150 lines, and most of the value is in twelve of them: the rules in plan.js that decide when not to move a file. Jev makes that possible by handing back a number alongside the answer, and Swytchcode makes it safe by only allowing two Drive methods to run at all.
If you want to try it, start with the Drive integration and the Jev integration, or point your coding agent at our skills file and ask it to build a Drive auto-filer with Jev.
More content
OpenAI Dots vs Instinct: Two Ways to Build a Personal Agent
Two personal-agent products launched within a week of each other and solved the same problem differently. Dots gives an agent its own computer inside your workspace; Instinct sends yours into other people's group chats. Both had to invent a permission layer, and neither exposes one to developers.
Jev Confidence Scores: How to Pick Thresholds Before Your Agent Acts
Jev hands back a number with every answer, and most people pick 0.8 because it looks reasonable. Here is what the number actually measures, why choice confidence means different things at three options and thirty, and how to find your own thresholds from logged outcomes.
How to Let Jev Triage Your Google Calendar Invites (Accept, Decline, or Ask)
Google Calendar has no accept or decline endpoint. RSVP is an event update that changes your own attendee record, which is a detail most tutorials get wrong. Here is a working invite triage agent with Jev deciding and Swytchcode doing the update.
