Reactive documents
Reactive documents are wiki pages with embedded live task lists. You insert a Live Task List block into any page (a PRD, a sprint retro, an onboarding doc, a project brief) and it renders a filtered view of the tracker that refreshes on its own. Everyone reading the page sees the CURRENT state of the work, without anyone editing the page.
Think of it as the answer to "this doc is stale". No status paragraph to update, no screenshot to recapture, no "as of last Tuesday" caveat. The tracker is the source of truth; the doc reads from it live.
What a Live Task List is
A small filtered list of tasks embedded inside your wiki page. It shows
one row per matching task with state icon, project chip (like PAR-482),
title, priority, and first assignee. A footer says how many rows are
shown and when it last refreshed.
The block polls the API every 30 seconds. When someone in the tracker closes a task, opens a new one that matches, or changes state, the list updates the next time it refreshes. You can also click the header to force an immediate refresh.
Behind it: a workspace-scoped GET /workspaces/{slug}/reactive/issues/
endpoint that accepts a small set of query params, plus a Tiptap Node
(liveIssues) that a React NodeView renders inside the editor.
When to use it
- PRDs. A "remaining scope" section that answers "how much is left" on every read instead of only on the day it was written.
- Sprint retros. A "carried over" section that shows exactly what slipped, computed from the sprint filter.
- Onboarding pages. A "starter tasks assigned to you" block that
personalises per reader via
assignee_id=me. - Team dashboards. A wiki page that turns into a live status board.
- Weekly digests. A page you never touch that always shows the last week's shipped work.
Insert one
In any wiki page editor, click the Sparkles button on the toolbar. The block inserts with a default filter (everyone's open work in the workspace, capped to 10 rows) and immediately fetches.
Click the block's title to rename it (Enter commits, Escape cancels). Click the gear icon on the block header to open the inline filter picker: State (checkboxes for backlog, unstarted, started, completed, cancelled), Assignee (anyone or me), Priority (checkboxes for urgent, high, medium, low, none), and Limit (1..100). Apply writes the new filter back into the block; a chip row under the header summarises any non-default filter so a reader sees at a glance what is being shown.
Readers of a published wiki page do not see the gear or the rename affordance; those are only exposed while the editor is editable. The block always renders the same list either way.
To insert programmatically:
editor.chain().focus().insertLiveIssues({
workspaceSlug: "acme",
title: "Backend team's open work",
filter: { state_group: "unstarted,started", assignee_id: "me", limit: 15 },
}).run();
The filter surface (v1)
| Key | Type | Notes |
|---|---|---|
state_group | comma-string | backlog, unstarted, started, completed, cancelled. Multi-valued: unstarted,started. |
assignee_id | UUID or "me" | "me" resolves to the caller. |
project_id | UUID | Limits to one project. Omit for workspace-wide. |
priority | comma-string | urgent, high, medium, low, none. Multi-valued. |
label_id | UUID | One label at a time in v1. |
limit | int | Default 20, hard cap 100. |
Filters are ANDed. Any unknown or malformed value is silently
dropped (safer than 400ing a wiki page render on a typo).
API
GET /workspaces/{slug}/reactive/issues/
?state_group=unstarted,started
&assignee_id=me
&project_id=<uuid>
&priority=urgent,high
&limit=20
Auth: workspace member (role 5+). Response shape:
{
"results": [
{
"id": "…",
"sequence_id": 42,
"project_id": "…",
"project_identifier": "PAR",
"name": "Ship the SQLite export",
"state_id": "…",
"state_name": "Todo",
"state_group": "unstarted",
"state_color": "#e0972b",
"priority": "high",
"assignee_ids": ["…"],
"assignee_names": ["Alice"],
"completed_at": null,
"updated_at": "2026-08-02T18:03:11Z"
}
],
"filter": { "state_group": "unstarted,started", "limit": "20" },
"total": 1,
"truncated": false,
"generated_at": "2026-08-02T18:15:42Z"
}
Why the pill polls every 30s (and not WebSocket)
30s is the sweet spot for "feels live" without hammering the API in docs with a dozen blocks open. WebSocket-pushed updates are on the roadmap and slot in transparently once the client sees a workspace event bus (the Yjs sidecar already carries wiki-page + board updates; extending it to tracker events is v2).
Where reactive docs go next (v2)
- Field-mirror block: a wiki heading or paragraph bound to an issue's title or status. Rename the heading, the issue updates. The "docs that don't rot" story becomes bidirectional.
- Formula block:
sum(sprint.42.issues.estimate),count(project.X.goals.open). Observable-style reactive expressions. - Filter builder UI: right now the block ships with a default filter; v2 adds an in-place picker so authors don't need to know the query-param shape.
- WebSocket subscription instead of polling for near-zero latency.