A protocol for authoring queries as data. A View is a producer-agnostic request — select instances of a class, filter them, reshape them into an output class — and running one yields an EphemeralPackage: a versionless, content-addressed result that describes its own shape through its imports.
Start here — which View do you want?View is the one base abstraction: a shape over a context class. It has exactly two specializations, and you choose by intent, not by weighing options:
You want computed data back — a query result you can address, cache, or hand to another system. Author a data View (a plain View) and run it; you get a Ephemeral Package. This guide covers that.
You want a rendered page or figure for a resource. Author a visual View — a look.ResourceView / PackageView / PublisherView (a subclass of view.View) attached with deriv.look. That is the kanonak.org/look-conventions guide; do not author projections there.
One base, one decision. The rest of this guide is the data-View path only — if you are styling a resource, stop here and follow look-conventions.
---
A View is a query expressed as data. Instead of asking how to fetch something, you declare what shape you want over which class, and a producer answers it. The request is a View; the answer is a Ephemeral Package. The producer in between is pluggable — the in-graph materializer answers from the local catalog today, and the same View could be answered by a database or an API binding tomorrow without the author changing a line.
A View has two class-level anchors. Bind is the input class — the instances the View iterates. Produces is the output class — what each result is typed as. A Projection bridges them: it computes a Value against a bound instance and stores it under the As property of a produces instance.
AuthorView:type: v.View
v.bind: Author # iterate every Author (subclass-aware)v.produces: AuthorSummary # each result is an AuthorSummaryv.projections:-v.as: summaryName # store under AuthorSummary.summaryNamev.value:# read penName off the bound Authortype: tx.PropertyRead
tx.readSource:{type: tx.VarRef,tx.varName: input }tx.readProp: penName
Run it and you get back the answer as instances of AuthorSummary, each referencing the stable author it came from:
kanonak view run example.org/bib/AuthorView
Because the View is computed entirely from the Expression vocabulary — the same one the rest of the protocol uses — a View introduces no new function language. It is a use of tx.*, not a parallel grammar.
View is one base — a shape over a context class — with two specializations. Choose by what you WANT, then follow only that path: a data View when you want computed data back (a query result), or a visual View (a look view) when you want a rendered page. They are not interchangeable styles of the same task; they answer different questions, so there is never a decision about "which option is better." A data View declares Bind + Produces + Projections and is RUN to yield a Ephemeral Package. A visual View is a look.ResourceView / PackageView / PublisherView (each a subclass of view.View), attached to a class with deriv.look, declaring look.bands; it is RENDERED, not run, and its context comes from the attachment rather than an authored bind. The visual path is the kanonak.org/look-conventions guide — this guide is the data path.
A data View MUST express its output through Projections and MUST NOT carry look.bands; a visual look view MUST express its output through look.bands and MUST NOT carry Projections or Produces. One View serves one intent.
The two specializations share the view.View base and its bind/path-validation machinery, but their output surfaces are disjoint: projections compute addressable data cells, bands render regions. Mixing them in one View is the "menu of options" confusion this convention exists to remove — an authoring tool, or an agent, picks the surface from the intent and never has both in play.
Author a data View (the subject of this guide) only when the goal is a computed, addressable RESULT. When the goal is presenting a resource, author a visual look view per kanonak.org/look-conventions instead — do not reach for v.projections.
Routing by intent at the very first step is what gives an agent one way in. The most common confusion is treating a page layout as a query (or vice versa); naming the fork up front, with one canonical home per branch, prevents it.
The same domain class answered two ways by intent: a data View that RESHAPES Author into an addressable AuthorSummary result, and a visual look view that RENDERS an Author page. Each uses only its own surface.
Value
# WANT DATA BACK → data View (this guide), run it:
AuthorView:
type: v.View
v.bind: Author
v.produces: AuthorSummary
v.projections:
- v.as: summaryName
v.value:
type: tx.PropertyRead
tx.readSource: { type: tx.VarRef, tx.varName: input }
tx.readProp: penName
# WANT A RENDERED PAGE → visual look view (look-conventions),
# attached to the class, rendered (no bind, no projections):
Author:
deriv.look:
type: look.ResourceView
look.bands:
- { type: look.Hero, look.title: penName }
Invalid — one View carrying both a data surface (v.projections) and a visual surface (look.bands). Split it: a data View to produce the result, and a visual look view (on the produces class) to style it.
A View separates the class it reads from the class it writes. Bind is the input context — the instances selected and iterated, with each one available to expressions as the input binding. Produces is the output schema — the class every result instance is typed as. Keeping them distinct is what lets a View reshape data rather than merely echo it: you read Author and emit AuthorSummary.
A data View — one authored standalone and RUN as a query (the subject of this guide) — MUST declare a Bind class. It is the context every Value expression is evaluated against; without it the View has nothing to project from. (A VISUAL look view does not author bind: its context is the class its deriv.look is attached to — see the Pick Your View by Intent convention. That is the only case where bind is omitted.)
The bound class is the data View's domain of discourse. The validator resolves every projection path against it, so a missing bind would leave every path uncheckable — the opposite of the protocol's no-magic-strings guarantee. A look view sidesteps this because the attachment supplies the same context; view.bind is therefore optional ONLY when supplied contextually that way, never for a standalone query View.
A reshaping View SHOULD declare a Produces class that is defined at design time, in the same durable package as the View, with its own properties — never a class synthesized at runtime inside the result.
Presentation binds to classes at design time. A Look declaration — for example a Resource View — names a class and its properties. You cannot attach a look to a class that only exists once a query has run. So if a View's result is meant to be aggregated and visualized, its output class must exist ahead of time for the styling to bind to. This mirrors how search results are typed by design-time classes and styled through look.
Each Projection is a typed bridge from input-space to output-space. Value is any Expression evaluated against the bound input instance — a Property Read for a single property, a Traverse for a multi-step path, a Count or Concat for derived values. As is the output property the result is stored under. Both ends are real references resolved through the object model — never a free-text column name.
Every property a Value reads MUST be in scope for the View's Bind class (the class, an ancestor, or rdfs.Resource), with the context advancing through each Traverse step's range.
A path that reads a property the bound class does not have would silently yield nothing at materialization time. Validating it against bind turns that into an authoring-time error instead of a quietly empty result.
A projection's As property MUST have a domain that covers the View's Produces class. Reuse an existing property where one fits, or define a new one in the View's package with proper domain and range.
The result instance is typed by produces, so the predicate it carries must be one produces instances can legally hold. Validating as against produces keeps the output graph resolvable and styleable rather than asserting foreign predicates.
A projection whose value walks a path — from the bound author, through wrote, to each book's title. The validator advances the context from Author to Book across the traversal, then checks title against Book.
Invalid when produces is AuthorSummary and sku is a property of some unrelated class — its domain does not cover AuthorSummary, so the result instance could not carry the value.
Where narrows which bound instances the View includes. Each predicate is a boolean Expression — typically a Equals, comparison, or Contains over a read — evaluated against the input instance. An instance is kept only when every predicate holds. Filtering reuses the one expression vocabulary rather than introducing a separate constraint grammar.
Each Where predicate SHOULD be an expression that yields a boolean, read against the bound input instance.
Predicates are the selection filter; a non-boolean predicate has no clear keep-or-drop meaning. Reusing Expression keeps one language for both projection and filtering.
Running a View yields a Ephemeral Package — a just-in-time package carrying the answer. The first resource in the package is the package header; everything after it is the response. Each response instance is typed by the View's Produces class, carries the projected values, and points back at the stable source it came from through Derived From — a reference, never a copy. The data that matters lives in what the result points at.
Each result instance MUST be typed by the View's Produces class and MUST carry a Derived From reference to the stable input instance it was derived from.
Typing rows by produces is what makes the result recognizable and styleable; derivedFrom keeps the result a set of references into the authoritative data rather than a detached copy that can drift.
The materialized result of AuthorView. The header is the Ephemeral Package; the le-guin row is an AuthorSummary carrying the projected summaryName and a Derived From reference back to the stable author.
A Ephemeral Package is versionless — semver is meaningless for a per-invocation artifact. Its identity is Content Hash, the sha256: of its body in canonical form. The package name carries a short, address-safe digest derived from that hash, so the colon-bearing hash never has to be parsed as part of an address. Provenance such as Resolved At lives only on the header, outside the hashed body.
A Ephemeral Package MUST NOT declare a version, and MUST record its Content Hash over its body only — never including the header or provenance fields.
Ephemerality is explicit via the type, not inferred from a missing version. Hashing the body alone — excluding Resolved At and the package's own name — is what makes identity deterministic: the same View over the same data yields the same hash, so identical results dedupe and cache cleanly even when produced at different times.
The name q-9d35e0e1fe11e330 is the address-safe prefix of the hash; the full hash lives in Content Hash; Resolved At records when it was produced but does not affect the hash.
A Ephemeral Package imports the transitive closure of every vocabulary its shape uses — the Produces class's package, the packages of the properties it carries, and the packages of the resources it references. A receiver that has never seen the query can fetch those vocabularies and fully understand the result, including styling it through whatever look the produces class declares.
A Ephemeral Package MUST import every package whose terms its body references, so the result is resolvable on its own.
Self-description through the import closure is what lets a result cross a system boundary with no prior schema agreement — the receiver resolves the shape from the imports rather than negotiating it out of band.