Scene Content Schema

Human-readable reference for the shared Excalidraw scene content format used by the API and MCP

Shared Format

This page describes the shared Excalidraw scene content format used by the public API scene-content endpoints and MCP tools such as get_scene_content, search_scene_content, edit_scene_content, and read_excalidraw_format.

This reference is derived from the same server-side schema used to validate scene content writes plus the persisted Excalidraw element model. It is meant to explain what the data means, not just list raw TypeScript fields.

It intentionally de-duplicates the large shared base across element types. The goal is to stay close to the real schema without forcing you to read the same base block repeated for every shape.

Whole scene payload

Scene content is an Excalidraw document with scene-level metadata plus an array of elements.

{
  "type": "excalidraw",
  "version": 2,
  "source": "https://plus.excalidraw.com",
  "appState": {
    "viewBackgroundColor": "#ffffff",
    "lockedMultiSelections": {}
  },
  "elements": [
    {
      "id": "Q4x6Lh5y2C9vK8mN3pR1s",
      "type": "rectangle",
      "x": 120,
      "y": 160,
      "width": 280,
      "height": 120,
      "strokeColor": "#1e1e1e",
      "backgroundColor": "#d0ebff",
      "fillStyle": "solid",
      "strokeWidth": 2,
      "strokeStyle": "solid",
      "roundness": { "type": 3 },
      "roughness": 1,
      "opacity": 100,
      "angle": 0,
      "seed": 123456789,
      "version": 12,
      "versionNonce": 987654321,
      "index": "a1",
      "isDeleted": false,
      "groupIds": [],
      "frameId": null,
      "boundElements": null,
      "updated": 1778156973482,
      "link": null,
      "locked": false
    }
  ],
  "sceneVersion": "15"
}

When a write operation includes image files, those files live in a separate files object keyed by fileId.

Scene-level fields

  • type: always excalidraw
  • version: scene-content schema version
  • source: source application or URL that produced the document
  • appState: the subset of app state stored with the scene
  • elements: array of persisted Excalidraw elements
  • sceneVersion: scene-level version string used by the app for reconciliation

Stored appState fields

The validated write schema currently stores these app-state fields:

  • viewBackgroundColor
  • lockedMultiSelections

Example:

{
  "appState": {
    "viewBackgroundColor": "#ffffff",
    "lockedMultiSelections": {}
  }
}

Files object

When image elements reference files, the payload can also include file records.

{
  "files": {
    "file_123": {
      "id": "file_123",
      "mimeType": "image/png",
      "created": 1778156973482,
      "dataURL": "data:image/png;base64,..."
    }
  }
}

File records can include:

  • id
  • mimeType
  • created
  • lastRetrieved (optional)
  • version (optional)
  • dataURL for full file payloads

Common element fields

Every persisted element type shares the same structural base:

  • Identity: id, type
  • Position and size: x, y, width, height, angle
  • Visual style: strokeColor, backgroundColor, fillStyle, strokeWidth, strokeStyle, roundness, roughness, opacity
  • Reconciliation and ordering: seed, version, versionNonce, index, updated
  • Lifecycle: isDeleted, locked
  • Grouping and framing: groupIds, frameId
  • Cross-element links: boundElements, link
  • Extension point: customData

What those fields mean

  • id: opaque persisted element ID. Treat it as a stable identifier, not a semantic name.
  • type: the element family, such as rectangle, text, arrow, or image.
  • x, y: top-left anchor in scene coordinates.
  • width, height: rendered bounds in scene units.
  • angle: element rotation in radians.
  • strokeColor: outline color for shapes, line color for lines and arrows, and glyph color for text.
  • backgroundColor: fill color for closed shapes. Use "transparent" for no fill.
  • fillStyle: one of hachure, cross-hatch, solid, or zigzag.
  • strokeStyle: one of solid, dashed, or dotted.
  • roundness: null or an object such as { "type": 3 } controlling corner or path rounding.
  • roughness: hand-drawn roughness level. Common values are 0, 1, and 2.
  • opacity: percentage from 0 to 100.
  • angle: stored in radians.
  • seed: stable rendering seed.
  • version: incremented when the element changes.
  • versionNonce: random nonce used alongside version during reconciliation.
  • index: fractional ordering key used to place the element in z/order.
  • groupIds: nested group membership, ordered from deepest to shallowest.
  • frameId: frame membership. This assigns membership only; it does not offset child coordinates.
  • boundElements: reverse references to elements attached to this one, usually arrows or bound text.
  • updated: last update timestamp in epoch milliseconds.
  • link: optional hyperlink.
  • customData: arbitrary app-specific metadata.

