Document Format

The structure of a Schematify graph document

A graph document describes a system in Schematify. It defines the entities, how they relate, what metadata they carry, and what live data they expect. You can write YAML or JSON and push it with the CLI, or create and edit the same structure through the Document Manager.

Top-level structure

Every document has the same outer shape.

id: my-system
label: My System
description: A graph of my infrastructure
node-types: []
root:
  id: Root
  label: Root
  links: []
  nodes: []
{
  "id": "my-system",
  "label": "My System",
  "description": "A graph of my infrastructure",
  "node-types": [],
  "root": {
    "id": "Root",
    "label": "Root",
    "links": [],
    "nodes": []
  }
}
  • id: unique identifier for the document.
  • label: display name shown in the Document Manager and viewer.
  • description: optional summary text.
  • node-types: visual definitions for node types (covered in Node types and packs).
  • root: the top-level node that contains everything else, including the graph’s links.
  • authoring: optional protected metadata added by Schematify when a script publishes the graph.

Scriptable Graph ownership

A graph published with a script includes this server-controlled field:

"authoring": {
  "type": "script"
}

Schematify does not allow its editor or normal document saves to replace that graph’s structure. Publishing the script can update it, while Copy to edit creates a new graph without the authoring field. Existing documents without this field remain editable.

Do not add authoring by hand. The script-publish operation owns it.

Nodes

Nodes are the entities in your graph. Every node lives inside root, either as a direct child or nested deeper in the hierarchy.

root:
  id: Root
  label: Root
  nodes:
    - id: api-gateway
      label: API Gateway
      node-type: microservices/service
      attributes:
        owner: platform-team
        region: us-east-1
{
  "root": {
    "id": "Root",
    "label": "Root",
    "nodes": [
      {
        "id": "api-gateway",
        "label": "API Gateway",
        "node-type": "microservices/service",
        "attributes": {
          "owner": "platform-team",
          "region": "us-east-1"
        }
      }
    ]
  }
}
  • id: unique among nodes with the same parent. Combined with its ancestors to form the node path used by links and search.
  • label: display name shown on the canvas. Falls back to id if omitted.
  • node-type: references a visual definition from a pack (e.g. microservices/service, databases/default). Defaults to base/default.
  • attributes: key-value metadata (covered in Attributes).

Hierarchy

Hierarchy is expressed by nesting nodes inside other nodes using the nodes field. Any node that contains children acts as a group in the viewer. It can be expanded to show its contents or collapsed to act as a summary.

root:
  id: Root
  label: Root
  nodes:
    - id: production
      label: Production
      node-type: base/collection
      nodes:
        - id: us-east
          label: US East
          node-type: base/collection
          nodes:
            - id: api-gateway
              label: API Gateway
              node-type: microservices/service

            - id: auth-service
              label: Auth Service
              node-type: microservices/service

        - id: eu-west
          label: EU West
          node-type: base/collection
          nodes:
            - id: api-gateway-eu
              label: API Gateway
              node-type: microservices/service
{
  "root": {
    "id": "Root",
    "label": "Root",
    "nodes": [
      {
        "id": "production",
        "label": "Production",
        "node-type": "base/collection",
        "nodes": [
          {
            "id": "us-east",
            "label": "US East",
            "node-type": "base/collection",
            "nodes": [
              {
                "id": "api-gateway",
                "label": "API Gateway",
                "node-type": "microservices/service"
              },
              {
                "id": "auth-service",
                "label": "Auth Service",
                "node-type": "microservices/service"
              }
            ]
          },
          {
            "id": "eu-west",
            "label": "EU West",
            "node-type": "base/collection",
            "nodes": [
              {
                "id": "api-gateway-eu",
                "label": "API Gateway",
                "node-type": "microservices/service"
              }
            ]
          }
        ]
      }
    ]
  }
}

This creates a three-level hierarchy: Production contains two regions, each region contains services. In the viewer, collapsing “Production” hides everything inside it. Collapsing “US East” hides its services but keeps “EU West” visible.

There’s no depth limit. Nest as deep as your system requires.

Links are the relationships between nodes that exist outside the parent-child hierarchy. A service in one branch of the tree can connect to a database in a completely different branch.

Links are defined in the root node’s links array. This keeps relationships in one place while the root’s nodes array describes containment.

id: my-system
label: My System
root:
  id: Root
  label: Root
  links:
    - from: production/us-east/api-gateway
      to: production/us-east/database
      direction: forwards

    - from: production/us-east/api-gateway
      to: production/eu-west/api-gateway-eu
      direction: bidirectional
      link-type: replication
  nodes: [] # Add the nodes referenced by the link paths.
{
  "id": "my-system",
  "label": "My System",
  "root": {
    "id": "Root",
    "label": "Root",
    "links": [
      {
        "from": "production/us-east/api-gateway",
        "to": "production/us-east/database",
        "direction": "forwards"
      },
      {
        "from": "production/us-east/api-gateway",
        "to": "production/eu-west/api-gateway-eu",
        "direction": "bidirectional",
        "link-type": "replication"
      }
    ],
    "nodes": []
  }
}
  • from / to: node paths, excluding the root node. For a node at root > production > us-east > api-gateway, the path is production/us-east/api-gateway.
  • direction: forwards (default), backwards, bidirectional, or none. Controls the arrow direction drawn on the canvas.
  • link-type: optional classification label (e.g. replication, dependency). Defaults to default.
  • attributes: optional key-value metadata on the link itself.
