ProductsDocsBlogConsultingAboutContactGet Started
Back to BlogAn office manager at an open key cabinet at dusk, hanging a departing colleague's keys on a tagged hook instead of throwing them away while handing a fresh set to a new hire — a desk-clearing box and an empty wastebasket behind her.
23 min readMageSheet Team

Automate Google Workspace Onboarding/Offboarding

Google WorkspaceApps ScriptAdmin SDKIT OperationsAutomationOnboardingOffboarding

A single person joining or leaving your company triggers about twelve administrative actions, and in most organizations under 200 people every one of them is done by hand. Create the account, place it in the right org unit, assign a license, add it to five groups, share the onboarding folder — then, months later, suspend it, revoke its sessions, strip the license, transfer the Drive files, reassign the calendar, redirect the mail, wipe the phone, and eventually delete it.

The whole sequence fits in roughly two hundred lines of Apps Script talking to the Admin SDK, running inside the Workspace account you already pay for. The identity and lifecycle tools that sell this back to you charge per managed user per month, and that meter runs on all 200 of your employees every month whether you hired anyone or not — for a workflow that actually fires a few dozen times a year. Get a quote for your own seat count before you compare; published list prices in this category are rare and negotiated ones are the norm.

But before any of that: roughly half of this checklist is already native, and paying a developer to rebuild it would be malpractice. So we start there.

What Google already does for you (don't rebuild these)

Native capabilityWhat it coversWhere it stops
Bulk CSV upload (Directory → Users)Creates or updates up to 150,000 users per file; 200 per file if the upload also assigns new licensesCreate only. No groups, no conditional OU logic, no sequencing, no record of what happened
Transfer data at deletionSuper admin can hand Drive & Docs, the primary Calendar and Data Studio assets to another user as part of the delete flowOnly at deletion, only those three apps, one target user, no audit trail
20-day restore windowA deleted account and its Gmail, Drive and primary calendar can be restored for 20 daysAfter 20 days it is gone permanently — not recoverable by Google support
Archived User licenseA reduced-cost add-on license that keeps a former employee's data available to admins and in Vault while blocking sign-in. Google lists it at $2 per license on Business Starter, $3 Standard, $4 Business Plus, $5 Enterprise Standard, $7 Enterprise Plus (Google's add-ons page, checked September 2026)It must match your edition — a Business Plus tenant buys Business Plus Archived User. Reseller and regional pricing can differ from list, so confirm yours in the console before you budget
Email alias on the old addressGoogle's own recommendation for catching mail sent to a departed employee: add their address as an alias on a current userThe address only frees up after the account is deleted, and propagation is not instant. Test the round trip on your own domain before you promise anyone mail continuity
Inbound SCIM (announced 9 July 2026, Business & Enterprise editions)Real-time provisioning, updates and deprovisioning driven by a SCIM-compatible IdP or HR systemRequires that IdP or HRIS. If your source of truth is a spreadsheet, there is nothing to sync from
Workspace Studio (the tool first announced as Workspace Flows)Google's native, Gemini-powered no-code builder for agents and flows, with third-party connectors and custom steps that can call Apps ScriptIts step catalogue is user-level work — Gmail, Drive, Docs, Sheets, Chat, Calendar, Tasks — not Admin SDK user lifecycle. It cannot create, suspend or delete an account

Read that table as a filter. If you run Okta, Entra ID, or an HRIS that speaks SCIM, use inbound SCIM and stop reading. It is native, real-time, and free with your edition. Writing Apps Script to duplicate it would be a worse version of something Google maintains for you.

What native tooling does not give you is the part that actually consumes an IT manager's afternoon:

  • Conditional mapping. "Warehouse hires go in /Operations/Warehouse, get Business Starter, and join warehouse@ and shift-alerts@; sales hires go elsewhere with a different SKU." That logic lives in your head or a wiki page, not in a CSV column.
  • Ordering and timing. Transfer must happen before deletion. Deletion must happen long after suspension. Nothing native enforces that gap.
  • An audit trail you own. Six months later, "who removed her from the finance group, and when?" should be answerable from a sheet, not from an admin log export.
  • A rehearsal. Native bulk actions have no dry run. You click, and it happens.

That last one is the reason this post spends more time on safety than on API calls.

Setup: which advanced services exist, and which one doesn't

