next-best-action-polishrun.md00_intake/stub.mdThe levers are wired and working, but a working rail isn't yet a guided one. The CRM "next-best-action" pattern leads with the single most important next step per record; personas with nothing to do need a clear locked state; and the unhappy paths (qualify out, return to bidding, dispute, gone cold) must be present so the workspace genuinely surfaces every lever — not just the forward ones. This final stub makes the workspace the definitive answer to "where is this lead and what can I do next?".
Polish the action model into its finished form:
positive transition for the current stage + persona as
the prominent next-best-action; everything else stays secondary (the demo's promotion logic).apps/demo/lib/workspace/levers.ts (LOCKED, primary-promotion logic, unhappy-path
catalogue) + apps/demo/components/workspace/{workspace-header,levers-rail}.tsx.accessibility skill for the a11y pass; keep confirm/destructive patterns consistent with @sustentus/ui.apps/web/components/workspace/**, apps/web/lib/workspace/levers.ts.01_define/output/spec.mdStubs 1–5 of the strictly-sequential lead-workspace epic delivered a per-persona workspace that
shows the right levers and, since wire-action-levers, actually performs the cleanly-backed ones. But
a working rail is not yet a guided one, so the epic's final promise — the workspace as the
definitive answer to "where is this lead and what can I do next?" — isn't met:
"Next best action appears here." — literally
annotated "wired in a later feature"). The promotion logic already exists (deriveLevers returns a
single primary and a per-persona primaryLocked), and the rail renders it, but the prominent
header CTA the design leads with was deferred to this stub.archive,
flagRework, and any future negative lever) fire on a single unconfirmed click — no
confirm-before-destructive guard.This is stub 6 of 6 (depends-on: wire-action-levers, satisfied) and it closes the Refine the
Bridge initiative's Q2-2026 Objective 3 (Validate Technical Infrastructure & Payout Flow — lead
transactions completed end-to-end): a lead-to-payout cycle is only genuinely runnable from one page
once every persona is guided to its single next step, every unhappy path is reachable and safe, and
the surface clears the accessibility and clarity bar.
Polish the existing action model into its finished form — presentational and safety work over the
levers/rail that already exist. No new server actions, no new business logic, no new levers beyond
the registry (the epic's reuse-only rule and the wire-action-levers cut still hold; unwired levers
stay disabled with their honest affordance).
Primary CTA surfacing (header). Replace the static placeholder in WorkspaceHeader with the
promoted next-best-action as a single prominent CTA — the deriveLevers primary for the current
persona + stage — porting the demo's treatment (a leading affordance, "Next best action — {label}").
When the persona has no forward action, the header shows that persona's primaryLocked message
(with a locked affordance) instead of an empty slot. Activating the header CTA runs the same
wired lever the rail would (via the existing dispatch), and renders disabled with the same "not
available here yet" affordance when that lever is unwired. The header becomes (or gains) a small
client CTA that shares the rail's dispatch rather than duplicating it. The rail keeps the full
inventory; the single canonical next-best-action is not shown twice as competing big buttons.
Per-persona empty/locked states. Ensure every persona whose stage offers no forward lever sees
its specific LOCKED[persona] message in the header and the rail (already defined in levers.ts),
never a blank CTA slot or empty rail.
Unhappy-path levers — present and correctly gated. Confirm the negative/loop levers are all reachable and gated by stage ∩ capability: qualify out, reject lead, return to bidding (quote rejected), flag BRD, mark milestone blocked / raise blocker, invoice reject / dispute, flag delivery reworked, and the drop-off/loop-back paths. These already exist in the catalogue; this stub verifies gating and reachability rather than adding levers.
Confirm-before-destructive (consistent). Every destructive (negative-intent) lever confirms
before it runs: those that already open a reason form keep that as their confirmation; those with no
form (e.g. archive, flagRework) gain an AlertDialog confirm step (@sustentus/ui
alert-dialog). No destructive action fires on a single unconfirmed click.
Accessibility pass (rail + CTA). Every lever control is keyboard reachable with a descriptive
accessible name; lever intent is not conveyed by the colour dot alone — it carries a text/aria
equivalent; focus order follows reading order; disabled/unwired levers are announced as unavailable;
and the form/confirm dialogs manage focus and are labelled. Lean on the accessibility skill.
Wording + empty-state tidy. A final jargon-free pass on the header, journey strip, section
labels and rail per persona (no raw status keys or internal terms shown to the customer), and a
consistent empty/locked state for every workspace section (reusing the existing EmptyState).
wire-action-levers left them — this stub does not wire the deferred
set (forward spine transitions, proposal.*, quote.approve, milestone.start/complete/block,
csat.submit, brd.approve/signOff, lead.assignManager, outreach.send, message.team, …).service-leads / projects / proposals /
finance / BRD pages — epic-level out of scope (coexist, deprecate later).apps/demo — design source, ported, never a dependency.AlertDialog. No
new confirm pattern is invented; form-backed destructive levers already double as their own
confirmation, and form-less ones get the shared @sustentus/ui alert dialog.levers.ts promotion logic and LOCKED map, the rail's dispatch, and the
wire-action-levers wired/unwired split are consumed as-is, not re-derived.Define read beyond the docs-only intake band (recorded per the intake contract): the current
workspace implementation it must polish — apps/web/lib/workspace/levers.ts,
apps/web/components/workspace/{workspace-header,levers-rail,action-button,workspace-view,sections}.tsx
— plus the wire-action-levers spec (the wired/unwired cut this stub must not re-open) and the demo
design source apps/demo/components/workspace/workspace-header.tsx. The polish target is code, not
prose, so the remaining-work scope had to be grounded in the real components rather than re-derived
from the stub alone.
02_build/output/notes.mdfeat: next-best-action-polish — surface header CTA, confirm destructive levers, a11y passdocs: next-best-action-polish — build notesThe action model was already logically complete after wire-action-levers (promotion, per-persona
LOCKED, the full unhappy-path catalogue, and the wired/unwired split). This stub is the
presentation, safety and a11y finish over it — no new server actions, no new levers, no new domain
logic.
components/workspace/next-best-action.tsx (new) — the promoted next-best-action, surfaced
prominently in the header. Renders the deriveLevers primary as the leading CTA (reusing
ActionButton) with an "ArrowRight · Next best action" framing, or the persona's primaryLocked
message (with a lock affordance) when there is no forward lever. Dispatches through the shared lever
actions, and renders disabled with the honest "not available here yet" affordance when the promoted
lever is unwired.components/workspace/lever-actions.tsx (new) — a LeverActionsProvider + useLeverActions
hook that lifts the dispatch (the DISPATCH map, pending transition, form dialog, confirm dialog,
toast) out of the rail so the header CTA and the rail's secondary levers share one dispatch
owner rather than duplicating it. activate() decides: unwired → nothing; has a form → open form;
consequential without a form (negative/loop) → confirm first; otherwise run.components/workspace/confirm-dialog.tsx (new) — confirm-before-destructive for form-less
consequential levers (archive, flag rework, dispute). Built on the already-exported @sustentus/ui
Dialog (Radix focus-trap + Escape) rather than adding/exporting AlertDialog, which would drag in
a mandatory Storybook story for a primitive only this feature uses. Reason-form levers (qualify out,
reject quote/invoice, …) keep using their form as the confirmation.components/workspace/levers-rail.tsx — now "More actions": consumes the shared dispatch,
renders only the secondary levers (the header owns the promoted one), and adds the a11y treatment.
Semantic ul/li; each lever's accessible name includes an intent word (forward / destructive
/ loop-back / neutral) so intent is not conveyed by the colour dot alone (dot is aria-hidden);
unwired levers announce "not available here yet". Tidy generic empty state when no secondary lever
applies.components/workspace/action-button.tsx — added an optional ariaLabel prop so the header CTA
gets the fuller accessible name "Next best action: …".components/workspace/workspace-view.tsx — wraps the page in LeverActionsProvider, renders
NextBestAction directly beneath the header (one visual header stack), and passes the lever set to
the rail.components/workspace/workspace-header.tsx — removed the dead placeholder
("Next best action appears here." / "wired in a later feature"); stays a pure server identity block.sections.tsx "Requirements (BRD)" → "Requirements"; lib/workspace/levers.ts
customer sign-off hint "Approve your BRD…" → "Approve your requirements…". Status labels and the
journey strip already read from the workflow engine's display names (no raw keys), and section
bodies already carry per-section EmptyStates.next-best-action.tsx, placeholder removed from workspace-header.tsx.LeverActionsProvider dispatch → existing server action → workspace refresh); an unwired promoted lever renders disabled with the honest affordance and mutates nothing.ConfirmDialog. needsConfirm gates negative/loop form-less levers.can()/stage gating from lever-registry/wire-action-levers are consumed unchanged; walkthrough below exercises the drop-off and loop-back paths.ul/li order, disabled levers announced unavailable, dialogs manage focus (Radix).EmptyState; the rail has a generic empty state; the header shows the persona locked message.Each persona is guided to its single next step; drop-off = qualify out, loop-back = return to bidding
/ dispute. Traced against deriveLevers (capability ∩ stage ∩ transition) and the wired set:
awaiting_confirmation: header CTA Accept quote (wired) → work_in_progress;
loop-back Reject quote (reason form) → back to bidding. work_in_progress: Pay invoice
(wired). Fully-served stages show "Nothing needs you right now — we'll be in touch."work_in_progress: Raise invoice / Raise a blocker (form) / Clear blocker
(wired); no forward step → "No delivery action for you at this stage."pending/backlog: Qualify out (reason form, drop-off). work_in_progress:
Approve invoice (wired); Reject/Dispute invoice (confirm/reason); Flag delivery reworked
(confirm, loop-back).completed: Rate the expert (score form); otherwise "Observer view — rate the
expert once delivery completes."apps/web tsc --noEmit run locally on the touched files: no type errors introduced (the
workspace files are clean; unrelated pre-existing errors on the base branch in sdm/vendor/expert
dashboards + demo-data were not touched by this change).Dialog (not AlertDialog) deliberately: adding
AlertDialog to @sustentus/ui would require a Storybook story per convention, for a primitive
only this feature needs.levers.ts logic, LOCKED, the wired/unwired split and gating are unchanged
except one customer-facing hint string.03_release/output/changelog.mdLive entry: apps/help/app/changelog/2026-07-02-next-best-action-polish/page.mdx
See your single next best action on every lead
A lead's workspace now leads with the one thing that most needs you next — your single next best action, shown prominently at the top for your role and stage. When nothing needs you, you'll see a clear "nothing to do right now" message instead of an empty panel, and every other action stays one glance away in the "More actions" list. Anything that can't be undone — archiving a lead, sending delivery back for rework, disputing an invoice — now asks you to confirm first, so nothing consequential happens on a single click. The whole panel is keyboard-friendly and screen-reader-labelled throughout.
03_release/output/investor-update.mdWho it's for: All six roles. What shipped: The lead workspace now leads with each role's single next best action, confirms before destructive steps, and meets the accessibility bar. Why it matters: Guiding each role to its next step completes the one-page lead-to-payout flow (Refine the Bridge · Q2 Objective 3).
It closes the six-part lead-workspace build.
Dig deeper: https://github.com/sustentus/sustentus/pull/573 · https://help.sustentus.com/changelog/2026-07-02-next-best-action-polish
03_release/output/release.mdmain as eec8ec36 on 2026-07-02 (both gates ticked: Spec approved + Ready to merge).c3ce924 — Quality Project ✅, Migrate preview database ✅, Migrate production database skipped (no migration), Vercel Preview Comments ✅.apps/docs page documents the lead workspace / levers (verified by search; same as the lever-registry + wire-action-levers precedents), and the change adds no app, package, route, env var, or build/CI step (the /workspace/[id] route already existed).feature-role-matrix), no new service-journey step. Guiding + confirming existing actions is a UI-surface polish (captured in the changelog), not a change to the documented product model.WorkspaceLinkButton, all six personas, per the wire-action-levers precedent). Changelog entry apps/help/app/changelog/2026-07-02-next-best-action-polish/page.mdx; investor draft at 03_release/output/investor-update.md.web and help-centre production deploys both READY (poll-deploy.sh on eec8ec36)./code-review medium (complexity: standard) — correctness, cleanup/altitude, and conventions angles, verified.
confirm-dialog.tsx hand-rebuilt a confirm dialog on the plain
Dialog primitive on the mistaken belief that AlertDialog was not exported from @sustentus/ui.
It is exported (via the base/index.ts barrel wildcard, which a direct grep of index.ts
missed) and is already the house pattern for every destructive confirm in apps/web (delete
buttons, invoice approvals). Rewrote ConfirmDialog on AlertDialog — gains the alertdialog ARIA
role and matches the codebase convention the stub asked for ("consistent with @sustentus/ui").
(Supersedes the build note's AlertDialog/Storybook reasoning, which was based on the wrong premise.)confirm-dialog.tsx header comment overclaimed "no
consequential action fires on a single click"; positive money levers (pay / approve invoice) run
immediately by design (spec scoped confirm to destructive levers). Reworded to "no destructive
action…".needsConfirm re-checks Boolean(lever.run) && !lever.run?.form, which
its only caller already guarantees. Kept as a self-contained defensive predicate — no behaviour
impact, not worth the churn.workspace-view rendering the client LeverActionsProvider around mixed server/client children is
the canonical valid pattern; only import type + a "use server" module cross into the client),
full DISPATCH↔WIRING parity, both context consumers inside the provider, and the primary/secondary
split (no lever rendered twice, none made dead). Conventions pass returned clean (arrow functions,
type, named imports, sentence case, @sustentus/ui imports, changelog house rules).next-best-action.tsx; workspace-header.tsx placeholder removed.LeverActionsProvider dispatch → existing action → refresh); unwired promoted lever renders disabled with the honest affordance, mutates nothing.AlertDialog (needsConfirm).aria-hidden dot + intent word in the label), ul/li order, disabled announced unavailable, AlertDialog/Dialog manage focus.EmptyState; rail has a generic empty state; header shows the persona locked message.02_build/output/notes.md (six personas, drop-off + loop-back).Read one file beyond the run's own + the diff: an existing AlertDialog consumer
(components/finances/invoice-approvals/invoice-approvals-table.tsx) to match the house confirm
pattern during the review fix. Recorded per the contract.