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 toidif omitted.node-type: references a visual definition from a pack (e.g.microservices/service,databases/default). Defaults tobase/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
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 atroot > production > us-east > api-gateway, the path isproduction/us-east/api-gateway.direction:forwards(default),backwards,bidirectional, ornone. Controls the arrow direction drawn on the canvas.link-type: optional classification label (e.g.replication,dependency). Defaults todefault.attributes: optional key-value metadata on the link itself.
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.
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
| Style | Description |
|---|---|
default | Icon and label (the standard look). No params required. |
property | Displays a single value prominently with a header. |
report | Displays a table of key-value rows. |
pie-chart | Proportional pie chart visualization. |
bar-chart | Vertical bar chart visualization. |
line-chart | Line graph visualization with data points. |
chart | Reserved 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).
| Source | Meaning |
|---|---|
attribute | Look up a static value from the node’s attributes. |
channel | Look up a live value from the node’s channels. |
literal | Use 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:0to1ratio controlling internal padding around the chart area. Defaults to0.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 }
}
}
]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 to1. Accepts0.5to3. Useful for keeping text readable on very large or very small charts.padding:0to1ratio controlling internal padding around the chart area. Defaults to0.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 }
}
}
]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 whendatasetsis present.datasets: multi-series dataset. An array of{ label, data }objects wherelabelis the series name shown in the legend anddataaccepts the same formats as the single-seriesdataparam. 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 usingdatasets.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 (default1, range0.5to3).padding:0to1ratio controlling internal padding around the chart area. Defaults to0.1.showDots:true|false(defaulttrue). Render circles at each data point.lineWidth: stroke width of the line (default2, range0.5to10).fill:true|false(defaultfalse). 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.
| Format | Example | Notes |
|---|---|---|
| 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:
| Source | Meaning |
|---|---|
attribute | Look up a static value from the node’s attributes. |
channel | Look up a live value from the node’s channels. |
literal | Use 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.
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.