Understand execution context and identity
Know who a Northwind script runs as — and why it matters for permissions.
Published Oct 11, 2025
Every time an Apps Script function runs, it runs as somebody. That identity decides what the script is allowed to do — whose Gmail it can read, whose Drive files it can touch, whose calendar it can edit. Most of the time you never think about it, because in the editor the script simply runs as you.
The trouble starts when the same code runs through a different door — a trigger someone else installed, a web app published to a wide audience, or a script another person opened. The identity shifts, the permissions shift with it, and code that worked yesterday silently fails or, worse, does the wrong thing. Understanding execution context up front saves you from debugging permission errors that have nothing to do with your logic.
Three identities
Apps Script actually tracks two separate notions of “who”, and the gap between them is where confusion lives.
- Effective user — the identity whose authorisation the script borrows. This is what determines access to Gmail, Drive, Calendar and so on.
Session.getEffectiveUser()returns it. - Active user — the person who triggered this particular run.
Session.getActiveUser()returns it, but only when privacy rules allow the script to know. - Owner / installer — the account that owns the script project or installed the trigger. In many contexts this is the effective user.
The table below shows how those line up across the contexts you will actually meet:
| Context | Session.getEffectiveUser() | Session.getActiveUser() |
|---|---|---|
| User invoking via UI | owner | the user |
| Simple trigger | owner | the user |
| Installable trigger | installer | installer |
| Web app (Execute as: me) | owner | the viewer (Workspace only) |
| Web app (Execute as: user) | viewer | viewer |
The pattern worth memorising: the effective user is whose permissions are in play, and the active user is just a label telling you who is on the other side. They are often different, and getActiveUser() returns an empty string whenever the runtime is not allowed to disclose the viewer.
Practical implications
These distinctions are not academic — each one maps to a bug you can hit in production.
- Scope of access follows the effective user. A script running as you can read your Gmail and your private Drive files. The same script running as a teammate — through a web app set to “Execute as: user” — can only see their data, so any code that hard-codes your inbox will fail for them.
- Installable triggers freeze the installer’s identity. A trigger installed by Person A keeps running as Person A indefinitely, using their quotas and their permissions. If Person A leaves the organisation and their account is suspended, the trigger stops firing — with no error anyone sees.
- External users are anonymous. On a public web app,
Session.getActiveUser().getEmail()returns an empty string for anyone outside your Workspace domain. Do not build access control or audit logging on the assumption that you always know who the visitor is. - Quotas are charged to the effective user. Daily limits on email, URL fetches and triggers are per-user. A web app set to “Execute as: me” spends your quota on every visitor’s request, which can exhaust it fast under load.
Choosing the web app execution mode
When you publish a web app, the “Execute as” setting is the single most important decision, because it fixes the effective user for every request:
| Execute as: me | Execute as: user accessing | |
|---|---|---|
| Effective user | You, the publisher | Whoever opens the app |
| Best for | A shared tool acting on your data | A tool each person uses on their data |
| Quota spent | Yours, for everyone | Each user’s own |
| Risk | Visitors borrow your full access | Each user only ever sees their own data |
Choose “Execute as: me” when the app is a controlled front end to data you own — a request form that writes to your spreadsheet. Choose “Execute as: user” when the app should operate on each person’s own Gmail, Drive or Calendar, so no one can reach anyone else’s data through it.
Audit
When a script behaves differently in two places, the fastest first step is to print both identities and see what the runtime actually thinks.
function whoAmI() {
// Effective user: whose permissions this run is borrowing.
console.log('effective:', Session.getEffectiveUser().getEmail());
// Active user: who triggered the run — may be empty if undisclosed.
console.log('active:', Session.getActiveUser().getEmail());
}
Run whoAmI from the editor and you will see your own address twice. Call the same function from inside a trigger handler or a web app and the output changes — that difference is the execution context made visible. When active comes back empty, the runtime is deliberately withholding the viewer’s identity, not erroring.
A useful habit is to drop a one-line identity log at the top of any handler that does something sensitive. When a permission error appears weeks later, the logs already tell you which account hit it.
For event-driven code, prefer the event object over Session. A trigger handler receives an e argument that names the person who caused the event — which is usually who you actually care about:
function onEditHandler(e) {
// e.user is the editor — NOT necessarily the effective user.
// The script still RUNS as the trigger installer.
console.log('edited by:', e.user ? e.user.getEmail() : 'unknown');
console.log('running as:', Session.getEffectiveUser().getEmail());
}
Logging both side by side makes the gap concrete: one line is who clicked, the other is whose permissions did the work.
Common mistakes
- Assuming a trigger runs as whoever caused the event. An installable
onEdittrigger runs as the person who installed it, not the person who edited the cell. ReadingSession.getActiveUser()to “see who edited” returns the installer, not the editor — use the event object’se.userinstead. - Building security on
getActiveUser(). It is empty for external users and can be spoofed-adjacent in edge cases. For real access control, restrict the web app’s audience or check membership server-side. - Hard-coding your own email or file IDs in code meant to run as other users. It works in the editor and breaks the moment the effective user changes.
- Forgetting that suspended accounts kill their triggers. When someone leaves, audit every trigger they installed and reinstall it under a service or shared account before their access is revoked.
- Confusing “Execute as: me” with “anyone can do anything”. It means visitors borrow your permissions for the script’s actions — so a careless web app can let strangers act with your full access. Scope the code tightly.