Lenses, program view, and workflows
Three surfaces turn the graph into something you can look at and act through: lenses (saved views), the program view (an auto-derived progress tree), and workflows (reviewable automation DAGs with tracked runs).
Lenses: saved views as nodes
Section titled “Lenses: saved views as nodes”A lens is a lens- node whose body carries fenced blocks — the view
definition is data in the graph, versioned and shareable like everything
else:
---id: lens-release-radartype: lensproject: parceltitle: Release radarsummary: Open work gating the carrier rollout, grouped by status.status: activedate: 2026-06-05---
What still gates the rollout, at a glance.
## query
```json{ "traverse": { "from": "task-carrier-rollout", "follow": ["blocks"], "direction": "in", "depth": 2 } }```
## render
```json{ "as": "board", "group": "status" }```The ## query block selects and traverses; ## render chooses the shape
(list, table, tree, board); an optional ## actions block declares
write affordances bound to registry transitions (a “close” button is a
declarative status change, gated exactly like any other write); and an
optional ## custom block is a sandboxed render function for the rare view
the declarative catalog cannot express. The JSON blocks are data — you can
ask “which lenses select on this status?” — and running a lens is a pure
function of the graph snapshot, so the same graph renders the same bytes.
Render a lens with spor lens <id>, the render_lens MCP tool (calling it
with no id lists the catalog), or in a browser via the server’s render
route. A read-only, expiring share link can be minted for teammates without
a checkout (spor share <lens-id>); a shared link never carries a
write-capable credential.
A workspace- node composes several lenses into one layout — a ## layout
block naming lens slots — and renders as a single view tree, so a team
dashboard is itself a node.
The program view: progress from blocks topology
Section titled “The program view: progress from blocks topology”For “where does the workstream stand?”, no lens authoring is needed. Given
any root node — an umbrella task, a milestone, anything other work
blocks — the program view walks every node that blocks it, transitively,
and derives each node’s bucket from the same truth the queue uses:
- done — terminal status, superseded, or retired by a live
resolves/answersedge (counted even while the status field lags); - blocked — live but gated by its own live unresolved blockers;
- active — live, unblocked, started;
- open — live, unblocked, not started.
The result is a progress bar plus a gating tree. Shared blockers render once
and repeat as marked leaves, counted once; depth and size caps report what
they skipped rather than truncating silently. A root that nothing blocks is
a successful empty result telling you how to model the program: add blocks
edges from the gating work.
Workflows: automation as reviewable nodes
Section titled “Workflows: automation as reviewable nodes”A wf- node defines a repeatable automation as a DAG of steps, carried as a
fenced JSON block (inputs, steps, concurrency) with an optional sandboxed
routing function. Because a workflow is a node, it is versioned, attributed,
and reviewed like everything else — and it is proposal-gated: created
through the server it lands as proposed and inert, and a different
identity must activate it. An agent may author a workflow; it may not
deploy one.
Each execution is a run- node (performs → wf-...) recording state and
lineage; triggered-by records what set it off. Starting a run
(spor run <workflow-id> or the run_workflow MCP tool) only creates the
run: workers — anything with a token — then claim ready steps over the claim
API, do the work, and report a verdict. Step claims are leases like
task claims: an expired lease frees the step, and a
stale worker’s late report conflicts instead of overwriting the recorded
outcome. Approval steps are excluded from worker-claimable work; they
surface in the decision queue for a person. Live runs that get stuck surface
in the queue too.
Run nodes are engine-managed: their state can only advance through the run engine’s claim/complete path, so a step cannot be hand-flipped to succeeded through the ordinary write API.