“What landed in the Inbox folder since last run?” is the start of half the Drive automations we write. listDriveFilesModifiedSince(folderIdOrFolder, sinceDate, opts) returns plain objects (id, name, url, mimeType, lastUpdated, size) for files updated after sinceDate. You can filter by MIME type, and subfolders are optional (off by default). Results come back oldest first, so a checkpoint can move forward one file at a time.
When to use
- Watching an intake folder and queueing new files for processing
- Nightly “what changed?” reports on a client folder
- Re-running a Drive job from a saved cursor after a timeout
How to use this snippet
/**
* List files in a folder updated strictly after sinceDate.
*
* @param {string|GoogleAppsScript.Drive.Folder} folderIdOrFolder
* @param {Date|string|number|null} sinceDate null = everything
* @param {{mimeType?: string|string[], recursive?: boolean}=} opts
* mimeType e.g. MimeType.PDF or ['application/pdf', 'image/png']
* recursive include subfolders (default false)
* @return {Array<{id: string, name: string, url: string, mimeType: string,
* lastUpdated: Date, size: number}>} oldest first
*/
function listDriveFilesModifiedSince(folderIdOrFolder, sinceDate, opts) {
opts = opts || {};
var folder = typeof folderIdOrFolder === 'string'
? DriveApp.getFolderById(folderIdOrFolder)
: folderIdOrFolder;
var since = sinceDate ? new Date(sinceDate) : new Date(0);
var mimes = opts.mimeType ? [].concat(opts.mimeType) : null;
// Let Drive do the rough filtering (the query works to the second, in UTC).
var q = "trashed = false and modifiedDate > '" +
Utilities.formatDate(since, 'UTC', "yyyy-MM-dd'T'HH:mm:ss") + "'";
var out = [];
(function walk(f) {
var files = f.searchFiles(q);
while (files.hasNext()) {
var file = files.next();
var updated = file.getLastUpdated();
// Exact millisecond check, so the file your cursor points at isn't returned again
if (updated.getTime() <= since.getTime()) continue;
if (mimes && mimes.indexOf(file.getMimeType()) === -1) continue;
out.push({
id: file.getId(),
name: file.getName(),
url: file.getUrl(),
mimeType: file.getMimeType(),
lastUpdated: updated,
size: file.getSize()
});
}
if (opts.recursive) {
var subs = f.getFolders();
while (subs.hasNext()) walk(subs.next());
}
})(folder);
// Oldest first: process in order, save each lastUpdated as your new cursor.
out.sort(function (a, b) { return a.lastUpdated - b.lastUpdated; });
return out;
}
Example
function reportNewPdfs() {
var props = PropertiesService.getScriptProperties();
var cursor = props.getProperty('inboxCursor'); // ISO string or null
var files = listDriveFilesModifiedSince(props.getProperty('inboxFolderId'), cursor, {
mimeType: MimeType.PDF
});
files.forEach(function (f) {
console.log('%s %s %s', f.lastUpdated.toISOString(), f.name, f.url);
// Advance after each file, so a timeout mid-loop doesn't lose progress
props.setProperty('inboxCursor', f.lastUpdated.toISOString());
});
}
Tips:
- “Modified” means updated. Renames, content edits and moves can bump
lastUpdated, so a file you already handled may show up again. Track processed ids (a queue sheet works) if that matters. - Keep the cursor as an ISO string in Script Properties, or use readWriteCheckpoint for a
{ cursor, updatedAt }record. - Google Docs/Sheets report
sizeas 0 because they don’t count as stored bytes. Don’t use size to spot empty files. - Recursive scans of deep trees get slow. Keep intake folders flat, or cap how many files you handle per run.
Tip: NitroGAS Co-Pilot can wrap listDriveFilesModifiedSince in a checkpointed time-driven job if you tell it which folder and what should happen to each new file.
Happy Coding!
