You deploy a doGet / doPost web app, share the URL, and suddenly writes land under the wrong account — or the script can’t see the sheet at all. The Execute as setting is the quiet culprit. Here’s how we pick it for bootstrapper tools.
TL;DR
- Execute as: Me — the script always runs as the deploying account. Good for internal webhooks, shared ops sheets the owner can write, and APIs that shouldn’t depend on who clicked the link.
- Execute as: User accessing the web app — the script runs as whoever opened the URL (after they authorize). Good for teammate-facing forms that must write their Drive/Sheets.
- Inside the handler,
Session.getActiveUser()/getEffectiveUser()tell you who Google thinks is acting — log them once while you’re debugging auth. - Who has access (Anyone / Anyone with Google account / domain) is a separate dial from Execute as.
- Soft tip: when the auth model feels fuzzy, Co-Pilot can leave a plain-English note next to the deploy settings.
Using the Apps Script editor a lot? NitroGAS drops free themes & snippets right into script.google.com — optional Co-Pilot when you want a boost.
The two identities
| Setting | Runs as | Typical fit |
|---|---|---|
| Me | The account that deployed | Nightly webhook, Zapier in, shared ops sheet owned by you |
| User accessing the web app | The person who hits the URL | “Submit my row” forms, per-user Drive folders |
Wrong combo symptoms:
- Sheet writes appear under your account when a teammate submitted the form (you wanted User).
SpreadsheetApp.openByIdfails for callers who don’t have access (you chose User but the sheet isn’t shared to them — or you needed Me with a service sheet).- “Authorization required” loops for anonymous callers (access is too tight for the audience).
function doPost(e) {
var active = Session.getActiveUser().getEmail();
var effective = Session.getEffectiveUser().getEmail();
console.log('active=', active, 'effective=', effective);
// With Execute as Me: effective is usually the deployer.
// With Execute as User: both trend toward the caller (after they authorize).
try {
var body = e && e.postData && e.postData.contents
? JSON.parse(e.postData.contents)
: {};
// ... write using the identity you intended ...
return jsonOk({ active: active, effective: effective });
} catch (err) {
return jsonErr(err.message || String(err), 500);
}
}
(jsonOk / jsonErr are small ContentService helpers — grab them from the code snippets library.)
Decision tree for bootstrappers
- Is this an unattended inbound webhook? (Stripe, GitHub, a cron elsewhere) → Execute as Me, restrict the URL (secret token in the path/header), keep the sheet owned by the deployer.
- Must each human write into their own files? → Execute as User, access = Google account (or domain), and document the one-time authorize click.
- Shared team sheet, humans click a link? → Usually Me so one service account of record owns the data; gate the UI with a simple shared secret or Apps Script-side allowlist of emails if needed.
- Still unsure? Deploy a doGet that only returns
jsonOk({ active, effective }), hit it while logged in as you and as a teammate, and read the emails. That five-minute experiment beats guessing.
Access vs Execute as
- Execute as = whose permissions the code uses.
- Who has access = who is allowed to invoke the URL.
- Anyone + Execute as Me is powerful (and easy to leak). Prefer a token check inside
doPosteven when the link is “Anyone.”
Deploy checklist
- Execute as chosen deliberately (table above)
- Access matches the audience
- New deployment (Manage deployments → Edit → New version) after code changes that matter
- One smoke
doGetthat logs active/effective - Sheet/Drive sharing matches the identity (Me needs deployer access; User needs each caller’s access)
Soft CTA
When you’re explaining this to a non-technical teammate, NitroGAS Co-Pilot can leave a plain-English comment above doGet/doPost that says which Execute-as you picked and why. Happy Coding.


