Data model
The JSON format of a diagram and how it is stored in Grail.
Diagram
A diagram is a JSON document validated by the zod schema in ui/app/model/schema.ts. A JSON Schema export lives in skill/diagram.schema.json (npm run export:schema regenerates it).
| Field | Type | Notes |
|---|---|---|
schemaVersion | "1.0" | |
id | string | UUID v4 |
name, description, owner | string | description and owner are optional |
createdAt, updatedAt | string | ISO 8601 UTC |
settings.background | dots | grid | blank | default dots |
settings.defaultTimeframe | { from, to } | expressions such as now()-2h or ISO 8601 |
settings.refreshInterval | off | 30s | 1m | 5m | 15m | 30m | default off |
settings.viewport | { x, y, zoom } | optional; saved with the diagram |
nodes, edges | arrays | see below |
Node
| Field | Type | Notes |
|---|---|---|
id | string | unique in the diagram |
type | entityNode | customNode | must match data.kind |
position | { x, y } | |
size | { w, h } | optional; set when the node is resized |
data | entity | custom | discriminated by kind |
Entity component
| Field | Type | Notes |
|---|---|---|
componentType | string | one of: frontend, mobile, service, process, genai, host, container, database, networkDevice, k8sCluster, k8sNamespace, k8sNode, workload, k8sPod, k8sService, k8sIngress, awsEc2, awsLambda, awsEcs, awsEks, awsRds, awsDynamoDb, awsElastiCache, awsS3, awsSqs, awsSns, awsLoadBalancer, awsApiGateway, azureVm, azureAppService, azureFunction, azureContainerApp, azureAks, azureSql, azureCosmosDb, azureRedis, azureStorage, azureServiceBus, azureEventHubs, azureLoadBalancer, azureAppGateway, azureApim, gcpVm, gcpCloudRun, gcpPubSub |
name, icon | string | icon = export name from @dynatrace/strato-icons |
entities | { id, name, classicId?, endpoint? }[] | entities picked in the editor; default []. Endpoint picks keep the service id in id and the endpoint in endpoint |
entityDql | string | optional; used only when nothing is picked. Returns id (and preferably name) |
failPoint.problemMatch | string | optional DQL filter fragment |
failPoint.warningMin, failingMin | integer ≥ 1 | problems open in the timeframe to turn orange / red |
kpi | KPI block | optional, see below |
Custom component
| Field | Type | Notes |
|---|---|---|
mode | entities | slos | |
entities.dql | string | returns id and name |
entities.subNameField | string | column used as child name (default name) |
entities.criterion | anyProblem | match | match uses problemMatch |
slos | { id, name }[] | SLOs mode |
maxVisibleRows | integer ≥ 1 | default 8 |
kpi | KPI block | optional, see below |
KPI block
| Field | Type | Notes |
|---|---|---|
enabled, title | boolean, string | show the block and its heading |
items[].id | string | unique within the block |
items[].dql | string | returns a numeric column |
items[].valueField | string | optional; default: first numeric column |
items[].labelMode | text | column | fixed name (first row) or one line per row |
items[].labelText, labelField | string | the fixed name, or the name column (default: first text column) |
items[].unit, decimals, maxRows | string, 0–10, 1–50 | unit text, decimals, lines in column mode |
items[].title | string | optional: name shown above the KPI's lines when they are named from a column |
items[].preset | string | optional: key of the ready-made KPI it came from (ui/app/model/kpiPresets.ts); the query may use $entityIds and friends |
dql, maxRows | string, integer | legacy single-table block (≤ 0.2); read as one column KPI |
Edge
| Field | Type | Notes |
|---|---|---|
source, target | string | node ids |
sourceHandle, targetHandle | t | r | b | l | optional node side |
type | normal | kpi | |
direction | forward | backward | none | default forward |
label | string | optional |
kpi.dql | string | returns a single value |
kpi.valueField, unit, decimals | optional column, unit text, 0–10 decimals | |
kpi.threshold | { direction, warning, failing } | above: high is bad · below: low is bad |
kpi.animated | boolean | default true |
Example
json
{
"schemaVersion": "1.0",
"id": "8f0d6a4e-2f1b-4c55-9e4a-1f2d3c4b5a69",
"name": "Checkout",
"createdAt": "2026-10-04T00:00:00Z",
"updatedAt": "2026-10-04T00:00:00Z",
"settings": { "background": "dots", "defaultTimeframe": { "from": "now()-2h", "to": "now()" }, "refreshInterval": "off" },
"nodes": [
{
"id": "web", "type": "entityNode", "position": { "x": 0, "y": 0 },
"data": {
"kind": "entity", "componentType": "frontend", "name": "Web shop", "icon": "ApplicationsIcon",
"entities": [{ "id": "FRONTEND-12DCB920668AFDF0", "name": "Web shop", "classicId": "APPLICATION-EA7C4B59F27D43EB" }],
"failPoint": { "warningMin": 1, "failingMin": 2 },
"kpi": {
"enabled": true, "title": "KPIs",
"items": [
{ "id": "k1", "dql": "fetch user.events\n| summarize events = count()", "valueField": "events",
"labelMode": "text", "labelText": "User events", "unit": "count", "decimals": 0, "maxRows": 5 }
]
}
}
},
{
"id": "api", "type": "entityNode", "position": { "x": 380, "y": 0 },
"data": {
"kind": "entity", "componentType": "service", "name": "Checkout API", "icon": "ServicesIcon",
"entityDql": "smartscapeNodes \"SERVICE\"\n| filter contains(name, \"checkout\", caseSensitive: false)\n| fields id, name",
"failPoint": { "problemMatch": "event.category == \"ERROR\"", "warningMin": 1, "failingMin": 1 }
}
}
],
"edges": [
{
"id": "e1", "source": "web", "target": "api", "sourceHandle": "r", "targetHandle": "l",
"type": "kpi", "direction": "forward", "label": "Response time",
"kpi": {
"dql": "timeseries r = avg(dt.service.request.response_time, scalar: true)\n| fieldsAdd v = r / 1000\n| fields v",
"unit": "ms", "decimals": 1, "animated": true,
"threshold": { "direction": "above", "warning": 300, "failing": 800 }
}
}
]
}Lookup row
Each diagram is one JSONL row of /lookups/custom-diagram-creator/diagrams:
| Column | Type | Notes |
|---|---|---|
id | string | lookup key |
name, description, owner | string | copies used by the list |
createdAt, updatedAt | string | ISO 8601; updatedAt drives the concurrency check |
deleted | boolean | true only for the metadata row and the placeholder kept when the last diagram is deleted |
payload | string | base64 of the UTF-8 JSON of the whole diagram |
dql
load "/lookups/custom-diagram-creator/diagrams"
| filter isFalseOrNull(deleted)
| fields id, name, description, owner, createdAt, updatedAt
| sort updatedAt desc