Going Stellar

Home/Methods

Apps · Google Workspace

How we build a Google Workspace add-on

We build each add-on on Apps Script and CardService with the narrowest scope that does the job, then take it through consent, verification and review. Approval proves little: what kept our add-ons working was testing on a clean account, testing what they write, and counting people rather than seats.

Updated

What this is for

We build small add-ons for Google Docs, Sheets, Slides and Gmail: a side panel that does one job on the open file. By early September we had built sixteen. This is our method from the first line of code to a live listing, and the measurements after it.

Most of it was learned after approval. Approval checks a listing and a video, not whether a stranger can use the product or whether it leaves their data alone.

Build: Apps Script, CardService and the narrowest scope

1. Apps Script with CardService. The panel is built from CardService widgets, not from HtmlService. The HTML route needs script.container.ui, which is a sensitive scope. The native panel is narrow and plain, and we accept that.

2. Choose the scope family before the feature. Google has three tiers. Non-sensitive scopes, such as documents.currentonly, need no OAuth verification. Sensitive ones, such as gmail.send, need a verification round. Restricted ones, which read a whole mailbox or Drive, add an external security assessment repeated every year. The family decides the tier, not the count: one of ours carries three currentonly scopes and went live without verification. We read the classification in the first hour of a build.

3. Know what the narrow scope does not give you. Under *.currentonly the file's content is allowed, its Drive metadata is not. Document.getName() throws; Sheet.getName(), a tab name, works. The difference is invisible in the code and fatal at runtime.

4. No backend. Licences are signed keys checked inside the add-on and exception logging is off, so nothing about the user leaves their account. Our only view of usage is Apps Script's execution log.

Consent screen, verification and listing

Use a standard Google Cloud project; the default one Apps Script creates cannot publish. Set the consent screen to In production: one add-on was rejected for that alone, because on Testing the reviewer got error 403: access denied. Our submission tool now refuses while the status is Testing.

Scopes must be identical in three places: the manifest, the Marketplace SDK configuration and the consent screen. The SDK keeps its own hand-typed list. The name must match in four: the listing, its web page, the consent screen and the SDK listing.

For a sensitive scope the order is fixed: branding verified and published, then OAuth verification, then Marketplace submission. A verified brand lapses after 7 days unless you also press Publish branding, and renaming the consent screen silently resets it.

In the listing, every Google mark carries ™, including Gmail, Calendar and Drive. Keep the short description to about 100 characters. Upload more than one screenshot at 1280×800; we were flagged for having one. Do not promise one permission: the install screen shows three lines, because Google attaches email and basic profile to every Marketplace add-on.

The review

After submission an email asks for three things: test credentials, an end-to-end screen recording showing how each requested scope is used, and verification for sensitive scopes. We reply in the thread: no credentials, because there is no login; an unlisted video; how the scope is used; and a licence key for the paid features.

In our first round verdicts came in 3 to 8 hours. Of 9 add-ons submitted on 18 and 19 August, 4 were approved, 2 rejected and 3 still in review. Both rejections were about form: a missing ™ and a consent screen that did not match the listing.

The rejection email arrives about 40 minutes before the console changes, so wait. While a draft is in review the SDK configuration is silently frozen: Save succeeds and the change is gone after a reload. A code fix needs no new review: a new version plus an update to the published deployment reaches users at once. Pushing code alone changes only the head version, which users never run.

The trap: installing from the store does not authorise the script

A Marketplace install grants the add-on's scopes; Apps Script separately requires the script to be authorised. While that is missing every service call is refused, and the error names the first call attempted, so it blames DocumentApp even when that scope is granted. Our rule that the panel must never crash made it worse: a try/catch swallowed the exception Google uses to draw the authorise prompt.

Our fix went through four versions in 19 days. The first passed the script's authorisation URL with no account hint, so people signed into several Google accounts landed on the script file, and we received 12 requests for access to our source code from 7 users across 5 add-ons in two days. The second left the URL out and the platform refused the card: a dead end on all 16 add-ons for 17 days. The third added the user's address, which is empty outside our domain. The fourth throws nothing for Google's own scopes; the documentation says that exception is meant for third-party services.

No test caught the three bad versions: each test browser had one signed-in account, and the mock drew a button regardless. The inbox caught them. We now install through the real store page on a fresh browser profile, with an account that has never seen the add-on, and check the effect on the document. That test showed a new user was never affected: the refusal hit existing grants after we changed scopes. Any scope change now requires a fresh authorisation before a fix counts as done.

Test the writes for real

Four times a bug reached users because our mock was kinder than the platform. It accepted a trigger interval under one hour, a method that exists only on another widget (a reviewer saw Add-on error), and getName(). That last one hid a dead add-on: Outline had 13 installs and zero opens in 10 days, because one unused field threw on every document.

