Producfy

Small Business Stack

Rename Invoice PDFs in Google Drive from a Tracking Sheet

How can I rename invoice PDFs in Google Drive from a Google Sheets tracking sheet, with a dry run before applying changes?

Use Google Sheets with a bound Apps Script to read approved Drive file IDs and replacement filenames, validate each row, and log a dry run without renaming files. After reviewing the log, set APPLY_CHANGES to true and rerun the script to rename eligible PDFs.

What you need

  • Google Sheets with an Invoice Rename tab and the specified headers
  • A spreadsheet-bound Apps Script project
  • An executing account with spreadsheet access, target-file access, and rename permission for applying changes

How this script runs

  1. You list file IDs and approved names in a sheet
  2. A dry run logs each planned rename and changes nothing
  3. You review the log
  4. You turn on the apply switch
  5. The script renames only the listed file IDs

How should the Invoice Rename tracking sheet be arranged?

The Google Sheets tab Invoice Rename needs these exact headers in columns A through C:

File ID Approved filename Approved
PASTE_FILE_ID_A Fictional_OrbitCo_INV-1042_2026-02.pdf YES
PASTE_FILE_ID_B Fictional_MapleDesk_INV-2088_2026-02.pdf YES
PASTE_FILE_ID_C Fictional_OrbitCo_INV-1043_2026-02.pdf YES
PASTE_FILE_ID_D Fictional_MapleDesk_INV-2089_2026-02.pdf NO

Company names are fictional; IDs are placeholders. Replace every placeholder with an accessible PDF’s actual ID: the value between /d/ and the following slash in its Drive file URL. Format column A as plain text first.

Column B supplies the complete replacement filename, including .pdf; the script does not extract invoice contents. Surrounding whitespace is trimmed. Only YES, ignoring capitalization, approves a row. IDs identify files independently of existing names.

How do you install the invoice renaming script with apply disabled?

Install the Apps Script from the tracking spreadsheet and keep APPLY_CHANGES set to false for the initial dry run.

  1. Open Extensions > Apps Script to create a spreadsheet-bound script.
  2. Replace the starter code with the complete script below.
  3. Keep APPLY_CHANGES = false and save.
  4. Select renameInvoicePdfs, then click Run.
  5. Review Google’s authorization prompt; authorize only appropriate access. The executing account needs spreadsheet and target-file access.
  6. Inspect the spreadsheet’s Rename Log tab.

No API key or secret is required.

const APPLY_CHANGES = false;

