Feature: AM Setup Wizard
Last updated: 2026-08-17
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).2026-08-17 β Specialist assignment dropped as a wizard step. Per the client self-service specialist selection spec (2026-08-14), the client now picks their own Specialist during onboarding instead of the AM pre-assigning one. The wizard is down to 3 steps: Company Info β Assign Experts β Send Invite. The old Step 2 page (
/ops/clients/[id]/setup/step-2) still exists and still works β it's just no longer part of the linear flow. It's reachable two ways: (1) the Reassign action on an active client's Specialists tab, and (2) a preassignment engagement launch link (/ops/clients/new?specialist=<slug>, e.g. Kaito's?specialist=amyβ see kaito-client-flow.md). Both paths are unaffected β an AM-preassigned Org is recognized by the client onboarding wizard, which auto-skips its own selection step for that Org.
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, optionally grants Expert coverage, and sends (or copies) the client invite. The client picks their own Specialist during Phase 2 onboarding (see below) β the AM no longer has to. The result is an Org in pending_client_confirmation status, ready for the client to confirm in Phase 2.
The wizard is 3 steps. (It was 4 through 2026-08-17 β see the note above. #1562 is the historical precedent for keeping the step-count test fixture in sync; any future step-count change must update it 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 β Expert assignment | Grant Expert coverage (org-scope or per-Specialist) β optional; a Specialist without an Expert is valid, since no Specialist exists yet in the default flow | /ops/clients/[id]/setup/step-3 |
| 3 β Client invite | Set trial end date, send invite or finish without invite (copy onboarding link) | /ops/clients/[id]/setup/step-4 |
The step-2/step-3/step-4 route names are unchanged from the 4-step era (only the wizard's own progress bar/labels were renumbered) β see the note above for why step-2 isn't in the table.
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.
Specialist Assignment (no longer a wizard step) + email_alias Previewβ
Route: /ops/clients/[id]/setup/step-2 (frontend/src/app/ops/clients/[id]/setup/step-2/page.tsx). As of 2026-08-17 this page is not part of the linear 3-step wizard above β the client picks their own Specialist during Phase 2 onboarding instead. The page still exists, fully functional, for the Reassign and preassignment-engagement paths described in the banner at the top of this doc.
The AM selects one or more Specialists. Assignment establishes the Specialist identity and its published session assets; it does not establish a confidence threshold, review gate, or per-turn tool grant. Any legacy threshold field that still exists during database/API migration is inert and must not affect reply delivery.
For each assignment:
- 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 2 β 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, current and future) or specialist-scoped (named Specialist IDs).
- Coverage is informational, not gating (spec 2026-08-14): in the default flow there's no Specialist to cover yet β the client picks one during onboarding β so this step is optional. An org-scope grant here still pays off: it covers whatever the client ends up picking, with nothing further to do. Coverage only becomes actionable once a Specialist exists (AM-preassigned, or after the client's pick).
| 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 3 β 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.
- AM profile has a name (
displayName/name).
As of spec 2026-08-14, the gate no longer requires a Specialist assignment or Expert coverage β the invite can go out with zero Specialists. The Step 3 checklist on this page still surfaces Specialist/Expert state informationally (specialistAssigned, expertMapped on the status response) for AM-preassigned Orgs, but neither blocks Send. See #538 for the (now-relaxed) original Expert-coverage requirement.
- 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 (steps 1-2)
pending_client_confirmation β invite sent, client not yet activated (end of wizard)
active β client completed Phase 2
pending_specialist_assignment remains a defined OrgStatus value (api/src/auth/auth.entities.ts) for backward compatibility, but nothing sets it anymore β it predates the Phase-0 gate relaxation, from when a Specialist was a required step before the invite could be sent.
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.