MeshWeaver provides four built-in operations for transferring node subtrees between locations, exporting to files, and importing from external sources. They all appear directly in a node's context menu — Move, Copy and Delete in the edit/organize section, Import and Export in the content section (see Node Menu); there is no "Actions" sub-menu.
| Operation | What it does |
|---|---|
| Export | Produces a ZIP archive (manifest.json + files/) of a node and its entire subtree |
| Import | Reads files or a ZIP and creates nodes in the mesh |
| Copy | Duplicates a node and all its descendants to a new namespace |
| Move | Relocates a node and its entire subtree to a new path |
The four node operations and how they relate to a mesh node subtree: Export serialises to files, Import reads files into the mesh, Copy duplicates to a new namespace, and Move relocates the entire subtree.
Export
Export serialises a node and its entire subtree into a self-contained ZIP archive. The archive has exactly two parts:
| Entry | Contents |
|---|---|
manifest.json |
The export-root path plus every descendant MeshNode as full JSON. Code source and Markdown bodies ride inside each node's Content — there are no separate .cs / .md entries. |
files/… |
The raw bytes of every editable content-collection file attached to the exported nodes. |
Subtree traversal uses the same snapshot query Copy uses (path:{root} scope:subtree), and the file reads plus the zipping run on the file-system IIoPool — never on the hub action block.
Permission
Export requires Permission.Export. Nodes the caller lacks it on are silently skipped: partial access yields a subset, no access yields an empty-manifest ZIP. A non-existent path also resolves to an empty-manifest ZIP rather than an error.
Programmatic Export
The programmatic surface is MeshOperations — the same object behind the MCP export / import tools. It is reactive: the observable is cold, so the work runs on Subscribe.
var ops = new MeshOperations(hub); // a facade over the hub, not a DI service
ops.Export("org/acme/project")
.Subscribe(zipBytes => File.WriteAllBytes(outputPath, zipBytes),
ex => logger.LogWarning(ex, "Export failed"));
There is no
IMeshExportService/IMeshImportService— both were deleted in the persistence cull (2026-05-12). The Export node-menu action opensExportLayoutArea, but the view's download path (NodeExportView) has not been rewired onto the new surface and reports so instead of producing a file.MeshOperations.Export/.Importare the working path.
Export–Import Round Trip
Exporting a subtree and re-importing the ZIP restores the original state:
Export org/acme → ZIP (manifest.json + files/)
→ Import the ZIP under a target namespace
→ Node paths are rewritten from the export root to the target;
names, types, content, and content-collection files are preserved
Import
Import reads files from a directory or ZIP and creates nodes in the mesh. Three sources are supported:
| Source | Description |
|---|---|
| Copy from Mesh Node | Duplicate an existing node tree within the mesh |
| Upload File | Single .md, .json, .yaml, .csv, or .html file |
| Upload Folder (ZIP) | Directory structure or ZIP archive |
Import uses FileFormatParserRegistry to parse each file into a MeshNode based on its extension (StaticRepoImporter does the work for a repo-shaped import).
Programmatic Import
MeshOperations.Import is the inverse of MeshOperations.Export: it unpacks the archive, recreates every node with its path rewritten from the export root to targetNamespace (through IMeshService.CreateNode, carrying the caller's AccessContext), then re-uploads every content-collection file through the standard upload path.
var ops = new MeshOperations(hub); // a facade over the hub, not a DI service
ops.Import("org/acme/project", zipBytes)
.Subscribe(summary => logger.LogInformation("{Summary}", summary),
ex => logger.LogWarning(ex, "Import failed"));
// summary is JSON: {status,exportRoot,targetNamespace,nodesImported,filesImported}
⚠️
ImportNodesRequest/ImportNodesResponseare declared inMeshWeaver.Mesh.Contractand registered in the type registry, but no hub handlesImportNodesRequest— posting one gets no answer. UseMeshOperations.Importuntil a handler exists.
Copy
Copy duplicates a node and all its descendants to a new namespace. The source node's ID is preserved under the target.
Example: Copying org/acme to org/backup creates:
org/backup/acme(root)org/backup/acme/child1org/backup/acme/child2- … and so on for the full subtree
Options
- Force — overwrite existing nodes at the destination (default: skip existing)
Programmatic Copy
CopyNodeTree returns IObservable<int> and is cold — the copy runs on Subscribe, and the single emission is the count of upserted nodes. Subscribe, don't await:
NodeCopyHelper.CopyNodeTree(
meshService, meshService, hub,
sourcePath: "org/acme",
targetNamespace: "org/backup",
force: false)
.Subscribe(
nodesCopied => logger.LogInformation("Copied {Count} nodes", nodesCopied),
ex => logger.LogWarning(ex, "Copy failed"));
hub may be any hub — it supplies the caller identity and reply address; every per-node request is routed to hub.NodeOperationTarget(). Permission checks live in the CreateOrUpdateNodeRequest handler, and force: true updates an existing target rather than deleting it.
Move
Move relocates a node and its entire subtree to a new path. It requires Delete permission on the source and Create permission on the target.
The move is implemented at the persistence layer, handling both same-partition and cross-partition moves (including PostgreSQL). Descendants are moved first, then the root node is relocated and the source is deleted.
A half-landed move is completed by re-issuing it
A move is a copy leg followed by a delete leg. When the reply is lost — the caller gave up while the copy was still running — the copy can land and the delete never run, leaving the same node at both addresses. Re-issuing the same MoveNodeRequest finishes it. The copy leg checks every target that is already taken against the stored source: when the node there IS the source relocated (same NodeType, same CreatedBy, same CreatedDate — the stamps a move copies verbatim and that nothing else can mint), it is kept as it stands, any node the first attempt never reached is carried, and the move goes on to delete the source.
Anything else at the target is a genuine collision: the move is refused with TargetAlreadyExists, and neither the source nor the node at the target is touched. A source with no creation stamp has no identity to recognise and is never matched.
The source must also be unchanged since the copy landed. The first attempt releases its hold on the source when it ends, so the old address can be written before the retry — and resuming would then keep the stale copy and delete the newer source. A move copies LastModified verbatim, so the retry resumes only when the source's LastModified is one the target has carried: its current one (nobody has written to the target), or one of its recorded versions (somebody has, and the target's later state is kept as the node's newer state). Otherwise the retry is refused with TargetAlreadyExists and touches nothing. On a store that keeps no version history, a target that has been written to since cannot prove this, and the retry is refused rather than guessed.
Programmatic Move
hub.Observe(new MoveNodeRequest("org/acme/old-name", "org/acme/new-name"),
o => o.WithTarget(address))
.Subscribe(
response =>
{
if (response.Message.Success)
Console.WriteLine($"Moved to: {response.Message.Node.Path}");
},
ex => logger.LogWarning(ex, "Move failed"));
File Format Details
Each content type serialises to a distinct, human-readable format. The sections below show the canonical shape for each.
Markdown (.md)
---
Name: "Getting Started"
Category: "Documentation"
Authors:
- "Jane Doe"
Tags:
- "tutorial"
---
# Getting Started
Your markdown content here...
NodeType defaults to Markdown and may be omitted. Only non-default values appear in the YAML front matter. If the name matches the ID and there's no other metadata, the YAML block may be omitted entirely.
Code (.cs)
// <meshweaver>
// Id: Person
// DisplayName: Person Data Model
// </meshweaver>
public record Person
{
[Key]
public string Id { get; init; } = string.Empty;
public string? Name { get; init; }
}
The <meshweaver> metadata block is optional. The primary type name is extracted from the code if no explicit Id is provided.
JSON (.json)
{
"id": "task-1",
"namespace": "org/acme/tasks",
"name": "Review submission",
"nodeType": "ACME/Task",
"content": {
"$type": "Task",
"title": "Review submission",
"priority": "High"
}
}
JSON is the fallback format for nodes that don't match markdown, agent, or code patterns.
MeshWeaver.Layout.Controls.Stack
.WithView(MeshWeaver.Layout.Controls.Markdown("### Node Operations at a Glance"))
.WithView(MeshWeaver.Layout.Controls.Markdown(
"| Operation | Requires | Scope |\n" +
"|-----------|----------|-------|\n" +
"| **Export** | `Permission.Export` (Editor+) | Node + full subtree → ZIP |\n" +
"| **Import** | Create permission on target | File, folder, or ZIP → nodes |\n" +
"| **Copy** | Read on source, Create on target | Node + full subtree → new namespace |\n" +
"| **Move** | Delete on source, Create on target | Node + full subtree → new path |"
))
See Also
- Node Menu Items — The menu system and the default item set
- Query Syntax — Search and filter nodes
- Node Type Configuration — Define custom node types
- CRUD Operations — Low-level data operations