Runbook: Workspace Provisioning Failure
P1. Each OSA (Org Γ Specialist Assignment) needs a Gsuite user + email_alias to send/receive mail. A stuck provisioning job means an AM clicked "Assign" but the workspace never came online β the Specialist has no mailbox and inbound email bounces. See
OWNERS.mdfor on-call contact.
This runbook covers workspace-provisioning BullMQ jobs that fail repeatedly (after the 3 automatic retries) when assigning a Specialist to an Org. Implementation: api/src/workspace-lifecycle/workspace-provisioning.processor.ts. Architecture: post-#1722 single-SA DWD model β see channels.md Β§System 4 for the current Specialist-email shape.
First, rule out disabled-mode. If jobs are completing with a
skipped: gsuite disabledlog (not failing), Gsuite isn't configured in this environment β that's expected behavior, not a failure. See gsuite-disabled-mode.md. This runbook is only for jobs that genuinely fail after retries.
Symptomsβ
- AM Setup Wizard step 2 shows the assignment but the OSA detail page shows
email_aliaspopulated,gsuite_user_id = NULL, andgsuite_provisioned_at = NULLβ the alias was computed but the actual Workspace user was never created. - Sentry
[workspace-provisioning-processor]onFailedevents firing forcreate-gsuite-userjobs. - BullMQ failed-jobs count
> 0for theworkspace-provisioningqueue (visible at/health/queues). - AM notification "Workspace provisioning failed for {Specialist} @ {Org} β please retry." (sent automatically by the processor's
onFailedhandler). - Inbound email bounces for the new
email_aliasβ Gsuite Admin SDK returns "user not found" because the user was never created.
Step 1 β Triage (within 15 min)β
1a. Identify the failed jobβ
SELECT id, name, data, failed_reason, attempts_made, finished_on
FROM bullmq_failed_jobs -- or via BullMQ UI / Arena
WHERE queue_name = 'workspace-provisioning'
AND finished_on > NOW() - INTERVAL '24 hours'
ORDER BY finished_on DESC;
Pull the OSA id from the job data ({ osaId: "...", orgId: "...", specialistId: "..." }).
1b. Classify the failure modeβ
failed_reason typically falls into one of:
| Pattern | Root cause | Next step |
|---|---|---|
unauthorized_client / invalid_grant | Service account creds missing / wrong / Domain-Wide Delegation not granted | Step 2a |
Quota exceeded / User license cap exceeded | Gsuite tenant out of seats | gsuite-license-cap.md |
entity already exists / 409 | email_alias collides with an existing user (e.g., manual creation) | Step 2b |
network timeout / ECONNRESET | Transient | Step 3 (just retry) |
Could not decrypt OAuth refresh token | MASTER_ENCRYPTION_KEY rotation broke an old row | oauth-token-refresh.md |
Service account key invalid / "Bad JWT signature" | GSUITE_SERVICE_ACCOUNT_KEY_JSON env var corrupted or expired | Step 2a |
If the failure mode doesn't match any of the above, capture the full stack from Sentry and escalate (see Step 5).
Step 2 β Fix the root causeβ
2a. Service account / DWD brokenβ
-
Verify
GSUITE_SERVICE_ACCOUNT_KEY_JSONenv is set and parses as valid JSON (no truncation in Secrets Manager). (Legacy alias:GOOGLE_SERVICE_ACCOUNT_JSONβ deprecated but still supported; canonical name takes precedence.) -
Verify Domain-Wide Delegation in Google Admin β Security β API Controls β Domain-wide Delegation. Client ID must match the service account's
client_id. Scopes must include the fullWORKSPACE_DWD_SCOPESset fromapi/src/workspace-lifecycle/workspace-lifecycle.constants.tsβ minimum-necessary 9 scopes (broader subsumes narrower where Google docs the relationship; Meet's two are independent):https://www.googleapis.com/auth/admin.directory.userhttps://www.googleapis.com/auth/admin.directory.user.securityhttps://www.googleapis.com/auth/gmail.modifyhttps://www.googleapis.com/auth/gmail.settings.basichttps://www.googleapis.com/auth/calendarhttps://www.googleapis.com/auth/drivehttps://www.googleapis.com/auth/contactshttps://www.googleapis.com/auth/meetings.space.createdhttps://www.googleapis.com/auth/meetings.space.readonly
DWD matches scopes by exact string. If the API returns
unauthorized_client, the most common cause is a scope present in code but missing in the Admin Console grant (or vice versa). Diff the two lists before deeper debugging. -
Re-confirm
GSUITE_ADMIN_USER_EMAILis the impersonated super-admin in the tenant. (Legacy alias:GOOGLE_ADMIN_EMAILβ deprecated but still supported; canonical name takes precedence.)
2b. Provisioning gated off for this orgβ
On dev/staging, provisioning is disabled by default unless the org has an explicit override. Check:
SELECT id, slug, gsuite_provisioning_enabled
FROM organizations
WHERE id = '<org_id>';
gsuite_provisioning_enabled | APP_ENV | Result |
|---|---|---|
NULL | production | Provisioned |
NULL | development / staging | Skipped |
true | any | Provisioned |
false | any | Skipped |
Remediate: If the org should provision on dev/staging:
curl -X PATCH -H "Authorization: Bearer <superadmin-jwt>" \
-H "Content-Type: application/json" \
-d '{"enabled": true}' \
https://api.<env>.h.work/admin/orgs/<org_id>/gsuite-provisioning
Then retry the assignment or trigger the provisioning hook manually.
2c. email_alias collisionβ
- Query the conflict:
curl -X GET "https://admin.googleapis.com/admin/directory/v1/users/<email_alias>" -H "Authorization: Bearer <token>" - If the user truly exists from manual creation, decide:
- If it's a stale manual test user β delete it in Google Admin, then retry the job.
- If it's a real prior assignment β file a P0 isolation bug; the OSAβemail_alias rule was violated.
Step 3 β Retry the jobβ
Once the root cause is fixed:
# Option A: re-enqueue via API (preferred β preserves OSA linkage):
curl -X POST -H "Authorization: Bearer <admin-jwt>" \
https://api.<env>.h.work/admin/workspace-provisioning/retry/<osa_id>
# Option B: manual BullMQ retry from the UI / Arena / scripts:
psql -c "SELECT bullmq_retry_job('<job_id>');"
Watch the next job's outcome in Sentry / /health/queues. Successful retry β gsuite_user_id + gsuite_provisioned_at populate on the org_specialist_assignments row.
Step 4 β Rollback (if retry still fails after Step 2)β
If the AM needs to un-stick the Setup Wizard while you debug:
- Mark the OSA inactive without deleting it:
UPDATE org_specialist_assignments
SET gsuite_archived_at = NOW(),
metadata = jsonb_set(coalesce(metadata, '{}'::jsonb), '{provisioning_failed}', 'true'::jsonb)
WHERE id = '<osa_id>'; - Notify the AM the assignment is on hold pending ops review.
- Continue debugging the failure in Sentry / Step 2.
Once fixed, clear the gsuite_archived_at and re-trigger the BullMQ job per Step 3.
Step 5 β Escalateβ
If after Step 2 + Step 3 the same OSA still fails twice in a row:
- Page Platform Lead (Eusden) + escalation channel (
#ops-incidents). - Capture:
osa_id,failed_reason, Sentry event link, full Sentry stack, BullMQ job payload. - Document timeline in the incident channel.
For tenant-wide Gsuite outages (multiple OSAs failing simultaneously), check Google Workspace Status Dashboard before assuming our bug.
Relatedβ
- oauth-token-refresh.md β when the failure is OAuth-decrypt
- gsuite-license-cap.md β when the failure is seat cap
- gsuite-tenant-suspended.md β when ALL provisioning fails simultaneously
- channels.md Β§System 4 β current Specialist-email schema + lifecycle