Unified Path gives you a single, consistent way to reference or embed anything in your MeshWeaver application — images, markdown files, layout areas, data schemas, and more — using a compact @ notation that works in markdown, autocomplete, and agent tool calls.

The bigger picture: this page is the syntax. For why one addressing scheme is the backbone of the whole platform — how it lets an agent go from a plain-English ask to a precise, typed, permission-checked action — see Addressability of Objects.

Unified Path Anatomy @address / prefix / path address MeshNode path prefix reserved keyword or collection path specific resource @ — Hyperlink (navigate) @Doc/DataMesh/QuerySyntax Opens the target node in the browser Works in markdown links and autocomplete @@ — Embed (render inline) @@content/logo.svg Renders content directly in the page Images, markdown, layout areas, data

Unified Path anatomy: every @ or @@ reference is composed of an address, a prefix, and a resource path.


Path Resolution

Every @ reference resolves to a mesh node path. Paths can be relative (to the current node) or absolute.

Style Syntax Example from Doc/DataMesh/CRUD
Child ChildName SubPage → Doc/DataMesh/CRUD/SubPage
Sibling ../SiblingName ../QuerySyntax → Doc/DataMesh/QuerySyntax
Parent's sibling ../../Name ../../GUI/Editor → Doc/GUI/Editor
Absolute /Full/Path /Doc/GUI/Editor → Doc/GUI/Editor

Relative paths treat the current node as a container — use ../ to navigate up. Absolute paths start with /.

These rules apply equally to @/@@ references and to standard markdown links [text](path).


Pattern

Every Unified Path follows this structure:

{address}/{prefix}/{path}
Component Description
address The MeshNode path, resolved via MeshCatalog
prefix A reserved keyword or a content collection name
path The specific resource within that address

Compatibility note: The legacy {prefix}:{path} colon format is still supported for backward compatibility. The slash form is preferred for new content.

Reserved Keywords

These prefixes have built-in meaning and map to specific layout areas:

Prefix What it accesses
data/ The node's Content data as JSON
schema/ The ContentType schema
model/ The data model
area/ A named layout area
content/ Files from the "content" collection
menu/ The menu structure

Content Collection Prefixes

Any prefix that is not a reserved keyword is treated as a content collection name. Collections store files (images, documents, markdown, etc.) associated with a mesh node.

Example prefix Description
content/ The "content" collection
assets/ The "assets" collection
files/ The "files" collection

Collections are registered in hub setup using AddFileSystemContentCollection, MapContentCollection, or similar methods:

config.AddFileSystemContentCollection("content", sp => "./content")
      .AddFileSystemContentCollection("assets", sp => "./assets")
      .MapContentCollection("avatars", "storage", "persons/avatars");

Listing and Downloading Collection Files

The get surface (MCP tool, agent plugin, REST) reads collections through the same unified path. The shapes, from listing to file download:

Goal Path Result
List the collection root @Node/content/ CollectionItemInfo[] — files and folders (isFolder: true)
List a subfolder @Node/content/content/SubFolder items of /SubFolder
Download a file at the root @Node/content/file.ext file content
Download a nested file @Node/content/content/SubFolder/file.ext file content
Named collection @Node/assets/logo.png file from the "assets" collection

The doubled segment in @Node/content/content/Sub/... is not a typo: the first content is the reserved prefix that switches into collection mode, the second segment names the collection. For files at the collection root the short form (@Node/content/file.ext) works because a single trailing segment is tried as a file in the default content collection first. When a path returns "Content collection 'X' not found" for what is actually a folder inside content, you used the short form on a nested path — switch to the doubled form.

Folder names with spaces need no quoting or URL-encoding: @Node/content/content/Trainingsdaten/Data Extraction is valid — segments are split on / only.

Writing goes through upload with the mirror shape {nodePath}/{collection}/{filePath} (nested paths allowed, collection must have IsEditable = true): upload @Node/content/Export/result.txt <base64>.

Size ceiling. One ceiling holds for every upload transport: a file of at most 200 MiB (decoded bytes), UploadLimits.MaxUploadBytes in MeshWeaver.Mesh.Operations, and a larger file is refused by name (Error: the file is N bytes; the upload ceiling is …). REST POST /api/mesh/upload declares its request-body limit from it (the file plus its multipart envelope), so a request inside the ceiling is not cut at Kestrel's 30 MB default. The MCP /mcp endpoint is mapped in MeshWeaver.Plugins and takes its limit from the same constant (UploadLimits.Base64JsonRequestBodyLimit, the file's base64 length plus the JSON-RPC envelope) in its own change; on an image without that change, /mcp still carries Kestrel's 30 MB default, i.e. about 22 MB of file. Both transports buffer the whole file in memory, and base64 adds a third — prefer the REST route for large files.

