SocialMedia — A Model Node Type, End to End

This page is the canonical reference example for a custom model node type. When you — or an agent operating with the /code skill — are asked to build something "as code" (a typed model with its own data and views), this is the shape to mirror.

See also: Creating Node Types for step-by-step theory, Business Rules for a calculation-heavy example with charts, and LinkedIn Publishing — Setup for wiring these posts up to actually publish to LinkedIn.


Folder Layout

Every file in this tree has a specific job. The sections below walk through each one.

Doc/DataMesh/SocialMedia/
  Post.json                              # NodeType definition (nodeType: "NodeType")
  Post/
    Source/                              # C# compiled at startup
      Platform.cs                        # Reference-data record
      SocialMediaPost.cs                 # Content record
      SocialMediaPostLayoutAreas.cs      # List + Detail layout areas
    Post-001.json                        # Instance (nodeType: "Doc/DataMesh/SocialMedia/Post")
  Profile.json                           # Second NodeType
  Profile/
    Source/
      SocialMediaProfile.cs
      SocialMediaProfileLayoutAreas.cs
    Roland-LinkedIn.json                 # Instance
Source/ (C#) Platform.cs reference data record SocialMediaPost.cs content record PostLayoutAreas.cs List + Detail views SocialMediaProfile.cs second NodeType compiled at startup NodeType JSON Post.json WithContentType<T>() AddData(Platform.All) AddLayout() + views nodeType: "NodeType" Runtime / Instances Live NodeType Hub editor, list + detail views, validation Post-001.json instance Roland-LinkedIn .json instance nodeType: "Doc/DataMesh/SocialMedia/Post" Platform dropdown from reference data defines wires *Source C# files compile at startup → NodeType JSON wires them into a live hub → instance `.json` files carry typed content.*

1. Reference Data — Platform.cs

Reference data is a small, closed set of lookups — platforms, statuses, categories. The pattern is always the same: a plain record with a [Key], typed static instances, and a static All[] array that seeds the in-memory data source.

// <meshweaver>
// Id: Platform
// DisplayName: Social Media Platform
// </meshweaver>

public record Platform
{
    [Key] public string Id { get; init; } = string.Empty;
    [Required] public string Name { get; init; } = string.Empty;
    public string Emoji { get; init; } = string.Empty;
    public string Color { get; init; } = "#0a66c2";

    public static readonly Platform LinkedIn  = new() { Id = "LinkedIn",  Name = "LinkedIn",    Emoji = "💼", Color = "#0a66c2" };
    public static readonly Platform Twitter   = new() { Id = "Twitter",   Name = "X / Twitter", Emoji = "🐦", Color = "#000000" };
    public static readonly Platform Instagram = new() { Id = "Instagram", Name = "Instagram",   Emoji = "📷", Color = "#e1306c" };

    public static readonly Platform[] All = [LinkedIn, Twitter, Instagram];
    public static Platform GetById(string? id) => All.FirstOrDefault(p => p.Id == id) ?? LinkedIn;
}

2. Content Record — SocialMediaPost.cs

The content record defines the shape of a single instance's content payload. Domain attributes drive the editor UI and wire reference-data lookups automatically.

// <meshweaver>
// Id: SocialMediaPost
// DisplayName: Social Media Post
// </meshweaver>

using MeshWeaver.Domain;

public record SocialMediaPost
{
    [Required]
    [MeshNodeProperty(nameof(MeshNode.Name))]   // syncs MeshNode.Name with Title
    public string Title { get; init; } = string.Empty;

    [Markdown(EditorHeight = "200px")]
    public string? Body { get; init; }

    [Dimension<Platform>]                        // renders as a Platform picker
    public string Platform { get; init; } = "LinkedIn";

    [DisplayName("Scheduled at")]
    public DateTimeOffset? ScheduledAt { get; init; }

    public int Impressions { get; init; }
    public int Likes { get; init; }
}
Attribute Purpose
[Required] Validation
[MeshNodeProperty(nameof(MeshNode.Name))] Mirrors the property value into MeshNode.Name
[Dimension<T>] Typed lookup rendered as a dropdown against reference data
[Markdown(...)] Rich-text editor with configurable height
[DisplayName(...)] UI label override

3. Layout Areas — SocialMediaPostLayoutAreas.cs

Layout areas are the views for instances of the type. They return IObservable<UiControl?> — never Task<…>, never async. Compose with Rx operators:

public static IObservable<UiControl?> List(LayoutAreaHost host, RenderingContext _)
{
    var meshService = host.Hub.ServiceProvider.GetRequiredService<IMeshService>();
    return meshService
        .Query<MeshNode>(MeshQueryRequest.FromQuery("namespace:Doc/DataMesh/SocialMedia/Post"))
        .Scan(ImmutableDictionary<string, MeshNode>.Empty, ApplyChanges)
        .Select(dict => (UiControl?)BuildList(dict.Values.ToImmutableList()));
}

The extension method below is how the layout areas get wired into the NodeType configuration:

public static LayoutDefinition AddSocialMediaPostLayoutAreas(this LayoutDefinition layout) =>
    layout.WithView("List", List).WithView("Detail", Detail);

4. NodeType JSON — Post.json

The JSON is the binding glue. It registers the type, points at its content record, seeds reference data, and wires custom layout areas.

{
  "id": "Post",
  "namespace": "Doc/DataMesh/SocialMedia",
  "name": "Social Media Post",
  "nodeType": "NodeType",
  "content": {
    "$type": "NodeTypeDefinition",
    "configuration": "config => config
      .WithContentType<SocialMediaPost>()
      .AddData(data => data.AddSource(source => source
        .WithType<Platform>(t => t.WithInitialData(Platform.All))))
      .AddDefaultLayoutAreas()
      .AddLayout(layout => layout
        .AddSocialMediaPostLayoutAreas()
        .WithDefaultArea(\"List\"))"
  }
}

Configuration-lambda quick reference:

Call Purpose
WithContentType<T>() The record type for new instances
AddData(data => data.AddSource(…)) Seed in-memory data sources (reference data)
AddDefaultLayoutAreas() Overview, Edit, Threads, Files
AddLayout(layout => layout.AddXxxLayoutAreas()) Custom views
WithDefaultArea("List") Which view opens by default

5. Instances — Post/Post-001.json

An instance sets nodeType to the namespace-qualified path of the NodeType (Doc/DataMesh/SocialMedia/Post), and its content matches the record ($type = class name).

Naming convention: Instance IDs should be meaningful — e.g. Roland-LinkedIn, Post-001 — not generic like SamplePost.

{
  "id": "Post-001",
  "namespace": "Doc/DataMesh/SocialMedia/Post",
  "name": "Why we bet on the actor model",
  "nodeType": "Doc/DataMesh/SocialMedia/Post",
  "content": {
    "$type": "SocialMediaPost",
    "title": "Why we bet on the actor model",
    "body": "Reactive systems live or die on isolation …",
    "profilePath": "Doc/DataMesh/SocialMedia/Profile/Roland-LinkedIn",
    "platform": "LinkedIn",
    "scheduledAt": "2026-04-05T09:00:00+02:00",
    "impressions": 4321,
    "likes": 187
  }
}

Live Profile Instance

The embedded view below is the Roland-LinkedIn profile instance, rendered by its Detail layout area:


Copy-This Checklist

When building a new model node type "as code", work through this list in order:

  1. ☐ Create a namespace folder under your target location.
  2. ☐ Add one .cs per content record in Source/, each with the <meshweaver> frontmatter.
  3. ☐ Add reference-data .cs files with [Key], static instances, and All[].
  4. ☐ Add a XxxLayoutAreas.cs with List/Detail views returning IObservable<UiControl?>.
  5. ☐ Write the Type.json with nodeType: "NodeType" and a configuration lambda.
  6. ☐ Write at least one instance JSON with nodeType set to the namespace-qualified path.
  7. Do not substitute a Markdown node for a typed view — Markdown is for documents, not structured data.
Reconnecting…
The server was updated. Reloading the page to pick up the latest version.