DEV Community

Cover image for Automating Google Drive Labels with Google Apps Script: A Step-by-Step Guide
Aryan Irani
Aryan Irani

Posted on • Originally published at aryanirani123.Medium

Automating Google Drive Labels with Google Apps Script: A Step-by-Step Guide

Welcome !
If you manage a Google Workspace environment, you already know that folders only get you so far. Eventually, you need metadata - Status, Department, Sensitivity, or custom fields like Invoice Amount.

Google Drive Labels solve this by turning Drive into a structured metadata system, not just a file store.

But there's a catch: users hate tagging files. If you rely on humans to manually open the details pane and select Invoice and type in a date, it won't happen.

To make Drive Labels actually useful, you need to automate them.
In this guide, we are going to write some Google Apps Script that interacts with the Drive Labels API and Drive API to programmatically fetch label definitions, identify the correct fields, and apply them to files with specific metadata.

Prerequisites 

Before writing a single line of code, a bit of setup is required. The Drive Labels API is not enabled by default in Apps Script projects.

Google Workspace edition

You'll need an edition that supports Drive Labels (Business Standard, Business Plus, or Enterprise). 

Admin setup 

Make sure at least one label (for example, Invoice) is published in the Admin Console under: Security → Access and Data Control → Label Manager

Apps Script services 

Open your Apps Script project, click the + next to Services, and enable:

  • Drive API 
  • Drive Labels API

The scripts - what each function does

fetchAvailableLabels() - lists published labels and returns a map 
fetchLabelFields(labelId) - reads a label's fields and their types
applyLabelToFile(fileId, labelId) - applies a label to a file (no field values).
applyLabelAndUpdateFields(fileId, labelId, fields) - applies a label and updates field values (types handled: text, integer, date, selection).
fullLabelAutomation(fileId) - example chain that demonstrates label suggestion + applying fields.

You can check out the code by clicking on the link given below.

https://github.com/aryanirani123/Google-Apps-Script/blob/master/DriveLabelsAutomation.js

Fetching all available Drive labels

We start with the fetchAvailableLabels() function. This is the discovery step - before we can apply any label, we first need to know which labels are actually available and visible to our account.

function fetchAvailableLabels() {
  try {
    const response = DriveLabels.Labels.list({ view: "LABEL_VIEW_FULL" });
    const labels = response.labels || [];
    if (labels.length === 0) {
      Logger.log("No labels found. Check Workspace admin for published labels.");
      return {};
    }

    const labelMap = {};
    labels.forEach(label => {
      if (label.properties?.title) {
        labelMap[label.properties.title] = label.id;
      }
    });

    Logger.log(`Fetched ${labels.length} labels: ${JSON.stringify(labelMap)}`);
    return labelMap;
  } catch (error) {
    Logger.log(`Error fetching labels: ${error.message}`);
    return {};
  }
}
Enter fullscreen mode Exit fullscreen mode

Inside the function we call DriveLabels.Labels.list({ view: "LABEL_VIEW_FULL" }); that hits the Drive Labels API and returns all published labels that the current user is allowed to see. Using LABEL_VIEW_FULL ensures we get complete metadata instead of just surface-level information. 

Next we safely extract the labels array from the response. If nothing is returned, we log a message suggesting the user to check whether the labels are published in the Admin Console. 

We then loop through each label and build a simple object map that looks like this:

{
  "Invoice": "label-id-123",
  "Contract": "label-id-456"
}
Enter fullscreen mode Exit fullscreen mode

This mapping is extremely useful because it lets us refer to the labels by human-readable titles instead of IDs for the rest of the workflow. 

Finally, we log the results and return the map. If anything fails (permission issues, API errors, etc), the catch block logs the error and returns an empty object instead of crashing the script. 

At this point, we now have a clean list of usable labels.

Fetching fields for a specific label

Once we know which label we're working with, the next step is to understand its field structure. That's what the fetchLabelFields(labelId) does.

