Skip to main content

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​

  1. Diana logs in β†’ lands on /ops/clients.
  2. Click "+ New Client" β†’ /ops/clients/new (wizard step 1).
  3. Fills Step 1 (Company Info): name "Acme Financial", slug "acme" (SlugAvailabilityInput confirms availability live), industry "Finance", company domains acme.co, primary contact amy@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.
  4. Continue β†’ POST createAmOrg β†’ org created in pending_am_setup status β†’ redirect to step 2.

1.2 β€” AM assigns Specialist​

  1. Step 2 (/ops/clients/[id]/setup/step2) shows specialist pool.
  2. 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.
  3. Continue β†’ assignment persisted β†’ step 3.

1.3 β€” AM saves profile (one-time)​

  1. 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​

  1. 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)
  2. Diana reviews the invite preview email (substituted with Acme's name + Bob's persona).
  3. β˜… 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.
  4. Step 4 now shows "Invite sent Β· expires in 7 days" with Resend / Revoke buttons.

1.5 β€” Client accepts the invite​

  1. Amy receives email "You're invited to your h.work workspace". Clicks the link β†’ /accept-invite?token=… β†’ validates token β†’ redirects to /onboarding.
  2. 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.
  3. Amy is redirected to /client/chat.

1.6 β€” First contact​

  1. Amy lands on /client/chat empty state. β˜… The Specialist card shows Bob with "Online now" green dot. CTA: claret "Message" button.
  2. 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​

  1. Amy lands on /client/chat empty state.
  2. β˜… Click "Message" β†’ POST /conversations β†’ new thread β†’ composer focused.
  3. Types: "Hi Bob, I have a customer escalation β€” John Smith (ID: JS-4821) failed KYC verification…"
  4. Press Enter β†’ optimistic message appended (immediately renders in the stream as outgoing claret bubble).
  5. POST /conversations/:id/messages β†’ real message replaces optimistic.

2.2 β€” Agent processes​

  1. Backend dispatches to the agent service (agent.client.ts β†’ Python FastAPI).
  2. The agent service retrieves Jumio data, computes a draft + confidence score (e.g. 82).
  3. β—‡ Confidence β‰₯ workspace threshold β†’ message is auto-sent (badge "AI" / blue).
  4. β—‡ Confidence < threshold β†’ ExpertQueueItem created β†’ routed to expert queue.

2.3 β€” Amy waits​

  1. 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).
  2. β˜… 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​

  1. Amy reads, replies "CONFIRM" β†’ loop continues.

Decision points

  • β—‡ Amy switches to email mid-thread β†’ next message arrives at bob@h.work β†’ routed by In-Reply-To header β†’ same conversation. ChannelBadge updates 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.md Design 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​

  1. David is on /workspace/queue. New item arrives β†’ Socket.io expert_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​

  1. 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}".
  2. β˜… Click row β†’ TicketDetail loads on the right.
  3. David reads the customer thread (Amy's messages + any prior context).

3.3 β€” Review the AI suggestion​

  1. AI suggestion panel shows:
    • The AI agent's draft (editable textarea).
    • ConfidenceBar 82/100 (color: warning).
    • Tools used: jumio.lookup(JS-4821).
    • Data retrieved: collapsible card with the Jumio response.
  2. β—‡ David agrees with the draft β†’ click "Accept & Send" β†’ respond() β†’ message sent as Bob β†’ ticket marked resolved β†’ next ticket auto-selected.
  3. β—‡ 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.
  4. β—‡ 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​

  1. 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_access grant (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​

  1. After send, ticket auto-marked resolved. Status dropdown moves to "Resolved". The conversation thread is preserved.
  2. 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​

  1. AM Diana assigns Bob to Acme Financial (Specialist Team = experts [Charlie, David] + the AI agent runtime).
  2. AM Eve (different AM) assigns Bob to Beta Corp (Specialist Team = experts [Charlie, David] + the AI agent runtime) β€” same humans, same AI, same persona.
  3. β˜… 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​

  1. 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".
  2. Carlos (Beta) opens beta.h.work/client/chat β†’ sidebar header reads "beta.h.work" β†’ also "Bob".
  3. β˜… 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​

  1. Both Amy and Carlos use the WhatsApp number assigned to Bob (one number from the WhatsApp Number Pool).
  2. Amy sends a WhatsApp from +1-555-AMY β†’ Amy previously paired her phone via an HW-XXXX code from /client/settings/channels, so the number is registered in whatsapp_linked_numbers for Acme β†’ routed to the Acme conversation. (Legacy whatsapp_sender_whitelist is consulted as a fallback only.)
  3. Carlos sends a WhatsApp from +1-555-CAR β†’ Carlos likewise paired his phone via an HW-XXXX code from Beta's /client/settings/channels β†’ matched via whatsapp_linked_numbers β†’ routed to the Beta conversation.
  4. β—‡ Unknown sender (no pairing-code link, no whitelist entry) β†’ polite auto-reply prompting the sender to pair via HW-XXXX; no conversation created.
  5. βœ— Sender fails the full pairing-code ladder (whatsapp_linked_numbers miss β†’ whatsapp_sender_whitelist fallback miss) β†’ silently dropped after the auto-reply (no expert-queue load). See ADR-002 update #191.

4.4 β€” David handles both​

  1. 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.
  2. The org-tabs bar in /workspace/queue shows "All Orgs Β· Acme Β· Beta".
  3. β˜… 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.
  4. David replies in both threads. Each response goes out as Bob to that org's customer.

4.5 β€” Identity from the client side​

  1. Amy receives "Reply from Bob" via WhatsApp / portal β€” she sees Bob.
  2. Carlos receives "Reply from Bob" β€” he sees Bob.
  3. β˜… Neither client knows the other exists, that David handled both, or that the AI agent drafted them.

Decision points

  • β—‡ AM wants to retire Bob β†’ SpecialistChangeRequest flow β†’ 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.work already 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​

  1. Acme Financial (org) has two Experts with active expert_access grants:
    • David β€” scope='org' grant covering all of Acme.
    • Erin β€” scope='specialist' grant covering (Acme Γ— Bob) only.
  2. β˜… 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​

  1. Sam opens the conversation in /ops/inbox (or David clicks "Reassign" in the ticket detail action bar in /workspace/queue).
  2. Reassignment picker shows only Experts who hold an expert_access grant matching this thread's (Org Γ— Specialist) β€” i.e. David (via scope='org') and Erin (via scope='specialist'). Experts without a matching grant are not selectable.
  3. Sam picks Erin β†’ confirm.
  4. PATCH /conversations/:id/assign (ADR-007 / #363 PR-2) β†’ updates conversations.assigned_expert_id β†’ appends a row to conversation_expert_assignments (audit trail, #363 PR-1 / #542).
  5. β˜… 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​

  1. David's queue auto-advances to the next item.
  2. 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>.
  3. 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 lacks expert_access for 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)​

  1. David clicks "+ Internal note" β†’ composer flips to dashed-amber mode.
  2. Types "@charlie what do you think about the override window here?" + sends.
  3. The note appears in the conversation pane with dashed-amber border + πŸ”’ icon. Visible only to experts.
  4. Charlie sees an in-app notification β†’ opens the ticket β†’ reads the note β†’ replies (also internal).
  5. 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