Tip

Put graph relationships in root.links. Node paths in each link refer to entries under root.nodes.

Attributes

Attributes are key-value metadata attached to nodes. They’re the primary way to attach information to your graph beyond labels and types.

- id: database
  label: PostgreSQL Primary
  node-type: databases/default
  attributes:
    engine: postgresql
    version: "16.2"
    region: us-east-1
    cpu_cores: 8
    tags:
      - production
      - critical
[
  {
    "id": "database",
    "label": "PostgreSQL Primary",
    "node-type": "databases/default",
    "attributes": {
      "engine": "postgresql",
      "version": "16.2",
      "region": "us-east-1",
      "cpu_cores": 8,
      "tags": ["production", "critical"]
    }
  }
]

Attribute values can be strings, numbers, booleans, arrays, or nested objects. They’re searchable in the viewer using the attributes. prefix (e.g. attributes.region:us-east), visible in the inspect modal, and can drive how nodes are rendered.

Nodes also support protected-attributes, which work identically but are reserved for system-managed data that shouldn’t be edited by hand.

Channels

Attributes describe what something is. Channels describe what’s happening to it right now.

A database node’s engine version doesn’t change between deployments: that’s an attribute. Its active connection count changes every second: that’s a channel.

Channels separate the contract from the data. The document declares what live data a node expects; the actual values arrive at runtime without touching the document.

Here’s a database node with only static attributes:

- id: database
  label: PostgreSQL Primary
  node-type: databases/default
  attributes:
    engine: postgresql
    version: "16.2"
[
  {
    "id": "database",
    "label": "PostgreSQL Primary",
    "node-type": "databases/default",
    "attributes": {
      "engine": "postgresql",
      "version": "16.2"
    }
  }
]

Now the same node, declaring that it expects live connection count and replication lag data:

- id: database
  label: PostgreSQL Primary
  node-type: databases/default
  attributes:
    engine: postgresql
    version: "16.2"
  channels:
    connections:
      stale-after: 5000
      default: 0
    replication-lag:
      label: Replication Lag
      stale-after: 10000
[
  {
    "id": "database",
    "label": "PostgreSQL Primary",
    "node-type": "databases/default",
    "attributes": {
      "engine": "postgresql",
      "version": "16.2"
    },
    "channels": {
      "connections": {
        "stale-after": 5000,
        "default": 0
      },
      "replication-lag": {
        "label": "Replication Lag",
        "stale-after": 10000
      }
    }
  }
]

Each channel declaration has three optional fields:

  • label: a display name for the channel. Falls back to the channel key (connections, replication-lag) if omitted.
  • stale-after: milliseconds. After this window, the last received value expires.
  • default: the value to show before any live data arrives. Useful for displaying a meaningful initial state before the pipeline connects.

The document defines the shape of the data; the pipeline fills it in. The viewer uses a fresh received value first. Before a value arrives, or after stale-after expires, it falls back to default. Without a default, the resolved value is missing.

Search channel values

Use the channels. query namespace to search current resolved values:

channels.connections>100
channels.replication-lag:0..500
has:channels.replication-lag

Only declared channels are searchable. Channel values do not participate in free-text search or autocomplete value suggestions, although declared field names can appear in field autocomplete. Query-backed smart filters update when a channel batch arrives and when a stale value falls back to its default or to a missing value.

Tip

Channels receive live values published with the CLI or from a Scriptable Graph. A process that can observe your system can use these supported workflows to keep a node’s live data current.

Render styles and param bindings

The render field on a node controls how it appears on the canvas beyond the default icon-and-label treatment. Two fields matter here: style determines the renderer, and params feeds data into it.

Styles

StyleDescription
defaultIcon and label (the standard look). No params required.
propertyDisplays a single value prominently with a header.
reportDisplays a table of key-value rows.
pie-chartProportional pie chart visualization.
bar-chartVertical bar chart visualization.
line-chartLine graph visualization with data points.
chartReserved for future use.

If a node has visible children it always renders as a group container regardless of style.

Param bindings

Each entry in params declares where its value comes from using a binding object with source and key (or value for literals).

SourceMeaning
attributeLook up a static value from the node’s attributes.
channelLook up a live value from the node’s channels.
literalUse the value directly.

Raw primitives (strings, numbers, booleans) are shorthand for { source: "literal", value: ... }.

Property node

A property node shows a single value with a header label. It expects two params:

  • display-value: the value to render prominently.
  • header: the text shown above the value.