function fetchLabelFields(labelId) {
  try {
    if (!labelId) {
      throw new Error("labelId is required. Run fetchAvailableLabels() first and copy an ID.");
    }

    const response = DriveLabels.Labels.list({ view: "LABEL_VIEW_FULL" });
    const labels = response.labels || [];
    if (labels.length === 0) {
      Logger.log("No labels found. Check Workspace admin for published labels.");
      return {};
    }

    const targetLabel = labels.find(label => label.id === labelId);
    if (!targetLabel) {
      Logger.log(`Label ID "${labelId}" not found in available labels. Run fetchAvailableLabels() to verify.`);
      return {};
    }

    const fields = targetLabel.fields || [];
    if (fields.length === 0) {
      Logger.log(`No fields found for label ${labelId}.`);
      return {};
    }

    const fieldMap = {};
    fields.forEach(field => {
      const displayName = field.properties?.displayName;
      if (displayName) {
        let type = 'text'; // Default
        if (field.integerOptions) type = 'integer';
        else if (field.dateOptions) type = 'date';
        else if (field.selectionOptions) type = 'selection';

        fieldMap[displayName] = {
          id: field.id,
          type: type
        };
      }
    });

    Logger.log(`Fetched ${fields.length} fields for label ${labelId}: ${JSON.stringify(fieldMap)}`);
    return fieldMap;
  } catch (error) {
    Logger.log(`Error fetching fields for label ${labelId}: ${error.message}`);
    return {};
  }
}
Enter fullscreen mode Exit fullscreen mode

The function starts with a validation check. If no labelId is passed, it immediately throws an error. This prevents silent failures and makes debugging much easier. 

Instead of calling a label-specific endpoint, the function safely reuses the full labels list and searches for the label using labels.find(label => label.id === labelId);

If the label isn't found, we log a clear message telling the user to verify the ID using fetchAvailableLabels().
Once the correct label is found, we extract its fields. Each field represents a piece of structured metadata such as:

  • Invoice Number
  • Invoice Date
  • Vendor Name
  • Status (dropdown/select)

We then loop through each field and store: 

  • The field ID
  • The field type (text, integer, date, or selection)

This creates another human-friendly mapping that looks like:

{
  "Invoice Number": { id: "field-id-1", type: "text" },
  "Invoice Date": { id: "field-id-2", type: "date" }
}
Enter fullscreen mode Exit fullscreen mode

This step is critical because Drive does not allow us to update fields using display names - only IDs work. This function bridges that gap.

Applying a label to a file

With label IDs in hand, we move on to the write operation: attaching a label to a file using applyLabelToFile(fileId, labelId) .

function applyLabelToFile(fileId, labelId) {
  try {
    if (!fileId || !labelId) {
      throw new Error("fileId and labelId are required.");
    }

    const request = {
      kind: "drive#modifyLabelsRequest",
      labelModifications: [
        {
          kind: "drive#labelModification",
          labelId: labelId,
          removeLabel: false
        }
      ]
    };

    const response = Drive.Files.modifyLabels(request, fileId);
    Logger.log(`Label ${labelId} applied to file ${fileId}: ${JSON.stringify(response.modifiedLabels)}`);
    return response.modifiedLabels || [];
  } catch (error) {
    Logger.log(`Error applying label ${labelId} to file ${fileId}: ${error.message}`);
    return [];
  }
}
Enter fullscreen mode Exit fullscreen mode

This function begins by validating that both fileId and labelId are present. If either is missing, we immediately fail with a clear message. 

We then build a modifyLabels request object. The key pieces here are:

  • labelId -> which label to apply 
  • removeLabel: false -> explicitly tells Drive to attach the label (not remove it) 

This request is sent using Drive.Files.modifyLabels(request, fileId); . If successful, the Drive API returns a list of modified labels, which we log and return. If anything goes wrong - permission, invalid IDs, or API errors. We catch it and return an empty array instead of breaking the flow.

At this point, the file will now visibly show the label inside Drive → File Details → Labels.

Applying a label and updating its fields

The applyLabelAndUpdateFields() function is the heart of this entire automation. Up until this point, we've only been discovering what labels and fields exist. This is the moment where metadata actually gets written back to the file in Google Drive.

