ADR 0038 — Task center (/tasks)
- Status: accepted
- Date: 2026-10-04
- Slice: P38 (builder-roadmap: DSL-first, codegen-first, UI-first)
Context
Workflows with a human node pause their run waiting for an answer (the
L6 HITL pause/resume seam). The only surfaces for that pause were
per-workflow REST endpoints — GET /api/workflows/{slug}/pauses and
POST /api/workflows/{slug}/resume/{id} — which (a) require knowing the
workflow slug, and (b) have no UI at all: a paused run is invisible
outside curl. As more workflows gain human gates, the operator needs
one place that answers the question "what is waiting on me?" across the
whole app.
Decision
1. GET /api/tasks (shared server codegen)
When any workflow has a human node, the generated server (app and
web lens — the same server_rs function) gains
GET /api/tasks → { "tasks": [ { workflow, pause_id, node, question, created_ms }, … ] }
— every paused run across all workflows (oldest first, the
BTreeMap key order), each entry naming its workflow so the consumer
can link to and resume it. The per-workflow pause/resume endpoints stay
unchanged. A pub fn paused_tasks() accessor exposes the same shape to
the in-crate (SSR) side; the handler wraps it.
2. The task center page (web codegen)
The web lens gains pages::TasksPage at /tasks (plus a tasks nav
entry with a check-square icon), both emitted only when a human node
exists:
- SSR reads
crate::server::paused_tasks()synchronously, so the paused table renders on the first paint (theSchedulesPagedual-mode pattern); the client refetches through/api/tasksafter hydration. - Each task row shows workflow, node, and the rendered question, plus an
answer input and a resume button that POSTs
{"answer": …}to the workflow's resume mount — the answer is sent as JSON when it parses, as text otherwise, and empty falls back to the server default (true). Success flashes a toast and reloads, so the answered run drops off the list; an error flashes inline. - An empty center renders "no runs are waiting on a human answer".
Consequences
- A paused run is always visible in one page, for every workflow, with a one-click answer path — no slug knowledge, no curl.
- The task list is process-local (like the pause store itself): it reflects the pauses of the running server instance.
- The page/endpoint are fail-closed on the same gate as the pause
endpoints: no human node → no
/api/tasks, no route, no nav entry. - The page's SSR HTML is e2e-assertable (GET
/tasks→ 200 + markup), continuing the P37 pattern for web slices.
Verification
- Render tests: the route, nav entry, component, and
GET /api/tasksare emitted for a workflow with a human node, and absent without one. The existinghuman_node_emits_pause_resume_infratest now also coverspaused_tasks/list_tasks/ the route. - Full-stack e2e (app block):
/api/tasksis empty after a resume, lists the new pause (workflow + pause id) after a run, the answer resumes the run, and the list clears afterward. - Full-stack e2e (
full_stack_webblock):/tasksSSR-renders the empty state, then the paused run (rendered question) with its resume control after a run pauses at the human node. - Conformance re-pinned: full-stack only (the only example with a human
node) — app + web
server.rs(endpoint), webmain.rs(route), webpages.rs(nav + page).