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
- Open Google Docs
- Create a new document (or open any doc)
- Go to Extensions → Apps Script
- 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
- Click Deploy → Test deployments
- Choose Google Workspace Add-on
- Install it
- Open any Google Doc
- 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