Feature: Org Onboarding
Last updated: 2026-08-18
This document is the single source of truth for org onboarding. Requirements and acceptance criteria were consolidated here on 2026-05-04.
Recent (2026-06-02): AM Setup Wizard slice 2 (#1319 β step 2
email_aliaspreview) and slice 3 (#1326 β OSA detail page Workspace section + Suspend/Restore, closes #1322) landed. Invite lifecycle locked in ADR-023 (#1286) β 7-day expiry, token rotation, user-initiated resend.Recent (2026-06-05/06):
- OTP verification now required before profile creation (#1949).
- Optional onboarding steps no longer block activation; step-6 escape hatch added (#1807).
- Waiting room flow rebuilt from first principles (#1904).
- Mic permission flow: user gesture required, mic-only recovery path (#2029); URL-deeplink + mic-required / camera-optional permissions (#2010).
- "Finish without invite" stranded-orgs banner safety net for AMs (#1698).
Overviewβ
Org onboarding is a two-phase process:
- Phase 1 (AM-assisted) β Internal Account Manager sets up the Org in the admin portal
- Phase 2 (Client confirmation) β Client Admin completes setup via a guided wizard at
/onboarding(link delivered via invite email)
The result is an active Org with at least one Specialist assigned, Email channel configured, and a Client Admin who has confirmed the setup.
Actorsβ
| Actor | Role | What They Do |
|---|---|---|
| Account Manager | Internal | Creates Org, assigns Specialists, sends invite |
| Client Admin | Client | Confirms setup (passwordless β no password step), configures channels |
| Super Admin | Internal | Can view/edit any onboarding; can impersonate AM |
Phase 0: Validation Gateβ
Before the invite is sent, the system hard-blocks until all pass:
- All required company fields complete
- AM's own profile complete
As of spec 2026-08-14, a Specialist assignment is no longer required β the client picks their own Specialist during Phase 2 onboarding, so the invite can go out with zero Specialists assigned. (Trial end date is optional, not gate-blocking β see am-setup-wizard.md Β§Step 3.)
Endpoint: GET /am/orgs/:orgId/phase0-status
Route note: AM-facing endpoints use the
/am/orgs/prefix internally (NestJS routing), but the frontend accesses them through/ops/clients/[id]/setup/*routes.
AM Wizard β 3 Stepsβ
Dedicated doc: the end-to-end AM Setup Wizard flow (step-by-step, with the
email_aliaspreview and the OSA Workspace Suspend/Restore surface) now lives in am-setup-wizard.md. The summary below is retained for the onboarding-flow context.2026-08-17: Specialist assignment (formerly Step 1.2) is no longer a wizard step β per the client self-service specialist selection spec (2026-08-14), the client now picks their own Specialist during Phase 2 onboarding instead of the AM pre-assigning one. See am-setup-wizard.md for the current 3-step flow and the AM-preassignment escape hatch (Reassign / Kaito-style launch links).
The AM wizard has 3 steps accessed from /ops/clients/new (step 1) and /ops/clients/[id]/setup/* (steps 2β3).
Phase 1: AM-Assisted Setupβ
Numbering note: the headings below match the 3-step numbering used in am-setup-wizard.md (Company Info β Expert Assignment β Send Invite). Specialist Assignment sits between Step 1 and Step 2 but isn't itself numbered β see below.
Step 1 β Company Informationβ
AM enters on behalf of the client:
Required: company legal name, subdomain (slug), industry, primary corporate email domain(s), admin email address (optional), company website, country/timezone
The AM wizard labels this field "Subdomain" in the UI. The code and DB call it
slug. After the client confirms in the onboardingworkspacestep, the slug is immutable.
Optional: company logo, billing email, company size, secondary email domains
Slug rules:
- Format:
^[a-z0-9][a-z0-9-]{1,61}[a-z0-9]$(3β63 chars) - Validated in real-time via
GET /orgs/slug-check?slug=... - Reserved word list enforced (api, app, www, admin, etc.) β see
config/reserved-slugs.ts - Not reserved until the client confirms in the
workspacestep
Email domain rules:
- Personal domains blocked (gmail, yahoo, etc.) β see
config/blocked-email-domains.ts - Admin email must match an approved corporate domain
- No DNS verification required
Specialist Assignment (optional AM action, not a wizard step)β
As of 2026-08-17 this is no longer numbered Step 1.2 β the client picks their own Specialist during Phase 2 onboarding (see the
specialist_selectionstep below) instead of the AM pre-assigning one. The mechanics below still apply whenever an AM does assign one directly (Reassign on an active client, or a preassignment engagement launch link).
AM selects 1+ Specialists from the platform pool. For each Specialist:
- Confidence field (integer 0β101) is retained for schema/analytics compatibility and defaults to 0. Managed Hermes delivery never consults it.
- Optional: designate a primary Specialist
Specialist constraints:
- Specialist β Org relationship is many-to-many via
OrgSpecialistAssignment - Specialist profile must be complete (name, avatar, skill tags) β the Email field was dropped from the Specialist Create form in #1097 (ADR-0002 Phase 2)
- Per-OSA email alias is auto-generated as
{firstname}.{org-slug}@{HWORK_DOMAIN}at assignment time via theOrganizationsService.assignSpecialisthook (#1089). The alias is stored onOrgSpecialistAssignment.email_alias, not on the catalog Specialist. - The catalog Specialist no longer carries an
emailcolumn. - Only SuperAdmin can create/modify Specialist personas. AMs assign existing ones.
Step 2 β Expert Assignmentβ
Route: /ops/clients/[id]/setup/step-3. AM grants Expert coverage for the Org
through expert_access (org-scope or named-Specialist-scope). Coverage is
informational, not gating (spec 2026-08-14): in the default flow there's no
Specialist to cover yet β the client picks their own in Phase 2 β so this step
is optional. Full detail: am-setup-wizard.md Β§Step 2.
The AM's own profile (name, avatar, email) is not edited as part of this wizard β it's a shared, cross-Org profile maintained elsewhere. Step 3 below just gates Send on it being complete already.
Step 3 β Send Client Invitationβ
After Phase 0 validation passes:
- System sends welcome email to admin email with AM name/avatar, org name, workspace URL, and CTA
- Invitation token expires in 7 days; resending issues a fresh 7-day window
- Trial clock does NOT start until Phase 2 completion
- Org status:
pending_client_confirmation - Stale orgs (90+ days in
pending_client_confirmation) are auto-archived; restorable by Super Admin
Step 3 UX (as of PR #243):
- Send button is enabled once company info, the trial date choice, and the AM profile gate all pass. Specialist assignment is not one of the gates (spec 2026-08-14) β an org with zero Specialists can send. The
trialEndDateSetcheck is satisfied client-side (trial date is set on this very step). - "Copy Onboarding Link" button is available before and after sending β AM can share the link directly without waiting for the email.
- "Complete Setup" CTA appears for orgs already in
pending_client_confirmationstatus (re-entry after first send).
Expert onboarding email templates (#1554 / #1560)β
See also: docs/features/expert-application.md β the full Tavus-driven application β interview β promote flow these emails fire on.
In parallel with client onboarding, Expert onboarding now uses 5 dedicated email templates rendered through Resend (#1554 / #1560). The 5 templates:
| Template key | Trigger | Audience |
|---|---|---|
expert.application_received | New marketing-lead Expert application submitted | Applicant |
expert.interview_scheduled | Interview slot booked via the application flow | Applicant |
expert.application_approved | SuperAdmin promotes the marketing lead to an Expert | New Expert |
expert.application_declined | SuperAdmin declines the marketing lead | Applicant |
expert.welcome | First successful login to /workspace | New Expert |
Templates live under api/src/email/templates/expert/ and are version-pinned;
copy changes go through PR review. AM/client-side onboarding emails are
unaffected β those still use the client templates documented in Step 3 above.
Phase 2: Client Confirmationβ
Client accesses the onboarding wizard via the invite email link (/onboarding route).
Week-1 slim-down (2026-07-20): the wizard shape is now server-owned and per-org.
GET /onboarding/:token/resume-stepreturns the ordered step-id list for the org plus the resume index, and the frontend renders exactly that list (frontend/src/app/onboarding/start/page.tsxno longer hardcodes a sequence). The canonical step ids, in order:
Step id Label Present when workspaceWorkspace setup Always agreementsAgreements onboarding_agreements_requiredflag ON (default ON; org-scoped OFF for internal dogfood orgs β skips the wizard step, thecompleteOnboardingguard, and the re-consent banner)business_contextClient info Always (skippable) β client reviews/corrects the AI-drafted company profile (web research seeded automatically, #6036) specialist_selectionChoose specialist Org has zero active Specialist assignments, or at least one active assignment has metadata.assigned_via === "client_onboarding"(spec 2026-08-14) β this is an "any match" check, not "only": an org with one AM-preassigned Specialist plus one client-picked Specialist still gets this step. Purely AM-preassigned orgs (no client-onboarding-origin assignment at all) skip it.specialist_intakeOnboard your Specialist (Tavus video call) onboarding_video_intake_enabledflag ON (default ON; org-scoped OFF override available for tenants that need the step skipped)doneDone Always Order note (#6036 + spec 2026-08-14):
business_contextruns beforespecialist_selectionandspecialist_intakeβ the client reviews/corrects the company profile and picks their Specialist from it first, so the intake call is never held with a Specialist the client didn't choose.The Business details (legal profile) and Your h.work team steps were removed from the wizard entirely β legal profile collection moves to post-onboarding client settings (the
/:token/legal-profileendpoints remain for that surface).Ops runbook β flag flips:
- To onboard an internal dogfood org without agreements, set an org-scoped
onboarding_agreements_required = falseoverride (Ops β Feature Flags) before sending the invite. The default is ON, so external clients are unaffected.- The shape is re-resolved at
completeOnboarding, so flippingonboarding_agreements_requiredON for an org that is already mid-wizard makes in-flight sessions get a403on their next completion attempt (fail-closed β we never activate a client with a missing acceptance-ledger row). Recovery is a page refresh:/onboarding/startre-fetches the shape and routes the client to the now-present agreements step. Expect this if you flip the flag on a live tenant.
UX Update (PRs #267-271, 2026-05-10): The onboarding wizard was simplified from 6 steps to 3 steps. Steps 2.4 (Set Up Channels) and 2.5 (Optional Steps) were removed as dedicated wizard steps. Channels and Billing are now accessed post-onboarding from
/client/settings/*.
The subsections below walk through each step id from the table above, in order.
workspace β Workspace Setupβ
Single combined step (folds the old separate "verify account" and "review company info" steps into one):
- No separate email-verification step (#3488): clicking the invite link
(emailed only to the admin) already proves email ownership, so the onboarding
session is bootstrapped from the token via
POST /:token/sessionwhen the wizard loads. The earlier emailed-OTP step re-verified the same address through the same channel and was removed as redundant double verification. - Client enters full name and reviews/edits company info. The slug is permanently reserved when this step is submitted, via a pessimistic write lock + server-side re-validation to prevent race conditions.
| Field | Client Can Change? |
|---|---|
| Full name | Yes β required |
| Company name | Yes |
| Slug | Yes (once, here only) β immutable after this step |
| Industry | Yes |
| Timezone | Yes (pre-filled from the browser's own timezone) |
| Website | Yes (optional) |
| Company domain(s) | No β read-only, AM-controlled |
| Admin email | No β read-only. Locked to the address the invite was sent to; the UI shows a lock icon and "can't be changed here." |
Job title and phone are still accepted by POST /:token/profile (SetProfileDto.jobTitle / .phone) but the current step UI doesn't collect them β no step asks for them anymore, so they go unset unless some other caller sends them. Country isn't collected in this step either (it's AM-entered in Phase 1 and not re-editable here).
agreements β Agreementsβ
Modal presents four documents in tabs: Terms of Service, Privacy Policy, Acceptable Use Policy, Data Processing Addendum. Each tab requires scroll-to-bottom before the corresponding accept checkbox is enabled (#1046 / #1048 / #1068).
- All four must be accepted before the wizard advances.
- Acceptance writes to the legal acceptance ledger (org Γ user Γ document version Γ timestamp) via
POST /:token/agreements. - A re-consent banner re-prompts when any document version is bumped post-acceptance.
Cross-reference: frontend/legal/ and the TOS/Privacy/AUP/DPA source documents.
business_context β Client Infoβ
GET /:token/company-researchruns public web research on the company and returns an AI-drafted profile (with citations) for the client to review (#6036).- Client accepts it (
POST /:token/company-researchβ saves to the KB) or rejects/skips it (POST /:token/company-research/rejectβ no KB write, but recorded so the intake-call script knows the profile wasn't confirmed). - Client can also optionally upload supporting documents, URLs, or free-text prompts directly (
POST /:token/business-contextβ up to 10 files, 10 MB/file cap, deliberately tighter than the KB's normal 50 MB limit). - Fully skippable either way; skipping is stamped onto
org.metadata.business_context_skippedwith a timestamp.
specialist_selection β Choose Specialistβ
Client browses an ops-curated, client-selectable catalog β GET /:token/catalog for the initial "top picks" load, GET /:token/specialists/catalog?q=... for the "Show all specialists" search/browse expansion β and picks one Specialist (POST /:token/specialist, same provisioning as an AM assignment, minus billing). GET /:token/specialist-selection-status guards against re-picking if a resume bounces the client back through business_context (#6118). See the visibility rule in the step table above β this step doesn't show for every org.
specialist_intake β Onboard Your Specialist (video call)β
In-browser video call (Daily-embedded) to meet the chosen Specialist. Client can start the call immediately or defer it to schedule for later.
Read-only Specialist info (name, avatar, skill tags, bio, per-OSA contact alias {firstname}.{org-slug}@{HWORK_DOMAIN}) is shown alongside; the client does not see the Specialist's composition (AI Agent + Experts behind the persona). To request a different Specialist after onboarding: Settings (AM fulfills the request).
Two now-orphaned endpoints from an earlier "Meet Your Specialist" design:
GET /:token/specialistandGET /:token/specialists(#887) are still served by the backend but nothing in the current wizard calls either one β see Dead/legacy endpoints below.
done β Onboarding Completeβ
- Org status:
pending_client_confirmationβactive - Trial clock starts (
org.trialStartedAt = now()) - Client Admin redirected to
/client/chat - AM receives email notification
- Welcome summary email sent to Client Admin (Specialist contact info, workspace URL, next steps)
- Onboarding wizard link becomes invalid (token
status = 'accepted')
Post-Onboarding Configuration: Channels (Slack, WhatsApp, Email settings) and Billing are configured from
/client/settings/*after onboarding completion. These were previously Steps 2.4 and 2.5 of the wizard but were moved to post-onboarding to reduce friction.
API Endpointsβ
All onboarding endpoints are prefixed with /onboarding/:token/. There is no
set-credentials / password endpoint β onboarding is fully passwordless (see
workspace above); an earlier version of this doc listed one that never
shipped this way.
| Endpoint | Description |
|---|---|
POST /:token/session | Bootstrap session from token (#3488) β session JWT |
POST /:token/verify-email | Send OTP to admin email (legacy, superseded by /session) |
POST /:token/confirm-otp | Verify OTP β session JWT (legacy, superseded by /session) |
POST /:token/profile | Save full name (+ optional job title, phone β unused by the current UI) |
GET /:token/org | Org details for the wizard |
POST /:token/confirm-slug | Finalize and reserve the slug |
GET /:token/agreements/versions | Current required version per document β the wizard fetches this instead of a hardcoded version, so a version bump on any one document doesn't break the whole step |
POST /:token/agreements | Record acceptance of all required legal agreements. The required version for each kind is resolved server-side, never taken from the request body |
POST /:token/business-context | Upload supporting files/URLs/prompts for the business_context step |
GET /:token/company-research | Run web research, return an AI-drafted company profile |
POST /:token/company-research | Save the client-approved profile to the KB |
POST /:token/company-research/reject | Record that the client rejected/skipped the researched profile |
GET /:token/catalog | Client-selectable Specialist catalog β "top picks" |
GET /:token/specialists/catalog?q= | Same catalog β search/browse ("Show all specialists") |
POST /:token/specialist | Client picks their Specialist |
GET /:token/specialist-selection-status | Whether the client already picked one (resume guard, #6118) |
GET /:token/resume-step | Server-owned wizard shape: ordered step ids + resume index |
POST /:token/complete | Finalize onboarding |
POST /:token/legal-profile | Save legal profile (post-onboarding Settings surface, not a wizard step) |
GET /:token/legal-profile | Read the saved legal profile |
Dead/legacy endpointsβ
Still served by the backend, but nothing in the current wizard calls them β left over from earlier wizard shapes:
| Endpoint | Origin | Superseded by |
|---|---|---|
GET /:token/specialist | Original single-Specialist card | specialist_intake's own display + GET /:token/catalog |
GET /:token/specialists | #887 "Your h.work team" step (removed in the week-1 slim-down) | same |
POST /:token/invite-members | Old "invite your team" wizard step (removed 2026-05-10, PRs #267-271) | post-onboarding /client/settings/* |
GET /:token/whitelist | Old in-wizard whitelist management | /am/orgs/:orgId/email-whitelist and /client/orgs/:orgId/email-whitelist |
POST /:token/whitelist | β³ | β³ |
DELETE /:token/whitelist/:id | β³ | β³ |
AM-side endpoints are under /am/orgs/ (NestJS routing) β see docs/api/reference.md.
Status After Audit (2026-06-04)β
The 2026-04-30 snapshot table previously shown here is superseded by the
weekly audit reports under docs/audits/. Open onboarding-area
items as of 2026-06-04: