Skip to main content
Relationships between items are first-class typed edges, not embedded references. An edge has a source item, a target item, an edge type, and an optional properties object. Edges are stored separately from items and queryable from both ends.

Edge shape

Edges sit in the same space as their source and target items. Cross-space edges don’t exist.

Querying edges

Edges are queryable from either direction, filtered by type.
The query language shorthand filters items by edge membership:

Listing edges by type

For taxonomy-style traversals — “every reply”, “every annotation”, “every parent-of edge” — list edges globally across the space:
Cursor + limit pagination, identical to the per-item edge listings. Space-scoped. This replaces the walk-every-item N+1 pattern that clients otherwise have to write to count or display all edges of a type. SDKs expose it as client.edges.list({ edge_type }).

Core edge types

Eight types ship in the core set. Each has documented constraints. Cardinalities are enforced at edge-creation time. Creating a second parent-of edge with the same target (child), or a second in-thread edge with the same source (member), is rejected with 400 edge_constraint_violation.

derived-from vs supersedes

Both point backward from a new item to an older one. They mean different things.
  • derived-from — “this came from that; both are still valid.” Multiple derivations from the same source coexist. Use for AI-generated alternatives, transformations, parallel takes.
  • supersedes — “this replaces that as the current version.” Linear chain, no branching. Cycles are rejected at edge-creation time. Use when one version is meant to take over from another.
Both can coexist on the same item. An AI-cleaned version of a note can be derived-from AND supersedes the original — it was produced from it and replaces it.

Cleanup, derivation, and replacement patterns

When one item produces another — an AI cleanup pass, a transcoder, a translator, a refinement — the choice of edge depends on the relationship being modeled. Four core edges apply, each capturing a different aspect: These aren’t mutually exclusive. An AI-cleaned version of a transcript can be derived-from the raw (provenance), supersedes the raw (replaces as current), AND in-thread a series of cleanup attempts (iteration history). The combination expresses what the integration is actually modeling. supersedes carries the cleanest semantic for replacement — the predecessor still exists (Marfa doesn’t delete A) but B is the “current” version. Use it when versions chain linearly with clear succession.

Cascade on delete

Each edge type declares a cascade_on_delete behavior: parent-of cascades by default (deleting a parent deletes children). Other core edge types default to orphan.

Threads are implicit

There is no core.thread type. A thread is whatever item is the target of a bunch of in-thread edges — its type gives the thread its semantics.
Example target types:
  • A chain of reply notes: core.note items in-thread to an opener core.note.
  • An ordered reading queue: core.bookmark items in-thread to an app-registered app.reading_list.
  • A timeline of a trip: various types in-thread to a core.event (the trip).
The in-thread edge is many-to-one on the source side: each item can be in at most one thread. Ordering uses the edge’s position property. Query a thread:

Grouping

Three grouping primitives, each with a narrow purpose.
  • Tags — flat labels for cross-cutting classification. See Metadata.
  • Threads — ordered sequences via in-thread edges. Single membership per source.
  • parent-of hierarchy — tree structure. Source is parent, target is child. Used both for natural hierarchy (folders, projects) and for app-registered group items (a karakeep.list, an albo.collection, a user.wine_cellar) — group is the parent, members are children.
Collection-like features in apps register a custom group type and use parent-of edges to connect members. The core stays out of the opinionation business on “what is a collection” — apps answer that differently.

Custom edge types

Apps register their own edge types when the core set doesn’t fit:
Admin only. Core edge names are reserved and collide with 409 conflict. Custom edge types don’t inherit. See POST /edges/types for the full schema.

Atomic item + edges write

A single POST /items call creates the item and its edges in one transaction:
Either the whole write succeeds or nothing is persisted.

Permissions

Edge writes are dual-gated: the caller needs both write permission on the source item’s type AND write permission on the edge type. Admin keys bypass both. See Permissions.

Reading across edges

The filter language doesn’t do graph traversal — an edge clause narrows the item list by edge membership, but it doesn’t return the connected items. Reading an item’s edge targets is a two-step flow: fetch the item (which hydrates edges inline), then fetch the targets by id. This is a real trade-off. Polymorphic references where one item points at some other typed item — a shopping-list entry that references a book, a wine bottle, or a gadget — carry the ceremony of that second call. In exchange, edges are queryable from both ends, constrained per type, and space-scoped alongside items. Clients that need a single round-trip batch-fetch targets after the first response and join client-side.