Enable Admin SDK Directory and Admin SDK Enterprise License Manager as advanced services in the Apps Script editor (Services → add), and make sure the Admin SDK is enabled for your domain. The available Admin SDK advanced services are Directory, Reports, Enterprise License Manager, Groups Settings, Groups Migration and Reseller.

How to read the code in this post. The snippets below are illustrations of the pattern, not a drop-in library, and they have not been run against your tenant. Apps Script advanced services are generated from the REST discovery document, so a method's arguments follow the REST call — request body first, then path parameters — but that mapping does move between versions. Check every signature you copy against the Admin SDK Directory API reference and let the editor's autocomplete confirm the shape before you point any of this at a real user. On irreversible operations, trust the reference over this post.

There is no Data Transfer advanced service. The Data Transfer API — the one that reassigns Drive ownership — has to be called over HTTP with UrlFetchApp, carrying the script's own OAuth token. That means declaring the scope explicitly in appsscript.json, because Apps Script cannot infer a scope from a raw fetch:

{
  "timeZone": "Europe/Istanbul",
  "dependencies": {
    "enabledAdvancedServices": [
      { "userSymbol": "AdminDirectory", "serviceId": "admin", "version": "directory_v1" },
      { "userSymbol": "AdminLicenseManager", "serviceId": "licensing", "version": "v1" }
    ]
  },
  "oauthScopes": [
    "https://www.googleapis.com/auth/admin.directory.user",
    "https://www.googleapis.com/auth/admin.directory.group",
    "https://www.googleapis.com/auth/admin.directory.user.security",
    "https://www.googleapis.com/auth/admin.directory.device.mobile.action",
    "https://www.googleapis.com/auth/admin.datatransfer",
    "https://www.googleapis.com/auth/apps.licensing",
    "https://www.googleapis.com/auth/spreadsheets",
    "https://www.googleapis.com/auth/script.scriptapp"
  ]
}

Note admin.directory.user.security separately from admin.directory.user — it is what lets you sign a user out of every session, delete their OAuth tokens and revoke app passwords. Those three actions are the difference between "they can't log in" and "they no longer have access."

The permission conversation you should have first

This script can delete your company. Treat the authorization model as the primary design decision, not an afterthought:

  • Own the script with a dedicated admin account, not a person's. automation-admin@yourdomain with 2-step verification, a strong password in your password manager, and no daily human user. When the IT lead leaves, the automation survives.
  • Use a custom role, not Super Admin, for everything that allows it. Creating users through the Admin console, for example, needs the create privilege plus update plus org-unit read — grant that trio, not the world. Get the data-transfer distinction right, because it is the one people over-grant on: a standalone transfer, the kind the code below performs on a still-suspended account, runs under a delegated admin holding the Data Transfer privilege together with Settings and User read-only. What Google reserves for super admins is transferring data as part of the delete flow in the Admin console. Sequencing the transfer before the deletion — which you should be doing anyway — is therefore also what keeps this script out of super-admin territory. If you do end up needing a super-admin action, isolate it in a separate, rarely-run script instead of elevating your everyday job.
  • Whoever can edit the script has the script's privileges. Restrict edit access on the Apps Script project as tightly as you restrict the admin role itself. A shared Drive full of editors is not an acceptable home for this.
  • Log every mutation to a sheet before you make it, with actor, timestamp, target and payload. When something goes wrong at 4pm on a Friday, the log is what tells you what to undo.

Onboarding: a sheet as the source of truth

One sheet, one row per hire, and a role column that drives everything else. The mapping table is the whole product:

const ROLE_MAP = {
  'Warehouse':  { ou: '/Operations/Warehouse', sku: '1010020027',
                  groups: ['warehouse@example.com', 'shift-alerts@example.com'] },
  'Sales':      { ou: '/Commercial/Sales',     sku: '1010020028',
                  groups: ['sales@example.com', 'crm-users@example.com'] },
  'Engineering':{ ou: '/Product/Engineering',  sku: '1010020028',
                  groups: ['eng@example.com', 'oncall@example.com'] }
};

const PRODUCT_ID = 'Google-Apps'; // every Workspace edition shares this product ID

Those SKU IDs are Google's published identifiers — 1010020027 Business Starter, 1010020028 Business Standard, 1010020025 Business Plus, 1010020026 Enterprise Standard, 1010020020 Enterprise Plus. Keep them in one constant rather than scattered through the code, because the day finance changes editions you want to edit one line.