function applyLabelAndUpdateFields(fileId, labelId, fields) {
  try {
    if (!fileId || !labelId || !fields || fields.length === 0) {
      throw new Error("fileId, labelId, and non-empty fields array are required.");
    }

    const fieldModifications = fields.map(field => {
      let fieldMod = {
        kind: "drive#labelFieldModification",
        fieldId: field.id
      };

      const value = field.value;
      if (!value) {
        Logger.log(`Skipping field ${field.id}: No value provided.`);
        return null;
      }

      switch (field.type) {
        case 'selection':
          fieldMod.setSelectionValues = [value];
          break;
        case 'integer':
          const intVal = parseInt(value);
          if (!isNaN(intVal)) {
            fieldMod.setIntegerValues = [intVal.toString()];
          } else {
            Logger.log(`Invalid integer for field ${field.id}: ${value}`);
            return null;
          }
          break;
        case 'date':
          const date = new Date(value);
          if (!isNaN(date.getTime())) {
            const isoDate = date.toISOString().split('T')[0];
            fieldMod.setDateValues = [isoDate];
          } else {
            Logger.log(`Invalid date for field ${field.id}: ${value}`);
            return null;
          }
          break;
        case 'text':
        default:
          fieldMod.setTextValues = [value];
          break;
      }

      return fieldMod;
    }).filter(mod => mod !== null);

    if (fieldModifications.length === 0) {
      Logger.log('No valid field modifications; applying label only.');
      return applyLabelToFile(fileId, labelId);
    }

    const request = {
      kind: "drive#modifyLabelsRequest",
      labelModifications: [
        {
          kind: "drive#labelModification",
          labelId: labelId,
          fieldModifications: fieldModifications,
          removeLabel: false
        }
      ]
    };

    const response = Drive.Files.modifyLabels(request, fileId);
    Logger.log(`Label ${labelId} and fields applied to file ${fileId}: ${JSON.stringify(response.modifiedLabels)}`);
    return response.modifiedLabels || [];
  } catch (error) {
    Logger.log(`Error applying label and fields to file ${fileId}: ${error.message}`);
    return [];
  }
}
Enter fullscreen mode Exit fullscreen mode

This function takes three inputs:

  • fileId → the Drive file we want to update
  • labelId → the label we want to apply
  • fields → an array of field definitions with values

Each field is expected to be passed in this simple format:

{
  id: "field-id",
  type: "text",
  value: "Some value"
}
Enter fullscreen mode Exit fullscreen mode

The function starts with a strict validation check. If any of these three inputs are missing - the file ID, the label ID, or a non-empty fields array - we immediately throw an error. This prevents silent failures where the script looks like it ran but didn't actually apply anything.

Next, the function transforms the human-friendly fields array into a Drive API–compatible request structure. This is done by iterating over each field and dynamically constructing a labelFieldModification object.

At this stage, the script does something very important: it does not assume that every field value is valid. If a field is missing a value, that field is skipped entirely rather than breaking the update for all fields. This makes the system resilient in real-world automation scenarios where extracted data might be partially incomplete.

For every field, we first construct a base object:

{
  kind: "drive#labelFieldModification",
  fieldId: field.id
}
Enter fullscreen mode Exit fullscreen mode

At this point, we've told Drive which field we want to modify, but not how.

Next, we check if a value is actually present. If the value is missing or empty, we log a message and skip that field entirely. This is important because sending empty field updates can cause API errors or overwrite existing values accidentally.

After that, the function switches behaviour based on the field's type:

  • Text fields: These are the simplest. We attach the value using:
setTextValues: [value]
Enter fullscreen mode Exit fullscreen mode

The API always expects values as arrays, even if there's only one value.

  • Integer fields: We first run parseInt(value) to verify that the value is actually numeric. If it's not a valid number, we log an error and skip the field. If it is valid, we convert it to a string and attach it using:
setIntegerValues: ["123"]
Enter fullscreen mode Exit fullscreen mode
  • Date fields: Dates are trickier because the API only accepts them in strict YYYY-MM-DD format. We solve this by:
  1. Creating a JavaScript Date object
  2. Converting it to ISO format
  3. Dropping the time portion using:
