USER_FLOWS.md β h.work
End-to-end user flows with decision points and error paths. Cross-reference: INFORMATION_ARCHITECTURE.md, VIEWS_BY_ROLE.md, USER_STORIES.md, EXAMPLE-FLOW.md, GLOSSARY.md. Last updated: 2026-06-06.
Symbols:
- β transitions to the next step
- β decision point
- β error path
- β key affordance / moment of truth
Flows below are the canonical happy paths. Edge cases that aren't covered here are tracked in USER_STORIES.md gaps.
Flow 1 β New Client Onboarding (AM β Client β first message)β
Actors: AM (e.g. Diana), client admin (e.g. Amy), platform (Phase-0 gate, invite system, OTP service). Goal: Acme Financial, a new customer, completes onboarding and lands in the client portal ready to message Bob. User stories: SA-C1, SA-C4, C-O1, C-O2 / O3 (channel setup), C-O4.
1.1 β AM creates the orgβ
- Diana logs in β lands on
/ops/clients. - Click "+ New Client" β
/ops/clients/new(wizard step 1). - Fills Step 1 (Company Info): name "Acme Financial", slug "acme" (
SlugAvailabilityInputconfirms availability live), industry "Finance", company domainsacme.co, primary contactamy@acme.co, country, timezone.- β Slug taken β red error inline β user picks alternative.
- β Email domain not in company domains β warning blocks Continue until either (a) email matches a domain or (b) domain added.
- Continue β POST
createAmOrgβ org created inpending_am_setupstatus β redirect to step 2.
1.2 β AM assigns Specialistβ
- Step 2 (
/ops/clients/[id]/setup/step2) shows specialist pool. - Diana searches "Bob", selects Bob's persona, marks Primary, sets confidence threshold (default 101 β always HITL β KYC is high-stakes).
- β Bob is at capacity (e.g. already serving N orgs) β soft warning "Bob serves 6 orgs already; consider an alternative" β not a block.
- Continue β assignment persisted β step 3.
1.3 β AM saves profile (one-time)β
- Step 3: if Diana hasn't filled her global AM profile before, she fills name + avatar + signature now. If she has, fields are pre-filled and she clicks Continue.
1.4 β Phase-0 gate + send inviteβ
- Step 4 shows Phase-0 checklist (5 items, fed by
getAmOrgPhase0):- β Company info confirmed
- β Slug reserved
- β Specialist assigned
- β AM profile complete
- β Trial end date set (default +30 days)
- Diana reviews the invite preview email (substituted with Acme's name + Bob's persona).
- β
Click "Send invite" β
sendOrgInvite()β invite token generated (7-day expiry), email sent to Amy from the platform.- β Phase-0 fails β "Send" disabled with red dot on missing item; Diana fixes upstream and returns.
- Step 4 now shows "Invite sent Β· expires in 7 days" with Resend / Revoke buttons.
1.5 β Client accepts the inviteβ
- Amy receives email "You're invited to your h.work workspace". Clicks the link β
/accept-invite?token=β¦β validates token β redirects to/onboarding. - Onboarding wizard (
/onboarding/step/[step]):- Phase 2.1: OTP verification (email-delivered 6-digit code) + set password + name/title/phone.
- Phase 2.2: Confirm company info + slug. β Slug becomes immutable on confirm.
- Phase 2.3: Meet your Specialist (Bob avatar + name; team composition hidden β never show the AI agent name or David).
- Phase 2.4: Channels setup. Email is on by default. WhatsApp toggle (skippable). Slack OAuth optional.
- Phase 2.5: Optional β invite teammates, billing, customize Specialist.
- Phase 2.6: Completion screen β "You're all set" β org status flips to
activeβ trial timer starts β invite token invalidated β welcome email sent.
- Amy is redirected to
/client/chat.
1.6 β First contactβ
- Amy lands on
/client/chatempty state. β The Specialist card shows Bob with "Online now" green dot. CTA: claret "Message" button. - Click β new thread created β composer focused.
Decision points
- If Amy doesn't accept within 7 days β token expires. Diana receives a notification and can resend (extends window).
- If onboarding is abandoned mid-flow, server-side state preserves progress. Stale orgs auto-archive after 90 days (
StaleOrgService).
Error paths
- β Slug confirmation fails (race β taken between AM input and confirm) β block with "Slug no longer available, please pick another"; AM is paged.
- β OTP email not received β Resend button + fallback to AM ("contact your account manager Diana for a new link").
Flow 2 β Client Sends First Messageβ
Actors: Amy (client admin), Bob's AI agent (the per-Specialist drafting model), David (expert), platform. Goal: Amy types a request, sees a response in the same thread, with the right "Human review" badge if HITL applied. User stories: C-W1, C-W2, C-W4, C-W5, C-CH1.
2.1 β Sendβ
- Amy lands on
/client/chatempty state. - β
Click "Message" β POST
/conversationsβ new thread β composer focused. - Types: "Hi Bob, I have a customer escalation β John Smith (ID: JS-4821) failed KYC verificationβ¦"
- Press Enter β optimistic message appended (immediately renders in the stream as outgoing claret bubble).
- POST
/conversations/:id/messagesβ real message replaces optimistic.
2.2 β Agent processesβ
- Backend dispatches to the agent service (
agent.client.tsβ Python FastAPI). - The agent service retrieves Jumio data, computes a draft + confidence score (e.g. 82).
- β Confidence β₯ workspace threshold β message is auto-sent (badge "AI" / blue).
- β Confidence < threshold β ExpertQueueItem created β routed to expert queue.
2.3 β Amy waitsβ
- While HITL: Amy sees a typing indicator (or just no new message). Real-time push via Socket.io with Redis adapter cross-pod fan-out (verified via
scripts/load/socketio-fanout-test.mjs); built-in reconnect on transient drops; reconnect banner on sustained drops; no polling fallback (removed in #843).- The Specialist online dot remains green (don't expose the queue).
- β Once the expert sends β message arrives in Amy's stream as Bob with the "Human review" purple badge inline above the bubble.
2.4 β Amy readsβ
- Amy reads, replies "CONFIRM" β loop continues.
Decision points
- β Amy switches to email mid-thread β next message arrives at
bob@h.workβ routed byIn-Reply-Toheader β same conversation.ChannelBadgeupdates to "Email" for that message. - β Confidence is high β "AI" blue badge.
- β Confidence is low + expert edits β "Human review" purple badge.
Error paths
- β Send fails (network) β optimistic bubble shows red retry. Toast "Failed to send β tap to retry".
- β Backend agent crashes β fail-open: a polite "I need a moment to look this up β coming right back" auto-message + queue item escalation. (See
CLAUDE.mdDesign Decisions: "Fail-open on agent errors".) - β Whitelist mismatch on inbound email β bounce with a polite reply; not surfaced in Amy's portal.
Flow 3 β Expert HITL Reviewβ
Actors: David (expert), Bob's AI agent, Amy (client, indirectly). Goal: David converts a queue item into a sent response in under a minute. User stories: E-R1 through E-R7, E-R13, E-R14, E-L1.
3.1 β Notificationβ
- David is on
/workspace/queue. New item arrives β Socket.ioexpert_queue_item_addedβ "1 new" pill increments and the row animates in at the top of the list.- β Push notification (P0 gap E-P3) β when implemented, fires for risk=critical items.
3.2 β Triageβ
- David scans the list (always sorted newest-first β no sort toggle). Risk pills color-code each row; the right pane header reads "Client Conversation with
{customer}". - β
Click row β
TicketDetailloads on the right. - David reads the customer thread (Amy's messages + any prior context).
3.3 β Review the AI suggestionβ
- AI suggestion panel shows:
- The AI agent's draft (editable textarea).
ConfidenceBar82/100 (color: warning).- Tools used:
jumio.lookup(JS-4821). - Data retrieved: collapsible card with the Jumio response.
- β David agrees with the draft β click "Accept & Send" β
respond()β message sent as Bob β ticket marked resolved β next ticket auto-selected. - β David wants a small edit β edit textarea inline β click "Send as Bob" β same flow but with edited content. Edit is captured as a Correction (E-L1) for the learning pipeline.
- β David disagrees entirely β click "Ignore & Write" β empty composer β David writes from scratch (the "No AI draft" path from
EXAMPLE-FLOW.md).
3.4 β Side actionsβ
- Before sending, David may:
- Click "Assign to me" β ticket marked "You" β colleagues won't pick it up.
- Click "Defer 24h" β ticket disappears from Pending; returns at
deferred_until. - Click "Reassign" β picker shows other Experts with a matching
expert_accessgrant (ADR-007) β ticket leaves David's queue. - Toggle "+ Internal note" β dashed-amber composer β POST with
is_internal=trueβ visible only to other experts (E-R9). - Open status dropdown β flip to snoozed / pending / resolved (E-R13).
3.5 β Resolutionβ
- After send, ticket auto-marked resolved. Status dropdown moves to "Resolved". The conversation thread is preserved.
- David moves to the next ticket. Queue auto-refreshes (E-R12).
Decision points
- β Risk level is critical β row has a red bar; SLA strip surfaces it.
- β Multi-step agentic task involved β "Open task" deep-link to
/workspace/tasks/[id]for step approval (E-A1βA3).
Error paths
- β Send fails β toast "Failed to send β try again". Ticket remains pending.
- β Conflicting edit (someone else assigned it mid-edit) β toast "Reassigned to [other expert]"; David's ticket is closed and the next one loads.
Flow 4 β Specialist Serves Multiple Orgs Simultaneouslyβ
Actors: Bob (Specialist persona), Amy (Acme Financial), Carlos (Beta Corp), David & Charlie (experts on Bob's team).
Goal: Show how one Specialist persona is shared across multiple orgs without leaking data.
User stories: foundational β supports the Specialist Team model in GLOSSARY.md.
4.1 β Two AMs both choose Bobβ
- AM Diana assigns Bob to Acme Financial (Specialist Team = experts [Charlie, David] + the AI agent runtime).
- AM Eve (different AM) assigns Bob to Beta Corp (Specialist Team = experts [Charlie, David] + the AI agent runtime) β same humans, same AI, same persona.
- β
Bob is one entity in the database (a Specialist persona row); the assignment is
OrgSpecialistAssignment(Bob β Org).
4.2 β Two clients message Bob in parallelβ
- Amy (Acme) opens
acme.h.work/client/chat(subdomain host per ADR-015 + canonical path/client/*per CD-12) β sidebar header reads "acme.h.work" β Specialist row "Bob". - Carlos (Beta) opens
beta.h.work/client/chatβ sidebar header reads "beta.h.work" β also "Bob". - β Each client thinks Bob is their specialist; neither sees the other org. RLS at the DB level enforces this.
4.3 β Inbound on shared channelsβ
- Both Amy and Carlos use the WhatsApp number assigned to Bob (one number from the WhatsApp Number Pool).
- Amy sends a WhatsApp from
+1-555-AMYβ Amy previously paired her phone via anHW-XXXXcode from/client/settings/channels, so the number is registered inwhatsapp_linked_numbersfor Acme β routed to the Acme conversation. (Legacywhatsapp_sender_whitelistis consulted as a fallback only.) - Carlos sends a WhatsApp from
+1-555-CARβ Carlos likewise paired his phone via anHW-XXXXcode from Beta's/client/settings/channelsβ matched viawhatsapp_linked_numbersβ routed to the Beta conversation. - β Unknown sender (no pairing-code link, no whitelist entry) β polite auto-reply prompting the sender to pair via
HW-XXXX; no conversation created. - β Sender fails the full pairing-code ladder (
whatsapp_linked_numbersmiss βwhatsapp_sender_whitelistfallback miss) β silently dropped after the auto-reply (no expert-queue load). See ADR-002 update #191.
4.4 β David handles bothβ
- David logs in once (
david@ex.h.work). His queue includes tickets from both Acme and Beta because he's on Bob's Specialist Team. - The org-tabs bar in
/workspace/queueshows "All Orgs Β· Acme Β· Beta". - β Each ticket is isolated β when David opens Amy's ticket, the customer thread is entirely Acme's data; when he opens Carlos's, entirely Beta's. No cross-contamination.
- David replies in both threads. Each response goes out as Bob to that org's customer.
4.5 β Identity from the client sideβ
- Amy receives "Reply from Bob" via WhatsApp / portal β she sees Bob.
- Carlos receives "Reply from Bob" β he sees Bob.
- β Neither client knows the other exists, that David handled both, or that the AI agent drafted them.
Decision points
- β AM wants to retire Bob β
SpecialistChangeRequestflow β Bob's pool slot returns to available, name reassigned via the pool. Existing org assignments require explicit replacement before tear-down. - β Bob's WhatsApp number is reused after retirement β number returns to pool with status
available(slugs are forever, numbers are recycled β see GLOSSARY.md).
Error paths
- β Pairing drift (Amy's phone number changes but she hasn't re-paired via
HW-XXXX) β her new number falls through to the auto-reply prompting re-pairing. AM gets notified if the bounce repeats. - β Specialist email collision at creation (
bob@h.workalready exists as a real Google Workspace mailbox per ADR-0002 Phase 2 / #1065) β AM is prompted with alternative names (bob2@h.work,bob.k@h.work).
Flow 5 β Expert Reassignment via expert_access (bonus flow)β
Actors: David (Expert currently handling a thread), Erin (another Expert), Sam (SuperAdmin / AM).
Goal: Move a conversation from David to Erin without losing context, in the post-pool model where Expert visibility is governed by expert_access grants (ADR-007). Expert pools were removed 2026-05-03.
User stories: SA-Q3 (reassignment audit trail), ADR-007 (expert_access).
5.1 β Contextβ
- Acme Financial (org) has two Experts with active
expert_accessgrants:- David β
scope='org'grant covering all of Acme. - Erin β
scope='specialist'grant covering (Acme Γ Bob) only.
- David β
- β
Either Expert can see the thread because their grant covers it; assignment is a separate per-thread pointer (
conversations.assigned_expert_id), not a visibility check.
5.2 β Sam reassigns the threadβ
- Sam opens the conversation in
/ops/inbox(or David clicks "Reassign" in the ticket detail action bar in/workspace/queue). - Reassignment picker shows only Experts who hold an
expert_accessgrant matching this thread's (Org Γ Specialist) β i.e. David (viascope='org') and Erin (viascope='specialist'). Experts without a matching grant are not selectable. - Sam picks Erin β confirm.
PATCH /conversations/:id/assign(ADR-007 / #363 PR-2) β updatesconversations.assigned_expert_idβ appends a row toconversation_expert_assignments(audit trail, #363 PR-1 / #542).- β Thread leaves David's queue; appears in Erin's queue with full history intact. Socket.io pushes the queue updates to both Experts in real time.
5.3 β Effectβ
- David's queue auto-advances to the next item.
- Erin opens the thread β sees the full conversation history, all prior internal notes, the latest agent draft, and the audit row showing it was reassigned from David by Sam at
<timestamp>. - The client (Amy) experiences no delay β Bob's online dot stays green; the persona is unchanged.
Decision points
- β Sam tries to reassign to an Expert with no matching grant β picker hides the option; direct API call returns
403("Expert lacksexpert_accessfor this (Org Γ Specialist)"). - β Reassignment to self (idempotent) β no-op; no audit row written.
Error paths
- β Grant revoked between selection and submit β
409 Conflict; UI re-fetches the picker. - β Thread closed mid-reassign β
410 Gone; toast "Conversation already closed".
Flow 6 β Internal Collaboration on a Ticketβ
Actors: David (handling), Charlie (asked to weigh in).
Goal: David asks Charlie about an ambiguous ticket without copying context elsewhere.
User stories: E-R9 (internal notes β implemented). E-C3 (inline ticket discussion β gap) β REMOVED, see below.
Today (implemented via internal notes)β
- David clicks "+ Internal note" β composer flips to dashed-amber mode.
- Types "@charlie what do you think about the override window here?" + sends.
- The note appears in the conversation pane with dashed-amber border + π icon. Visible only to experts.
- Charlie sees an in-app notification β opens the ticket β reads the note β replies (also internal).
- Once aligned, David switches off internal mode β composes the customer-facing response β sends as Bob.
Future (E-C3 gap β proposed) β REMOVEDβ
β οΈ REMOVED. The proposed "Discussion tab" / multi-expert collab feature was retired along with the Collab module; the team uses Slack for cross-expert coordination outside the ticket. Internal notes (above) remain the only in-app mechanism. See
docs/features/collab.md.
Owner: Design + Platform team