Exact shared object shapes

roundness

{ "type": 3 }

Or:

{ "type": 2, "value": 12 }

boundElements

[
  { "id": "someArrowId", "type": "arrow" },
  { "id": "someLabelId", "type": "text" }
]

index

index is either a fractional ordering string or null for newly created or not-yet-indexed elements.

IDs on write vs persisted IDs

The write schema accepts scene-scoped IDs as strings or numbers. If the IDs are not already valid persisted Excalidraw IDs, the server normalizes them when it can. Persisted scene content should be treated as canonical output.

Persisted element types

The persisted scene-content schema accepts these element types:

  • rectangle
  • diamond
  • ellipse
  • embeddable
  • frame
  • magicframe
  • iframe
  • image
  • text
  • line
  • arrow
  • freedraw

selection is an editor-only type and is not part of persisted scene content.

Shape elements

rectangle, diamond, ellipse, and embeddable share only the common base fields above.

Use these when you need:

  • regular containers or cards: rectangle
  • decision-style shapes: diamond
  • circular or soft rounded shapes: ellipse
  • generic embedded surface placeholders: embeddable

Persisted rectangle example

{
  "id": "rEcTaNgLeHiJkLmNoPqRs",
  "type": "rectangle",
  "x": 120,
  "y": 160,
  "width": 280,
  "height": 120,
  "strokeColor": "#1e1e1e",
  "backgroundColor": "#d0ebff",
  "fillStyle": "solid",
  "strokeWidth": 2,
  "strokeStyle": "solid",
  "roundness": { "type": 3 },
  "roughness": 1,
  "opacity": 100,
  "angle": 0,
  "seed": 123456789,
  "version": 12,
  "versionNonce": 987654321,
  "index": "a1",
  "isDeleted": false,
  "groupIds": [],
  "frameId": null,
  "boundElements": null,
  "updated": 1778156973482,
  "link": null,
  "locked": false
}

Persisted diamond example

{
  "id": "dIaMoNdEfGhIjKlMnOpQr",
  "type": "diamond",
  "x": 480,
  "y": 180,
  "width": 180,
  "height": 140,
  "strokeColor": "#1e1e1e",
  "backgroundColor": "#fff3bf",
  "fillStyle": "solid",
  "strokeWidth": 2,
  "strokeStyle": "solid",
  "roundness": null,
  "roughness": 1,
  "opacity": 100,
  "angle": 0,
  "seed": 441209331,
  "version": 7,
  "versionNonce": 287441903,
  "index": "a2",
  "isDeleted": false,
  "groupIds": [],
  "frameId": null,
  "boundElements": null,
  "updated": 1778156973482,
  "link": null,
  "locked": false
}

Persisted ellipse example

{
  "id": "eLlIpSeEfGhIjKlMnOpQr",
  "type": "ellipse",
  "x": 720,
  "y": 180,
  "width": 200,
  "height": 120,
  "strokeColor": "#1e1e1e",
  "backgroundColor": "#d3f9d8",
  "fillStyle": "solid",
  "strokeWidth": 2,
  "strokeStyle": "solid",
  "roundness": null,
  "roughness": 1,
  "opacity": 100,
  "angle": 0,
  "seed": 610448223,
  "version": 9,
  "versionNonce": 140338112,
  "index": "a2V",
  "isDeleted": false,
  "groupIds": [],
  "frameId": null,
  "boundElements": null,
  "updated": 1778156973482,
  "link": null,
  "locked": false
}

Frame elements

frame and magicframe add:

  • name: string | null

Use frames for slide-like composition, named sections, or grouped canvas areas. All child elements still use absolute scene coordinates; frameId only records membership.

magicframe uses the same persisted shape as frame, but it represents a specialized product concept rather than a separate structural schema family.