date.toISOString().split("T")[0]
Enter fullscreen mode Exit fullscreen mode

This guarantees the format Drive expects.
Selection (dropdown) fields: These use:

setSelectionValues: [value]
Enter fullscreen mode Exit fullscreen mode

Note on selection fields
In real production systems, selection (dropdown) fields require option IDs - not display values. This demo assumes a direct mapping for simplicity. In the Gemini-powered automation covered next, option IDs are resolved dynamically.

If at any point a field fails validation - invalid date, non-numeric integer, missing value - that field is excluded from the request entirely. This ensures that one bad field never breaks the entire automation.

After processing all fields, we run a cleanup step using:

.filter(mod => mod !== null)
Enter fullscreen mode Exit fullscreen mode

This removes any skipped or invalid field updates and leaves us with only clean, valid modifications.

Next comes a very important safety check: If no valid field modifications remain, we don't try to update fields at all.

Instead, we gracefully fall back to:

applyLabelToFile(fileId, labelId)
Enter fullscreen mode Exit fullscreen mode

This guarantees that the label still gets applied even if field data is missing.

If valid fields do exist, we build the final modifyLabels request. This request bundles both actions into one atomic update:

  • Applying the label
  • Updating all valid fields

This is done in a single API call using:

Drive.Files.modifyLabels(request, fileId);
Enter fullscreen mode Exit fullscreen mode

From Drive's perspective, this happens as one clean transaction instead of multiple partial updates.

Finally, the API response is logged and returned so we can inspect what actually changed. If anything fails - permission issues, API limits, or malformed requests - the catch block logs the error and safely returns an empty array instead of crashing the full workflow.

Full automation: chaining everything together

Up to this point, every function we've written has been doing one focused job - fetching labels, fetching fields, applying a label, or updating fields. The fullLabelAutomation(fileId) function is where all of those pieces finally come together into a single, end-to-end automation pipeline.

Think of this function as the orchestration layer. It doesn't introduce any new low-level API logic. Instead, it coordinates all the building blocks we already created and runs them in the right order.

The function starts by logging the file ID being processed. This may seem minor, but it's extremely useful when you're running batch jobs or automations on multiple files and need traceability in your execution logs.

The very first real step is calling:

const labelMap = fetchAvailableLabels();
Enter fullscreen mode Exit fullscreen mode

This ensures that every run starts with a fresh snapshot of currently published and visible labels. We're not hardcoding label IDs anywhere in this workflow, which is important because label configurations can change over time in the Admin Console.

Immediately after that, we verify that at least one label was found. If not, the script exits cleanly with a helpful message. This avoids unnecessary API calls when the environment itself isn't ready.

Next comes the label suggestion step:

const suggestedLabel = "Invoice";
Enter fullscreen mode Exit fullscreen mode

Right now this is hardcoded for demo purposes, but this is the exact point where you would normally plug in:

  • Text extraction from the document
  • Regex-based classification
  • Or an AI model for document categorization

What matters here is that we treat the suggested label as just another input and still validate it against what actually exists in Drive. We don't blindly trust the suggestion. We look it up in the labelMap, and if it doesn't exist, we log all available labels and safely exit. This kind of validation is critical in production systems.

Once a valid label is identified, we move on to schema discovery:

const fieldMap = fetchLabelFields(labelId);
Enter fullscreen mode Exit fullscreen mode

At this stage, the script dynamically learns what fields exist on the selected label and what types they are. This allows the automation to adapt even if the label schema changes later - for example, if a new field is added in the Admin Console.

If no fields are found, the script doesn't fail. Instead, it gracefully falls back to applying only the label using applyLabelToFile(). This ensures that even minimal labels without metadata fields still work with the automation.

If fields do exist, we then build the structured fields array. This is where raw business data gets mapped to Drive metadata.

In our demo, this is done using mock values like:

  • Invoice Number
  • Invoice Date
  • Vendor Name

Each field entry is constructed using the dynamically fetched field ID and type, combined with the value you want to store. The end result is a clean, API-ready representation of your metadata.

