Building a Google Workspace add-on
A Workspace add-on is server-side Apps Script (.gs) that renders CardService cards in
the host sidebar (Docs, Sheets, …). No frontend, no DOM, no Node — you write trigger
functions that return cards. Source lives in integrations/google-workspace-addon/. It
is a thin client over your own public REST API: CardService renders the UI, UrlFetchApp
calls the API with a Bearer key. Read this before the first file; the command-level
playbook is pooriaarab/scripts scripts/google-workspace-addon/README.md.
The trap that wastes a day: there is no fetch
Apps Script has no fetch, no Node APIs, no async/await — every call is synchronous
and every external request goes through UrlFetchApp.fetch(url, options). Three hard
requirements ride on that one call:
- Scope.
https://www.googleapis.com/auth/script.external_request must be in
oauthScopes in appsscript.json or the fetch is refused at runtime.
- Whitelist. Add your API origin to
urlFetchWhitelist (HTTPS URL prefixes,
trailing /) in the manifest. Technically optional, but reviewers ask for it and it
pins the add-on to your domain.
- Errors. Set
muteHttpExceptions: true or UrlFetchApp throws on any non-2xx
and you never see the API's error body.
Rule: before debugging "my API call fails," confirm all three — scope in the
manifest, origin in the whitelist, muteHttpExceptions set. A missing one looks like a
generic authorization failure, not a network error.
The other traps (each cost a round-trip)
- oauthScopes must match in three places —
appsscript.json, the GCP OAuth consent
screen, and what the code actually calls. A mismatch gives users an "unverified app"
wall or an auth error. Prefer .currentonly host scopes (documents.currentonly,
spreadsheets.currentonly) — full documents/drive scopes drag you into
restricted-scope verification.
- Triggers return Cards; action handlers return ActionResponses. A
homepageTrigger.runFunction must return CardService.newCardBuilder()….build(). A
button's setFunctionName(…) handler must return
CardService.newActionResponseBuilder() with navigation/notification. Return the
wrong type and the sidebar fails with no useful error.
- User keys live in user properties.
PropertiesService.getUserProperties() is
per-user; getScriptProperties() is shared by every user of the add-on. Store the API
key in user properties — script properties leak one user's key to all.
- Deploy ≠ edit. The Marketplace listing points at a versioned deployment ID
(Deploy → New deployment → Add-on).
clasp push changes nothing users see until you
create a new deployment version. During development use Deploy → Test deployments
to install into Docs/Sheets.
- The default GCP project is a dead end. An Apps Script project starts on a hidden
auto-created GCP project; you cannot fully configure the OAuth consent screen or the
Marketplace SDK there. Switch to a standard GCP project you own first (Project
Settings → Google Cloud Platform (GCP) Project → Change project).
Build path
- Develop with clasp (
@google/clasp): clasp login, clasp create --type standalone, clasp push (pushes appsscript.json + all .gs files), clasp open.
Or paste the files into script.google.com and enable the manifest via Project
Settings → "Show appsscript.json in editor".
- Manifest (
appsscript.json): addOns.common (name, logoUrl, layoutProperties,
homepageTrigger, universalActions) + per-host blocks (addOns.docs,
addOns.sheets) with their own homepage triggers, plus oauthScopes and
urlFetchWhitelist.
- UI: CardService builders only — card sections, widgets,
CardService.newAction().setFunctionName(…). No HTML.
- API: one thin
UrlFetchApp client over your public REST API. Keep business logic
server-side; the add-on renders and relays.
Submission — Google Workspace Marketplace
Submittable: portal-review (Google Cloud console; OAuth verification is the heavy gate). Steps:
- Switch the script to a standard GCP project (trap 5).
- Configure the OAuth consent screen (External for public distribution). Add the
exact scopes from
appsscript.json.
- Enable the Google Workspace Marketplace SDK in
console.cloud.google.com.
- App Configuration tab: check Google Workspace Add-on + the host extensions, enter
the deployment ID (Deploy → New deployment → Add-on in the Apps Script editor),
fill developer name, website, support email; set visibility Public.
- Store Listing tab: app name, short + detailed descriptions, graphics (128×128 app
icon, 220×140 tile card, 1280×800 or 640×400 screenshots), category, pricing,
terms-of-service and privacy-policy URLs.
- OAuth verification. Public listings with sensitive scopes must pass Google's
verification: submit from the consent-screen page with a demo video of the add-on
flow. This takes days to weeks and bounces on any scope/consent mismatch (exact video
- timeline requirements: TBD — confirm at first submission).
- Publish in the Marketplace SDK. Google reviews the listing (days, not minutes).
For domain-internal use, set visibility Private in step 4 — no OAuth verification, no
marketplace review.
Parity checklist (prove in real Docs + Sheets before submitting)
install via Test deployments · save the API key from a clean state · read the current
doc/sheet · call the API · see success/error surfaced in a card · refresh.
Related skills
canva-app — the same "sandboxed thin client over your API" shape on Canva; different
iframe/allow-list rules.
adobe-express-addon / figma-plugin — other in-editor add-on surfaces with their own
manifest quirks.
connector-directory-submission — the cross-marketplace submission router.
1---2name: google-workspace-addon3description: Build, test, deploy, and publish a Google Workspace add-on (an Apps Script + CardService add-on under integrations/google-workspace-addon/ that runs in the Docs/Sheets sidebars) and get it listed on the Google Workspace Marketplace. Use when creating a new Workspace add-on, writing CardService cards, calling an external API from Apps Script, deploying with clasp, configuring appsscript.json scopes, or figuring out why the OAuth consent screen or marketplace review bounces. Covers the whole path plus the traps that each cost a round-trip: there is no fetch in Apps Script (UrlFetchApp only, and it needs the script.external_request scope plus a urlFetchWhitelist entry or the call is refused), oauthScopes must match across manifest, consent screen, and code, homepage triggers return Cards but action handlers return ActionResponses, user keys belong in user properties not script properties, and the real submission wall is Google's OAuth verification (demo video, days-to-weeks), not the listing form. Sibling of the4---56# Building a Google Workspace add-on78A Workspace add-on is **server-side Apps Script (`.gs`) that renders CardService cards in9the host sidebar** (Docs, Sheets, …). No frontend, no DOM, no Node — you write trigger10functions that return cards. Source lives in `integrations/google-workspace-addon/`. It11is a thin client over your own public REST API: CardService renders the UI, `UrlFetchApp`12calls the API with a Bearer key. Read this before the first file; the command-level13playbook is `pooriaarab/scripts` `scripts/google-workspace-addon/README.md`.1415## The trap that wastes a day: there is no `fetch`1617Apps Script has **no `fetch`, no Node APIs, no async/await** — every call is synchronous18and every external request goes through `UrlFetchApp.fetch(url, options)`. Three hard19requirements ride on that one call:20211. **Scope.** `https://www.googleapis.com/auth/script.external_request` must be in22 `oauthScopes` in `appsscript.json` or the fetch is refused at runtime.232. **Whitelist.** Add your API origin to `urlFetchWhitelist` (HTTPS URL prefixes,24 trailing `/`) in the manifest. Technically optional, but reviewers ask for it and it25 pins the add-on to your domain.263. **Errors.** Set `muteHttpExceptions: true` or `UrlFetchApp` **throws** on any non-2xx27 and you never see the API's error body.2829**Rule:** before debugging "my API call fails," confirm all three — scope in the30manifest, origin in the whitelist, `muteHttpExceptions` set. A missing one looks like a31generic authorization failure, not a network error.3233## The other traps (each cost a round-trip)34351. **oauthScopes must match in three places** — `appsscript.json`, the GCP OAuth consent36 screen, and what the code actually calls. A mismatch gives users an "unverified app"37 wall or an auth error. Prefer `.currentonly` host scopes (`documents.currentonly`,38 `spreadsheets.currentonly`) — full `documents`/`drive` scopes drag you into39 restricted-scope verification.402. **Triggers return Cards; action handlers return ActionResponses.** A41 `homepageTrigger.runFunction` must `return CardService.newCardBuilder()….build()`. A42 button's `setFunctionName(…)` handler must return43 `CardService.newActionResponseBuilder()` with navigation/notification. Return the44 wrong type and the sidebar fails with no useful error.453. **User keys live in user properties.** `PropertiesService.getUserProperties()` is46 per-user; `getScriptProperties()` is shared by every user of the add-on. Store the API47 key in user properties — script properties leak one user's key to all.484. **Deploy ≠ edit.** The Marketplace listing points at a **versioned deployment ID**49 (Deploy → New deployment → Add-on). `clasp push` changes nothing users see until you50 create a new deployment version. During development use **Deploy → Test deployments**51 to install into Docs/Sheets.525. **The default GCP project is a dead end.** An Apps Script project starts on a hidden53 auto-created GCP project; you cannot fully configure the OAuth consent screen or the54 Marketplace SDK there. Switch to a standard GCP project you own first (Project55 Settings → Google Cloud Platform (GCP) Project → Change project).5657## Build path5859- Develop with **clasp** (`@google/clasp`): `clasp login`, `clasp create --type60 standalone`, `clasp push` (pushes `appsscript.json` + all `.gs` files), `clasp open`.61 Or paste the files into `script.google.com` and enable the manifest via Project62 Settings → "Show appsscript.json in editor".63- Manifest (`appsscript.json`): `addOns.common` (`name`, `logoUrl`, `layoutProperties`,64 `homepageTrigger`, `universalActions`) + per-host blocks (`addOns.docs`,65 `addOns.sheets`) with their own homepage triggers, plus `oauthScopes` and66 `urlFetchWhitelist`.67- UI: CardService builders only — card sections, widgets,68 `CardService.newAction().setFunctionName(…)`. No HTML.69- API: one thin `UrlFetchApp` client over your public REST API. Keep business logic70 server-side; the add-on renders and relays.7172## Submission — Google Workspace Marketplace7374**Submittable: portal-review** (Google Cloud console; OAuth verification is the heavy gate). Steps:75761. Switch the script to a standard GCP project (trap 5).772. Configure the **OAuth consent screen** (External for public distribution). Add the78 exact scopes from `appsscript.json`.793. Enable the **Google Workspace Marketplace SDK** in `console.cloud.google.com`.804. **App Configuration tab:** check Google Workspace Add-on + the host extensions, enter81 the **deployment ID** (Deploy → New deployment → Add-on in the Apps Script editor),82 fill developer name, website, support email; set visibility **Public**.835. **Store Listing tab:** app name, short + detailed descriptions, graphics (128×128 app84 icon, 220×140 tile card, 1280×800 or 640×400 screenshots), category, pricing,85 terms-of-service and privacy-policy URLs.866. **OAuth verification.** Public listings with sensitive scopes must pass Google's87 verification: submit from the consent-screen page with a **demo video** of the add-on88 flow. This takes days to weeks and bounces on any scope/consent mismatch (exact video89 + timeline requirements: TBD — confirm at first submission).907. **Publish** in the Marketplace SDK. Google reviews the listing (days, not minutes).9192For domain-internal use, set visibility **Private** in step 4 — no OAuth verification, no93marketplace review.9495## Parity checklist (prove in real Docs + Sheets before submitting)9697install via Test deployments · save the API key from a clean state · read the current98doc/sheet · call the API · see success/error surfaced in a card · refresh.99100## Related skills101102- `canva-app` — the same "sandboxed thin client over your API" shape on Canva; different103 iframe/allow-list rules.104- `adobe-express-addon` / `figma-plugin` — other in-editor add-on surfaces with their own105 manifest quirks.106- `connector-directory-submission` — the cross-marketplace submission router.