NodeTypes are the blueprints of the mesh. Every node you create — an Organization, a Story, a Project — is an instance of a NodeType that defines its content schema, hub behaviour, and how it renders in the UI. This guide walks through every aspect of creating and configuring them.


What is a NodeType?

A NodeType is itself a mesh node (with nodeType: "NodeType"). It acts as a reusable template that combines three concerns:

Concern What it controls
Content schema The C# record type that models instance data
Hub configuration How instance hubs are wired (layout areas, content collections, …)
Display properties Icon, description, sort order shown in the UI

When a user navigates to a node whose nodeType points at your definition, the framework compiles the content record, applies the configuration expression, and mounts the resulting layout areas — all dynamically, without a deploy.

NodeType content schema configuration lambda Compile C# record → assembly cached + invalidated Instance Hub layout areas wired WithContentType applied UI Views Details · Catalog Thumbnail · Settings first use activate mount Every instance hub is activated on demand — no restart required when the NodeType definition changes.

NodeType lifecycle: definition compiled on first activation, hub wired from the configuration lambda, views mounted for every instance.


NodeType Structure

A NodeType has two parts: its node properties (the metadata stored in the mesh) and a NodeTypeDefinition content block (the rich configuration payload).

Node properties

{
  "id": "Organization",
  "namespace": "",
  "name": "Organization",
  "nodeType": "NodeType",
  "description": "An organization containing projects",
  "icon": "Building",
  "order": 10,
  "isPersistent": true
}

NodeTypeDefinition content

{
  "$type": "NodeTypeDefinition",
  "id": "Organization",
  "namespace": "",
  "displayName": "Organization",
  "icon": "Building",
  "description": "An organization containing projects",
  "order": 10,
  "configuration": "config => config.WithContentType<Organization>().AddDefaultLayoutAreas()"
}

The configuration field is the heart of the definition — a C# lambda expression evaluated at runtime that wires up the hub for every instance of this type.


The Configuration Expression

The configuration string is a C# lambda (config => …) that receives an IHubConfiguration and returns it, fully wired. The framework compiles and caches this expression the first time an instance hub is activated.

WithContentType<T>()

Binds the named C# record as the authoritative content type for all instances:

config.WithContentType<Organization>()

AddDefaultLayoutAreas()

Registers the five standard views — Details, Catalog, Thumbnail, Metadata, and Settings — in one call:

config.WithContentType<Organization>().AddDefaultLayoutAreas()

AddLayout(…)

Adds custom views on top of the defaults. The view name appears in the navigation bar:

config
  .WithContentType<Story>()
  .AddDefaultLayoutAreas()
  .AddLayout(layout => layout
    .WithView("Timeline", StoryViews.Timeline))

MapContentCollection(…)

Maps a named property to a blob-storage collection (files, images, …):

config.WithContentType<Organization>()
  .AddDefaultLayoutAreas()
  .MapContentCollection("logos", "storage", $"logos/{config.Address.Segments.Last()}")

Defining the Content Record

Each NodeType references a C# record that models its instance data. Place the source in a Source/dataModel.json file alongside the NodeType definition:

using System.ComponentModel.DataAnnotations;

public record Organization
{
    [Key]
    public string Id { get; init; } = string.Empty;

    public string Name { get; init; } = string.Empty;

    public string? Description { get; init; }

    public string Icon { get; init; } = "Building";
}

The [Key] attribute marks the identifier property. MeshWeaver compiles this record dynamically — no manual build step required. The compiled assembly is cached and invalidated automatically when the source node changes.


Namespace Hierarchy

NodeTypes participate in the same namespace hierarchy as all other nodes.

Root-namespace types (global)

A NodeType with an empty namespace defines a global type available everywhere:

Scoped types

NodeTypes can be restricted to a namespace, so they only appear and are creatable within that context:

This scoping lets you model domain-specific types (a Story inside Systemorph/Marketing) without polluting the global namespace.


Catalog Behaviour

The Catalog view for a NodeType automatically queries for all instances within the current namespace. The framework builds the query dynamically:

No manual childrenQuery configuration is needed. Adding a new instance immediately appears in the catalog.


Default Views Reference

AddDefaultLayoutAreas() registers five views that cover the most common UI needs:

View Purpose
Details Full content editor / reader for a single instance
Catalog Paginated list of all instances of this type
Thumbnail Compact card for use in grid / gallery layouts
Metadata Technical properties (path, version, timestamps)
Settings NodeType definitions scoped to this namespace

End-to-End Example: a Story NodeType

The following three files create a fully working Story type inside the Systemorph/Marketing namespace.

1. Story.json — the NodeType definition

{
  "id": "Story",
  "namespace": "Systemorph/Marketing",
  "name": "Story",
  "nodeType": "NodeType",
  "content": {
    "$type": "NodeTypeDefinition",
    "id": "Story",
    "namespace": "Systemorph/Marketing",
    "displayName": "Story",
    "configuration": "config => config.WithContentType<Story>().AddDefaultLayoutAreas()"
  }
}

2. Story/Source/dataModel.json — the content record

{
  "code": "public record Story { [Key] public string Id { get; init; } public string Title { get; init; } public string? Markdown { get; init; } }"
}

3. An instance — ClaimsProcessing.json

{
  "id": "ClaimsProcessing",
  "namespace": "Systemorph/Marketing",
  "name": "Claims Processing",
  "nodeType": "Systemorph/Marketing/Story",
  "content": {
    "$type": "Story",
    "id": "ClaimsProcessing",
    "title": "Claims Processing Pipeline",
    "markdown": "# Overview\n..."
  }
}

Once these files are saved, MeshWeaver compiles the record, wires the hub, and the instance is immediately navigable — no restart required.


Live Preview

The cell below renders a summary card showing how a NodeType's display properties map to the UI. Adapt the values to match your own type.

var props = new[]
{
    ("Name",        "Story"),
    ("Namespace",   "Systemorph/Marketing"),
    ("Icon",        "BookOpen"),
    ("Description", "A narrative story within a marketing campaign"),
    ("Order",       "20"),
    ("Views",       "Details · Catalog · Thumbnail · Metadata · Settings"),
};

var rows = string.Join("\n", props.Select(p =>
    $"| **{p.Item1}** | `{p.Item2}` |"));

MeshWeaver.Layout.Controls.Markdown(
    $"### NodeType summary card\n\n| Property | Value |\n|---|---|\n{rows}")

Best Practices

  1. Use meaningful namespaces. Group related types together so scoped queries and catalog views stay focused.
  2. Set order explicitly. Controls sorting in catalog and type-picker UI; lower numbers appear first.
  3. Choose Fluent UI icons. Browse available names at the Fluent UI icon gallery — they render consistently across themes.
  4. Write clear descriptions. The description surfaces in the Catalog header and the type-picker tooltip; a single sentence is enough.
  5. Keep content records small. Large records with many optional fields slow schema compilation. Prefer composition (satellite nodes) over monolithic records.
Reconnecting…
The server was updated. Reloading the page to pick up the latest version.