Persisted frame example

{
  "id": "aBcDeFgHiJkLmNoPqRsTu",
  "type": "frame",
  "x": 100,
  "y": 80,
  "width": 854,
  "height": 480,
  "strokeColor": "#1e1e1e",
  "backgroundColor": "transparent",
  "fillStyle": "solid",
  "strokeWidth": 2,
  "strokeStyle": "solid",
  "roundness": { "type": 3 },
  "roughness": 1,
  "opacity": 100,
  "angle": 0,
  "seed": 218347651,
  "version": 14,
  "versionNonce": 551239001,
  "index": "a3",
  "isDeleted": false,
  "groupIds": [],
  "frameId": null,
  "boundElements": null,
  "updated": 1778156973482,
  "link": null,
  "locked": false,
  "name": "Intro Slide"
}

Text elements

text adds:

  • fontSize
  • fontFamily (stored as a numeric family ID)
  • text
  • textAlign: left, center, right
  • verticalAlign: top, middle, bottom
  • containerId
  • originalText
  • autoResize
  • lineHeight (unitless)

Notes:

  • autoResize: true means the text element width follows the text content.
  • autoResize: false means the text wraps inside the current bounds.
  • originalText preserves the raw authored text value.
  • lineHeight must be multiplied by fontSize to estimate pixel line height.

Shape labels are stored as text elements

Persisted scene content does not store a shape label inline on the shape. Instead:

  • the shape lists the label in boundElements
  • the label is a separate text element
  • that text element points back to the shape with containerId

This matters because MCP edit_scene_content lets you use a higher-level label helper, but the saved scene still becomes a normal text element plus bindings.

Persisted text example

{
  "id": "tExTgHiJkLmNoPqRsTuVw",
  "type": "text",
  "x": 164,
  "y": 128,
  "width": 420,
  "height": 72,
  "strokeColor": "#1e1e1e",
  "backgroundColor": "transparent",
  "fillStyle": "solid",
  "strokeWidth": 1,
  "strokeStyle": "solid",
  "roundness": null,
  "roughness": 1,
  "opacity": 100,
  "angle": 0,
  "seed": 382940115,
  "version": 8,
  "versionNonce": 740191222,
  "index": "a4",
  "isDeleted": false,
  "groupIds": [],
  "frameId": "aBcDeFgHiJkLmNoPqRsTu",
  "boundElements": null,
  "updated": 1778156973482,
  "link": null,
  "locked": false,
  "fontSize": 32,
  "fontFamily": 1,
  "text": "Quarterly Architecture Overview",
  "textAlign": "left",
  "verticalAlign": "top",
  "containerId": null,
  "originalText": "Quarterly Architecture Overview",
  "autoResize": true,
  "lineHeight": 1.25
}

Image elements

image adds:

  • fileId
  • status: pending, saved, or error
  • scale: [xScale, yScale]
  • crop

Notes:

  • fileId should reference a file entry when the image is persisted.
  • scale is used for mirroring or flipping an image.
  • crop is either null or a crop rectangle with both displayed and natural dimensions.

crop shape

{
  "x": 120,
  "y": 40,
  "width": 800,
  "height": 450,
  "naturalWidth": 1600,
  "naturalHeight": 900
}

Persisted image example

{
  "id": "iMaGeFgHiJkLmNoPqRsTu",
  "type": "image",
  "x": 240,
  "y": 220,
  "width": 320,
  "height": 180,
  "strokeColor": "transparent",
  "backgroundColor": "transparent",
  "fillStyle": "solid",
  "strokeWidth": 1,
  "strokeStyle": "solid",
  "roundness": null,
  "roughness": 0,
  "opacity": 100,
  "angle": 0,
  "seed": 640182993,
  "version": 19,
  "versionNonce": 208441771,
  "index": "a7",
  "isDeleted": false,
  "groupIds": [],
  "frameId": "aBcDeFgHiJkLmNoPqRsTu",
  "boundElements": null,
  "updated": 1778156973482,
  "link": null,
  "locked": false,
  "fileId": "file_123",
  "status": "saved",
  "scale": [1, 1],
  "crop": null
}

Iframe elements

