appscript.dev
Automation Advanced Drive Sheets

Auto-categorize a photo library

Tag Northwind Drive images by visual content — product, team, event, behind-the-scenes.

Published Feb 3, 2026

Northwind’s shared Drive holds thousands of photos with names like IMG_4821.jpg. When the marketing team needs a product shot or a team picture, they scroll. The pictures are perfectly useful — they just are not findable, because the filename says nothing about what is in the frame.

This automation walks a Drive folder of images, sends each one to Claude’s vision model, and asks for a single category label — product, team, event, workspace, or other. Every result lands in an index sheet alongside the file name and a link, so a search of that sheet replaces an afternoon of scrolling.

What you’ll need

  • A Drive folder of images to classify — the script processes anything with an image/ MIME type and skips everything else.
  • A Google Sheet to act as the index. The script appends rows to its first tab, so add a header row (File name, Category, Link) yourself first.
  • An Anthropic API key saved as ANTHROPIC_API_KEY in Script Properties — see Store API keys and secrets securely.
  • The folder ID is passed in as an argument; the index sheet ID goes in the config block at the top.

The script

// The sheet that collects the file name, category, and link for each image.
const PHOTO_INDEX_ID = '1abcPhotoIndexId';

// The fixed set of labels Claude must choose from.
const CATEGORIES = ['product', 'team', 'event', 'workspace', 'other'];

// Vision model used for classification — Haiku is fast and cheap enough.
const VISION_MODEL = 'claude-haiku-4-5-20251001';

/**
 * Walks a Drive folder and classifies every image inside it, appending one
 * row per image to the index sheet.
 *
 * @param {string} folderId  ID of the Drive folder to scan.
 */
function categorisePhotos(folderId) {
  const files = DriveApp.getFolderById(folderId).getFiles();
  const sheet = SpreadsheetApp.openById(PHOTO_INDEX_ID).getSheets()[0];

  let count = 0;
  while (files.hasNext()) {
    const f = files.next();

    // 1. Skip anything that is not an image.
    if (!f.getMimeType().startsWith('image/')) continue;

    // 2. Base64-encode the image bytes for the API payload.
    const b64 = Utilities.base64Encode(f.getBlob().getBytes());

    // 3. Ask Claude for a single category label.
    const cat = visionClassify(b64, f.getMimeType());

    // 4. Record the result in the index sheet.
    sheet.appendRow([f.getName(), cat, f.getUrl()]);
    count++;
  }

  Logger.log('Categorised ' + count + ' images.');
}

/**
 * Sends one image to Claude's vision model and returns its category label.
 *
 * @param {string} b64   Base64-encoded image data.
 * @param {string} mime  MIME type of the image (e.g. "image/jpeg").
 * @return {string} One of CATEGORIES.
 */
function visionClassify(b64, mime) {
  // The key lives in Script Properties — never pasted into the code.
  const key = PropertiesService.getScriptProperties()
    .getProperty('ANTHROPIC_API_KEY');

  const res = UrlFetchApp.fetch('https://api.anthropic.com/v1/messages', {
    method: 'post',
    contentType: 'application/json',
    headers: { 'x-api-key': key, 'anthropic-version': '2023-06-01' },
    payload: JSON.stringify({
      model: VISION_MODEL,
      max_tokens: 20,
      messages: [{
        role: 'user',
        content: [
          { type: 'image', source: { type: 'base64', media_type: mime, data: b64 } },
          { type: 'text', text: 'Pick one: ' + CATEGORIES.join(', ') +
            '. Return only the label.' },
        ],
      }],
    }),
    muteHttpExceptions: true,
  });
  return JSON.parse(res.getContentText()).content[0].text.trim();
}

How it works

  1. categorisePhotos opens the given folder and grabs an iterator over its files, plus the first tab of the index sheet.
  2. For each file it checks the MIME type and skips anything that is not an image — documents, videos, and sub-folders are ignored.
  3. It reads the image bytes and base64-encodes them, the format the Anthropic API expects for inline images.
  4. It calls visionClassify, which sends the encoded image and a short instruction pinning the answer to one of the five CATEGORIES.
  5. max_tokens is set to 20 — the reply is a single word, so there is no reason to allow more.
  6. The file name, returned category, and Drive link are appended as a row to the index sheet.

Example run

A folder contains four images. After a run, the index sheet reads:

File nameCategoryLink
IMG_4821.jpgproducthttps://drive.google.com/
IMG_4822.jpgteamhttps://drive.google.com/
DSC_0190.jpgeventhttps://drive.google.com/
screenshot.pngworkspacehttps://drive.google.com/

Now anyone can filter the sheet to category = product and get every product shot in seconds, with a link straight to the file.

Run it

This is an on-demand job — run it when a batch of new photos arrives:

  1. The function takes a folderId argument, so call it from a small wrapper rather than running categorisePhotos directly:
function runPhotoCategorisation() {
  categorisePhotos('1abcPhotoFolderId');
}
  1. In the Apps Script editor, select runPhotoCategorisation and click Run.
  2. Approve the authorisation prompt the first time, then open the index sheet to see the results.

Watch out for

  • Every run re-processes the whole folder and appends fresh rows — there is no deduplication. Run it on a folder of new uploads, or move processed images into an archive folder afterwards.
  • Each image is a separate API call. A folder of hundreds of photos costs hundreds of calls and can hit the six-minute execution limit — split large folders into batches.
  • Large images make large base64 payloads. Very high-resolution photos can push the request size up and slow each call; resize before classifying if the folder is full of full-resolution originals.
  • The model returns free text. It is told to return only a label, but if it ever replies with a stray sentence that text lands in the sheet as-is — add a check that the result is in CATEGORIES if you need it strict.
  • Vision classification is a judgement, not a fact. Ambiguous shots (a product on a desk, a team at an event) may land in a category you would not have picked. Spot-check the index before relying on it.

Related