Content

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.

Chaitrali Kakde

DevRel Engineer

AI AgentOct 12, 2026

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 warning

Two 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, or npm install -g swytchcode, then swy 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/runtime

Then set up Swytchcode in the same folder:

swy init --mode=sandbox
Terminal running swytchcode init and choosing an editor and execution mode

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 doctor

Three 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
Claude Code running swytchcode list and swytchcode info commands to find the right methods before executing

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 other key 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 into contracts.
  • name_is_descriptive is the cheapest safety net here. Asking Jev to judge the input rather than only the answer catches the scan_0012.pdf case 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.js

On 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 --apply

Make 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_year is already in the answers. Add a second level to DESTINATIONS and build the path from both answers.
  • Flag duplicates. Add a noul for "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.create and 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