The create call itself is unremarkable:

function createUser(row) {
  const map = ROLE_MAP[row.role];
  if (!map) throw new Error(`no role mapping for "${row.role}"`);

  const user = AdminDirectory.Users.insert({
    primaryEmail: row.email,
    name: { givenName: row.firstName, familyName: row.lastName },
    password: Utilities.getUuid(),          // never a pattern like Welcome2026
    changePasswordAtNextLogin: true,
    orgUnitPath: map.ou,
    externalIds: [{ value: row.employeeId, type: 'organization' }]
  });

  AdminLicenseManager.LicenseAssignments.insert(
    { userId: user.primaryEmail }, PRODUCT_ID, map.sku
  );

  map.groups.forEach(g => AdminDirectory.Members.insert(
    { email: user.primaryEmail, role: 'MEMBER' }, g
  ));

  return user.id;   // the immutable ID — write this back to the sheet
}

Two details worth more than they look. The random password with changePasswordAtNextLogin removes the habit of predictable starter passwords, which is one of the most reliably exploited weaknesses in small-company Workspace tenants; deliver it out of band to the new hire's personal address or their manager. Writing user.id back to the sheet is your insurance policy: restoring a deleted account requires that immutable ID, and after deletion you cannot look it up by email as easily as you would like.

From here the same function extends naturally — create a personal Drive folder from a template, drop a welcome doc in it, add the laptop asset to your inventory sheet, file the IT ticket. That is ordinary Apps Script, and it is the part where each company's flow diverges.

The dry run: the pattern that earns its keep

Offboarding code does irreversible things. The single most valuable habit is to separate deciding from doing: one function computes a plan, a human looks at it, a second function executes exactly that plan and nothing else.

/** PHASE 1 — decide. Reads only. Writes a reviewable plan, returns a token. */
function planOffboarding(email) {
  const user = AdminDirectory.Users.get(email);
  const groups = (AdminDirectory.Groups.list({ userKey: email }).groups || []);
  const devices = (AdminDirectory.Mobiledevices.list('my_customer',
                    { query: `email:${email}` }).mobiledevices || []);

  const actions = [];
  actions.push({ op: 'SIGN_OUT',      target: email, reversible: 'n/a' });
  actions.push({ op: 'SUSPEND',       target: email, reversible: 'yes — one API call' });
  groups.forEach(g => actions.push(
                { op: 'REMOVE_GROUP', target: email, detail: g.email,
                  reversible: 'yes — re-add' }));
  actions.push({ op: 'REVOKE_TOKENS', target: email, reversible: 'no — user re-consents' });
  devices.forEach(d => actions.push(
                { op: 'ACCOUNT_WIPE', target: d.resourceId, detail: d.model,
                  reversible: 'NO — work data removed from device' }));
  actions.push({ op: 'DROP_LICENSE',  target: email, detail: user.orgUnitPath,
                 reversible: 'yes — reassign' });

  const token = Utilities.getUuid().slice(0, 6).toUpperCase();
  writePlanToSheet(email, token, actions);
  CacheService.getScriptCache()
    .put(`plan_${token}`, JSON.stringify({ email, actions }), 21600); // 6h
  Logger.log(`Plan %s written for %s — %s actions. Review the sheet, then run
    applyOffboarding('%s')`, token, email, actions.length, token);
  return token;
}

/** PHASE 2 — do. Executes only what the reviewed plan contains. */
function applyOffboarding(token) {
  const raw = CacheService.getScriptCache().get(`plan_${token}`);
  if (!raw) throw new Error('plan expired or unknown token — re-run planOffboarding');
  const plan = JSON.parse(raw);

  plan.actions.forEach(a => {
    try {
      execute(a);
      audit(plan.email, a, 'ok', '');
    } catch (err) {
      audit(plan.email, a, 'failed', err.message);
      throw err;            // stop on first failure; never half-finish silently
    }
  });
  CacheService.getScriptCache().remove(`plan_${token}`);
}

writePlanToSheet, execute and audit are your own thin helpers — a sheet append, a switch statement over the op values, and another sheet append. Keep execute boring: one case per operation, no cleverness, no branching on anything the plan does not contain.