- id: latency-display
  label: API Gateway Latency
  node-type: microservices/service
  attributes:
    owner: platform-team
  channels:
    latency-p99:
      label: P99 Latency (ms)
      stale-after: 10000
  render:
    style: property
    params:
      display-value:
        source: channel
        key: latency-p99
      header:
        source: attribute
        key: owner
[
  {
    "id": "latency-display",
    "label": "API Gateway Latency",
    "node-type": "microservices/service",
    "attributes": {
      "owner": "platform-team"
    },
    "channels": {
      "latency-p99": {
        "label": "P99 Latency (ms)",
        "stale-after": 10000
      }
    },
    "render": {
      "style": "property",
      "params": {
        "display-value": {
          "source": "channel",
          "key": "latency-p99"
        },
        "header": {
          "source": "attribute",
          "key": "owner"
        }
      }
    }
  }
]

In this example the header reads the static owner attribute (“platform-team”) and the value displays the live latency-p99 channel. When the pipeline publishes a new latency reading, the node updates in real time without touching the document.

Report node

A report node displays multiple rows of key-value data. It expects one param:

  • attributeIds: an array of keys to display. Each key is resolved by checking channel values first, then falling back to attributes.
- id: db-report
  label: PostgreSQL Primary
  node-type: databases/default
  attributes:
    engine: postgresql
    version: "16.2"
  channels:
    connections:
      stale-after: 5000
      default: 0
    replication-lag:
      label: Replication Lag
      stale-after: 10000
  render:
    style: report
    params:
      attributeIds:
        - engine
        - version
        - connections
        - replication-lag
[
  {
    "id": "db-report",
    "label": "PostgreSQL Primary",
    "node-type": "databases/default",
    "attributes": {
      "engine": "postgresql",
      "version": "16.2"
    },
    "channels": {
      "connections": {
        "stale-after": 5000,
        "default": 0
      },
      "replication-lag": {
        "label": "Replication Lag",
        "stale-after": 10000
      }
    },
    "render": {
      "style": "report",
      "params": {
        "attributeIds": ["engine", "version", "connections", "replication-lag"]
      }
    }
  }
]

Here engine and version resolve from attributes (static), while connections and replication-lag resolve from channels (live). The report node checks channels first for each key, so if a channel and an attribute share the same name, the live channel value takes priority.

Pie chart node

A pie chart node renders a proportional pie from a data param. It expects one required param and several optional ones:

  • data: the dataset to visualize. Accepts multiple formats (see Chart data formats below).
  • header: optional title displayed above the chart.
  • legend: "right" (default), "bottom", or "false" to hide the legend entirely.
  • padding: 0 to 1 ratio controlling internal padding around the chart area. Defaults to 0.1.

The colour palette is derived from the node’s background colour. Hover tooltips show each slice’s label, value, and percentage. Empty data renders a placeholder circle.

- id: traffic-breakdown
  label: Traffic Breakdown
  node-type: base/default
  attributes:
    data:
      - label: GET
        value: 450
      - label: POST
        value: 230
      - label: PUT
        value: 85
      - label: DELETE
        value: 35
  render:
    style: pie-chart
    params:
      header:
        source: literal
        value: HTTP Methods
      data:
        source: attribute
        key: data
      legend:
        source: literal
        value: right
    scale:
      x: 3.5
      y: 2.5
[
  {
    "id": "traffic-breakdown",
    "label": "Traffic Breakdown",
    "node-type": "base/default",
    "attributes": {
      "data": [
        { "label": "GET", "value": 450 },
        { "label": "POST", "value": 230 },
        { "label": "PUT", "value": 85 },
        { "label": "DELETE", "value": 35 }
      ]
    },
    "render": {
      "style": "pie-chart",
      "params": {
        "header": { "source": "literal", "value": "HTTP Methods" },
        "data": { "source": "attribute", "key": "data" },
        "legend": { "source": "literal", "value": "right" }
      },
      "scale": { "x": 3.5, "y": 2.5 }
    }
  }
]

Pie charts work with channels too. A node can declare its data as a channel with a default value, and the chart updates in real time as new values arrive from the pipeline:

- id: error-distribution
  label: Error Distribution
  node-type: base/default
  channels:
    error_data:
      label: Error Breakdown
      stale-after: 60000
      default:
        - label: 4xx
          value: 120
        - label: 5xx
          value: 30
        - label: Timeout
          value: 15
  render:
    style: pie-chart
    params:
      header:
        source: literal
        value: Errors
      data:
        source: channel
        key: error_data
      legend:
        source: literal
        value: bottom
    scale:
      x: 1.5
      y: 1.5
[
  {
    "id": "error-distribution",
    "label": "Error Distribution",
    "node-type": "base/default",
    "channels": {
      "error_data": {
        "label": "Error Breakdown",
        "stale-after": 60000,
        "default": [
          { "label": "4xx", "value": 120 },
          { "label": "5xx", "value": 30 },
          { "label": "Timeout", "value": 15 }
        ]
      }
    },
    "render": {
      "style": "pie-chart",
      "params": {
        "header": { "source": "literal", "value": "Errors" },
        "data": { "source": "channel", "key": "error_data" },
        "legend": { "source": "literal", "value": "bottom" }
      },
      "scale": { "x": 1.5, "y": 1.5 }
    }
  }
]
Tip

Use render.scale to control the chart’s canvas size. The base size is 200×200 pixels, so scale: { x: 3.5, y: 2.5 } produces a 700×500 chart area, wide enough for a legend alongside the pie.

Bar chart node

A bar chart node renders a vertical bar chart from a data param. It supports the following params:

  • data: the dataset to visualize. Accepts multiple formats (see Chart data formats below).
  • header: optional title displayed above the chart.
  • yLabel: optional label for the vertical axis, rendered rotated alongside the y-axis.
  • barLabels: controls labels above each bar: "percent", "value", "both", or "none" (default).
  • sort: sort bars before rendering: "asc", "desc", or "none" (default, preserves data order).
  • maxBars: truncate to the top N bars after sorting. Useful for showing only the most significant items.
  • domainMin: explicit y-axis minimum. Overrides the auto-calculated domain.
  • domainMax: explicit y-axis maximum. Overrides the auto-calculated domain.
  • baseline: shift the zero-line to an arbitrary value. Bars above the baseline extend upward, bars below extend downward.
  • labelScale: multiplier for all chart text (header, axis labels, bar labels, y-axis label). Defaults to 1. Accepts 0.5 to 3. Useful for keeping text readable on very large or very small charts.
  • padding: 0 to 1 ratio controlling internal padding around the chart area. Defaults to 0.1.

Bars below zero render downward from the baseline. Axis ticks use abbreviated labels (e.g. 1.2k, 3.5M). Hover tooltips show each bar’s label, value, and percentage of the total.

- id: response-codes
  label: HTTP Response Codes
  node-type: base/default
  attributes:
    data:
      - label: "200"
        value: 1250
      - label: "301"
        value: 180
      - label: "400"
        value: 95
      - label: "404"
        value: 62
      - label: "500"
        value: 28
  render:
    style: bar-chart
    params:
      header:
        source: literal
        value: Response Codes
      data:
        source: attribute
        key: data
      yLabel:
        source: literal
        value: Requests
      barLabels:
        source: literal
        value: percent
      sort:
        source: literal
        value: desc
    scale:
      x: 3.5
      y: 2.5
[
  {
    "id": "response-codes",
    "label": "HTTP Response Codes",
    "node-type": "base/default",
    "attributes": {
      "data": [
        { "label": "200", "value": 1250 },
        { "label": "301", "value": 180 },
        { "label": "400", "value": 95 },
        { "label": "404", "value": 62 },
        { "label": "500", "value": 28 }
      ]
    },
    "render": {
      "style": "bar-chart",
      "params": {
        "header": { "source": "literal", "value": "Response Codes" },
        "data": { "source": "attribute", "key": "data" },
        "yLabel": { "source": "literal", "value": "Requests" },
        "barLabels": { "source": "literal", "value": "percent" },
        "sort": { "source": "literal", "value": "desc" }
      },
      "scale": { "x": 3.5, "y": 2.5 }
    }
  }
]

Here’s a bar chart with negative values, showing error rate changes compared to the previous day. Bars above zero indicate increases, bars below indicate decreases:

- id: error-delta
  label: Error Rate Delta
  node-type: base/default
  attributes:
    data:
      - label: Auth
        value: -12
      - label: DB
        value: 25
      - label: Cache
        value: -5
      - label: Queue
        value: 8
      - label: DNS
        value: -2
  render:
    style: bar-chart
    params:
      header:
        source: literal
        value: "Error Delta (vs Yesterday)"
      data:
        source: attribute
        key: data
      yLabel:
        source: literal
        value: delta
      barLabels:
        source: literal
        value: value
    scale:
      x: 3.0
      y: 2.0
[
  {
    "id": "error-delta",
    "label": "Error Rate Delta",
    "node-type": "base/default",
    "attributes": {
      "data": [
        { "label": "Auth", "value": -12 },
        { "label": "DB", "value": 25 },
        { "label": "Cache", "value": -5 },
        { "label": "Queue", "value": 8 },
        { "label": "DNS", "value": -2 }
      ]
    },
    "render": {
      "style": "bar-chart",
      "params": {
        "header": { "source": "literal", "value": "Error Delta (vs Yesterday)" },
        "data": { "source": "attribute", "key": "data" },
        "yLabel": { "source": "literal", "value": "delta" },
        "barLabels": { "source": "literal", "value": "value" }
      },
      "scale": { "x": 3.0, "y": 2.0 }
    }
  }
]

Like pie charts, bar charts work with channels for real-time data. Use domainMin and domainMax to lock the y-axis range so the chart doesn’t rescale as live values fluctuate:

- id: daily-throughput
  label: Daily Throughput
  node-type: base/default
  channels:
    throughput_data:
      label: Throughput
      stale-after: 60000
      default:
        - label: Mon
          value: 4200
        - label: Tue
          value: 3800
        - label: Wed
          value: 5100
        - label: Thu
          value: 4700
        - label: Fri
          value: 3900
        - label: Sat
          value: 1200
        - label: Sun
          value: 800
  render:
    style: bar-chart
    params:
      header:
        source: literal
        value: Weekly Throughput
      data:
        source: channel
        key: throughput_data
      yLabel:
        source: literal
        value: req/day
      domainMin:
        source: literal
        value: 0
      domainMax:
        source: literal
        value: 6000
    scale:
      x: 3.0
      y: 2.0
[
  {
    "id": "daily-throughput",
    "label": "Daily Throughput",
    "node-type": "base/default",
    "channels": {
      "throughput_data": {
        "label": "Throughput",
        "stale-after": 60000,
        "default": [
          { "label": "Mon", "value": 4200 },
          { "label": "Tue", "value": 3800 },
          { "label": "Wed", "value": 5100 },
          { "label": "Thu", "value": 4700 },
          { "label": "Fri", "value": 3900 },
          { "label": "Sat", "value": 1200 },
          { "label": "Sun", "value": 800 }
        ]
      }
    },
    "render": {
      "style": "bar-chart",
      "params": {
        "header": { "source": "literal", "value": "Weekly Throughput" },
        "data": { "source": "channel", "key": "throughput_data" },
        "yLabel": { "source": "literal", "value": "req/day" },
        "domainMin": { "source": "literal", "value": 0 },
        "domainMax": { "source": "literal", "value": 6000 }
      },
      "scale": { "x": 3.0, "y": 2.0 }
    }
  }
]
Tip

Combine sort: "desc" with maxBars to build a “top N” chart. For example, sort: "desc" and maxBars: 5 shows only the five largest bars, regardless of how many items are in the dataset.

Line chart

A line chart connects data points with a polyline and optional dots. It supports multiple series on a shared axis.

Supported params:

  • data: single-series dataset to plot. Accepts the same formats as bar and pie charts. Ignored when datasets is present.
  • datasets: multi-series dataset. An array of { label, data } objects where label is the series name shown in the legend and data accepts the same formats as the single-series data param. When provided, a color-keyed legend is drawn automatically above the chart area. Series colors are derived from the node background color by rotating hue at equal intervals.
  • header: optional title displayed above the chart.
  • yLabel: optional label for the vertical axis.
  • sort: "asc" | "desc" | "none" (default "none"). Sorts data points by value before plotting. Applied per-series when using datasets.
  • domainMin / domainMax: explicit y-axis bounds. Applies across all series. Useful for locking the scale when data updates via channels.
  • baseline: shift the zero-line to an arbitrary value.
  • labelScale: multiplier for all chart text (default 1, range 0.5 to 3).
  • padding: 0 to 1 ratio controlling internal padding around the chart area. Defaults to 0.1.
  • showDots: true | false (default true). Render circles at each data point.
  • lineWidth: stroke width of the line (default 2, range 0.5 to 10).
  • fill: true | false (default false). Fill the area between the line and the baseline. Applied to all series.

Negative values are supported. The chart draws grid lines and axis ticks with abbreviated labels. Hover tooltips show a vertical crosshair; in multi-series mode they identify both the data point and the series name.

- id: weekly-latency
  label: Weekly P99 Latency
  node-type: base/default
  attributes:
    data:
      - label: Mon
        value: 45
      - label: Tue
        value: 52
      - label: Wed
        value: 49
      - label: Thu
        value: 63
      - label: Fri
        value: 58
      - label: Sat
        value: 31
      - label: Sun
        value: 28
  render:
    style: line-chart
    params:
      header:
        source: literal
        value: P99 Latency (ms)
      data:
        source: attribute
        key: data
      yLabel:
        source: literal
        value: ms
    scale:
      x: 3.5
      y: 2.5
[
  {
    "id": "weekly-latency",
    "label": "Weekly P99 Latency",
    "node-type": "base/default",
    "attributes": {
      "data": [
        { "label": "Mon", "value": 45 },
        { "label": "Tue", "value": 52 },
        { "label": "Wed", "value": 49 },
        { "label": "Thu", "value": 63 },
        { "label": "Fri", "value": 58 },
        { "label": "Sat", "value": 31 },
        { "label": "Sun", "value": 28 }
      ]
    },
    "render": {
      "style": "line-chart",
      "params": {
        "header": { "source": "literal", "value": "P99 Latency (ms)" },
        "data": { "source": "attribute", "key": "data" },
        "yLabel": { "source": "literal", "value": "ms" }
      },
      "scale": { "x": 3.5, "y": 2.5 }
    }
  }
]

Here’s a line chart with area fill enabled, useful for visualising volume over time:

- id: daily-requests
  label: Daily Requests
  node-type: base/default
  attributes:
    data:
      - label: Mon
        value: 4200
      - label: Tue
        value: 3800
      - label: Wed
        value: 5100
      - label: Thu
        value: 4700
      - label: Fri
        value: 3900
      - label: Sat
        value: 1200
      - label: Sun
        value: 800
  render:
    style: line-chart
    params:
      header:
        source: literal
        value: Daily Requests
      data:
        source: attribute
        key: data
      yLabel:
        source: literal
        value: req/day
      fill:
        source: literal
        value: true
      domainMin:
        source: literal
        value: 0
    scale:
      x: 3.0
      y: 2.0
[
  {
    "id": "daily-requests",
    "label": "Daily Requests",
    "node-type": "base/default",
    "attributes": {
      "data": [
        { "label": "Mon", "value": 4200 },
        { "label": "Tue", "value": 3800 },
        { "label": "Wed", "value": 5100 },
        { "label": "Thu", "value": 4700 },
        { "label": "Fri", "value": 3900 },
        { "label": "Sat", "value": 1200 },
        { "label": "Sun", "value": 800 }
      ]
    },
    "render": {
      "style": "line-chart",
      "params": {
        "header": { "source": "literal", "value": "Daily Requests" },
        "data": { "source": "attribute", "key": "data" },
        "yLabel": { "source": "literal", "value": "req/day" },
        "fill": { "source": "literal", "value": true },
        "domainMin": { "source": "literal", "value": 0 }
      },
      "scale": { "x": 3.0, "y": 2.0 }
    }
  }
]

Like bar charts, line charts work with channels for real-time data. Use domainMin and domainMax to lock the y-axis range:

- id: live-throughput
  label: Live Throughput
  node-type: base/default
  channels:
    throughput_data:
      label: Throughput
      stale-after: 60000
      default:
        - label: "00:00"
          value: 120
        - label: "04:00"
          value: 45
        - label: "08:00"
          value: 310
        - label: "12:00"
          value: 520
        - label: "16:00"
          value: 480
        - label: "20:00"
          value: 290
  render:
    style: line-chart
    params:
      header:
        source: literal
        value: Throughput (req/s)
      data:
        source: channel
        key: throughput_data
      yLabel:
        source: literal
        value: req/s
      domainMin:
        source: literal
        value: 0
      domainMax:
        source: literal
        value: 600
      fill:
        source: literal
        value: true
    scale:
      x: 3.0
      y: 2.0
[
  {
    "id": "live-throughput",
    "label": "Live Throughput",
    "node-type": "base/default",
    "channels": {
      "throughput_data": {
        "label": "Throughput",
        "stale-after": 60000,
        "default": [
          { "label": "00:00", "value": 120 },
          { "label": "04:00", "value": 45 },
          { "label": "08:00", "value": 310 },
          { "label": "12:00", "value": 520 },
          { "label": "16:00", "value": 480 },
          { "label": "20:00", "value": 290 }
        ]
      }
    },
    "render": {
      "style": "line-chart",
      "params": {
        "header": { "source": "literal", "value": "Throughput (req/s)" },
        "data": { "source": "channel", "key": "throughput_data" },
        "yLabel": { "source": "literal", "value": "req/s" },
        "domainMin": { "source": "literal", "value": 0 },
        "domainMax": { "source": "literal", "value": 600 },
        "fill": { "source": "literal", "value": true }
      },
      "scale": { "x": 3.0, "y": 2.0 }
    }
  }
]

To plot multiple series on the same chart, use datasets instead of data. Each entry provides a label (shown in the legend) and a data value in any supported format. The y-axis domain is computed across all series, and series colors are auto-generated from the node background:

- id: latency-percentiles
  label: Latency Percentiles
  node-type: base/default
  attributes:
    datasets:
      - label: p50
        data:
          - label: Mon
            value: 18
          - label: Tue
            value: 21
          - label: Wed
            value: 19
      - label: p95
        data:
          - label: Mon
            value: 45
          - label: Tue
            value: 52
          - label: Wed
            value: 49
      - label: p99
        data:
          - label: Mon
            value: 92
          - label: Tue
            value: 118
          - label: Wed
            value: 105
  render:
    style: line-chart
    params:
      header:
        source: literal
        value: API Latency (ms)
      datasets:
        source: attribute
        key: datasets
      yLabel:
        source: literal
        value: ms
      domainMin:
        source: literal
        value: 0
    scale:
      x: 3.5
      y: 2.5
[
  {
    "id": "latency-percentiles",
    "label": "Latency Percentiles",
    "node-type": "base/default",
    "attributes": {
      "datasets": [
        {
          "label": "p50",
          "data": [
            { "label": "Mon", "value": 18 },
            { "label": "Tue", "value": 21 },
            { "label": "Wed", "value": 19 }
          ]
        },
        {
          "label": "p95",
          "data": [
            { "label": "Mon", "value": 45 },
            { "label": "Tue", "value": 52 },
            { "label": "Wed", "value": 49 }
          ]
        },
        {
          "label": "p99",
          "data": [
            { "label": "Mon", "value": 92 },
            { "label": "Tue", "value": 118 },
            { "label": "Wed", "value": 105 }
          ]
        }
      ]
    },
    "render": {
      "style": "line-chart",
      "params": {
        "header": { "source": "literal", "value": "API Latency (ms)" },
        "datasets": { "source": "attribute", "key": "datasets" },
        "yLabel": { "source": "literal", "value": "ms" },
        "domainMin": { "source": "literal", "value": 0 }
      },
      "scale": { "x": 3.5, "y": 2.5 }
    }
  }
]

