Playbooks
The Reversible Queue Playbook
By Flora May dela Cruz
How to design a prioritized work queue where every action is reversible, every state is legible, and nothing gets lost — for alerts, recommendations, tickets, leads, anything.
Purpose
Every enterprise product eventually grows a queue. Alerts to triage, recommendations to act on, tickets to resolve, leads to qualify, content to review. These queues all suffer from the same failure modes: users can’t tell what’s been touched, the wrong action is destructive, “done” means seven different things, and once an item leaves the active list it might as well be deleted.
This playbook is a small, opinionated state machine and the interaction patterns that go with it. Apply it once and the queue becomes a place where the user can move fast, recover instantly, and trust the system to remember what they decided.
When to use it
- Any prioritized list of work items where the user takes action on each item
- Any feed of system-generated suggestions (recommendations, alerts, nudges, opportunities) that the user accepts, rejects, or defers
- Any inbox where items have a resolution — not just a read state
- Any backlog that grows faster than the user can clear it, so deferral and dismissal matter as much as completion
Skip it when the list is read-only (no per-item action), when items are immutable events in a log, or when the user is supposed to view all of them in order (a true feed, not a queue).
Core framework
The state machine
Every item in the queue is in exactly one of four states. Every transition is one click, one keypress, or one drag — and every transition is reversible for a short window.
┌──────────────┐
│ │
│ ACTIVE │── primary CTA ─► (item-specific resolution)
│ │
└──┬───┬────┬──┘
┌───────────┘ │ └───────────┐
│ │ │
▼ ▼ ▼
┌─────────┐ ┌──────────┐ ┌───────────┐
│ SNOOZED │ │COMPLETED │ │ DISMISSED │
│ until │ │ done │ │ not now, │
│ date │ │ resolved│ │ not ever │
└────┬────┘ └─────┬────┘ └─────┬─────┘
│ wake │ reopen │ restore
└──────────────►│◄────────────────┘
│
(returns to ACTIVE)
Active is the default. The item is in front of the user, awaiting a decision.
Completed means the item is resolved — the user took the recommended action, or marked it irrelevant on the merits. Completed items are the success state of the queue.
Snoozed means not now, but ask again. The user defers the decision to a future date. Snoozed items have a wake time and they will rejoin Active on or after that time.
Dismissed means not now and probably not ever — the user is rejecting the recommendation itself, not deferring the decision. Dismiss typically captures a reason, because dismiss is feedback to the system that generated the item.
The four interaction rules
- The primary CTA is the item’s resolution. Not “view details.” Not “open.” The card’s main button does the most likely correct thing — apply the recommendation, escalate the alert, assign the ticket. Everything else is in the overflow.
- Every state-change is reversible for at least 8 seconds. A toast appears after every transition with an
Undobutton. Undo restores the item to its prior state with its prior position, scroll, and selection. After the window expires, the action is still reversible from the destination state (re-open from Completed, wake from Snoozed, restore from Dismissed) — just no longer one-click from the source. - Items group by state, not by type. Active first. Then Snoozed (with wake date prominent). Then Completed and Dismissed, collapsed by default. The user is rarely interested in dismissed items, but they need to be findable.
- The detail panel does not steal the queue. Clicking “View details” opens a side panel, not a new page. The queue stays visible. When the user acts inside the panel, the panel closes and focus returns to the next active item — not the one they just resolved.
The card anatomy
A queue card carries five pieces of information and exactly one primary action.
┌─────────────────────────────────────────────────────────┐
│ ▌▌ [Severity strip on the left edge — color + shape] │
│ ▌▌ │
│ ▌▌ Type chip · Detected <relative time> │
│ ▌▌ Title — what is the user being asked to decide │
│ ▌▌ One-line description — why it matters │
│ ▌▌ │
│ ▌▌ [Optional small viz: trend, count, mini-chart] │
│ ▌▌ │
│ ▌▌ [Primary CTA — the resolution] [⋯ overflow] │
└─────────────────────────────────────────────────────────┘
The overflow menu (⋯) contains only the three state transitions: Complete, Snooze, Dismiss. Nothing else. If you find yourself adding a fourth, it almost certainly belongs in the detail panel.
Reusable template
The state model (drop into a spec)
### Queue: <queue name>
**Item states:** Active · Snoozed · Completed · Dismissed
**Default state:** Active
**Item identity:** <unique id, stable across state changes>
**Transitions:**
| From | To | Trigger | Captures | Reversible? |
|---------|-----------|--------------------------------|-----------------|-------------|
| Active | Completed | Primary CTA or overflow → Complete | <result data> | Yes — re-open |
| Active | Snoozed | Overflow → Snooze | Wake date (req) | Yes — wake now |
| Active | Dismissed | Overflow → Dismiss | Reason (req) | Yes — restore |
| Snoozed | Active | Wake date reached, or manual wake | none | Yes — re-snooze |
| Completed | Active | Re-open from item | none | Yes — re-complete |
| Dismissed | Active | Restore from item | none | Yes — re-dismiss |
**Visual treatment per state:**
| State | Card variant | Icon | Position in list |
|-----------|------------------------|-----------------|------------------|
| Active | Full card with CTA | severity strip | Top, grouped by severity |
| Snoozed | Muted, wake-date chip | clock icon | Below active, sorted by wake |
| Completed | Muted, action-date chip | check icon | Collapsed section |
| Dismissed | Muted, reason tooltip | strike-through | Collapsed section |
**Toast behaviors:**
| After action | Message | Undo? | Duration |
|--------------|----------------------------------|-------|----------|
| Completed | "<n> item(s) completed" | Yes | 8s |
| Snoozed | "Snoozed until <date>" | Yes | 8s |
| Dismissed | "<n> item(s) dismissed" | Yes | 8s |
| Re-opened | "Re-opened" | No | 4s |
**Bulk operations:**
- Multi-select via checkbox + Shift-click range
- Bulk CTA respects per-item rules (e.g. Snooze still asks for a wake date once, applied to all)
- Bulk Undo restores all items to their prior states atomically
The interaction inventory
A checklist to walk before handoff:
- [ ] Primary CTA resolves the item (not just opens it)
- [ ] Overflow menu contains only state transitions
- [ ] Detail panel opens to the side, queue remains visible
- [ ] On state change, panel closes and focus moves to next active item
- [ ] Every action shows a toast with an Undo button
- [ ] Undo restores prior state, prior position, prior scroll
- [ ] Snooze requires a wake date (asks before committing)
- [ ] Dismiss requires a reason (asks before committing)
- [ ] Snoozed items rejoin Active automatically at wake time
- [ ] Completed and Dismissed sections collapse by default
- [ ] Empty state for "no active items" celebrates without misleading
- [ ] Filters preserve the user's selection across state changes
- [ ] Keyboard: J/K navigates items; E completes; S snoozes; D dismisses; U undoes
AI-assisted workflow
AI is most useful here for generating the recommendations themselves, summarizing items, and suggesting actions — not for managing the queue mechanics.
When the AI proposes an item
If the items in your queue are AI-generated (recommendations, anomaly alerts, smart suggestions), each proposal needs three things adjacent to the card body:
- A plain-language reason for why the item appeared — what signal triggered it
- A confidence indicator — a score, a label (“strong” / “needs review”), or both
- A correction path — a thumbs-down or “this is wrong because…” that feeds back into the model
A dismissed item should always send a signal to whatever generated it. Otherwise the user is dismissing the same recommendation every week.
Prompt: triage assistant for the user
You are a triage assistant for an enterprise queue of <item type>. Given the
list of active items below, group them into three buckets:
1. Act now — items where the user should take action today
2. Defer — items that can safely be snoozed and the suggested wake date
3. Likely dismiss — items where context suggests the recommendation is not
applicable, with a short reason
Do not invent fields. If you can't determine bucket for an item, place it
in "Act now" and flag with `?? — needs review`.
Items:
<paste structured item list>
Prompt: dismiss-reason classifier (for the team running the queue)
Below is the free-text reason a user gave when dismissing a recommendation,
plus the recommendation title. Classify the reason into one of:
- `not applicable` — the recommendation does not apply to my situation
- `already done` — I've already addressed this outside the system
- `wrong target` — the recommendation is aimed at the wrong object
- `too risky` — I understand the recommendation but the cost is too high
- `low value` — I see the recommendation; it's not worth my time
- `other` — none of the above; provide a one-line summary
Output: { classification, confidence, optional_summary }
Refuse to invent a classification if the reason is empty.
The classified dismissals are the queue’s most valuable feedback loop — they tell the team running the queue which kinds of recommendations to stop generating.
Collaboration considerations
- For PMs: the queue is a product surface, not a list. Treat the state machine as part of the PRD. Specify what “completed” means on the merits — what changed in the underlying system when an item leaves Active. Specify whether snoozed items can be re-snoozed and whether there is a maximum defer time.
- For developers: every state transition is an idempotent operation against the item’s identity. Build state as a server-side enum, not as a client-side filter. Bulk operations are one request, not N. The toast’s Undo is a real transaction — design the API so the prior state can be restored exactly.
- For research: test the language of the three overflow actions. “Dismiss” vs. “Ignore” vs. “Reject” vs. “Hide” are not interchangeable. Users will assume the most destructive interpretation if the word is unfamiliar.
- For accessibility: the toast is a live region. Undo must be reachable by keyboard before the toast dismisses. Bulk-select state is announced. Snooze and Dismiss confirm dialogs follow the focus-and-return pattern from the accessibility annotation playbook.
- For data/ML: if items are model-generated, dismiss-with-reason is a labeled training signal. Wire it.
Common failure patterns
- “Mark as read” instead of “complete.” A queue is not an inbox. Read state ≠ resolution. If your queue lets items disappear because they were opened, the user will lose track of what they actually decided.
- Destructive primary action. The most prominent button on the card is “Dismiss” or “Delete.” Users now click cautiously instead of moving fast. The primary action should be the helpful one.
- No reason on dismiss. The system can’t learn. Users dismiss the same recommendation forever. Worse, the team running the queue can’t tell which recommendations are bad.
- Snooze without wake. Items snoozed “forever” or with no date are just hidden. They never come back. Snooze without a wake date is dismiss with extra steps.
- The “Resolved” graveyard. Completed items are out of sight and out of mind. There’s no way to re-open. The user is paranoid about completing because they’re not sure they can recover. Re-open from any state must be one click.
- No undo. Bulk Dismiss with no undo is the queue’s worst-case bug. One slip and the user has lost a day’s work and has no recovery path short of a support ticket.
- Filters that reset. The user filters by severity, acts on three items, and the filter resets. They’re now staring at the wrong list. State changes should not invalidate filters.
- Detail-page navigation. Clicking an item navigates away to a detail page. The user comes back to a different scroll position, a different sort, sometimes a different filter. Use a side panel.
- Severity that lies. Everything is
Highbecause the system can’t distinguish. Users learn to ignore severity. Severity must be earned — either tune the generator, or collapse to three levels max. - Toast that disappears too fast. 3-second toast. AT users miss it. Mouse-driven users see it flash. Anything below 6 seconds is hostile to undo.
Generalized example
A fictional content-moderation review queue for a community platform. Moderators see flagged posts and decide whether to remove, hide, allow, or escalate.
State machine. Each flagged post is an item.
- Active — flagged, awaiting moderator decision
- Completed — moderator decided (removed / hidden / allowed / escalated)
- Snoozed — needs more context (e.g. “let it sit for an hour, see if more reports come in”) — wakes at chosen time
- Dismissed — moderator says the flag itself is invalid; the post stays and the flag is what’s rejected, with a reason that feeds the auto-flagging model
Primary CTA. Each card’s primary button is the most likely resolution given the auto-classifier’s confidence:
- High-confidence harmful → Remove
- Low-confidence borderline → Allow (the default, because over-moderation has a high cost in community platforms)
- The CTA label is set per-card by the upstream classifier; the moderator always has the overflow to choose another action.
Severity strip. Three levels — Severe (immediate harm), Moderate (policy violation), Low (borderline). Severity is paired with a shape (triangle / circle / square), so colorblind moderators read it identically. Severity sorts within the Active section: Severe at the top.
Detail panel. Clicking a card opens a side panel with the full post, the reporter’s stated reason, the auto-classifier’s signals, and the moderator’s action history on this user. When the moderator acts, the panel closes and the next active item slides into focus.
Toast. “Removed 3 posts. Undo” — 8-second dwell, dismiss-on-outside-click, persistent in the recent-actions log accessible from the top bar. Undo restores all three posts to Active and unflags any classifier feedback that was sent.
Dismiss with reason. When a moderator dismisses a flag, they pick a reason: not a violation, already handled, wrong post, false report. Those four categories go straight to the team owning the auto-flagger. After two weeks, the team can see “47% of flags on posts with <signal pattern> were dismissed as not a violation” — that’s the next tuning iteration.
Filters. Severity, reporter type, post age, classifier confidence. Filters persist across state changes. A moderator who’s filtering for Severe + last 6 hours keeps that filter as items flow through.
Bulk. Moderators select multiple cards with checkboxes. Bulk Remove asks for a single reason applied to all. Bulk Undo restores all of them atomically.
Empty state. “All clear. Nice work.” With a small chart showing today’s pace and a link to the dismissed-flag analytics — because in moderation, what didn’t need action is as informative as what did.
Public-safe review (verified before publish)
- No employer or client product names, codenames, or org names
- No customer names, segment sizes, or identifiable details
- No internal metrics, thresholds, OKRs, or telemetry numbers
- No roadmap, ship dates, or future plans
- No architecture, service names, API shapes, or schema fields from real systems
- No screenshots showing real chrome, real data, or recognizable surfaces
- No internal-only workflows, tools, or terminology
- Every example is fictional or abstracted; numbers are illustrative
- A peer outside any employer could read this and learn nothing proprietary
Take this playbook with you
Drop your email to copy the markdown or download the file. One email unlocks every playbook in the Toybox.
No spam. Occasional notes on new playbooks. Unsubscribe in one click.