function renameInvoicePdfs() {
  const lock = LockService.getDocumentLock();
  lock.waitLock(30000);

  try {
    const ss = SpreadsheetApp.getActiveSpreadsheet();
    const input = ss.getSheetByName('Invoice Rename');
    if (!input) {
      throw new Error('Create a tab named Invoice Rename.');
    }

    const expectedHeaders = [
      'File ID', 'Approved filename', 'Approved'
    ];
    const actualHeaders =
      input.getRange(1, 1, 1, 3).getDisplayValues()[0];

    if (actualHeaders.some((value, index) =>
      value !== expectedHeaders[index])) {
      throw new Error(
        'A1:C1 must be File ID, Approved filename, Approved.'
      );
    }

    if (input.getLastRow() < 2) {
      throw new Error('Add at least one input row.');
    }

    const rows = input
      .getRange(2, 1, input.getLastRow() - 1, 3)
      .getDisplayValues()
      .map(row => row.map(value => value.trim()));

    const logHeaders = [
      'Run time', 'Mode', 'Sheet row', 'File ID',
      'Current name', 'Approved filename', 'Result', 'Detail'
    ];
    const log = ss.getSheetByName('Rename Log') ||
      ss.insertSheet('Rename Log');

    if (log.getLastRow() === 0) {
      log.getRange(1, 1, 1, logHeaders.length)
        .setValues([logHeaders]);
    } else {
      const headers = log
        .getRange(1, 1, 1, logHeaders.length)
        .getDisplayValues()[0];
      if (headers.some((value, index) =>
        value !== logHeaders[index])) {
        throw new Error(
          'Rename Log headers differ. Archive that tab and rerun.'
        );
      }
    }

    const counts = new Map();
    for (const [id, , approval] of rows) {
      if (approval.toUpperCase() === 'YES' && id) {
        counts.set(id, (counts.get(id) || 0) + 1);
      }
    }

    const runTime = new Date();
    const mode = APPLY_CHANGES ? 'APPLY' : 'DRY_RUN';

    const record = (sheetRow, id, current, desired, result, detail) => {
      const logRow = log.getLastRow() + 1;
      const values = [
        runTime, mode, sheetRow, id,
        current, desired, result, detail
      ].map(safeLogCell);
      log.getRange(logRow, 1, 1, values.length)
        .setValues([values]);
      return logRow;
    };

    const finish = (logRow, result, detail) => {
      log.getRange(logRow, 7, 1, 2)
        .setValues([[result, safeLogCell(detail)]]);
    };

    for (let index = 0; index < rows.length; index++) {
      const [id, desired, approval] = rows[index];
      const sheetRow = index + 2;
      if (!id && !desired && !approval) continue;

      let current = '';
      let attemptRow = 0;

      try {
        if (approval.toUpperCase() !== 'YES') {
          record(
            sheetRow, id, current, desired,
            'SKIPPED', 'Approval is not YES.'
          );
          continue;
        }

        if (!/^[A-Za-z0-9_-]{10,}$/.test(id)) {
          throw new Error('Enter a Drive file ID, not a URL.');
        }
        if (counts.get(id) > 1) {
          throw new Error('Duplicate approved file ID in input.');
        }
        if (!/^.+\.pdf$/i.test(desired) ||
            /[\\/:*?"<>|\x00-\x1F\x7F]/.test(desired)) {
          throw new Error(
            'Use a nonempty PDF name without control characters ' +
            'or any of these: \\ / : * ? " < > |'
          );
        }

        const file = DriveApp.getFileById(id);
        current = file.getName();

        if (file.isTrashed()) {
          throw new Error('Target file is in trash.');
        }
        if (file.getMimeType() !== MimeType.PDF) {
          throw new Error('Target is not a PDF file.');
        }
        if (current === desired) {
          record(
            sheetRow, id, current, desired,
            'UNCHANGED', 'Name already matches.'
          );
          continue;
        }
        if (!APPLY_CHANGES) {
          record(
            sheetRow, id, current, desired,
            'READY', 'Dry run only; no rename performed.'
          );
          continue;
        }

        attemptRow = record(
          sheetRow, id, current, desired,
          'ATTEMPT', 'Rename requested; verify if left unresolved.'
        );
        SpreadsheetApp.flush();

        file.setName(desired);
        finish(attemptRow, 'RENAMED', 'Rename completed.');
      } catch (error) {
        const detail = 'Check Drive before retry: ' +
          String(error && error.message ? error.message : error);
        if (attemptRow) {
          finish(attemptRow, 'ERROR', detail);
        } else {
          record(
            sheetRow, id, current, desired, 'ERROR', detail
          );
        }
      }
    }

    SpreadsheetApp.flush();
  } finally {
    lock.releaseLock();
  }
}

function safeLogCell(value) {
  if (typeof value === 'string' && /^[=+@-]/.test(value)) {
    return "'" + value;
  }
  return value;
}

Filename restrictions are stricter than Drive’s rules to reduce Windows download problems. safeLogCell prevents logged text beginning with formula-like characters from becoming an active spreadsheet formula.

What should the invoice rename dry-run log show?

The invoice rename dry run logs READY, UNCHANGED, or SKIPPED for valid sample rows without changing filenames.

Assume the placeholders are replaced with accessible PDF IDs having these names:

Placeholder replaced Current Drive filename
PASTE_FILE_ID_A scan_1042.pdf
PASTE_FILE_ID_B attachment_2088.pdf
PASTE_FILE_ID_C Fictional_OrbitCo_INV-1043_2026-02.pdf
PASTE_FILE_ID_D attachment_2089.pdf

Expected selected log columns:

Mode Sheet row Current name Result
DRY_RUN 2 scan_1042.pdf READY
DRY_RUN 3 attachment_2088.pdf READY
DRY_RUN 4 Fictional_OrbitCo_INV-1043_2026-02.pdf UNCHANGED
DRY_RUN 5 SKIPPED

The full log includes requested filenames and details. Row 5 has no current name because approval is checked before Drive access.

READY confirms readability and validation, not rename permission. With apply enabled and sufficient permissions, rows 2 and 3 become RENAMED; rows 4 and 5 retain their respective results.

Why does renameInvoicePdfs rename only approved PDFs while supporting a dry run?

renameInvoicePdfs checks approval and PDF eligibility before letting APPLY_CHANGES control whether a filename changes or only a preview is logged.

First, getDocumentLock() and waitLock(30000) serialize cooperating script executions rather than allowing overlapping runs. The script opens the bound spreadsheet, checks its input layout, and reads displayed cell text with getDisplayValues(). Trimming each value makes surrounding spaces irrelevant. It then prepares an append-only log rather than replacing earlier results.

A Map counts approved IDs before processing, allowing whole-input duplicate checks rather than considering each row alone. The logging helpers attach a shared timestamp and mode; safeLogCell() treats formula-like strings as literal text.

The row loop checks approval before accessing Drive, avoiding unnecessary file lookups. It validates the ID and requested filename, then uses getFileById() rather than searching by a changeable name. File-state and MIME-type checks establish PDF eligibility. Comparing current and desired names avoids redundant writes; the dry-run branch records READY instead of calling setName().

In apply mode, the script records and flushes ATTEMPT before setName(), preserving a pre-change audit entry, then updates its result. Row-level catches record errors independently. A final flush sends pending spreadsheet writes, and finally releases the lock.

How do you enable invoice renaming after reviewing the log?

Enable invoice renaming by setting const APPLY_CHANGES = true;, saving, and rerunning renameInvoicePdfs only after reviewing the latest dry-run entries.

Compare current and approved names, opening unclear files in Drive. A valid ID can identify the wrong invoice; PDF contents are not inspected. Resolve errors first.

Apply rereads the sheet, not a frozen queue. If inputs change after review, repeat the dry run. Avoid editing either tab during execution.

After applying, restore false, save, and inspect the newest appended results. Authorization alone does not enable renaming. Only names change; contents, locations, and file IDs remain unchanged.

How do you fix invoice rename ID, permission, and interrupted-run failures?

Fix invoice rename failures by correcting the affected inputs or permissions and verifying Drive state before retrying with apply enabled.

Failure Exact fix
Enter a Drive file ID, not a URL. or placeholder access error Replace the cell with the actual ID, excluding URL punctuation and parameters. Replace every placeholder; rerun with apply disabled.
Duplicate approved file ID in input. Keep one approved row per ID; delete duplicates or mark them NO. Rerun the dry run.
READY followed by an apply permission error Ask the owner or shared-drive manager to grant rename permission, or have an authorized account execute the bound script. Repeat the dry run before applying.
Unresolved ATTEMPT or incomplete execution Disable apply, inspect the affected file in Drive, then rerun the dry run. A matching name becomes UNCHANGED; an eligible unmatched name becomes READY.

Duplicate approved IDs are blocked even when requested names match. Different IDs may receive identical names; check column B separately if uniqueness matters. Authorization does not override file permissions.

ATTEMPT is flushed before renaming, so interruption can leave a successful rename without a success log. Verify apply errors too. Row-level errors ordinarily allow later rows to continue; earlier renames are not rolled back.

When should invoice PDFs use another renaming method?

Use another method for very large invoice queues or files whose names are controlled by another application.

Individual Drive calls and log writes can exceed Apps Script execution limits. Recurring large batches need chunked jobs, a saved cursor, scheduled executions, and per-file audit logs; Script Properties can hold job state.

For application-managed attachments, use that application’s supported naming workflow. Direct Drive renames may be overwritten or leave displayed attachment names inconsistent.

Related questions

Does READY mean the script has permission to rename the PDF?

READY means the PDF was readable and passed the script's validation checks. Renaming requires additional file permissions, so an apply run can still report a permission error.

Can different PDFs receive the same approved filename?

The script permits identical approved filenames for different file IDs because Google Drive allows duplicate names. Repeated file IDs among approved rows are blocked, even when their requested filenames match.

Does applying changes use a saved copy of the dry run?

The apply run reads the tracking sheet again rather than using a frozen dry-run queue. If the sheet changes after review, disable apply and perform another dry run before enabling renaming.

Will renaming change PDF contents or Drive file IDs?

The script changes filenames only; it does not move files or modify PDF contents. Existing Drive file IDs remain unchanged.