Skip to main content

USER_FLOWS.md — human.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-05-04.

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 human.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 Felix/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), Felix (AI agent on Bob's team), 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. Felix 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 if connected; polling fallback every 15s.
    • 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), Felix (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:
    • Felix'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 Felix" 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" → pool dropdown → 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 = [Charlie, David] + [Felix]).
  2. AM Eve (different AM) assigns Bob to Beta Corp (Specialist Team = [Charlie, David] + [Felix]) — 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.human.work/client/chat → sidebar header reads "acme.human.work" → Specialist row "Bob".
  2. Carlos (Beta) opens beta.human.work/client/chat → sidebar header reads "beta.human.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 → matches Acme's whitelist → routed to Acme conversation.
  3. Carlos sends a WhatsApp from +1-555-CAR → matches Beta's whitelist → routed to Beta conversation.
  4. ◇ Unknown sender → polite auto-reply, no conversation created.
  5. ✗ Sender on neither whitelist → silently dropped after the auto-reply (no expert queue load).

4.4 — David handles both

  1. David logs in once (david@ex.human.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 Felix 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

  • ✗ Whitelist drift (Amy's number changes but the whitelist isn't updated) → polite bounce. AM gets notified to update the whitelist.
  • ✗ Specialist email collision at creation (bob@h.work already exists as a routing alias) → AM is prompted with alternative names (bob2@h.work, bob.k@h.work).

Flow 5 — Expert Pool Reassignment (bonus flow)

Actors: David (expert in KYC pool), pool admin (superadmin). Goal: Move a ticket from the wrong pool to the right one without losing context.

  1. David picks up a ticket but realizes it's a billing question, not KYC.
  2. ★ Click "Reassign" in the ticket detail action bar → dropdown of pools.
  3. Pick "Billing" → confirm.
  4. reassign() API → ticket leaves David's queue → appears in the Billing pool's queue with all history intact.
  5. David's queue auto-advances to the next item.
  6. The customer experiences no delay — they still see Bob's online dot.

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).

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)

  • A dedicated "Discussion" tab anchored to the ticket, separate from internal notes (which are part of the message stream). Allows multi-expert threads without polluting the conversation.

Owner: Design + Platform team