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
Mesh Node Subtree root child1 child2 Export ZIP (manifest + files) Import File / Folder / ZIP Copy → new namespace Move → new path reads from writes to duplicates relocates

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 opens ExportLayoutArea, 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 / .Import are 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 / ImportNodesResponse are declared in MeshWeaver.Mesh.Contract and registered in the type registry, but no hub handles ImportNodesRequest — posting one gets no answer. Use MeshOperations.Import until 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:

Options

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.

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

Reconnecting…
The server was updated. Reloading the page to pick up the latest version.