Chart data formats

All chart styles (pie-chart, bar-chart, and line-chart) accept the data param in several formats. The renderer coerces the value automatically, so you can use whichever shape is most convenient for your data source.

FormatExampleNotes
Array of objects[{ "label": "A", "value": 10 }, ...]Most explicit.
Array of tuples[["A", 10], ["B", 20]]Compact alternative.
Array of numbers[10, 20, 30]Labels auto-generated as “Slice 1” / “Bar 1” / “Point 1”, etc.
Record (object map){ "A": 10, "B": 20 }Keys become labels.

Invalid entries (null, non-finite numbers) are silently dropped. For pie charts, negative and zero values are also dropped since they have no visual meaning in a proportional pie. Bar charts and line charts allow negative values.

Node types and packs

Every node has a node-type that controls its visual appearance on the canvas. Schematify ships built-in node types in packs. A document can also include type definitions in its top-level node-types array.

A node type ID is namespaced: base/default, microservices/service, databases/default, aws/lambda. The namespace is the pack name, and the ID after the slash references a specific item in that pack.

- id: api-gateway
  label: API Gateway
  node-type: microservices/service
[
  {
    "id": "api-gateway",
    "label": "API Gateway",
    "node-type": "microservices/service"
  }
]

If you need custom visuals for your graph, you can include node type definitions directly in the document using the node-types array at the top level. Each entry provides an id and a texture (the image, dimensions, background colour, and shape used to render the node).

node-types:
  - id: custom/monitoring
    texture:
      url: "/my-textures/monitoring.svg"
      width: 92
      height: 92
      background: "#4a90d9"
{
  "node-types": [
    {
      "id": "custom/monitoring",
      "texture": {
        "url": "/my-textures/monitoring.svg",
        "width": 92,
        "height": 92,
        "background": "#4a90d9"
      }
    }
  ]
}

When a node references custom/monitoring as its type, the viewer uses this texture definition instead of looking it up in a pack.

Node status

Nodes can carry a status indicator that displays as a coloured badge on the canvas. Status is set via the node-status field, which has two sub-fields:

  • type: the status badge ID. Can be a raw string (literal shorthand) or a param binding ({ source, key }).
  • report: an array of values to surface in tooltips and the inspect modal. Each entry can be a raw string (looked up from attributes) or a param binding.

Static status (literal shorthand)

The simplest form uses raw strings, identical to how params work elsewhere in the document.

- id: api-gateway
  label: API Gateway
  node-type: microservices/service
  node-status:
    type: base/healthy
    report:
      - cpu_usage
      - uptime
[
  {
    "id": "api-gateway",
    "label": "API Gateway",
    "node-type": "microservices/service",
    "node-status": {
      "type": "base/healthy",
      "report": ["cpu_usage", "uptime"]
    }
  }
]

Here type is the literal status ID "base/healthy", and each report entry is a string key resolved from the node’s attributes.

Channel-driven status (realtime)

Because type and report entries accept param bindings, status can be driven by live channel data.

- id: api-gateway
  label: API Gateway
  node-type: microservices/service
  channels:
    health-check:
      stale-after: 5000
      default: base/unknown
    cpu_usage:
      stale-after: 10000
  node-status:
    type:
      source: channel
      key: health-check
    report:
      - source: channel
        key: cpu_usage
      - source: attribute
        key: uptime
[
  {
    "id": "api-gateway",
    "label": "API Gateway",
    "node-type": "microservices/service",
    "channels": {
      "health-check": {
        "stale-after": 5000,
        "default": "base/unknown"
      },
      "cpu_usage": {
        "stale-after": 10000
      }
    },
    "node-status": {
      "type": { "source": "channel", "key": "health-check" },
      "report": [
        { "source": "channel", "key": "cpu_usage" },
        { "source": "attribute", "key": "uptime" }
      ]
    }
  }
]

When the pipeline publishes a new value to the health-check channel, the badge updates in real time. The report mixes live channel data (cpu_usage) with static attributes (uptime).

Binding sources

The same binding model used by render.params applies here:

SourceMeaning
attributeLook up a static value from the node’s attributes.
channelLook up a live value from the node’s channels.
literalUse the value directly.

Raw primitives (strings, numbers, booleans) are shorthand for { source: "literal", value: ... }.

Status bubbling

The type value references a status badge definition from the active packs. The base pack includes base/healthy, base/warning, base/alert, base/critical, base/info, base/maintenance, and base/unknown.

