Built a Google Docs AddOn from Scratch

How to Build a Google Docs Add-On with Apps Script (Step-by-Step)

https://github.com/lsvekis/Google-Docs-Addon

Google Docs is incredibly powerful—but when documents are copied from Word, PDFs, LMS systems, or email, formatting quickly turns into chaos.

Instead of fixing formatting manually every time, you can build a Google Docs Add-on using Google Apps Script that runs directly inside Docs and gives users one-click tools.

In this guide, you’ll learn exactly how to create a Google Docs add-on from scratch, including:

  • Project setup
  • Add-on manifest configuration
  • Sidebar UI with buttons
  • Running document-editing functions safely
  • Best practices for Docs add-ons
  • Full working code you can customize

By the end, you’ll have a real add-on—not just a script.


What Is a Google Docs Add-On?

A Google Docs Add-on is an extension that:

  • Appears inside Google Docs under Extensions
  • Runs with limited permissions
  • Uses a sidebar UI
  • Can be published to the Google Workspace Marketplace

Unlike old menu-based scripts, add-ons:

  • Feel more “app-like”
  • Are safer (least-privilege access)
  • Are reusable across documents

What We’ll Build

We’ll build a Doc Cleanup Tools add-on that can:

  • Set normal text to 12px
  • Normalize spacing before/after paragraphs
  • Set line spacing to 1
  • Remove empty lines
  • Reduce heading levels
  • Clean up pasted formatting

You can replace these tools with any functionality you want later.


Step 1: Create a New Apps Script Project

  1. Open Google Docs
  2. Create a new document (or open any doc)
  3. Go to Extensions → Apps Script
  4. Rename the project (e.g. Doc Cleanup Tools)

You now have a container-bound Apps Script project.


Step 2: Configure the Add-On Manifest (appsscript.json)

The manifest tells Google:

  • This is a Docs add-on
  • What permissions it needs
  • What UI to show

Open Project Settings and enable Show appsscript.json.

Replace it with:

{
  "timeZone": "America/Toronto",
  "runtimeVersion": "V8",
  "exceptionLogging": "STACKDRIVER",
  "oauthScopes": [
    "https://www.googleapis.com/auth/documents.currentonly"
  ],
  "addOns": {
    "common": {
      "name": "Doc Cleanup Tools",
      "logoUrl": "https://your-public-logo-url.png",
      "homepageTrigger": {
        "runFunction": "onHomepage"
      }
    },
    "docs": {
      "homepageTrigger": {
        "runFunction": "onHomepage"
      }
    }
  }
}

Why documents.currentonly?

This scope:

  • Only allows access to the currently open document
  • Is required for Marketplace approval
  • Builds user trust

Step 3: Build the Add-On Sidebar UI

Create a file called Code.gs.

This file handles:

  • Sidebar layout
  • Buttons
  • Notifications
  • Safe execution

Sidebar Entry Point

function onHomepage() {
  const card = CardService.newCardBuilder()
    .setHeader(
      CardService.newCardHeader()
        .setTitle("Doc Cleanup Tools")
        .setSubtitle("Formatting tools for the current document")
    )
    .addSection(buildActionsSection_())
    .build();

  return [card];
}

Google calls onHomepage() automatically when the add-on opens.


Add Buttons to the Sidebar

function buildActionsSection_() {
  const section = CardService.newCardSection()
    .addWidget(
      CardService.newTextParagraph()
        .setText("Run cleanup actions on the currently open document.")
    );

  section.addWidget(button_("Set Normal text to 12px", "run_setNormalTextTo12px"));
  section.addWidget(button_("Set line spacing to 1", "run_setLineSpacingToOne"));
  section.addWidget(button_("Remove ALL empty lines", "run_removeAllEmptyLines"));

  return section;
}

Each button calls a wrapper function (not the document function directly).


Button Helper

function button_(label, fnName) {
  return CardService.newTextButton()
    .setText(label)
    .setOnClickAction(
      CardService.newAction().setFunctionName(fnName)
    );
}

Step 4: Run Functions Safely (Best Practice)

Add-ons must return UI responses.
You should never run document functions directly.

function runSafely_(label, fn) {
  try {
    fn();
    return notify_("✅ Done: " + label);
  } catch (err) {
    return notify_("❌ Error: " + err.message);
  }
}

function notify_(msg) {
  return CardService.newActionResponseBuilder()
    .setNotification(
      CardService.newNotification().setText(msg)
    )
    .build();
}

Button Wrappers

function run_setNormalTextTo12px() {
  return runSafely_("Set Normal text to 12px", setNormalTextTo12px);
}

function run_setLineSpacingToOne() {
  return runSafely_("Set line spacing to 1", setLineSpacingToOne);
}

function run_removeAllEmptyLines() {
  return runSafely_("Removed empty lines", removeAllEmptyLines);
}

Step 5: Write the Document Logic

Create a second file called DocTools.gs.

Helper: Get All Paragraphs (Including Tables)

function getAllParagraphLikeElements_() {
  const body = DocumentApp.getActiveDocument().getBody();
  const out = [];

  const walk = el => {
    const type = el.getType();

    if (type === DocumentApp.ElementType.PARAGRAPH ||
        type === DocumentApp.ElementType.LIST_ITEM) {
      out.push(el);
      return;
    }

    if (typeof el.getNumChildren === "function") {
      for (let i = 0; i < el.getNumChildren(); i++) {
        walk(el.getChild(i));
      }
    }
  };

  walk(body);
  return out;
}

This is critical—Docs contain nested content (tables, lists, footnotes).


Set Normal Text to 12px

function setNormalTextTo12px() {
  const els = getAllParagraphLikeElements_();

  els.forEach(p => {
    if (p.getHeading &&
        p.getHeading() === DocumentApp.ParagraphHeading.NORMAL) {
      p.editAsText().setFontSize(12);
    }
  });
}

Set Line Spacing to 1

function setLineSpacingToOne() {
  const els = getAllParagraphLikeElements_();

  els.forEach(p => {
    try {
      p.setLineSpacing(1);
    } catch (e) {}
  });
}

Remove All Empty Lines (Safely)

function removeAllEmptyLines() {
  const body = DocumentApp.getActiveDocument().getBody();

  for (let i = body.getNumChildren() - 1; i >= 0; i--) {
    const el = body.getChild(i);
    if (el.getType() !== DocumentApp.ElementType.PARAGRAPH) continue;

    const text = el.asParagraph().getText().trim();
    if (text === "" && body.getNumChildren() > 1) {
      body.removeChild(el);
    }
  }
}

Step 6: Test the Add-On

  1. Click Deploy → Test deployments
  2. Choose Google Workspace Add-on
  3. Install it
  4. Open any Google Doc
  5. Go to Extensions → Doc Cleanup Tools

Your sidebar should load instantly.


Step 7: Customize Your Own Add-On

Once you understand the structure, you can:

  • Replace formatting tools with AI tools
  • Add document validators
  • Build content generators
  • Add course prep tools
  • Add accessibility checks
  • Build internal team tools

The add-on framework stays the same—only the logic changes.


Key Takeaways

  • Google Docs add-ons are modern, safe, and powerful
  • Always use:
    • documents.currentonly
    • Sidebar UI
    • Wrapper functions
  • Separate UI logic from document logic
  • Never call saveAndClose() in add-ons