Binary formats: what gets extracted, what does not

get runs binary documents through registered IContentTransformers before returning text:

Extension Behavior
.docx Converted to markdown (DocSharp transformer)
.pdf Extracted to page text (PdfPig transformer)
.xlsx, .xls Converted to markdown tables, one per worksheet (ClosedXML transformer)
.md, .csv, .txt, source files Returned as text verbatim, bounded — see below
images (.png, .jpg, .gif, .webp, .tif/.tiff, …) Never returned as bytes. get returns the image's Document node (name + AI description) when content indexing has captioned it; otherwise a short placeholder (name, media type, size).
other binaries (.pptx, .zip, audio, video, fonts, … and any file whose bytes are not text) Never returned as bytes. A named placeholder: [binary file: deck.pptx (PowerPoint presentation, <media type>, N bytes)], the reason, and how to read it instead

The .docx, .pdf and .xlsx/.xls transformers live in the optional MeshWeaver.Markdown.Export module. On a host without it those formats fall under "other binaries" and answer the named placeholder, never raw bytes.

Binary detection. FileContentProvider reads every file that has no transformer and is not a raster image through ContentCollection.GetBoundedText. That read refuses to decode a file in two cases. The first is a known binary extension (BinaryContentSniffer.KnownBinaryTypes), which is decided without reading the content. The second is a leading 8 KiB sample that contains NUL bytes or non-whitespace control bytes. A protobuf message is valid UTF-8 with no NULs, and it is still binary. A byte-order mark still selects UTF-8, UTF-16 or UTF-32 text. The 4-byte UTF-32 marks are checked first, because the UTF-32LE mark begins with the UTF-16LE one. Legacy Latin-1 text with no control characters is still read as text. The sample only lets a binary file be refused early. The encoding that decodes the text is decided again over the whole bounded head, so a Latin-1 é that first appears after 8 KiB is not decoded as UTF-8. A store that reports no length, such as a non-seekable stream, is never drained to measure the file. The placeholder then says size unknown, and the truncation marker says the file is larger than the bytes returned. A file of this kind used to be decoded raw. In one case an agent's Get of a .pptx put the ZIP container's bytes into the conversation, and the provider refused the next model request for its size (issue #7064). Below such a cap, the model would have worked from garbage without any error.

Size bound. A text read decodes at most FileContentProvider.MaxTextBytes (256 KiB). A larger file returns its head followed by an explicit marker, [truncated: big.txt is N bytes; only the first M bytes are returned …], so the cut is never silent. A transformer's converted text is bounded to the same number of characters and carries the same marker. A row limit (numberOfRows) is a cut the caller asked for, so it carries no marker. ContentCollection.GetContentAsText stays unbounded for callers that are not a model or a wire, such as the layout area that renders a converted document.

.pptx has no text converter. Its OpenXML parsing would belong in MeshWeaver.Markdown.Export, next to the .docx transformer. The content-collection core deliberately carries no office-document dependency, so core answers .pptx with the named placeholder.

The transformer seam is a single registration (AddContentService), so the agent Get tool and the MCP get tool return byte-for-byte the same text for a given file — there is no second code path.

For a binary format with no transformer, two reliable patterns:

  1. Server-side executable Code node (ExecuteScript): resolve IContentService, open the stream, and parse in-process — no bytes cross the agent boundary. Print results to the activity log or create mesh nodes directly.
  2. Lossless export through the collection: a script converts the file to Base64 text and saves it back via collection.SaveFileAsync(...); get on the resulting .b64 file is lossless text, which the client decodes back to the original bytes.

A note on collection resolution inside scripts: the kernel's IContentService does not know node-scoped collections by their plain name. Fetch the node's collection config first and register it under a qualified name — the same handshake upload performs:

// 🚨 ObserveCompletion, never Rx's ToTask bridge (forbidden repo-wide, 2026-08-30) and never a
//    bare `await Mesh.Observe(...)`: both resume the rest of this script INLINE on the mesh
//    thread that delivered the response. FirstAsync (not Take(1)): the next line dereferences
//    delivery.Message, so an empty completion must FAULT rather than settle with null.
var delivery = await Mesh.Observe(
        new GetDataRequest(new ContentCollectionReference(new[] { "content" })),
        o => o.WithTarget((Address)"MySpace"))
    .FirstAsync()
    .ObserveCompletion(
        ex => Log.LogWarning(ex, "collection config read faulted AFTER the wait settled"),
        Ct);
var cfg = JsonSerializer.Deserialize<ContentCollectionConfig[]>(
        ((GetDataResponse)delivery.Message).Data is JsonElement je ? je : default,
        Mesh.JsonSerializerOptions)
    .First(c => c.Name == "content");
contentService.AddConfiguration(cfg with { Name = "MySpace/content", Address = (Address)"MySpace" });
var collection = await contentService.GetCollectionAsync("MySpace/content", ct);

Files are also served over HTTP at /api/content/{address}/{collection}/{filePath} — the access-controlled content route: the owning node's hub gates every request on Read, and the response is never shared-cacheable. Useful for download links inside markdown, not for agent tooling. (/static carries application build assets only and applies no access check — never point it at content.)


@ vs @@ — Navigate or Embed

Two operators, one simple distinction:

Syntax Behavior
@path Hyperlink — navigates to the content
@@path Inline embed — renders the content in place

Important: References must appear at the start of a line.


⚠️ @/ Is Local-Only — Never Use It in URLs or href Attributes

The @ prefix is a Unified Content Reference — it exists inside markdown, autocomplete, and agent tool calls. It is never part of an HTTP URL or an HTML href attribute.

Context Correct Wrong
Native markdown link [Reinsurance](@/Systemorph/Reinsurance) — (Markdig strips the @ automatically)
Raw HTML inside markdown <a href="/Systemorph/Reinsurance"> <a href="@/Systemorph/Reinsurance">
HTTP URL / shared link https://portal.example.com/Systemorph/Reinsurance https://portal.example.com/@/Systemorph/Reinsurance
Agent tool call Get('@/Systemorph/Reinsurance') —
Autocomplete search @Syst… —

Why it matters: Markdig's LinkUrlCleanupExtension strips the leading @ from [text](@/X) and resolves it into a proper /X URL at render time. That extension does not reach inside raw HTML — any <a href="@/X"> in an HTML block passes through verbatim, producing a broken https://host/@/X link.

Safety net: The portal registers a redirect middleware that permanently redirects GET /@/X → GET /X (301). Broken links will still navigate correctly, but fix the source whenever you spot @/ inside an href.


Syntax in Practice

No Prefix → Layout Area

When you use @path or @@path without a prefix, it refers to a layout area of the target node:

@../Thumbnail              navigates to the sibling node's Thumbnail area
@@Thumbnail                embeds a child's Thumbnail area inline
@../../DataMesh/Thumbnail  navigates up and across to another node

With Prefix → Specific Resource Type

Reserved keywords let you target a precise resource type on any node:

@@node/content/file.md    embeds a file from the "content" collection
@@node/data/              embeds the node's Content data as JSON
@@node/schema/            embeds the ContentType schema
@@node/area/Details       embeds a named layout area

Quick Examples

1. Inline Image

@@content/logo.svg

2. Embedded Markdown

@@content/sample.md

3. Layout Area (Thumbnail)

@@Thumbnail

4. Node Data (Self)

@@data/

5. Schema (Self)

@@schema/

@../QuerySyntax
@Syntax
@Doc/DataMesh/QuerySyntax @Doc/DataMesh/UnifiedPath/Syntax

Autocomplete

Type @ in any editor to trigger path suggestions. Autocomplete is case-insensitive.

Input What you see
@ Available namespaces
@Doc Nodes matching Doc
@Doc/DataMesh/ Child nodes under Doc/DataMesh

Detailed Documentation

@Doc/DataMesh/UnifiedPath/Syntax @Doc/DataMesh/UnifiedPath/ContentPrefix @Doc/DataMesh/UnifiedPath/DataPrefix @Doc/DataMesh/UnifiedPath/AreaPrefix @Doc/DataMesh/UnifiedPath/SchemaPrefix
Reconnecting…
The connection to the server was interrupted. Trying to restore it…
Trying again…
The connection could not be restored. Reloading the page…
The server was updated. Reloading the page to pick up the latest version.