Repository navigation
feat: add Blocked task status with blocked reason #307
Description
Groomed 2026-10-22 — ready for implementation.
Original Description
Problem
Task statuses are Draft, Todo and Completed (InProgress is proposed in #239). There is no way to mark a task as waiting on something external, such as a person, a decision or another task. Blocked work looks the same as actionable work, so it keeps carrying forward in the journal and cluttering the todo list.
Proposal
- Add a
Blockedstatus (PascalCase, per the enum rule) toModels/Status.cs. - Add an optional
BlockedReason(short text) and aBlockedAttimestamp (UTC,Zsuffix), set on the transition intoBlockedand cleared on the transition out. - Update validators (
TaskItemValidator), the EF migration, DTOs, the regenerated API client (npm run gen:api), and the status-handling code paths inTaskRepositoryandJournalEntryRepository. - UI: see "UI decisions" below. These need to be settled before the frontend work starts.
- Define the transitions. Proposed:
TodoorInProgress->Blocked-> back to the previous status, andBlocked->Completedis allowed. Decide whetherDraftcan be blocked.
UI decisions (to settle before implementation)
Affected surfaces: TaskListRow, TodosSection (journal), TasksPage, ProjectDetailPage, TaskTypeahead, TaskHistoryPanel.
- Setting blocked: how does the user mark a task blocked? Options: a status menu on the row, a dedicated "Block" action, or a keyboard shortcut. The current checkbox toggle is binary (Todo/Completed), so it can't express a third state.
- Reason entry: prompt for the reason at block time (inline input or popover), or allow blocking first and adding the reason later? Is the reason required or optional?
- Visual treatment: badge color, icon and label. It must work in light and dark mode and not rely on color alone. It must stay distinct from
InProgress(feat: add InProgress task status with StartedAt #239) andCompleted. The.journal-page .classnamespecificity rule applies to any new buttons. - Display of the reason: inline under the title, tooltip only, or expand on click? Decide truncation.
- Journal todos: do blocked tasks stay in the todo list, move to a separate "Blocked" group, or appear dimmed in place? Does a blocked task still show the checkbox?
- Tasks page and project page: add a Blocked filter or tab, and decide the sort position and counts. Decide whether project progress counts blocked as incomplete.
- Unblocking: one-click unblock that restores the previous status, and what the user sees if the previous status is unknown.
- History panel: how Blocked/unblocked events and reasons appear in
TaskHistoryPanel. - Accessibility: accessible name for the blocked state, and keyboard reachability of the block and unblock actions.
- Mobile/narrow layouts: where the badge and reason go when the row is tight.
Output of this section: a short decision log (or mockups) in this issue's comments before the frontend work begins.
Open questions
- Should blocked tasks carry forward into the user's today? Proposal: yes, but marked as blocked. Carry-forward must still never move tasks on entry create or switch.
- Should blocked tasks count as incomplete in project progress and weekly review? Proposal: yes, reported separately.
- Should a task be able to reference the task blocking it, or is free-text reason enough for v1? Proposal: free text only.
- Sequencing with feat: add InProgress task status with StartedAt #239: do both in one migration, or land feat: add InProgress task status with StartedAt #239 first? The UI decisions above should account for both statuses together.
Acceptance criteria
- A task can be set to
Blockedwith an optional reason and returns to its prior status when unblocked. BlockedAtis set and cleared correctly, and is UTC with aZsuffix.- The UI decisions above are recorded in this issue, and the implementation follows them.
- Blocked tasks are visually distinct in every list and the journal, in light and dark mode.
- Journal carry-forward behavior for blocked tasks is defined and tested.
- Backend (75%) and frontend (80%) coverage thresholds still pass. Test fixtures use PascalCase (
'Blocked').
Follow-up
The taskflow-assistant MCP needs to expose the new status and reason in list_tasks, update_task and weekly_review once this ships. Tracked in nevridge/taskflow-assistant#12.
Related
#239 (InProgress status)
#308 (journal updates may clear unsent fields; BlockedReason would be wiped the same way unless that is fixed first or the field is merge-on-null)
User Story
As a TaskFlow user, I want to mark a task as Blocked with an optional short reason, and unblock it later so it returns to its previous status, so that work waiting on something external is visibly separate from work I can act on today.
The story is generic over "previous status", so InProgress (#239) follows the same rules as Todo once it exists.
Acceptance Criteria
Setting Blocked (backend)
- A Todo task updated to Blocked with reason "Waiting on Sam" returns
status"Blocked",blockedReason, a UTCblockedAtwith aZsuffix andpreviousStatus"Todo".IsCompletestays false. - Blocking with no reason succeeds, and
blockedReasonis empty. The reason is free text. - A reason over 200 characters is rejected with a validation error. A reason of 200 characters or fewer is saved trimmed.
- Updating a Draft task to Blocked is rejected with a 422 and a specific error code. The task is unchanged.
- Changing only the reason on a Blocked task saves it, leaves
previousStatusandblockedAtunchanged, and writes aBlockedReasonChangedevent. - An update that omits
blockedReasonkeeps the stored reason (merge-on-null). An empty string clears it.
Leaving Blocked (backend)
- Unblocking a task with
previousStatusTodo sets Todo and clearsblockedReason,blockedAtandpreviousStatus. - If
previousStatusis null, unknown or Draft, unblocking sets Todo. - Setting a Blocked task to Completed completes it (
IsCompletetrue) and clears all three blocked fields. The past-day reopen guard (TASK_REOPEN_PAST_DAY_NOT_ALLOWED) is not applied to unblocking. - A task in any status other than Blocked has empty
blockedReason,blockedAtandpreviousStatus.
History and counts
- History shows "Blocked" with the reason, "Unblocked, back to " and "Blocked reason updated", instead of a generic
StatusChanged. - Blocked tasks count in
OpenTaskCountand are also reported in a newBlockedCount.ProjectRowandProjectDetailPageshow the existing open-count format with the blocked part added, for example "7 open (2 blocked) / 12". The "(M blocked)" part appears only when M > 0.
Journal, carry-forward and counts
- A Blocked task on a past day carries into the user's own today and stays Blocked, with its reason and
blockedAtintact. Creating or switching an entry still never moves tasks. - In the "open first" and "done last" sort modes, order is open, blocked, completed, kept stable within each group. Child rows follow their parent. "Manual" mode keeps the user's order.
openTodoCountand the X/Y done/total figures count Blocked as not done. A newblockedTodoCountfeeds the TODOs card, which reads "X/Y · M blocked", with the suffix shown only when M > 0.- Subtask progress reads "2/5 done, 1 blocked", with the blocked part shown only when N > 0.
TaskTypeaheadshows the Blocked badge on blocked tasks.
Journal row behavior
- A Blocked journal row's checkbox is enabled, shows a blocked glyph and has
aria-label"Mark done (blocked)". Checking it completes the task and clears the blocked fields. It looks different from a normal checkbox because completed tasks on past days can't be reopened. - The Blocked badge is a real button with
aria-label"Unblock" and a visible "Unblock" tooltip. Clicking it unblocks the task immediately and restores its previous status (criteria 7 and 8). The change rolls back if the save fails. - A Todo journal row has a real "Block" button, placed after
.todo-historyand before.todo-x. It appears on row hover,focus-withinand:focus-visible. Clicking it opens an inline reason input labeled "Blocked reason, optional". Enter saves, including an empty reason, and Esc cancels. Saving is optimistic with rollback. - After a block or unblock in the journal, focus returns to the row's checkbox.
Tasks page and forms
- Draft rows have no Block button. On Todo rows, a Block button (
aria-label"Block task: <title>") opens the same inline reason entry, trimmed and limited to 200 characters. Blocked is set only through the Block action. Clicking a Blocked row's badge (aria-label"Unblock task: <title>") unblocks it. STATUS_OPTIONSandDEFAULT_STATUS_FILTERinclude Blocked, and a Blocked chip shows a count. Sort order is Draft, Todo, InProgress, Blocked, Completed.TaskFormoffers a Blocked option with a reason field.ProjectDetailPage'sStatusBadgesupports Blocked, and an unknown status shows its raw value instead of "Draft". Status constants come from one shared module.
Visual treatment and accessibility
- In light and dark mode, any Blocked row shows a "Blocked" badge with an SVG icon and a dashed border, plus a 3px left accent. The state is carried by the text and icon, and the color is derived from the
dangertoken viacolor-mix. The reason is a muted single line with an ellipsis, with the full text in thetitleattribute and the history panel. Double-clicking the reason edits it. - On narrow layouts the badge shows the icon plus a short label, the Block button is icon-only with its
aria-labelkept, and the reason input sits on its own full-width line. - These journal classes use the
.journal-page .classnameform (0-2-0) so they beat the.journal-page buttonreset:.todo-block,.todo-blocked-badgeand.todo.is-blocked .todo-check, plus.todo-block-reasonif it sets color or font size.
Cross-cutting
- A journal toggle or title edit on a Blocked task doesn't wipe
blockedReason. Criterion 6 guarantees this whether or not bug: journal todo toggle and title edit may clear task description and due date #308 is fixed first. - Tests cover at least:
- the Block button becoming visible on focus-within
- the checkbox label and glyph
- the badge unblock
- sort tiers in each mode
- the "· M blocked" suffix appearing only when M > 0
- backend criteria 1-11 and 13
- Coverage stays at 75% (backend) and 80% (frontend). Fixtures use
'Blocked', and the regenerated API client includes the new fields. - The UX decisions for this feature are captured in this issue's acceptance criteria and edge cases, and the implementation follows them.
Edge Cases
- Blocked parent with children: confirm that the child-completion guard and parent auto-complete (
TaskItemsController297-301 and 315-348) treat Blocked children as not done, and add a test. - Completed checkbox on past days: make sure the glyph and label make the irreversible completion clear.
- Concurrent edits:
PreviousStatusis recorded server-side on entry, and the last write wins. - Existing data: existing rows get null in the three new columns. Confirm the migration is backward compatible and that
BlockedAtis in the UTC converter. - Repeated carry-forward: a task carried over several days stays Blocked and
BlockedAtdoesn't reset. - Sequencing: feat: add InProgress task status with StartedAt #239 and bug: journal todo toggle and title edit may clear task description and due date #308 touch the same status-handling code, so rebase carefully if they merge first or in parallel.
Out of Scope
- The
taskflow-assistantMCP changes (nevridge/taskflow-assistant#12). - Implementing InProgress (feat: add InProgress task status with StartedAt #239).
- Fixing bug: journal todo toggle and title edit may clear task description and due date #308 generally. Only
BlockedReason's merge-on-null is in scope.
Activity
nevridge commentedon Oct 22, 2026
Manual UX testing checklist for #307 (Blocked status)
Branch:
feature/blocked-task-status. Test in light and dark themes, and once at a narrow width (about 700px or a touch device).Journal
Tasks page and forms
Projects
nevridge commentedon Oct 22, 2026
UX Test Results
PR: #311 | Issue: #307
Branch: feature/blocked-task-status | SHA: e574da5 | Working tree: clean
Run: uxtest-20261022134544 | Date: 2026-10-22
Summary
Total steps: 8 | Passed: 8 | Failed: 0 | Skipped: 0
One usability finding (below) did not fail a step.
Passed Steps
Blockedevent.title. The Blocked row's badge isUnblock task: <title>.Failed Steps
none
Skipped Steps
none
Finding (not a step failure)
Acceptance Criteria Coverage
BlockedReasonChangedeventfocus-withinvisibility not checked)Fixtures and API-driven steps
uxtest-20261022134544-.feat(tasks): add Blocked task status with reason (#307) (#311)