Roam Research Docs · Developer documentation
[[links]] and ((refs)) connect them, but that is an untyped graph. Attributes (written Name:: value) add typed relationships on top.
Name:: value assertion is anchored to a real block, the attribute name is a real page, and the value can be another entity that carries its own attributes — so edges are addressable, can be annotated, and can serve as endpoints of further edges.
[entity attribute value], and each of those three positions records both what it points to and where it came from — which is what lets relationships be reified and chained.
[e a v]**
[e-map a-map v-map] = entity, attribute, value.
Name:: resolves to a [[Name]] page.
{:source :value} map (an "sv-map")**
{:source <node> :value <node-or-string>}.
:source is always a node reference — the provenance, i.e. which block wrote this part of the relationship. This is what reifies the edge: the relationship knows the concrete block it lives in, so it can be re-derived when that block changes, and pointed at by other relationships.
:value is the logical target. For e it is the entity, for a it is the attribute page. For v only, it may also be a plain string. (Only the value position can be a string; entity and attribute positions are always node refs.)
:entity/attrs**
:entity/attrs on the entity being described — i.e. on the node that appears as the e :value, not on the attribute block. It is a set: #{ [e a v] [e a v] ... }.
[[Project Apollo]] assert", you pull :entity/attrs off the Apollo page.
:attrs/lookup**
:entity/attrs is a nested blob, which Datascript cannot index by content. So alongside it, each entity carries a flat, many-cardinality ref :attrs/lookup listing every node mentioned anywhere in :entity/attrs.
:attrs/_lookup, is the workhorse for queries. To find every entity with a Status attribute: walk :attrs/_lookup backwards from the [[Status]] page to candidate entities, then check their :entity/attrs for triples whose a :value is [[Status]]. The same trick answers "everything [[Jane Doe]] is a value of".
Name:: value as a plain datom [entity :SomeAttr value]? Native datoms can't carry what Roam attributes need:
[e a v] is just a fact; there's no entity for the relationship itself to reference or hang more attributes on. Roam anchors every assertion to a real block, so the edge is an addressable node — a Datascript attribute gives you no such handle.
:Status) you can't link to, rename, give backlinks, or attach its own attributes to. Roam attributes *are* pages (so the a :value is always a page).
:db/valueType and cardinality in the schema up front; a Roam attribute's value is a string, a ref, one or many — decided per instance by what the user typed.
[e a v] doesn't record *which block authored it* — and the :source on every position is what lets us recompute and retract exactly the triples a block contributes when its text changes. Datascript's only per-datom metadata is the transaction entity, the wrong granularity.
Project Apollo (page-apollo)
Status:: Active (blk-status)
Owner:: [[Jane Doe]] (blk-owner)
Tags::(blk-tags)
[[urgent]] (blk-tag1)
[[backend]] (blk-tag2)
:entity/attrs stored on Project Apollo (page-apollo):
#{;; Project Apollo --Status--> "Active" (inline text becomes a string)
[{:source [:block/uid "page-apollo"] :value [:block/uid "page-apollo"]}
{:source [:block/uid "blk-status"] :value [:block/uid "page-status"]}
{:source [:block/uid "blk-status"] :value "Active"}]
;; Project Apollo --Owner--> [[Jane Doe]] (inline ref becomes a node)
[{:source [:block/uid "page-apollo"] :value [:block/uid "page-apollo"]}
{:source [:block/uid "blk-owner"] :value [:block/uid "page-owner"]}
{:source [:block/uid "blk-owner"] :value [:block/uid "page-jane"]}]
;; Project Apollo --Tags--> [[urgent]] (child block becomes a node)
[{:source [:block/uid "page-apollo"] :value [:block/uid "page-apollo"]}
{:source [:block/uid "blk-tags"] :value [:block/uid "page-tags"]}
{:source [:block/uid "blk-tag1"] :value [:block/uid "page-urgent"]}]
;; Project Apollo --Tags--> [[backend]]
[{:source [:block/uid "page-apollo"] :value [:block/uid "page-apollo"]}
{:source [:block/uid "blk-tags"] :value [:block/uid "page-tags"]}
{:source [:block/uid "blk-tag2"] :value [:block/uid "page-backend"]}]}
:attrs/lookup is the flat, de-duplicated index of everything referenced:
[{:block/uid "page-apollo"}
{:block/uid "blk-status"}
{:block/uid "page-status"}
{:block/uid "blk-owner"}
{:block/uid "page-owner"}
{:block/uid "page-jane"}
{:block/uid "blk-tags"}
{:block/uid "page-tags"}
{:block/uid "blk-tag1"}
{:block/uid "page-urgent"}
{:block/uid "blk-tag2"}
{:block/uid "page-backend"}]
[[Status]]'s :attrs/_lookup includes page-apollo, and you can find Apollo from Status.
Status:: Active) → the v :value is the literal string "Active".
Owner:: [[Jane Doe]]) → the v :value is the referenced node.
[[ref]] child resolves to the referenced page, while a plain-text child resolves to that child block's own node (a block ref), not a string.
[[hello [[world]]]] picks up hello, not the nested world).
blk-owner), you can hang attributes on the edge itself (e.g. Role:: Lead and Since:: 2024 nested under Owner:: [[Jane Doe]]).
blk-owner — i.e. the ownership relationship is now an entity with its own :entity/attrs. Combined with the :attrs/_lookup index, that is everything you need to traverse: from any node, find the relationships it participates in (forward via :entity/attrs, backward via :attrs/_lookup), and each relationship is itself a node you can keep walking from.
markdown version · view in Roam Research · exported 2026-07-03