Skip to main content

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

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 — Specialist assignmentPick 1+ Specialists, set confidence threshold + optional rate override; see the auto-generated email_alias preview/ops/clients/[id]/setup/step-2
3 — Expert assignmentGrant Expert coverage (org-scope or per-Specialist); coverage validation requires ≥1 Expert per Specialist/ops/clients/[id]/setup/step-3
4 — Client inviteSet 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

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.

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: 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 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 legacy org_experts table.
  • 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).
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/:assignmentIdgetSpecialistAssignmentDetail, 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-statusOrganizationsService.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.

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 (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

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.