This is also the exact point where you would plug in:

  • OCR output from PDFs
  • Extracted text from Google Docs
  • Parsed values from emails or forms
  • Or structured data returned by an AI model

Once the fields array is prepared, everything is handed off to the core execution function:

applyLabelAndUpdateFields(fileId, labelId, fields);
Enter fullscreen mode Exit fullscreen mode

This single call applies the label and writes all valid field values in one atomic operation. From Drive's perspective, the file moves instantly from unlabeled to fully enriched with structured metadata.

Adding a Clean Entry Point with runDemo()

Instead of directly calling fullLabelAutomation(fileId) at the bottom of the script, we now wrap it inside a simple launcher function:

function runDemo() {
  // Replace this with your actual File ID
  const myFileId = "your_file_id"; 

  fullLabelAutomation(myFileId);
}
Enter fullscreen mode Exit fullscreen mode

This small addition significantly improves how the automation is executed and tested.

It gives us a clean entry point for the entire workflow. You can now simply select runDemo from the function dropdown and click Run-without touching the core logic every time.

More importantly, this mirrors how real-world systems behave. In production, fullLabelAutomation() would typically be triggered by:

  • A Drive folder upload trigger
  • A time-based batch job
  • A UI button inside an Add-on
  • Or a webhook or API call

In short, runDemo() cleanly separates execution control from automation logic, which is exactly how scalable Drive automation systems should be structured.

Once this function completes successfully, you can open the file in Google Drive, click on File details → Labels, and see the label and populated metadata live.

What we have built here is a robust labeling engine. It solves the hardest part of the Drive Labels API: translating human-readable names (like Invoice) into the cryptic IDs that Google requires.

Because we separated the logic into distinct steps - Fetch, Map, Build, Apply - this script is highly resilient. If you change a field name in the Admin Console, you only have to update your mapping logic, not the entire payload structure.

Recap: What We've Built So Far

At this point in the walkthrough, we've quietly assembled a complete Google Drive Labels automation pipeline.

We started by dynamically discovering which labels exist in the workspace instead of hardcoding anything. Then we learned how to inspect a label's schema at runtime and understand what fields it contains and what types those fields expect. From there, we moved into actually applying labels to files and finally into enriching those labels with structured field values.

Individually, each function does one small, focused job. But together, they form something much more powerful: a reusable metadata engine for Google Drive.

By the time we reached the full automation function, everything clicked into place:

  • Label discovery
  • Schema discovery
  • Validation
  • Field mapping
  • Atomic application of labels and values

The result is a system where files don't just get tagged - they get structured, searchable metadata that Drive can actually understand and act on. This is the foundation for any serious document automation strategy.

How This Becomes Real Automation (Beyond a Demo)

Right now, we've been running this using a manual function trigger with a single test file. That's perfect for learning and debugging - but the real power of this setup shows up when you connect it to actual workflows.

In a production environment, this same pipeline can be triggered automatically when:

  • A new file is uploaded to a specific Drive folder
  • An invoice gets dropped into an Accounts folder
  • A contract is generated from a form submission
  • A shared drive receives new documents from vendors
  • A scheduled batch process scans unlabelled files every night

At that point, there is no manual step left. Files flow into Drive, your automation picks them up, and labels + structured fields get applied in the background without anyone needing to open the document.

And the best part is - all of this runs on top of tools most teams already have: Google Drive, Google Apps Script, and Workspace APIs.

Conclusion

If there's one key takeaway from this walkthrough, it's this: Google Drive Labels are not just visual tags - they're a powerful metadata platform hiding in plain sight.

With Google Apps Script, you now have full programmatic control over discovering labels, understanding their schemas, and enriching files with structured, queryable metadata. Once this foundation is in place, Drive stops being just a storage system and starts behaving like an intelligent document platform.

In the next part, we'll take this further by introducing fully automatic, Gemini-powered labeling, where documents are analyzed and classified without any manual input.

https://github.com/aryanirani123/Google-Apps-Script/blob/master/DriveLabelsAutomation.js

Feel free to reach out if you have any issues/feedback at aryanirani123@gmail.com.

Top comments (0)