Feature: AM Setup Wizard
Last updated: 2026-06-25
Dedicated doc for the internal AM-facing setup wizard. This is Phase 1 of org onboarding — the AM-assisted half. The client-facing Phase 2 (confirmation wizard at
/onboarding) and the validation gate that joins the two phases are documented in onboarding.md; read both together.Shipped across three slices: slice 1 (#1097 — base 4-step wizard), slice 2 (#1319 — step 2
email_aliaspreview), slice 3 (#1326 — OSA detail page Workspace section + Suspend/Restore, closes #1322).
Overview
The AM Setup Wizard is how an internal Account Manager (AM) stands up a client Org before the client ever logs in. The AM enters company info, assigns one or more Specialists, grants Expert coverage, and finally sends (or copies) the client invite. The result is an Org in pending_client_confirmation status, ready for the client to confirm in Phase 2.
The wizard is 4 steps, locked in by #1562 (a stale test fixture previously asserted 5 steps). Any future step-count change must update that fixture in the same PR.
| Step | What the AM does | Route |
|---|---|---|
| 1 — Company info | Legal name, subdomain (slug), industry, corporate email domain(s), admin email, website, country/timezone | /ops/clients/new (create); /ops/clients/[id]/setup/step-1 (re-edit) |
| 2 — Specialist assignment | Pick 1+ Specialists, set confidence threshold + optional rate override; see the auto-generated email_alias preview | /ops/clients/[id]/setup/step-2 |
| 3 — Expert assignment | Grant Expert coverage (org-scope or per-Specialist); coverage validation requires ≥1 Expert per Specialist | /ops/clients/[id]/setup/step-3 |
| 4 — Client invite | Set trial end date, send invite or finish without invite (copy onboarding link) | /ops/clients/[id]/setup/step-4 |
Route note: the AM-facing REST endpoints are prefixed
/am/orgs/:orgId/*internally (NestJS). The frontend surfaces them through/ops/clients/[id]/setup/*pages. Don't conflate the two prefixes.
Actors
| Actor | Role | What they do here |
|---|---|---|
| Account Manager | Internal | Drives all 4 steps; assigns existing Specialists and Experts |
| Super Admin | Internal | Can drive any AM wizard; only role that can create/modify Specialist personas |
AMs assign Specialists from the platform pool — they cannot create new personas.
Step 1 — Company Information
Route: /ops/clients/new (frontend/src/app/ops/clients/new/page.tsx) for the initial org creation. Once the org exists, re-editing step 1 happens at /ops/clients/[id]/setup/step-1 (frontend/src/app/ops/clients/[id]/setup/step-1/page.tsx).
Required: company legal name, subdomain (slug), industry, primary corporate email domain(s), admin email, website, country/timezone.
Optional: logo, billing email, company size, secondary email domains (max 10 corporate domains).
- The UI labels the field "Subdomain"; code and DB call it
slug. Slug is validated in real time and only permanently reserved when the client confirms in Phase 2 (onboarding Step 2.2). - Personal email domains are blocked (gmail, yahoo, outlook, icloud, proton, etc.).
- Industry is a fixed 13-option list; timezone is the IANA TZ list (
COMMON_TIMEZONES).
See onboarding.md §Step 1.1 for the full slug/domain rule set.
Step 2 — Specialist Assignment + email_alias Preview
Route: /ops/clients/[id]/setup/step-2 (frontend/src/app/ops/clients/[id]/setup/step-2/page.tsx).
The AM selects 1+ Specialists. For each one:
- Confidence threshold — integer
0–101; default 101 = always-HITL / never autoreply (root contract). - Monthly rate — optional override of the catalog rate.
monthlyRate: nullmeans "use catalog rate". Overrides are bounded[1, MAX_OVERRIDE_RATE]whereMAX_OVERRIDE_RATE = 99999(kept in sync withapi/.../organizations.dto.ts, #2312) and require anoverrideReason. - First assigned Specialist is auto-promoted to primary (single-primary invariant).
The Specialist ↔ Org relationship is the many-to-many OSA (org_specialist_assignments / OrgSpecialistAssignment).
email_alias preview (slice 2 — #1319)
Each Specialist card shows a live preview of the workspace email that will be created on assignment:
{firstname}.{org_slug}@{HWORK_DOMAIN}
- The preview is client-side only — no API round-trip. It's computed by
computeEmailAliasPreview(firstName, orgSlug)infrontend/src/lib/email-alias.ts, which approximates the backend format (lowercase, strip to[a-z0-9-]). It returnsnullif inputs are missing, so the UI hides the line rather than rendering a malformed address. - The preview is approximate, not byte-for-byte parity with the stored alias. The backend
generateEmailAlias(below) NFKD-normalizes and reduces the first name to[a-z]only, while the preview keeps digits and hyphens. So e.g.Jean-Paulpreviews asjean-paul.…but is actually stored asjeanpaul.…. Treat the preview as a sanity-check aid, not a contract — depend on the server value (OrgSpecialistAssignment.email_alias) for anything authoritative. - The card tooltips it as "Auto-generated workspace email — created on assignment."
HWORK_DOMAINresolves per-env viaNEXT_PUBLIC_HWORK_DOMAIN(devh852.work, stagingh853.work, prodh.work).
The authoritative alias is generated server-side at assignment time by generateEmailAlias(firstName, orgSlug, domain) (api/src/common/email-alias-generator.ts):
- NFKD-normalizes
firstName, strips diacritics, lowercases, keeps[a-z]only. - Enforces RFC 5321 limits (local-part ≤ 64 chars; org_slug ≤ 30 chars).
- Throws
BadRequestExceptioniffirstNamenormalizes to empty or the slug is malformed. - The result is stored on
OrgSpecialistAssignment.email_alias(per-OSA, not on the catalog Specialist — the catalog Specialist carries noemailcolumn since #1097 / ADR-0002 Phase 2). A Specialist serving multiple clients gets distinct addresses (amy.acme@h.workvsamy.kaito@h.work).
Reassign mode
If the Org is already active, step 2 acts as a reassignment surface: on confirm it returns the AM to the Specialists tab rather than advancing to step 3.
Assignment-related endpoints
| Action | Endpoint | Frontend helper |
|---|---|---|
| Assign | POST /am/orgs/:orgId/specialist | assignAmSpecialist |
| Update (rate/reason) | PATCH /am/orgs/:orgId/specialist-assignments/:assignmentId | updateAmSpecialistAssignment |
| Unassign | DELETE /am/orgs/:orgId/specialist/:specialistId | unassignAmSpecialist |
Assignment runs in two phases per save: Phase 1 assigns/updates (POST new, PATCH changed, skip unchanged); Phase 2 unassigns deselected entries only if Phase 1 succeeded (#2161).
Step 3 — Expert Assignment / OSA Workspace
Route: /ops/clients/[id]/setup/step-3 (frontend/src/app/ops/clients/[id]/setup/step-3/page.tsx).
The AM grants Expert coverage for the Org. Experts are the sole human reviewers behind the Specialist personas.
- Grants are made through
expert_access(dual-scope, ADR-007) — never a per-surface ACL, never the legacyorg_expertstable. - A grant is either org-scope (covers all Specialists) or specialist-scoped (named Specialist IDs).
- Coverage validation: before Continue, every assigned Specialist must be covered by ≥1 Expert (org-scope or specialist-specific).
| Action | Endpoint | Frontend helper |
|---|---|---|
| List Experts | GET /am/experts/list | listAmExperts |
| Current grants | GET org expert mappings | getAmOrgExpertMappings |
| Coverage summary | GET specialist coverage | getAmOrgSpecialistCoverage |
| Grant | POST grant ({ expertId, scope, specialistIds? }) | grantAmOrgExpert |
| Remove | DELETE grant | removeAmOrgExpert |
OSA detail page — Workspace section + Suspend/Restore (slice 3 — #1326 / closes #1322)
Each assignment has a detail page at /ops/clients/[id]/specialists/[assignmentId] (frontend/src/app/ops/clients/[id]/specialists/[assignmentId]/page.tsx). It is the AM/SuperAdmin surface for the Specialist's Google Workspace mailbox lifecycle.
The Workspace section shows:
email_alias(read-only)- Gsuite user ID (
Not provisioneduntil the mailbox job runs) - Provisioned at / Suspended at / Archived at (ISO timestamps)
- A status badge derived from the timestamps: Archived > Suspended > Provisioned.
OSA mailbox lifecycle is tracked by timestamps, not an enum:
| Timestamp | Meaning |
|---|---|
gsuiteProvisionedAt | Mailbox created in Google Workspace |
gsuiteSuspendedAt | Mailbox paused — inbound + outbound mail stops |
gsuiteArchivedAt | Mailbox permanently deleted (terminal) |
Actions (each guarded by the timestamps above):
| Button | Shown when | Endpoint | Service method |
|---|---|---|---|
| Provision now | no Gsuite user, not archived | POST .../specialist-assignments/:assignmentId/provision | provisionSpecialistAssignment |
| Suspend | has Gsuite user, not suspended, not archived | POST .../specialist-assignments/:assignmentId/suspend | suspendSpecialistAssignment |
| Restore | suspended, not archived | POST .../specialist-assignments/:assignmentId/restore | restoreSpecialistAssignment |
- Suspend confirms with "Inbound + outbound mail will stop until you Restore." and refuses if there's no Gsuite user, the OSA is already suspended, or it's archived. Sets
gsuiteSuspendedAt. - Restore (#1322) is the exact inverse — clears
gsuiteSuspendedAt, re-enables mail. - Provision now kicks an async background job;
gsuite_user_idappears once the job runs (~30s). It honours an explicit per-org disable but otherwise bypasses the env-level provisioning gate.
Read endpoint: GET /am/orgs/:orgId/specialist-assignments/:assignmentId → getSpecialistAssignmentDetail, returning the OSA detail DTO with Workspace metadata joined to Specialist fields. Frontend helpers: getOsaDetail, suspendOsa, restoreOsa, provisionOsa (frontend/src/lib/api.ts).
Controller for all of the above: api/src/organizations/organizations.controller.ts; service: api/src/organizations/organizations.service.ts.
Step 4 — Client Invitation
Route: /ops/clients/[id]/setup/step-4 (frontend/src/app/ops/clients/[id]/setup/step-4/page.tsx).
Before the invite can be sent, the Phase 0 validation gate must pass. The status endpoint is GET /am/orgs/:orgId/phase0-status → OrganizationsService.getPhase0Status; the actual send/finish paths re-check OrganizationsService.checkPhase0Gate (both in api/src/organizations/organizations.service.ts). The gate requires:
- Required company fields: name, slug, industry, ≥1 corporate domain, admin/invite email, timezone.
- ≥1 Specialist assigned, and the assigned Specialist has a name (
firstName/name). Avatar, skill tags, and email are not gate-blocking — catalog-derived Specialists don't carry those, and channel email is provisioned downstream. - AM profile has a name (
displayName/name). - Expert coverage (#538): every assigned Specialist covered by an org-scope or specialist-scope Expert grant.
- Trial end date is optional. Step 4 allows
trialMode === "none"— the org simply has no trial expiry. This is independent of how the org is activated (see the two paths below): a no-trial org is not auto-active just because the date is omitted. When a date is set, it must be strictly in the future. Either way, if the org is activated via the invite path the trial clock does not start here — it starts when the client completes Phase 2. - Send invite (
sendOrgInvite(orgId, trialEndDate?, startDate?)) emails the admin a 7-day-expiry invite (ADR-023) and moves the Org topending_client_confirmation. - Copy Onboarding Link is available before and after sending — the AM can share the link directly without waiting on email.
- Finish without invite (
finishOrgSetup) activates the org immediately (status →active) without sending the email or waiting on Phase 2 — the "stranded orgs" safety-net AMs use, surfaced by the banner from #1698. This is the only Phase 1 path that activates an org directly; the Send invite path always leaves it inpending_client_confirmationuntil the client completes Phase 2.
Org status transitions
The AM wizard walks an Org through the early OrgStatus values (api/src/auth/auth.entities.ts):
pending_am_setup → AM creating the org (step 1)
pending_specialist_assignment → company info done, no Specialists yet
pending_client_confirmation → invite sent, client not yet activated (end of wizard)
active → client completed Phase 2
trial_ending, suspended, and deactivated are later lifecycle states, not produced by the wizard.
Source map
| Concern | Path |
|---|---|
| Wizard pages | frontend/src/app/ops/clients/new/page.tsx (create) + frontend/src/app/ops/clients/[id]/setup/step-{1..4}/page.tsx |
| OSA detail / Workspace section | frontend/src/app/ops/clients/[id]/specialists/[assignmentId]/page.tsx |
| Email alias preview (FE) | frontend/src/lib/email-alias.ts |
| Email alias generator (BE) | api/src/common/email-alias-generator.ts |
| AM endpoints | api/src/organizations/organizations.controller.ts |
| Org lifecycle service | api/src/organizations/organizations.service.ts |
| Phase 0 gate | OrganizationsService.getPhase0Status / checkPhase0Gate in api/src/organizations/organizations.service.ts |
| API helpers | frontend/src/lib/api.ts |
Related
- onboarding.md — Phase 0 gate, Phase 2 client confirmation wizard, invite lifecycle.
- ADR-007 — Expert access dual-scope.
- ADR-023 — invite token lifecycle (7-day expiry, rotation, resend).
- roles-and-permissions.md — AM / SuperAdmin / Expert role boundaries.