A named, published view assembled from a canvas, with a slug, a publisher, and a revision history.
A composition is a named, published view assembled from a canvas. It is the durable counterpart to a workspace: where a workspace is a free-form thinking space with an audience of one and a tendency to be thrown away, a composition is a stable artifact with a slug people link to, a publisher, and a revision history. Its node id is its slug, because ids in a graph file are slugs by convention and minting a separate slug property would give one thing two identities.
Naming a selection and handing other people a link to it is an old move. A saved search, a shared dashboardDashboardData & AnalyticsAn analytics dashboardView reference →, a report definition are all the same instinct, and they fail in the same two ways. Either the view is exported, at which point it stops tracking the data and quietly becomes a screenshot with a decay date, or it stays inside the tool that made it and stops being readable by anything else.
The second failure is the quieter one, because the view keeps working for the person who made it. The composition splits the problem in half. The arrangement is captured once and versioned like a printing plate. The content is never captured: each member's href is re-resolved against current graph data at render, which is what makes a composition a live surfaceSurfaceExperience DesignA place inside a screen, the features that occupy it, and the rule that decides who wins itView reference → rather than a stale image.
At 0.32.0 the last opaque piece was closed. A composition can now declare member_query, a portable statement of what it selects, in the spec rather than in a tool-namespaced bag key. The reasoning is worth keeping: a query living in one tool's own namespace can be read by exactly one tool, so the layer it drives can be rendered by exactly one tool, and views being queries would stop being a property of the standard and become a property of one application.
A researcher spends a week arranging personaPersonaUserAn archetype representing a user segmentView reference → cards on a canvas until the grouping finally reads correctly, then needsNeedUserA user need, pain, desire, or constraintView reference → six other people to see it. Exporting the canvas gives them an image that is wrong within a fortnight. Sending them the canvas gives them a private thinking space that was never arranged for anyone else and that the researcher is still editing.
Publishing it as a composition keeps both halves. The arrangement is frozen at publish under the slug persona-review, so the grouping that took a week to find survives. The cards are pointers, not copies, so a persona edited tomorrow shows edited. When the researcher rearranges and republishes, rev increments and the link does not change, which is the whole reason a slug is worth having.
Membership can be derived instead of placed. A quality board that declares member_query admits every open bugBugProduct SpecificationA defect or unexpected behaviourView reference → automatically, and each admitted member carries derived: true. That flag is not decoration. Membership is derived and position is authored, and any signature meant to answer whether a person changed this composition must exclude the derived half, or a viewer who simply opened the board is recorded as having written to it.
In the Unified Product Graph, CompositionWorkspaceA published arrangement of graph entities that has an identity of its own: a gallery, a board, a deck. Membership can be derived by running its query, position is authored by a person, and both are stored because both are real. lives in the Workspace domain and is containment-free by ruling: it hangs under no parent and anchors relationally instead. What it shows is compositionCompositionfocusesany entitycross-domain, a polymorphic edge that makes the question of which published views show this persona answerable to a tool that cannot parse the publishing tool's URLs. Which canvas it came from reuses composition_focuses_nodeWorkspaceproducedany entitycausal, so provenance costs no new edge.workspace_produced_node
Two 0.32.0 additions matter for anyone building against it. member_query holds a portable selection: entity types, canonical phase ids, six-bucket status categories, tags, property predicates, and a from_focus traversal that deliberately carries no node ids, because the anchor set is the focus edge and the query only says how to walk from it. presentation holds advisory rendering intent, kept separate so that views being queries stays literally true: a selection is a fact about the graph and travels, while a lane arrangement is a preference of the tool that drew it and does not. A consumer that honours the query and ignores the presentation is conformant, and that is the failure mode worth designing for.
Both of those fields are now shared. Since 0.33.0 member_query and presentation are declared on the workspace as well, because the two types are halves of one pair and a canvas is query-driven while it is being worked on rather than only once it is published. The query itself also grew a faithful form. Alongside the named fields, a member query can carry clauses: every clause in the selection, including the negations, declared date windows, and edge conditions that the named fields cannot express. A precedence rule is what keeps the two forms from disagreeing. A reader that finds clauses uses it and ignores the named fields; a reader that finds only named fields lifts them into clauses; a writer emitting both keeps the named fields positive-only, because they have nowhere to put a negation. The named fields are not deprecated by this and are not meant to be: they are the readable form of the common case, and most selections never need a clause. One caution is worth carrying. A date clause over created_at or updated_at reads store metadata, so the window itself is portable but the values it reads are maintained by the store rather than authored.
Both of those shapes tightened at 0.34.0, and each change is about what a consumer is allowed to do when it cannot honour something. presentation gains orphan_disposition, either root or hide. It answers a question 0.33.0's nest_by left open: what becomes of a member the query admitted that the nesting relation cannot reach. Drawing it as an extra root and omitting it entirely were both conformant, and the spec had no way to say which the author meant.
Absent means root, and the reason is measurement rather than taste. On an imported tracker graph of 1,118 nodes and 3,685 edges, one tree selects 218 members and renders exactly one card under hide, and three more fall from 185 to one. Under an absent-means-hide default a conformant consumer would draw a nearly blank tree over 218 matching nodes with nothing to tell the author anything was missing. Under absent-means-root, everything the scope admits is visible somewhere becomes a checkable invariant, and the majority that prefers hiding pays for it explicitly, which is the right way round for a default that decides whether data disappears. The orphans are the dataset rather than an edge case, and the cause is structural: tracker-imported work wires through cycle and project relations, which are reference-axis edges and never nesting, so the containment edges these trees walk do not reach it.
That is also why the field belongs in the spec rather than to the application drawing the tree. Choosing between the two dispositions changes which nodes appear, not where pixels go, and presentation is advisory precisely because a consumer may ignore it and stay conformant. A field deciding whether data disappears cannot be ignored safely, so it has to default to the safe reading when ignored, which is exactly what absent-means-root buys. Presentation stays advisory overall on that basis rather than in spite of it.
The second change is to the clause list. A clause is now a discriminated union on dimension: the type axis carries entity types, and every other axis carries strings. The wire format does not change at all, so there is no migration and no fixture. What changes is that a TypeScript consumer writing a plain string array on a type clause stops compiling. Calling that additive would be dishonest, and a stated break with one known site is better than a silent narrowing with none, which is what shipping nothing would have preserved. The round-trip rule stands regardless of the union, because a union constrains authors and not JSON arriving from a file: the clause list is authoritative, the named fields are a positive-only projection of it, and a reader that rewrites clauses into shorthand and back must refuse rather than quietly drop what it cannot express.
Type-specific fields on BaseNode
member_queryobjectWhen present, membership is DERIVED: members are produced by running this query rather than authored by placement. The clause list is authoritative and the named fields are a positive-only projection of it; since 0.34.0 a clause is a discriminated union on `dimension`, so the `type` axis carries entity types rather than free strings.
This is the portable statement of what the layer shows. On a composition, `CompositionMember.href` remains the publishing tool's own resolved route and stays opaque to everyone else; a member may carry both, and then the href is a fast path while the query is the meaning. A consumer that cannot parse the href can still render the layer, which is the whole reason the declaration is here rather than in a tool-namespaced bag key. A layer with no `member_query` is authored, which is what every composition written before 0.32.0 is. DECLARED ON BOTH HALVES OF THE PAIR since 0.33.0. A layer is query-driven while it is being worked on, not only once it is published, so declaring the query only on the durable composition would make it a fact invented at publish time rather than one recorded.
presentationobjectAdvisory rendering intent for the layer as a whole: `group_by`, `sort`, `layout`, `nest_by`, and `orphan_disposition` (0.34.0, absent means `'root'`). A consumer may ignore it entirely and still be conformant, because every default it then applies is the safe one.
THE DESCRIPTION LISTS THE FIELDS ON PURPOSE. This property is `object` in the runtime property registry, so an agent reading `get_entity_schema` gets an opaque blob and this sentence. For an object-typed property the description IS the declared shape, which is why `check:editorial` hashes it (E.4, 0.34.0) and why a field added to `UPGViewPresentation` without a word here would be invisible to every gate and every agent at once.
membersobject[]The frozen member arrangement, captured at publish. Layout and pointers only, never resolved content.
revnumberMonotonic revision, bumped on each republish of the same slug.
A published revision is a fact about the composition, user-visible as a new print from the same plate, so it is serialised. Worth distinguishing from a store's concurrency token, which shares the name in some backends but is a fact about the table rather than about the thing, and is not spec data.
published_atstringISO timestamp of the most recent publish or republish.
published_bystringPublisher handle or email. Display scalar, same posture as `WorkspaceProperties.owner`.
idstringrequiredUnique identifier (UUID)
typeNodeTyperequiredDiscriminator for the entity type
titlestringrequiredDisplay name
descriptionstringOptional detailed description
statusstringLifecycle status
tagsstring[]Freeform tags for filtering
3 phases, initial: draft