Each status definition sets a priority and whether it bubbles through a group. In the base pack, base/critical, base/alert, and base/warning bubble. If several bubble-enabled statuses exist in a branch, the viewer surfaces the highest-priority one on the group.

Tip

Channel-driven values do not appear under node-status.type or as autocomplete value suggestions. The declared source channel remains searchable explicitly, for example channels.health-check=base/critical.

For more on how status bubbling works visually, see Interface Overview.

Full example

A small infrastructure graph that puts the pieces together.

id: infra-dashboard
label: Infrastructure Dashboard
description: Production services and data stores

node-types: []

root:
  id: Root
  label: Root
  node-type: base/collection
  links:
    - from: services/api-gateway
      to: data/database
      direction: forwards
    - from: services/api-gateway
      to: data/cache
      direction: forwards
    - from: services/worker
      to: data/database
      direction: forwards
  nodes:
    - id: services
      label: Services
      node-type: base/collection
      nodes:
        - id: api-gateway
          label: API Gateway
          node-type: microservices/service
          attributes:
            owner: platform-team
            region: us-east-1
          channels:
            status:
              stale-after: 30000
              default: base/unknown
            latency-p99:
              label: P99 Latency (ms)
              stale-after: 10000
          node-status:
            type: base/healthy

        - id: worker
          label: Background Worker
          node-type: microservices/service
          attributes:
            owner: data-team
            region: us-east-1
          channels:
            queue-depth:
              stale-after: 5000
              default: 0
          node-status:
            type: base/warning

    - id: data
      label: Data Stores
      node-type: base/collection
      nodes:
        - id: database
          label: PostgreSQL Primary
          node-type: databases/default
          attributes:
            engine: postgresql
            version: "16.2"
          channels:
            connections:
              stale-after: 5000
              default: 0
            replication-lag:
              label: Replication Lag
              stale-after: 10000

        - id: cache
          label: Redis
          node-type: databases/default
          attributes:
            engine: redis
            version: "7.2"
          channels:
            memory-usage:
              label: Memory Usage (%)
              stale-after: 10000
              default: 0
          node-status:
            type: base/healthy
{
  "id": "infra-dashboard",
  "label": "Infrastructure Dashboard",
  "description": "Production services and data stores",
  "node-types": [],
  "root": {
    "id": "Root",
    "label": "Root",
    "node-type": "base/collection",
    "links": [
      {
        "from": "services/api-gateway",
        "to": "data/database",
        "direction": "forwards"
      },
      {
        "from": "services/api-gateway",
        "to": "data/cache",
        "direction": "forwards"
      },
      {
        "from": "services/worker",
        "to": "data/database",
        "direction": "forwards"
      }
    ],
    "nodes": [
      {
        "id": "services",
        "label": "Services",
        "node-type": "base/collection",
        "nodes": [
          {
            "id": "api-gateway",
            "label": "API Gateway",
            "node-type": "microservices/service",
            "attributes": {
              "owner": "platform-team",
              "region": "us-east-1"
            },
            "channels": {
              "status": {
                "stale-after": 30000,
                "default": "base/unknown"
              },
              "latency-p99": {
                "label": "P99 Latency (ms)",
                "stale-after": 10000
              }
            },
            "node-status": {
              "type": "base/healthy"
            }
          },
          {
            "id": "worker",
            "label": "Background Worker",
            "node-type": "microservices/service",
            "attributes": {
              "owner": "data-team",
              "region": "us-east-1"
            },
            "channels": {
              "queue-depth": {
                "stale-after": 5000,
                "default": 0
              }
            },
            "node-status": {
              "type": "base/warning"
            }
          }
        ]
      },
      {
        "id": "data",
        "label": "Data Stores",
        "node-type": "base/collection",
        "nodes": [
          {
            "id": "database",
            "label": "PostgreSQL Primary",
            "node-type": "databases/default",
            "attributes": {
              "engine": "postgresql",
              "version": "16.2"
            },
            "channels": {
              "connections": {
                "stale-after": 5000,
                "default": 0
              },
              "replication-lag": {
                "label": "Replication Lag",
                "stale-after": 10000
              }
            }
          },
          {
            "id": "cache",
            "label": "Redis",
            "node-type": "databases/default",
            "attributes": {
              "engine": "redis",
              "version": "7.2"
            },
            "channels": {
              "memory-usage": {
                "label": "Memory Usage (%)",
                "stale-after": 10000,
                "default": 0
              }
            },
            "node-status": {
              "type": "base/healthy"
            }
          }
        ]
      }
    ]
  }
}

This document defines two groups (Services and Data Stores), four nodes with attributes and channels, three cross-cutting links, and status on three of the four nodes. Each group surfaces only the statuses in its own branch. The base/warning status on the worker node bubbles up to the Services group, not to Data Stores.

What’s next

  • Document Manager provides a structured editor for this format.
  • CLI validates and pushes YAML or JSON documents.
  • Scriptable Graphs generates the same document structure from TypeScript.