Writes were worse, because a mock with no state cannot see them. Change Case read cells with getValues(), which returns a formula's result, and wrote the block back with setValues(), replacing formulas with results. The panel said those cells were left alone; Undo reported three restored cells and restored nothing that mattered. The mocked setValues did nothing. In Alphabetize, 53 checks were green while the sort path had never run, because the check accepted any non-empty message, including the error.

The rules now: the mock throws where the platform throws, with a method whitelist per widget; anything that writes is tested against a stateful model of the destination; assertions check the shape of the success message; and every fix is proved by mutation, putting the bug back and watching the suite fail. Verification ends in a real document, not in the panel.

Measuring usage, and seats against people

The public user count counts domain seats: when an administrator installs for an organisation, every employee is added. On 31 August our public total was 2,209; the console's Individual End User Installs chart, which counts people who installed it themselves, gave 434. Change Case showed 1,892 users: 2 organisations, 1,875 seats and 32 people. Our ranking was upside down.

Usage comes from the Apps Script executions API, which keeps about 10 days of history, not 30, so our 30-day label turned ‘not this week’ into ‘never’. Functions the report did not recognise looked like buttons nobody pressed. The tool now prints the window it covered and reads button handlers from the published code.

We split ‘installs but no use’ into three cases: never opened, opened but nothing done, used but not paid. Only the third is about the offer. And before trusting a zero, prove the instrument can see a one: open the add-on yourself and check your execution appears.

Where we were wrong

We first concluded that every published add-on was unusable for everyone. The clean-account test and a five-star review refuted it: the failure hit a subset.

We reported the account-hint fix as done when it existed only on our machine. The next day brought six more access requests from five people on four add-ons, none from the one add-on we had actually deployed. Nothing is fixed until the served code is checked.

Our only one-star review said an add-on ‘doesn't even work’. The empty panel told the person to press Sort; the only button on screen was Refresh, because a side panel cannot see a selection made after it opened. They were right. The main action now exists in the empty state.

We also declared an add-on invisible after a correct install. It sat in the More Add-ons overflow; one screenshot settled it.

Limits, and a checklist

This method gets an add-on approved and keeps it honest. It does not make people use it: why many installs never become use is still open for us. Consoles also change under automation without notice, so every saved setting is confirmed by a reload.

Checklist. Scope tier read in the first hour. No file metadata under currentonly. Consent screen in production, branding verified and published. Scopes identical in three places, name in four. ™ on every Google mark. Clean-profile install with an account that has never seen the add-on. Stateful mocks that throw like the platform, mutation-tested. Fix checked on the served version. Seats and people read separately, and every zero checked against your own execution.

Questions and answers

Which OAuth scopes need verification for a Google Workspace add-on?

OAuth verification for a Google Workspace add-on depends on the scope tier. Non-sensitive scopes, such as documents.currentonly, need no OAuth verification. Sensitive scopes, such as gmail.send, need a verification round. Restricted scopes, which read a whole mailbox or Drive, add an external security assessment repeated every year. The scope family decides the tier, not the number of scopes, so an add-on with several currentonly scopes can go live without verification.

Why was my Google Workspace add-on rejected in Marketplace review?

Google Workspace add-on rejections are often about form rather than code. Common causes include a consent screen left on Testing, which gives the reviewer an access-denied error, a missing trademark symbol on a Google mark, and a consent screen name that does not match the listing. Scopes must be identical in the manifest, the Marketplace SDK configuration and the consent screen, and the name must match everywhere the listing appears.

Why does my Apps Script add-on fail with a permission error after installing it from the Marketplace?

An Apps Script add-on can fail after a Marketplace install because the install grants the add-on's scopes while Apps Script separately requires the script itself to be authorised. Until that happens every service call is refused, and the error names the first call attempted, even if that scope was granted. A try/catch that swallows the exception also hides the authorise prompt. Any scope change needs a fresh authorisation test.

How should I test a Google Workspace add-on before release?

Testing a Google Workspace add-on should end in a real document, not in the panel. Install it through the real store page on a fresh browser profile, with an account that has never seen the add-on. Mocks should throw where the platform throws, using a method whitelist per widget, and anything that writes should be tested against a stateful model of the destination. Every fix should be proved by mutation.

Does the user count on a Google Workspace Marketplace listing show how many people use an add-on?

The user count on a Google Workspace Marketplace listing does not show how many people use an add-on. It counts domain seats, so when an administrator installs for an organisation, every employee is added. The console chart for individual end-user installs counts people who installed it themselves, and usage comes from the Apps Script execution log, which keeps only a short history. Seats and people should be read separately.

How to cite this

Going Stellar (2026). How we build a Google Workspace add-on. GOING STELLAR S.R.L..
https://goingstellar.ro/methods/how-we-build-a-google-workspace-add-on/

Read as Markdown

All methods

Contact

Tell us what is not working

Write in English or Romanian. A person reads it — usually the same person who would do the work — and answers within one business day.