Three properties make this safe rather than merely tidy. The plan is data, not code — it can be read by a non-developer in a spreadsheet row. The token means you cannot apply a plan you have not generated, and it expires, so a stale plan from last week cannot be replayed against an account that has since changed. And the loop stops on the first failure with an audit row written, which leaves you a knowable state instead of a mystery.

Notice what the plan does not contain: there is no DELETE op, and no transfer op either. That is deliberate, not an omission. Deletion is the one action with a 20-day fuse on it and no second chance afterwards, so it does not belong in the same function as the reversible containment steps that run the hour someone resigns — keep it in its own script, run by hand, weeks later, against a row that already records a confirmed data transfer. The same goes for the transfer itself, which has to be polled to completion rather than fired and forgotten.

For anything destructive, add one more gate: require the reviewer to type the target email into a confirmation cell. It sounds like theatre until the day somebody runs the leaver script against the wrong row.

Pair this with proper alerting so a failed step reaches a human the same hour. The patterns in our guide to Apps Script error handling, logging and alerts are exactly the layer this code needs in production — a job that silently half-offboards someone is worse than no automation at all.

Scoping note. We build lifecycle automation like this as fixed-scope projects rather than retainers — the decision table, the dry-run harness and the audit sheet are the deliverable, and you own the script in your own Workspace afterwards. If the risky part for you is the offboarding half, send us your current leaver checklist on WhatsApp and we will tell you which steps are safe to automate and which ones should stay in human hands. The feasibility review costs nothing.

The offboarding decision table

This is the part no per-user SaaS subscription can decide for you, because it depends on your retention policy, your industry and your tolerance for risk.

TimingActionReversible?Why then
Within the hourSign out of all sessions, then suspendYesSuspending blocks new sign-ins but does not log out sessions already open — Google's own guidance is to sign the user out manually. Nothing is lost either way
Within the hourRemove from groups; revoke OAuth tokens and app passwordsMostlyGroup mail and third-party app access are not covered by suspension
Within the houradmin_account_wipe on company-managed mobile devicesNoRemoves work data. Prefer this over admin_remote_wipe, which factory-resets a personal phone
Day 1–7Transfer Drive & Docs (both PRIVATE and SHARED) and Calendar via Data Transfer APIEffectively noReassigns ownership in place, so links and history survive
Day 1–7Decide manager access to the mailbox: delegation while suspended, or Vault exportYesMail is the commonest "we still need something from that account"
Day 7–30Drop the paid license, or move to an Archived User licenseYesGoogle bills a suspended account at the same rate as an active one
Day 30+Delete the account, then add the address as an alias on a current user20 days onlyMail continuity without keeping a licensed account alive

Suspend, don't delete is the whole philosophy in two words. Suspension blocks new sign-ins, preserves every file, share, calendar and mail archive exactly as it was, and reverses with a single API call — the entire cost is that the seat keeps billing. Deletion is the opposite kind of action: it starts a 20-day clock, and after it expires nothing brings the account back, not Vault and not Google support. A code path that deletes on the same day someone resigns is a data-loss incident waiting for a trigger.

Which is why the recovery path deserves to be designed before the deletion path. Inside those 20 days a deleted account is restored with users.undelete, and that call wants the account's immutable user ID rather than its email address — which is precisely why onboarding writes user.id back to the sheet, months before anyone imagines needing it. If the only record of a deleted account was its email, the restore is a support conversation instead of one API call. Keep the ID, the deletion date and the 20-day expiry in the same audit row, and have the monthly job flag any deletion whose window is about to close while its data transfer is still unconfirmed.

The day-zero block in code:

function containAccess(email) {
  AdminDirectory.Users.signOut(email);                       // kill live sessions
  AdminDirectory.Users.update({ suspended: true }, email);   // block new sign-ins
  (AdminDirectory.Tokens.list(email).items || [])
    .forEach(t => AdminDirectory.Tokens.remove(email, t.clientId));   // 3rd-party apps
  (AdminDirectory.Asps.list(email).items || [])
    .forEach(a => AdminDirectory.Asps.remove(email, a.codeId));       // app passwords
}

The order matters and the reason is documented: suspending an account stops new sign-ins, but Google states plainly that sessions already open are not logged out and that an admin should sign the user out explicitly. So signOut is not decoration on top of suspend — it is the half that actually ends the laptop still open on someone's kitchen table. Tokens and app-specific passwords are a separate surface again: Google does not document suspension as revoking an OAuth grant already issued to a third-party app, so treat them as live until you have removed them yourself. App passwords in particular are the thing nobody remembers exists.