iframe uses the shared base schema. In practice, integrations often store extra runtime metadata in customData, but the persisted schema treats that as open-ended custom data rather than a fixed iframe-only contract.

The editor/runtime type model may also attach richer iframe-specific customData, for example generation state objects. That is useful to know when reading existing scenes, but it is intentionally not locked into the public scene-content validation schema.

Persisted iframe example

{
  "id": "iFrAmEeFgHiJkLmNoPqRs",
  "type": "iframe",
  "x": 180,
  "y": 560,
  "width": 560,
  "height": 315,
  "strokeColor": "transparent",
  "backgroundColor": "transparent",
  "fillStyle": "solid",
  "strokeWidth": 1,
  "strokeStyle": "solid",
  "roundness": { "type": 3 },
  "roughness": 1,
  "opacity": 100,
  "angle": 0,
  "seed": 380784165,
  "version": 25,
  "versionNonce": 829784709,
  "index": "aE",
  "isDeleted": false,
  "groupIds": [],
  "frameId": null,
  "boundElements": null,
  "updated": 1778156973482,
  "link": "https://www.youtube.com/watch?v=kTRfKvzhxK8",
  "locked": false
}

Line and arrow elements

line and arrow both add:

  • points
  • startBinding
  • endBinding
  • startArrowhead
  • endArrowhead

Bindings

Bindings attach a connector to another element and look like this:

{
  "elementId": "Q4x6Lh5y2C9vK8mN3pR1s",
  "fixedPoint": [1, 0.5],
  "mode": "inside"
}
  • elementId: target element ID
  • fixedPoint: normalized target position relative to the target width/height
  • mode: inside, orbit, or skip

The binding target must be a bindable element such as a rectangle, diamond, ellipse, text, image, iframe, embeddable, frame, or magicframe.

fixedPoint is a local ratio, not an absolute scene coordinate. Common useful positions are:

  • right edge: [1, 0.5]
  • left edge: [0, 0.5]
  • top edge: [0.5, 0]
  • bottom edge: [0.5, 1]

Arrowheads

Supported arrowhead values are:

  • arrow
  • bar
  • circle
  • circle_outline
  • triangle
  • triangle_outline
  • diamond
  • diamond_outline
  • cardinality_one
  • cardinality_many
  • cardinality_one_or_many
  • cardinality_exactly_one
  • cardinality_zero_or_one
  • cardinality_zero_or_many
  • null

Line-specific and arrow-specific fields

  • line may use polygon: boolean
  • arrow may use elbowed: boolean
  • elbow arrows may also include fixedSegments, startIsSpecial, and endIsSpecial

Elbow-arrow fields

When elbowed is true, an arrow may also carry:

  • fixedSegments: explicit routed segments
  • startIsSpecial
  • endIsSpecial

fixedSegments looks like this:

[
  {
    "start": [0, 0],
    "end": [120, 0],
    "index": 0
  }
]

These are advanced routing details. Most integrations should preserve them when editing an existing elbow arrow unless they intentionally want to reroute it.

Persisted line example

{
  "id": "lInEeFgHiJkLmNoPqRsTu",
  "type": "line",
  "x": 160,
  "y": 360,
  "width": 260,
  "height": 60,
  "strokeColor": "#495057",
  "backgroundColor": "transparent",
  "fillStyle": "solid",
  "strokeWidth": 2,
  "strokeStyle": "dashed",
  "roundness": null,
  "roughness": 1,
  "opacity": 100,
  "angle": 0,
  "seed": 551102334,
  "version": 5,
  "versionNonce": 223401992,
  "index": "b0",
  "isDeleted": false,
  "groupIds": [],
  "frameId": null,
  "boundElements": null,
  "updated": 1778156973482,
  "link": null,
  "locked": false,
  "points": [[0, 0], [80, 20], [160, 20], [260, 60]],
  "startBinding": null,
  "endBinding": null,
  "startArrowhead": null,
  "endArrowhead": null,
  "polygon": false
}

Persisted arrow example

