Build a watched-folder processing pipeline
Act on every new file dropped in a Northwind folder — convert, log, route, notify.
Published Aug 5, 2025
Northwind has half a dozen “drop boxes” in Drive — one for supplier invoices, one for raw photoshoot exports, one for contractor timesheets — and the same manual loop repeats in each. Someone uploads a file, someone else opens it, renames it, logs it in a sheet, drops a copy somewhere, then pings the team. By the time the third person gets involved, two files have been forgotten.
This script turns that loop into a pipeline. It watches one folder, and on a
schedule it picks up every file added since the last run, passes each file
through a process function, then stores the run time so the next pass picks
up exactly where this one left off. Replace the body of process and you’ve
got a folder that quietly logs, routes, or transforms anything dropped into
it — without doubles, without misses.
What you’ll need
- The Drive folder ID you want to watch. Share it with anyone who needs to
drop files in; the script reads it via
DriveApp. - A Google Sheet to log activity. The default
processfunction writes one row per file: timestamp, name, MIME type, URL. - Permission to set time-driven triggers (your own account is fine; a shared service account is better for production).
The script
// The folder being watched. Anything dropped here is picked up next run.
const WATCH_FOLDER_ID = '1abcWatchedFolderId';
// The sheet that records every processed file.
const LOG_SHEET_ID = '1abcLogSheetId';
// Script Properties key for the cursor — the last time we ran.
const CURSOR_KEY = 'LAST_RUN';
/**
* Pick up every file added since the last run, process each one, then
* advance the cursor. Safe to run on a 5-minute trigger — repeats won't
* reprocess the same file because the cursor only moves on success.
*/
function processNewFiles() {
const props = PropertiesService.getScriptProperties();
const lastRun = new Date(props.getProperty(CURSOR_KEY) || 0);
const now = new Date();
const files = DriveApp.getFolderById(WATCH_FOLDER_ID).getFiles();
let processed = 0;
let errors = 0;
while (files.hasNext()) {
const f = files.next();
// Drive returns files in no particular order — filter by created date.
if (f.getDateCreated() <= lastRun) continue;
try {
processFile(f);
processed++;
} catch (e) {
// Don't poison the cursor if one file blows up — log and continue.
errors++;
Logger.log('Failed on ' + f.getName() + ': ' + e.message);
}
}
// Only advance the cursor when the run completes. If the script
// crashes outright, the next run picks up from the same lastRun.
props.setProperty(CURSOR_KEY, now.toISOString());
Logger.log('Processed ' + processed + ' file(s), ' + errors + ' error(s).');
}
/**
* The work done on each new file. Swap this body for whatever your
* pipeline needs — convert formats, move into archive folders, send a
* notification, kick off a doc generator.
*
* @param {GoogleAppsScript.Drive.File} file - The newly added file.
*/
function processFile(file) {
// Default behaviour: append a row to the log sheet.
SpreadsheetApp.openById(LOG_SHEET_ID).getSheets()[0]
.appendRow([
new Date(),
file.getName(),
file.getMimeType(),
file.getUrl(),
]);
}
/**
* One-off helper: reset the cursor so the next run reprocesses every
* file in the folder. Useful for backfills or after a code change.
*/
function resetCursor() {
PropertiesService.getScriptProperties().deleteProperty(CURSOR_KEY);
Logger.log('Cursor cleared — next run will process every file.');
}
How it works
processNewFilesreads the cursor — the timestamp of the last successful run — out of Script Properties. On the very first run, the property is missing and the cursor defaults to the Unix epoch, so every file in the folder gets processed once.- It records
nowat the top of the function. That timestamp is what we commit to the cursor at the end, so any files dropped during the run are still picked up next time — the cursor never skips ahead of work that has actually finished. - The file iterator walks the watched folder. Drive returns files in no guaranteed order, so we don’t rely on order — we compare each file’s creation date against the cursor and skip anything older.
processFileis the swappable step. The default writes to a log sheet, but the real value of the script is that you replace its body: convert a PDF to a Doc, move the file into a dated archive folder, fire off a Slack webhook, kick off a Gmail draft. The framing around it stays identical.- Errors on a single file are caught and logged, but don’t roll back the
cursor. That’s deliberate — one bad upload shouldn’t stop the next ten
from being processed. If the script crashes outright, the cursor isn’t
advanced and the next run retries everything from
lastRun. resetCursoris the manual override. Delete the property and the next run treats every file as new — handy after a code change that should apply to historic uploads, or when you’ve just enabled the watcher on an existing folder.
Example run
Suppose the watched folder is empty at 09:00 and the cursor is set to 08:55. Between 09:00 and 09:05, two files arrive:
| Time uploaded | File |
|---|---|
| 09:01 | invoice-april.pdf |
| 09:04 | photos-may.zip |
At 09:05 the trigger fires. processNewFiles:
- Reads
lastRun = 08:55andnow = 09:05. - Walks the folder, finds both files have
dateCreated > 08:55, processes each one — the log sheet gains two rows. - Writes
09:05back as the new cursor.
At 09:10, nothing new has been uploaded. The trigger fires, the iterator finds no files newer than 09:05, no rows are added, and the cursor moves to 09:10. No duplicate log entries; no missed files.
Trigger it
Set it on a clock:
- In the Apps Script editor, open Triggers (clock icon on the left).
- Add a time-driven trigger for
processNewFiles— every 5 minutes is a sensible default for an active folder, hourly for a slow one. - Drop a test file into the watched folder, wait for the next tick, and confirm the log sheet shows a new row.
If you ever need to reprocess everything — say you’ve changed what
processFile does — run resetCursor once, then the next scheduled tick
will treat the whole folder as new.
Watch out for
- The cursor is
dateCreated, notdateModified. Editing an existing file doesn’t trigger a reprocess — only newly uploaded files do. If you want to react to edits too, switch togetLastUpdated()and accept that every edit will fire the pipeline. - The default folder iterator only sees the top level. To watch a tree,
recurse into subfolders inside the loop, or use the Drive Advanced
Service with a
qfilter onmodifiedTime. - The six-minute execution limit will bite if a single run has to process hundreds of files. Process in batches, or move heavy work into a separate trigger that consumes a queue this script writes to.
- One bad file shouldn’t break the pipeline. The
try/catcharoundprocessFileis essential — without it, one rogue upload could permanently stall the cursor. - The cursor only advances after a successful run. If you change the
script in a way that always errors before reaching the
setPropertycall, the same files will keep being retried — fix the code or callresetCursorto start fresh.
Related
Build a recurring file-delivery system
Drop a fresh report file into a Northwind client folder weekly — they don't even ask.
Updated Dec 15, 2025
Build a Drive search index in Sheets
Make Northwind's file metadata searchable in a Sheet — like Spotlight for Drive.
Updated Dec 7, 2025
Build a shared-folder onboarding kit
Auto-grant new Northwind hires the folders they need on day one.
Updated Nov 29, 2025
Route saved email attachments to project folders
File Gmail attachments into the right Northwind client folder based on subject keywords.
Updated Nov 25, 2025
Bundle a folder of images into one PDF
Combine Northwind scans into a single deliverable PDF using a generation service.
Updated Nov 17, 2025