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: alwaysexcalidrawversion: scene-content schema versionsource: source application or URL that produced the documentappState: the subset of app state stored with the sceneelements: array of persisted Excalidraw elementssceneVersion: scene-level version string used by the app for reconciliation
Stored appState fields
The validated write schema currently stores these app-state fields:
viewBackgroundColorlockedMultiSelections
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:
idmimeTypecreatedlastRetrieved(optional)version(optional)dataURLfor 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 asrectangle,text,arrow, orimage.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 ofhachure,cross-hatch,solid, orzigzag.strokeStyle: one ofsolid,dashed, ordotted.roundness:nullor an object such as{ "type": 3 }controlling corner or path rounding.roughness: hand-drawn roughness level. Common values are0,1, and2.opacity: percentage from0to100.angle: stored in radians.seed: stable rendering seed.version: incremented when the element changes.versionNonce: random nonce used alongsideversionduring 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:
rectanglediamondellipseembeddableframemagicframeiframeimagetextlinearrowfreedraw
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:
fontSizefontFamily(stored as a numeric family ID)texttextAlign:left,center,rightverticalAlign:top,middle,bottomcontainerIdoriginalTextautoResizelineHeight(unitless)
Notes:
autoResize: truemeans the text element width follows the text content.autoResize: falsemeans the text wraps inside the current bounds.originalTextpreserves the raw authored text value.lineHeightmust be multiplied byfontSizeto 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
textelement - 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:
fileIdstatus:pending,saved, orerrorscale:[xScale, yScale]crop
Notes:
fileIdshould reference a file entry when the image is persisted.scaleis used for mirroring or flipping an image.cropis eithernullor 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:
pointsstartBindingendBindingstartArrowheadendArrowhead
Bindings
Bindings attach a connector to another element and look like this:
{
"elementId": "Q4x6Lh5y2C9vK8mN3pR1s",
"fixedPoint": [1, 0.5],
"mode": "inside"
}elementId: target element IDfixedPoint: normalized target position relative to the target width/heightmode:inside,orbit, orskip
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:
arrowbarcirclecircle_outlinetriangletriangle_outlinediamonddiamond_outlinecardinality_onecardinality_manycardinality_one_or_manycardinality_exactly_onecardinality_zero_or_onecardinality_zero_or_manynull
Line-specific and arrow-specific fields
linemay usepolygon: booleanarrowmay useelbowed: boolean- elbow arrows may also include
fixedSegments,startIsSpecial, andendIsSpecial
Elbow-arrow fields
When elbowed is true, an arrow may also carry:
fixedSegments: explicit routed segmentsstartIsSpecialendIsSpecial
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:
pointspressuressimulatePressure
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:
frameIdmust point to aframeormagicframecontainerIdmust point torectangle,diamond,ellipse, orarrow- bound text must point back to its container correctly
- bound arrows must point back to their bound shape correctly
- image
fileIdvalues must reference a file when files are present
It also constrains relation object shapes, for example:
boundElements[].typemust bearrowortext- binding
modemust beinside,orbit, orskip - line and arrow
pointsmust 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_contentworks with higher-leveladd,update, anddeleteoperations.
MCP also supports helper concepts that are not persisted as final element fields:
tempIdfor same-request references between newly added elementslabelfor 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
selectionelements - open-ended
customDatacontents - 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
frameIdfor grouping into frames, not for relative positioning.
Related docs
Last updated on