Skip to main content

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_alias preview), 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.)

StepWhat the AM doesRoute
1 β€” Company infoLegal 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 assignmentGrant 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 inviteSet 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​

ActorRoleWhat they do here
Account ManagerInternalDrives all 4 steps; assigns existing Specialists and Experts
Super AdminInternalCan 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: null means "use catalog rate". Overrides are bounded [1, MAX_OVERRIDE_RATE] where MAX_OVERRIDE_RATE = 99999 (kept in sync with api/.../organizations.dto.ts, #2312) and require an overrideReason.
  • 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) in frontend/src/lib/email-alias.ts, which approximates the backend format (lowercase, strip to [a-z0-9-]). It returns null if 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-Paul previews as jean-paul.… but is actually stored as jeanpaul.…. 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_DOMAIN resolves per-env via NEXT_PUBLIC_HWORK_DOMAIN (dev h852.work, staging h853.work, prod h.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 BadRequestException if firstName normalizes 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 no email column since #1097 / ADR-0002 Phase 2). A Specialist serving multiple clients gets distinct addresses (amy.acme@h.work vs amy.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.

ActionEndpointFrontend helper
AssignPOST /am/orgs/:orgId/specialistassignAmSpecialist
Update (rate/reason)PATCH /am/orgs/:orgId/specialist-assignments/:assignmentIdupdateAmSpecialistAssignment
UnassignDELETE /am/orgs/:orgId/specialist/:specialistIdunassignAmSpecialist

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 legacy org_experts table.
  • 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).
ActionEndpointFrontend helper
List ExpertsGET /am/experts/listlistAmExperts
Current grantsGET org expert mappingsgetAmOrgExpertMappings
Coverage summaryGET specialist coveragegetAmOrgSpecialistCoverage
GrantPOST grant ({ expertId, scope, specialistIds? })grantAmOrgExpert
RemoveDELETE grantremoveAmOrgExpert

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 provisioned until 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:

TimestampMeaning
gsuiteProvisionedAtMailbox created in Google Workspace
gsuiteSuspendedAtMailbox paused β€” inbound + outbound mail stops
gsuiteArchivedAtMailbox permanently deleted (terminal)

Actions (each guarded by the timestamps above):

ButtonShown whenEndpointService method
Provision nowno Gsuite user, not archivedPOST .../specialist-assignments/:assignmentId/provisionprovisionSpecialistAssignment
Suspendhas Gsuite user, not suspended, not archivedPOST .../specialist-assignments/:assignmentId/suspendsuspendSpecialistAssignment
Restoresuspended, not archivedPOST .../specialist-assignments/:assignmentId/restorerestoreSpecialistAssignment
  • 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_id appears 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.

See onboarding.md Β§Phase 0.

  • 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 to pending_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 in pending_client_confirmation until 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​

ConcernPath
Wizard pagesfrontend/src/app/ops/clients/new/page.tsx (create) + frontend/src/app/ops/clients/[id]/setup/step-{1..4}/page.tsx
OSA detail / Workspace sectionfrontend/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 endpointsapi/src/organizations/organizations.controller.ts
Org lifecycle serviceapi/src/organizations/organizations.service.ts
Phase 0 gateOrganizationsService.getPhase0Status / checkPhase0Gate in api/src/organizations/organizations.service.ts
API helpersfrontend/src/lib/api.ts
  • 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.