graview-linked-records
graview-linked-records is one of Graview's 31 authoring skills: instructions an AI coding assistant such as Claude Code or Codex reads before it changes a Graview app. What it teaches: Link a record in one Graview app to a record in another app — a relation whose target is another app's kind, read there as whoever is looking, walked by computed fields and rules, its far changes kept in this app's history — and verify it with graview check and by opening it as a viewer who may not see the other app.
Installed into a project by graview skills install ., and read by whichever assistant is working beside you.
The skill
Link a record in another app
A relation usually goes to a kind of the same app. When the thing lives in an app of its own — a purchase scenario that buys a house kept in the house app — the relation goes THERE: its target is @ + the other app's address + / + its kind. Linking picks a record in that app; the other app is never changed. What the far record is, and whether this viewer may see it, is the host's to say, every time, as the viewer.
Do this
- Declare the relation to the other app's kind. In a document:
``json "scenario": { "fields": { "name": { "type": "string", "required": true }, "budget": { "type": "number", "format": "money" } }, "edges": { "forHouse": { "to": ["@nick/house/house"], "cardinality": "one", "description": "the house this scenario buys" } } } ``
The address is the one an app-link field would hold: @owner/app, the id the host knows the app by (@app_kf5eydr9z58jzf/house), or its https address (@https://example.com/a/house/house). Every target names ONE app; mix in a kind of this app, a second app, an inverse or appendOnly, and the check refuses it in a sentence. In TypeScript the link is a field it is kept in: forHouse: linkedRecordField("@nick/house/house").optional() ({ cardinality: "many" } for several). A host that changes a document by edits sends {"op": "add-relation", "kind": "scenario", "relation": "forHouse", "to": ["@nick/house/house"], "cardinality": "one"}, refused by the same finding codes; the studio makes one with its act for linking another app's records.
- Let the record's own edit link one. The link is kept on the record that links, as the far kind and its id —
"house:h1", or a list — so the derivededit-scenariosets it, undoes it, logs it and hides it from a seat a sight keeps it from, like any field. The faces draw a picker that lists what the host finds there for this viewer; an agent finds the value withfind_linkedand passes it to the edit. A declared act may notconnectsit or set it (act-linked).
- Walk it by name.
forHouse.listPricereads the far record's field, in a computed field, a view's block or a rule:
``json "computed": { "price": { "expr": "forHouse.listPrice", "label": "Asking price" } }, "rules": { "within-budget": { "over": "scenario", "when": "budget != null", "require": "forHouse.listPrice <= budget" } } ``
It is read AS THE VIEWER: a field they may not see there reads as nothing. A link they may not open, or not read yet, is said, never thrown — the computed value says why it has none, and a rule that would fail without it is "could not be judged" rather than broken. out('forHouse') does not walk it (rule-linked).
- Show it. A record's page and the scene's record panel draw the far record with its own card — label, describe line, a glance at its fields, its app's name — and
{ "field": "forHouse" }in a card names it. Pressing it opens it in its own app through the host. A viewer who may not open that app sees "linked record (no access)" and nothing of it.
- Hand the host's reader in. The host implements
LinkedRecordsand passes it where it passesresolveApp(mount,<Embed>,GraviewProvider,PagesApp) aslinkedRecords: `resolve(ref, viewer)answers{ address, kind, id, label, describe?, card?, fields, href?, app? },{ denied: true }ornull;find({ address, kinds, query, limit }, viewer)the records it may link;open(ref, viewer)`, optional, opens one in place. `linkedRecordsOver({ "@nick/house": { store, name, opens, href } })from@graview/core/linked` is that host over stores in memory — tests and a host holding both apps use it as is. When the far record changes there, the host callsrecordLinkedChange(store, { ref, fields, app }): one op per linking record, authored by the far app, naming what changed and none of its values, so Activity and the record's history say it.
Then find out whether it worked
pnpm build && npx graview check ./app.gdd.json
npx graview describe ./app.gdd.json # "Linked from other apps" says each relation and how to link oneReport the actual output. What it catches here: linked-target, linked-mixed, linked-apps, linked-inverse, linked-append-only, act-linked, rule-linked. Then open a record that links one as a viewer the host refuses that app, and confirm the page says "linked record (no access)" and shows nothing of it — not its name, not a value worked out from it.
What the check cannot see
- Whether the other app exists, has that kind, or lets this viewer in: the host answers that, every time, and nothing in the document can.
- Whether a far field the expression names (
listPrice) is one the other app has: a field it lacks, or hides from this viewer, reads as nothing. - Whether linking across apps was the right shape at all. If both kinds change together and the same people keep them, one app with a local relation is simpler.