_private/qwestly-hire-docs/features/hire-onboarding.md
Table of Contents
Hire onboarding (first-job wizard)
This document describes how the hire onboarding feature works today in qwestly-hire: routes, gates, steps, APIs, UI shell, and debugging. For original initiative intent and historical decisions, see docs/plan/simplify-hm-onboarding.md.
Purpose
Guide a hiring manager from company assignment through first job draft, optional HM notes for matching context, then review and publish, then mark hire onboarding complete so the user can use the main app (/). The wizard lives under /onboarding (not /hire/onboarding).
Completion gate
Hire onboarding is complete when the MongoDB user has onboarding_completed_at set (non-null). That is the primary server-side signal.
- JWT / site token exposes
hire_onboarding_complete, derived from that date (and related auth refresh logic insrc/services/auth0.ts). has_completed_signupis not used for this gate anymore (removed from the hire completion path; legacy DB cleanup may still use the migration scriptscripts/migrate-has-completed-signup.tswhere applicable).
Mid-wizard state may include User.onboarding_progress: current_step is one of company | roles | job | hm-notes | review (see HIRE_ONBOARDING_STEPS in src/lib/hire-onboarding/state.ts), optional completed_steps, and optional draft_job_id (Mongo job id used to build resume URLs with ?jobId= for hm-notes and review).
Wizard shell (layout & step indicator)
src/app/onboarding/layout.tsxwraps all steps in aContainer(max-w-2xl, centered,min-w-0 overflow-x-hidden, vertical paddingpy-6 sm:py-8 lg:py-12, aligned with candidate onboarding step shell rhythm) and rendersHireOnboardingProgressabove page content (src/app/onboarding/_components/hire-onboarding-progress.tsx).- The progress UI shows three labeled pillars (Company โ Job โ Review). The pathname still includes
/onboarding/rolesand/onboarding/hm-notes; those URLs highlight the Job pillar (same visual โtrackโ as the job stepโconnector fill uses the same rules asrolesforhm-notes). Connectors use theme colors (bg-primary/bg-border). On narrow viewports, step circles and connector track are slightly smaller and pillar labels truncate with tighter padding; fromsmupward they match the larger, non-truncating layout. HireOnboardingStepShell(src/app/onboarding/_components/hire-onboarding-step-shell.tsx) is the shared page title + optional lead + inverted lightCardwrapper used by onboarding step clients (andreview-step-markup.tsx). DefaultCardContentpadding isp-6 md:p-8(aligned with candidate form cards). PropscardContentClassName,cardClassName,headerClassName, anddescriptionClassNameextend defaults per step (e.g. companylg:px-10 xl:px-14, reviewmin-w-0on header/description,overflow-x-hiddenon dense steps).- Icon column (candidate-style grid): Company, roles, job, hm-notes, and complete lay out card body as
grid grid-cols-[auto_1fr] gap-3 md:gap-4 items-startwith a left decorative icon (aria-hidden, typically 32ร32; job scalesParseRoleIconto match). Icons live insrc/components/icons/onboarding.tsx(TargetCompaniesIcon,MultipleUserIcon,BlueDocumentIcon,LaptopCheckIcon) except job, which usesParseRoleIconfromsrc/components/icons/app-icons.tsx. Review usesHireOnboardingStepShellbut no left icon columnโthe summary and actions stay full width inside a singlespace-y-6column. - Step content does not nest another outer
Containerโwidth comes from the layoutContaineronly.
Routes
| Path | Role |
|---|---|
/onboarding |
Redirects to the canonical step for the user (see resolveOnboardingRedirectPath in src/lib/hire-onboarding/state.ts). |
/onboarding/company |
Assign a company via CompanyAssignmentForm. |
/onboarding/roles |
Choose desired roles when required by wizard state (User.desired_roles). |
/onboarding/job |
Job title, optional JD URL / pasted text via JobPostingDetailsForm; creates draft via POST /api/jobs, then advances to hm-notes (not straight to review). |
/onboarding/hm-notes?jobId=โฆ |
Static route hm-notes/page.tsx (not [step]): optional free-text notes merged into hm_additional_info via PATCH /api/jobs/:id, then first matching llm_role_summary generation when eligible (blocks on fetchGeneratedRoleSummary before navigating to review). |
/onboarding/review?jobId=โฆ |
Review title + role summary. Optional roleSummaryFailed=1 if summary generation failed on the hm-notes stepโreview shows an error state and then strips the flag from the URL (see below). |
/onboarding/complete?jobId=โฆ |
Post-publish confirmation (complete-onboarding-client.tsx): Continue to dashboard after onboarding_completed_at is set. |
Server layout: src/app/onboarding/ (page.tsx, [step]/page.tsx, hm-notes/page.tsx, complete/page.tsx, _components/*, layout.tsx).
Canonical redirect (resolveOnboardingRedirectPath)
After login, users without completed onboarding are sent to a single โnextโ URL:
- No user /
resetHireProgressdebug / no companies assigned โ/onboarding/company. onboarding_completed_atset (and not forced incomplete by debug) โ/.- Wizard pinned to
rolesโ/onboarding/roles. desired_rolesempty โ/onboarding/roles.- Has companies, but
onboarding_progress.current_stepis missing or not a known step โ/onboarding/job(resume default). - Otherwise โ
/onboarding/${current_step}, and whencurrent_stepishm-notesorreviewanddraft_job_idis set โ append?jobId=<draft_job_id>so refresh/deep links reopen the correct draft.
Redirects and access
- Dashboard (
/) (src/app/dashboard/page.tsx): If hire onboarding is not complete, redirects into the wizard usinghasCompletedHireOnboarding,resolveOnboardingRedirectPath, and optional debug query params (see below). This matches the candidate app pattern (onboarding redirect at the main entry / dashboard, not in the request proxy). - Proxy (
src/proxy.ts): Handles Auth0, session, token refresh, andauthorized/hire_onboarding_completein the site token; it does not redirect incomplete users into onboarding (avoids allowlist drift and duplicate routing vsresolveOnboardingRedirectPath).
Step behavior (as implemented)
1. Company (/onboarding/company)
- UI:
HireOnboardingStepShell+TargetCompaniesIconcolumn +CompanyAssignmentForm(src/components/company-assignment-form/) withshowPageHeader={false}(company-step-client.tsx). Heading scale steps up from mobile (text-2xlโsm:text-3xlโmd:text-4xl); card paddingp-6 md:p-8with optional horizontallg:px-10 xl:px-14on this step only. - Flows: Three flat views โ name (default) typeahead by company name against the hire DB, external lookup by domain or LinkedIn URL (browser โ api-python
GET /api/public/linkedin-company), and manual fallback (SimpleCompanyForm: required name with inline Add company; optional website / description in a collapsible, collapsed by default โ same idea as optional job details on/onboarding/job). Manual submit usesPOST /api/companieswithbuildPublicCompanyPostBody(only public-route fields). Only one body renders at a time; the two non-active views are surfaced as ghost buttons under the active body so the user can pivot without losing an in-flight selection. The name view delegates the typeahead toConnectedCompanySearchInputfrom@/packages/ui(companySearchProfile="candidate"so the package's segmented toggle does not render โ the host owns the external/manual UX). When merged suggestions are sparse, the list can include a โtry another wayโ row that switches the user to the external view (seedocs/features/company-search-hire-app.md, Name view). - After an external lookup hit: A preview card shows logo on the left; name, optional tagline, and a row with website hostname (from
domain) and LinkedIn (with external-link affordance) on the right; a single centered primary action โSelect this companyโ (not full-width). Vendor-only rows (no hireCompany._idyet) are persisted via the publicPOST /api/companies(browser, same-origin) which OR-dedupes onlinkedin_company_id || domain || linkedin_url. Hire-DB rows short-circuit the POST. - Persist:
POST /api/onboarding/companywithcompany_ids. The route also accepts optionaldesired_rolesfor other callers; the hire wizard usually sets desired roles on/onboarding/roleswhen that step is shown, not from the company form. - Client navigates to
/onboarding/roles, preserving hire-debug query params;resetHireProgressis dropped after this navigation so later steps are not pinned back to company (appendHireDebugAfterCompanyStepinsrc/lib/hire-onboarding/state.ts). Server-side canonical routing may still send users who already havedesired_rolesstraight tojob/ later steps when they hit/onboarding.
2. Roles (/onboarding/roles)
- UI:
HireOnboardingStepShell+MultipleUserIconcolumn (roles-step-client.tsx). Helper copy is a native<label htmlFor="roles">(normal weight, gray) aboveRoleSelectionForm; user picks the closest function match (โContinueโ). - Persist:
POST /api/onboarding/companywithcompany_ids(from props) anddesired_roles. On success, client navigates to/onboarding/jobandrouter.refresh().
3. Job (/onboarding/job)
- UI:
HireOnboardingStepShell+ParseRoleIconcolumn + form (job-step-client.tsx).JobPostingDetailsForm(src/components/jobs/job-posting-details-form.tsx) โ job title (required), optional posting URL and pasted text in a collapsible โoptional job detailsโ section (collapsed by default). Shared with other job flows for consistent behavior. - No URL:
POST /api/jobscreates a draft with title (+ optionalhm_additional_infofrom pasted text), then advances to/onboarding/hm-notes?jobId=โฆ(not directly to review). No matching-summary generation on this step. - With URL: Analyze runs before creating the job row (
POST /api/jobs/analyze-url). Errors show next to the URL field. On success, scrape result is cached client-side. - Title conflict: If scraped title differs from the user-entered title, a choice UI appears. While the choice is showing, the primary action is a disabled โContinueโ button. After the user picks โYour titleโ or โFrom postingโ, the primary action becomes an enabled โContinueโ that only creates the job (no second full โSubmitโ pass through analyze). If there is no conflict, analyze and draft creation still happen on that same first submit when appropriate.
- After create: Progress is updated via
POST /api/onboarding/progresstowardhm-notesanddraft_job_idis stored on the user for resume URLs (src/app/api/onboarding/[[...slug]]/route.ts).
4. HM notes (/onboarding/hm-notes?jobId=โฆ)
- Static page
src/app/onboarding/hm-notes/page.tsx(avoids collision with[step]). Server validatesjobId, ownership, and thatonboarding_progress.current_stepishm-notes(otherwise redirects to canonical wizard path). - UI:
HireOnboardingStepShell+BlueDocumentIconcolumn (hm-notes-step-client.tsx) โ optional free-text notes for hiring-manager context. Notes are merged into existinghm_additional_infoviamergeHiringManagerNotes(src/lib/hire-onboarding/merge-hm-notes.ts). If the merged text differs from what is already on the job, the client **PATCH**es/api/jobs/:idso the draft reflects merged content before matching-summary generation. - Matching summary: After any PATCH (or if merge matches server state), if the draft is eligible (
jobHasSubstanceForMatchingSummaryinsrc/lib/hire-onboarding/matching-summary-eligibility.ts), the client callsfetchGeneratedRoleSummary. On success, summary text is stored client-side for review. On failure, navigation to review still happens withroleSummaryFailed=1so the review step can show failure + retry UX. - Title-only / no substance: If after merge the draft is still ineligible for matching summary, the client calls
POST /api/onboarding/publish(same as review Submit), then/onboarding/complete?jobId=โฆโ review is skipped because there is nothing meaningful to confirm beyond what was already captured. - Otherwise, progress is updated via
POST /api/onboarding/progresswhen moving toreview.
5. Review (/onboarding/review?jobId=โฆ)
- UI:
HireOnboardingStepShellwith dynamictitle/descriptionfrom view-model state (review-step-markup.tsx); no left icon column (dense summary + edit flows). Same shell supports edit summary from dashboard (flow === 'edit-summary') with optional back link. - Loaded only for a valid
jobIdowned by the user (src/app/onboarding/[step]/page.tsx). Invalid/missingjobIdredirects to/onboarding/job. roleSummaryFailed=1: If present, initial state shows a summary error; auseEffectremoves the query param after load so refreshes donโt keep the flag. This flag is set when generation fails on the hm-notes step (not the job step).- Role summary: May load from the job, client storage, or
fetchGeneratedRoleSummarywhen substantive JD text exists and generation did not already fail; loading/error/retry paths are inreview-step-client.tsx. - Edit: When a summary exists, user can Edit (ProseMirror) and save via
PUT /api/jobs/{id}/matching-summary. In edit mode, job title can also be edited and saved withPATCH /api/jobs/{id}. - Submit:
POST /api/onboarding/publishwith{ job_id }sets the jobactive, setsUser.onboarding_completed_at, finalizesonboarding_progress, and the client redirects to/onboarding/complete?jobId=โฆ(then Continue to dashboard).
6. Complete (/onboarding/complete?jobId=โฆ)
- UI:
HireOnboardingStepShell+LaptopCheckIconcolumn (complete-onboarding-client.tsx): confirmation copy and Continue to dashboard after publish. Server page:complete/page.tsx.
Primary APIs
| Endpoint | Purpose |
|---|---|
POST /api/onboarding/company |
Persist company (company_ids; optional desired_roles) and set progress toward job step. |
POST /api/onboarding/progress |
Persist User.onboarding_progress between steps. Server validates current_step / completed_steps against allowed wizard steps, requires a valid Mongo draft_job_id owned by the user when current_step is hm-notes or review, and strips unknown fields before save. |
POST /api/onboarding/publish |
Activate job + set onboarding_completed_at (review Submit, or hm-notes when the draft remains ineligible for matching summary). |
POST /api/jobs |
Create draft job (with or without URL analysis payload). |
POST /api/jobs/analyze-url |
URL scrape / structured analysis (job step, before create when URL present). |
PUT /api/jobs/{id}/matching-summary |
Save edited matching summary on review. |
PATCH /api/jobs/{id} |
Update job fields (e.g. merged HM notes on hm-notes, title on review). |
| Matching summary generation | First run on hm-notes when eligible after optional PATCH; review may refetch/retry (see hm-notes-step-client / review-step-client / job APIs). |
Onboarding POSTs above are implemented in src/app/api/onboarding/[[...slug]]/route.ts with ApiRouter (same URL paths as before).
Debug / QA (non-production-friendly)
Restricted to primary @qwestly.com accounts (no plus addressing), via canUseHireOnboardingDebug in src/lib/auth-utils.ts: the ?debug=1 Jotai toggle, floating panel, and hire-debug query keys have no effect for other users (server redirects ignore simulation flags; client navigations do not preserve them).
With the floating debug UI enabled (Jotai hireDebug / debug panel), eligible testers can use hire-debug query keys (debug, forceHireOnboarding, resetHireProgress) โ see HIRE_DEBUG_QUERY_KEYS in src/lib/hire-onboarding/state.ts.
forceHireOnboarding=1: Treat hire onboarding as incomplete for redirects even ifonboarding_completed_atis set.resetHireProgress=1: Pin canonical wizard entry to company until cleared (e.g. after company step navigation strips it).src/components/hire-onboarding-debug-panel.tsx: โTest onboarding from startโ navigates to/onboarding/companywith those flags; โLeave test modeโ strips simulation flags.
These flags are URL-only simulation for routing; they do not replace DB migrations for legacy users.
Key source files
| Area | Location |
|---|---|
| Gate + redirect helpers | src/lib/hire-onboarding/state.ts |
| Matching-summary eligibility | src/lib/hire-onboarding/matching-summary-eligibility.ts |
| Wizard pages | src/app/onboarding/page.tsx, src/app/onboarding/[step]/page.tsx, src/app/onboarding/hm-notes/page.tsx, src/app/onboarding/complete/page.tsx |
| Layout + step indicator | src/app/onboarding/layout.tsx, _components/hire-onboarding-progress.tsx |
| Shared step shell (title + card) | _components/hire-onboarding-step-shell.tsx |
| Step UIs | _components/company-step-client.tsx, roles-step-client.tsx, job-step-client.tsx, hm-notes-step-client.tsx, review-step-markup.tsx (via review-step-client.tsx), complete/complete-onboarding-client.tsx |
| Onboarding step icons | src/components/icons/onboarding.tsx (and ParseRoleIcon in src/components/icons/app-icons.tsx for job) |
| HM notes merge helper | src/lib/hire-onboarding/merge-hm-notes.ts |
| Job posting form (shared) | src/components/jobs/job-posting-details-form.tsx |
| Company search / preview / select | src/components/company-assignment-form/ |
| User types | src/types/user.d.ts (onboarding_completed_at, onboarding_progress) |
| Dashboard gate | src/app/dashboard/page.tsx |
Onboarding POST APIs (company / progress / publish) |
src/app/api/onboarding/[[...slug]]/route.ts |
| Proxy (Auth0 + session + token; no onboarding redirect) | src/proxy.ts |
Related documentation
- Plan / roadmap / gaps:
docs/plan/simplify-hm-onboarding.md - Auth and signup (may still mention legacy fields in places):
authentication-and-signup.mdโ prefer this hire-onboarding doc for wizard behavior and hire completion semantics.
Changelog (maintenance)
| Date | Notes |
|---|---|
| 2026-04 | Documented wizard shell (layout, HireOnboardingProgress), company preview card UX, canonical redirect when company exists but progress is missing, hire wizard payload vs optional desired_roles, job optional collapsible + roleSummaryFailed, review query cleanup. |
| 2026-05 | hm-notes step, static hm-notes/page.tsx, draft_job_id + ?jobId= resume URLs, JobPostingDetailsForm on job step, matching-summary generation moved to hm-notes (failure โ roleSummaryFailed on review); when still ineligible after merge, publish from hm-notes โ /onboarding/complete (skip review). Progress/API table and file index updates. |
| 2026-05-11 | Hire wizard responsive shell: layout Container max-w-2xl, overflow-x-hidden, tighter default padding; HireOnboardingProgress scales down on small screens (circles, labels, connectors). Company step drops nested Container; typography and card padding tuned for mobile. |
| 2026-05-12 | HireOnboardingStepShell extracts shared title + inverted card; step icon columns (company / roles / job / hm-notes / complete) + review without icon; layout py-6 sm:py-8 lg:py-12; packages/ui bump for explicitParentClearOnly on company search (see company-search hire doc). |
Update this file when you change routes, gates, step UX, or onboarding APIs so it stays the as-built reference for the wizard.