And the transfer, over HTTP because there is no advanced service for it:

function transferDriveAndCalendar(fromEmail, toEmail) {
  const fromId = AdminDirectory.Users.get(fromEmail).id;
  const toId   = AdminDirectory.Users.get(toEmail).id;

  const body = {
    oldOwnerUserId: fromId,
    newOwnerUserId: toId,
    applicationDataTransfers: [
      { applicationId: '55656082996',                        // Drive and Docs
        applicationTransferParams: [
          { key: 'PRIVACY_LEVEL', value: ['PRIVATE', 'SHARED'] }   // BOTH. See below
        ] },
      { applicationId: '435070579839',                       // Calendar
        applicationTransferParams: [
          { key: 'RELEASE_RESOURCES', value: ['TRUE'] }
        ] }
    ]
  };

  const res = UrlFetchApp.fetch(
    'https://admin.googleapis.com/admin/datatransfer/v1/transfers',
    { method: 'post', contentType: 'application/json',
      payload: JSON.stringify(body), muteHttpExceptions: true,
      headers: { Authorization: 'Bearer ' + ScriptApp.getOAuthToken() } });

  if (res.getResponseCode() >= 300) {
    throw new Error(`transfer failed ${res.getResponseCode()}: ${res.getContentText()}`);
  }
  return JSON.parse(res.getContentText()).id;   // poll this before you delete anything
}

The PRIVACY_LEVEL default is SHARED only. Omit the parameter and every file the person never shared with anyone — which is most of a typical knowledge worker's Drive — stays behind and disappears with the account. Pass both values. Then poll the transfer with transfers.get until its status is complete rather than assuming a 200 on insert means the files have moved; deletion must wait for that confirmation, not for a timer.

Then there is the list of things this API will not move for you, which is the part that turns into a phone call three weeks after the account is gone. Write it into the manual half of your checklist:

  • Shared drive files are owned by the organization, not the person, so they need no transfer — but check that the departing user was not the only manager on a shared drive, or nobody can administer it afterwards.
  • Secondary calendars the user created are not covered. Only their primary calendar is, and even there the transfer reaches a narrow slice: future-dated, non-private events that have at least one guest or booked resource. Past events and solo entries do not come across. If the event conditions are not met, the API creates an empty calendar for the new owner rather than failing loudly.
  • Groups the user created stay behind with them as owner. Reassign group ownership before deletion or you inherit a distribution list nobody can administer.
  • Anything living outside Workspace. A personal API key in a third-party service, a domain registrar login, a Cloud project where they are the sole owner — none of that is Admin SDK territory, and it is where the genuinely expensive lock-outs happen.

None of this is a reason to skip the automation. It is a reason the sheet needs a "handled manually" column with a name against each row.

The license audit nobody runs

Because suspended accounts keep consuming seats, the single highest-ROI script in this whole family is the one that reconciles headcount against billing. The Admin console shows you users and it shows you licenses; it does not join them.

function auditLicenses(skuId) {
  const DOMAIN = 'example.com';   // Licensing API wants your primary domain, not 'my_customer'
  const assigned = {};
  let token;
  do {
    const page = AdminLicenseManager.LicenseAssignments
      .listForProductAndSku(PRODUCT_ID, skuId,
                            { customerId: DOMAIN, maxResults: 100, pageToken: token });
    (page.items || []).forEach(a => assigned[a.userId.toLowerCase()] = true);
    token = page.nextPageToken;
  } while (token);

  const waste = [];
  let users;
  let uToken;
  do {
    users = AdminDirectory.Users.list({ customer: 'my_customer', maxResults: 200,
                                        pageToken: uToken, projection: 'basic' });
    (users.users || []).forEach(u => {
      const licensed = assigned[u.primaryEmail.toLowerCase()];
      if (!licensed) return;
      if (u.suspended) waste.push([u.primaryEmail, 'suspended but licensed']);
      else if (u.lastLoginTime === '1970-01-01T00:00:00.000Z')
        waste.push([u.primaryEmail, 'licensed, never signed in']);
    });
    uToken = users.nextPageToken;
  } while (uToken);

  return waste;   // × your per-seat rate = the size of the problem
}