{
  "id": "aRrOwFgHiJkLmNoPqRsTu",
  "type": "arrow",
  "x": 400,
  "y": 220,
  "width": 260,
  "height": 120,
  "strokeColor": "#1e1e1e",
  "backgroundColor": "transparent",
  "fillStyle": "solid",
  "strokeWidth": 2,
  "strokeStyle": "solid",
  "roundness": { "type": 2 },
  "roughness": 1,
  "opacity": 100,
  "angle": 0,
  "seed": 938441250,
  "version": 11,
  "versionNonce": 398440211,
  "index": "b1",
  "isDeleted": false,
  "groupIds": [],
  "frameId": null,
  "boundElements": null,
  "updated": 1778156973482,
  "link": null,
  "locked": false,
  "points": [[0, 0], [260, 120]],
  "startBinding": {
    "elementId": "Q4x6Lh5y2C9vK8mN3pR1s",
    "fixedPoint": [1, 0.5],
    "mode": "inside"
  },
  "endBinding": {
    "elementId": "nOdE2FgHiJkLmNoPqRsTu",
    "fixedPoint": [0, 0.5],
    "mode": "inside"
  },
  "startArrowhead": null,
  "endArrowhead": "triangle",
  "elbowed": false
}

Freedraw elements

freedraw adds:

  • points
  • pressures
  • simulatePressure

Use this for pen-like strokes where the shape is defined by sampled points rather than a simple box or connector path.

In practice, pressures usually tracks the point sequence and simulatePressure controls whether the stroke should synthesize pressure-like variation.

Persisted freedraw example

{
  "id": "fReEdRaWjKkLmNoPqRsTu",
  "type": "freedraw",
  "x": 120,
  "y": 420,
  "width": 180,
  "height": 64,
  "strokeColor": "#e03131",
  "backgroundColor": "transparent",
  "fillStyle": "solid",
  "strokeWidth": 2,
  "strokeStyle": "solid",
  "roundness": null,
  "roughness": 1,
  "opacity": 100,
  "angle": 0,
  "seed": 111281902,
  "version": 6,
  "versionNonce": 503819441,
  "index": "b4",
  "isDeleted": false,
  "groupIds": [],
  "frameId": null,
  "boundElements": null,
  "updated": 1778156973482,
  "link": null,
  "locked": false,
  "points": [[0, 0], [24, 8], [52, 18], [95, 35], [180, 64]],
  "pressures": [0.3, 0.4, 0.55, 0.5, 0.45],
  "simulatePressure": false
}

Reference integrity rules

The shared validation schema enforces a few important relationships:

  • frameId must point to a frame or magicframe
  • containerId must point to rectangle, diamond, ellipse, or arrow
  • bound text must point back to its container correctly
  • bound arrows must point back to their bound shape correctly
  • image fileId values must reference a file when files are present

It also constrains relation object shapes, for example:

  • boundElements[].type must be arrow or text
  • binding mode must be inside, orbit, or skip
  • line and arrow points must be arrays of finite numeric tuples

API and MCP differences

The persisted element format is shared, but the authoring ergonomics differ:

  • REST scene-content endpoints work with full scene content objects and raw element arrays.
  • MCP edit_scene_content works with higher-level add, update, and delete operations.

MCP also supports helper concepts that are not persisted as final element fields:

  • tempId for same-request references between newly added elements
  • label for shape-owned text expansion
  • high-level add/update/delete operation grouping

Those helpers are conveniences at write time only. Persisted scene content still uses normal element IDs, text nodes, containerId, and boundElements.

What this page does not try to freeze

This page describes the shared persisted scene-content contract and its main authoring semantics. It does not try to freeze every editor-internal runtime detail that may exist in upstream Excalidraw types, especially when those details are not enforced by the public validation schema.

Examples:

  • editor-only selection elements
  • open-ended customData contents
  • runtime-only convenience helpers used by MCP writes
  • implementation details of rendering, reconciliation, or layout helpers outside the persisted content contract

Practical guidance

  • Treat IDs, versions, nonces, and index keys as opaque fields.
  • Prefer rectangle, diamond, ellipse, frame, and text as your main semantic building blocks.
  • Use text containers and bindings instead of inventing a custom inline label shape format.
  • Use arrow bindings when a connector should stay attached to a shape.
  • Use frameId for grouping into frames, not for relative positioning.

Last updated on

On this page