Skip to content

The node model

A Spor node is one markdown file in the graph home’s nodes/ directory: YAML frontmatter for the structured fields, a short prose body underneath. Here is a decision from a fictional team building a parcel-tracking product:

---
id: dec-carrier-webhooks
type: decision
project: parcel
title: Carrier status updates arrive as signed webhooks, not polling
summary: We ingest carrier status via signed webhooks with a replay window,
because polling every carrier API burned rate limits and lagged by minutes.
status: active
date: 2026-05-14
edges:
- {type: derived-from, to: spec-tracking-events}
- {type: supersedes, to: dec-carrier-polling}
---
Polling was the first design and it worked until the third carrier. Webhooks
give us push latency and one verification path; the replay window covers
carriers that redeliver. Polling remains only as the reconciliation sweep.

A node records a single fact. If you find yourself writing “also” a lot, split it — two facts in one node means one of them is invisible to every edge, filter, and queue signal that would otherwise find it. The body stays short, a few paragraphs at most, written for a reader with zero session context.

The id must equal the filename minus .md, be kebab-case, and start with its type’s prefix: dec- for decisions, task- for tasks, issue- for issues, and so on (the full table is on Node types). An id never changes once created. Everything else about a node can move — its status, its edges, even its title — but the id is the stable reference that edges, briefings, and commit trailers point at.

summary is mandatory, and it is the field most consumers see. When Spor compiles a briefing, most nodes appear at summary resolution; only the nodes that score highest are shown with their full body. Write the summary as one or two sentences that carry the fact and the why on their own:

  • Weak: “Decision about webhooks.”
  • Strong: “We ingest carrier status via signed webhooks with a replay window, because polling burned rate limits and lagged by minutes.”

If the summary only makes sense next to the body, the briefing that shows it without the body will mislead.

The graph home is a git repository, and git is the source of truth for system time. A node’s created_at is the first commit that touched its file; updated_at is the last. Neither is stored in the node bytes, which keeps files byte-identical across reads and makes history tamper-evident.

The frontmatter date field is different: it records when the underlying event happened (the day the decision was made), not when the node was written. Explicit created_at/updated_at frontmatter is accepted as an override for graphs whose git history was squashed or rebased, and date is the last-resort fallback when git has nothing.

Edges are written on the source node as - {type: <edge>, to: <id>} entries. An edge may point at an id that does not exist yet; the compiler skips it, and the dangling reference marks a node worth creating — don’t delete it. An edge may also carry extra flat attributes after to:, such as the per-assignment profile override on an assigned edge (- {type: assigned, to: agent-jo-laptop, profile: profile-reviewer}).

Two optional scalar fields connect nodes to work outside the graph:

  • commits: [parcel-api@1a2b3c4, ...] links a node to the code commits that implement it. Commits are deliberately not nodes — a node per commit would mirror git log and drown the curated graph.
  • wake: YYYY-MM-DD parks a queueable node as dormant until the date arrives, the renew-the-certificate shape — see the decision queue.

Completing work needs a durable why: the resolver gate

Section titled “Completing work needs a durable why: the resolver gate”

Flipping a task to done or an issue to resolved requires a live inbound resolves edge from a decision or artifact node. This is the resolver gate, and it is the node model’s central discipline: the outcome must live on the graph, where its neighborhood can surface it, instead of evaporating into a status flip.

A heavyweight closure earns a decision node (the why). A trivial one earns a few-line artifact (what was done, like a commit message). Either satisfies the gate. A task abandoned as won’t-do is exempt — abandoning produces nothing worth recording.

The gate runs at write time on both create and update, so a node can no more be born done than be flipped there without a resolver. The resolver must also be in a resolving state: an artifact whose delivery status is still in-review or approved keeps the task live; merged, released, or no delivery status resolves it. Which statuses count as resolving is registry data, so a team can retune the bar by editing a schema node — see Schemas are nodes.

When a node is written through the Spor server, the server stamps author: Name <email> and authored_via: mcp|rest|capture|dispatch|gardener from the authenticated identity. Any author supplied in the payload is discarded. Locally written nodes may omit both. The authored_via stamp is the durable machine-vs-human signal: capture marks nodes drafted by the ingestion path, gardener marks automated sweep findings, dispatch marks work written by an agent on behalf of its owner.