A lastLoginTime of the Unix epoch means the account has never been signed into once — usually a hire who never started, a test account, or a shared mailbox nobody told billing about. Run this monthly on a time-driven trigger, email the diff rather than the full list, and the script pays for itself in one or two findings.

One caveat before you present the number to finance: on the Flexible plan removing a seat lowers next month's bill, but on the Annual plan your committed seat count is fixed until renewal, so reclaiming a license changes nothing immediately. On an annual commitment the audit's value is a renewal negotiation — a dated list of paid-for-but-unused seats to take into that conversation — rather than a saving this month. Say which plan you are on before quoting the figure.

Quotas, limits and the failure modes

Honest constraints, from Google's published limits:

  • Apps Script's six-minute execution ceiling is the one you will hit first on bulk work. A single joiner or leaver is seconds; a 400-row import is not. Chunk the rows, save a cursor, and have the run schedule its own continuation.
  • User creation is capped at 10 per domain per second, and org unit creation at roughly one per second. Do not parallelise your way around these.
  • Per-project rate limit defaults to 2,400 queries per minute per user, raisable in the Cloud console. But a 429 rateLimitExceeded is per Workspace account and cannot be raised — the only answer is backoff.
  • Retry with exponential backoff on 403 userRateLimitExceeded, 403 quotaExceeded and 429: 1s, 2s, 4s, 8s, 16s plus jitter, then give up rather than hammering.
  • Users.get right after Users.insert can lag. Directory propagation is not instantaneous; do not assume a freshly created account is immediately visible to every subsequent call.

If you are also building an internal web UI on top of this so HR can trigger a joiner without touching the Apps Script editor, the session and role-checking patterns in Apps Script authentication without a database are the right foundation — and a lifecycle tool is precisely the kind of app where "who is allowed to press this button" deserves real enforcement.

When you should buy instead of build

The credibility of this whole argument depends on naming where it breaks:

  • You already have a SCIM-capable IdP or HRIS. Use inbound SCIM. It is native, real-time, and included in Business and Enterprise editions.
  • Your lifecycle spans 20 SaaS apps, not just Workspace. Deprovisioning Slack, Jira, Salesforce, Zoom and a dozen others is what the SaaS-management platforms genuinely earn their per-user fee for. Rebuilding twenty vendor integrations is not a fixed-scope project.
  • Over ~500 seats with constant churn. At that volume you want a dedicated identity platform with proper approval workflows and reporting, not a script.
  • Compliance requires vendor attestation. If an auditor needs SOC 2 evidence covering the offboarding control itself, a vendor certificate is easier to hand over than your own code, however well logged.
  • Nobody will maintain it. A script no one understands is a liability. If there is no in-house owner, either buy the tool or buy a maintenance arrangement with whoever builds it.

Where building wins is the middle: 20 to 200 people, Workspace as the centre of gravity, a spreadsheet as the real source of truth, mapping rules that are specific to how your company is organised, and a preference for owning the thing outright. That case is common, and it is not well served — the market sells subscriptions, and the honest alternative is barely documented. Our broader guide to replacing subscription SaaS with Google Workspace maps the same trade-off across CRM, billing and inventory; user lifecycle is simply the version of it that IT owns.

One last thing worth saying plainly: the value here is not the API calls. Anyone can read the Directory API reference. The value is the decision table, the ordering, the dry run and the audit trail — the parts that make an irreversible operation safe to automate. Budget your effort accordingly.

Want this built for your business?

If your joiner-leaver process currently lives in a checklist doc and somebody's memory, the honest first step is a conversation about what your twelve steps actually are — including which of them Google already does for free, because that list is usually longer than people expect.

Two low-pressure ways to start:

  • Book a free 30-minute discovery call and we will walk your current onboarding and offboarding checklist, mark what is native, what is worth scripting, and what should stay manual on purpose.
  • Message us on WhatsApp with your checklist and roughly how many joiners and leavers you handle a year.

If the answer is "use inbound SCIM, you don't need us," you will hear that. Fixed scope, your Google account, your code afterwards — and see what Apps Script development actually costs or how to evaluate an Apps Script developer before you talk to anyone, including us.

Frequently Asked Questions

Should I suspend or delete a Google Workspace account when someone leaves?

