Skip to content

How includes work

An include lets one node show the text of another. You write a passage once, in one node, and every node that includes it shows the current text, so one edit updates them all. A code of conduct, a safety paragraph or a tone guide are the usual candidates.

An include is a single tag inside a node's text:

{{> policies:conduct}}

When anyone reads the node, the tag is replaced by the text of policies:conduct. The exact grammar and every setting are on the include syntax reference; this page is about what the mechanism does and why it behaves as it does.

What is live and what is not

This page describes what is built and merged today. Where a feature is not built yet, it says so. Boot-prompt compilation, the three prompt pointers, the install memory and the worker signature are separate pieces of the same workstream and are not covered here.

What you see when you read a node

Reading a node resolves its include tags, with your own access. You get the included text in place of the tag, and anything nested inside it resolves too. This happens on every read surface: the GraphQL Node.content field (so node, nextNode, search hits, edge ends and mutation payloads all agree; nodeBatch is the one exception, see below), the MCP node reads (hadron_get_node, hadron_get_nodes, hadron_get_next_node), and exports such as a task node exported as a skill.

Resolution happens as text, before the node's {{variables}} are filled in. A variable inside the included text is therefore filled from the includer's data, the same as a variable written in the node itself.

The stored node is never rewritten. The tag stays in the node; only the reading is resolved.

Reading the text as written

Sometimes you need the tags, not the text: to edit the node, or to check what was actually written. Ask for the raw read:

  • GraphQL: content(raw: true), or raw: true on node and nextNode.
  • MCP: pass raw: true to the node reads.

Two reads deliberately do not resolve:

  • Past revisions and restore exports. A past revision returns the text it stored, because resolving it against today's targets would misstate what was stored. nodeExport(full: true) and a memory export also keep the tags, so a restore round-trips.
  • The bulk read nodeBatch. It returns the stored text unless you pass content(raw: false). Some released command-line tools write back what they read through it, and resolving by default would save the included text over their tags. This is permanent, not a transition.

Search matching and embeddings are built from the stored text, never the resolved text. Searching for words that only appear in an included passage does not find the node that includes it. The content a search hit returns follows the ordinary rules above: resolved by default, stored with raw: true.

When an include cannot be shown

If the target is missing, deleted, or not readable by you, the reader sees one notice in its place:

[Include unavailable]

The rest of the node still resolves. The notice is identical for a node that does not exist and a node you may not read. That is deliberate: a reader must never be able to learn that a hidden node exists by watching what the notice says. A target nested too deeply, or a cycle, shows the same notice.

Nesting works to a depth of 10, counting from the node you read: its own tags are level 1, so ten nested levels resolve and an eleventh shows the notice. A cycle shows the notice at the first repeat. A node that is included twice, by two different routes, is fine.

Currently, a target in an encrypted memory you hold no key for also shows the notice, and chat-message and record nodes are not resolved at all. These two are how the code behaves today, not promises the platform makes.

Pinning a revision

By default an include shows the target's latest revision. A tag can pin a revision with rev=N, and can ask for the newest approved or signed text. Of those settings, only rev=N is built; see the reference for the exact state of each.

What a write checks today

When you save a node, Hadron parses every tag in it and checks the ones you add or change:

  • A malformed tag is refused, new or not. So is a tag you add or change that would make the node include itself, directly or through other includes. A cycle that formed later through tags you did not touch does not block an unrelated save.
  • A tag you add or change whose target is missing, deleted or not readable to you is refused, with one error for all three cases, so the refusal tells you nothing about whether a hidden node exists. The error names only the tag you wrote.
  • A tag that pins a revision the target does not have is still accepted. You find out when you read it and see the notice.

Tags that were already in the node are not checked again for access. If you can no longer read the target of a tag you did not touch, an unrelated edit still saves, and the tag keeps its derived edge. Changing a tag's settings, or writing a different form of the tag for the same node, counts as changing it; spacing inside the braces does not.

The check asks whether the target exists and whether you can read its memory. Deciding that never decrypts the target, so a readable encrypted target passes without a session key. Nesting deeper than 10 is not refused at save time; the read shows the notice past level 10.

A known gap: showing the syntax itself

The rule is that a tag inside code formatting, a code span or a fenced block, is literal text, so a document can show an example of the syntax. The parser keeps such a tag literal. The read does not yet: the template step still drops it, so the example text disappears when the node is read. Until this is fixed, do not rely on code formatting to show an include tag; use the raw read to check what a node holds.