Suspend first, always. A suspended account keeps every file, share, calendar and mail archive intact, blocks new sign-ins immediately, and is reversible with a single API call — so it is the correct same-hour action the moment access must stop. One catch worth knowing: suspension does not close sessions that are already open, so sign the user out of all devices in the same step rather than assuming suspension did it. Deleting is the irreversible step and it should be a scheduled decision weeks later, after data has been transferred and someone has confirmed nothing is still depended on. Google keeps a deleted account restorable for 20 days, after which not even Google support can bring it back. Note that Google bills a suspended user at the same rate as an active one, which is why the usual sequence is suspend on day zero, transfer data in the first week, then either delete or move the account to a reduced-cost Archived User license once the retention window you have chosen has passed.

Does the Admin SDK Directory API require a super administrator?

Not for most of it. User creation, updates, suspension, group membership and device actions can all run under a delegated admin with a custom role holding only the specific privileges you need — creating users in the Admin console, for instance, requires the create, update and org-unit-read privileges together. Data transfer is the part people misread: a standalone Drive and Calendar transfer between two live accounts needs the Data Transfer privilege plus Settings and User read-only, not super admin, while transferring data as part of the Admin console's delete flow is genuinely restricted to super admins. Transfer first and delete later and you stay out of super-admin territory. The practical setup is a dedicated, non-human admin account with 2-step verification and a narrow custom role, owning the script. Never run lifecycle automation from the personal super-admin account of whoever happens to be the IT lead — when that person leaves, your automation leaves with them.

Can I create Google Workspace users from a Google Sheet?

Yes, and it is the most common entry point. A sheet holds one row per new hire with name, personal email, department, start date and role; Apps Script reads the rows, maps the role to an org unit, a license SKU and a set of groups, then calls AdminDirectory.Users.insert for each one. The write-back matters as much as the create: store the returned immutable user ID in the sheet, because that ID is what you will need later to restore a deleted account. Google's own bulk CSV upload in the Admin console does the plain create just as well, with a limit of 150,000 users per file — 200 if you are also assigning new licenses in the same upload. Scripting wins when the create is only step one of eight and the mapping rules are yours.

What is the safest way to transfer Drive files when an employee leaves?

Use the Admin SDK Data Transfer API, which reassigns ownership in place rather than copying files, so links, comments and revision history survive. Three applications are supported: Drive and Docs, Calendar and Data Studio. For Drive you pass a PRIVACY_LEVEL parameter of PRIVATE, SHARED, or both — SHARED alone is the default and will quietly skip every unshared file, which is the most common transfer mistake. Know what it will not move: shared drive files are owned by the organization and need no transfer, secondary calendars the user created are not covered, and groups the user created stay behind with them as owner. Even on the primary calendar the transfer only reaches future, non-private events with a guest or a booked resource. Transfer before you delete, confirm the transfer status has completed rather than assuming, and keep the account suspended until then.

How do I find Google Workspace licenses we are paying for but nobody uses?

Join two lists the Admin console does not join for you. Pull every user with the Directory API, including suspended ones, and pull every license assignment for your product and SKU with the Licensing API. Three categories fall out: suspended users still holding a full paid license, active users whose lastLoginTime comes back as the Unix epoch, meaning they have never signed in at all, and licensed assignments pointing at accounts that no longer exist. Multiply each by your per-seat rate for the size of the problem, but check your billing plan before promising a saving: Flexible plan seats drop off next month's invoice, while an Annual plan commitment holds until renewal, so there the list is ammunition for the renewal conversation rather than money back today. Run it monthly on a time-driven trigger and mail yourself the diff rather than the full list, so only changes need attention.

Is Apps Script fast enough to run user lifecycle jobs at scale?

For the transaction volumes a 20-200 person company generates, comfortably. The binding constraints are Apps Script's six-minute execution ceiling and the Admin SDK's own rate limits: user creation cannot exceed 10 per domain per second, org unit creation is capped at roughly one per second, and a 429 rateLimitExceeded is enforced per Workspace account and cannot be raised. So a single joiner or leaver is trivial, while a 400-row bulk import needs chunking with a saved cursor and a follow-up trigger, plus exponential backoff on 403 and 429 responses. If you are provisioning thousands of accounts in one window, a server-side job with a service account is the better tool.

Stay Updated

Get the latest insights on AI, e-commerce, and Magento delivered to your inbox.