Dynamic Layouts – Agent API Reference

Help

Dynamic Layouts – Agent API Reference

Audience: This document is structured as a precise data-model reference intended to be pasted into an AI agent's system prompt. It describes the JSON shape of a Dynamic Layout exactly as consumed by the VenueVue signage renderer.

When invoking an agent, prepend this entire document with a short task instruction such as: "You generate VenueVue Dynamic Layout JSON. Use only the fields described in the reference below. Output a single JSON object matching the DynamicLayout schema. Do not invent fields."


High-level model

A DynamicLayout is a top-level record describing a canvas and an ordered list of elements that render on it.

{
  "name": "string",                  // Friendly name (required)
  "canvasWidth": 1920,                // Design pixels (default 1920)
  "canvasHeight": 1080,               // Design pixels (default 1080)
  "duration": null,                   // Optional total runtime in seconds; null = loop forever
  "elements": [ /* Element[] */ ]     // Ordered list, lower index renders first (z-index also applies)
}

canvasWidth × canvasHeight defines the design pixel coordinate space. At playback, the renderer scales the entire canvas to fit the display. Pixel positions inside elements (positionUnits === 'px') are interpreted against this design canvas, not the physical display.


Element — common fields

Every element in elements[] (and every element inside an inline group's data.elements[]) shares the following shape. Type-specific fields are layered on top — see the next section.

{
  "id": "el_<timestamp>_<counter>",   // Required, must be unique within the parent elements array
  "name": "string",                   // Friendly label; "" allowed
  "type": "asset" | "widget" | "html" | "url" | "shape" | "dynamicLayout" | "playlist",
  "zIndex": 0,                        // Higher renders on top; ties break by array order

  // ----- Position (design canvas) -----
  "position": {
    "top": 10,                        // Numeric value
    "left": 10,
    "width": 30,
    "height": 30
  },
  "positionUnits": {                  // Unit per dimension
    "top": "%" | "px",
    "left": "%" | "px",
    "width": "%" | "px",
    "height": "%" | "px"
  },

  // ----- Visibility breakpoints (against the RENDERED canvas pixel size) -----
  "breakpoints": {
    "minWidth": null,                 // Hide if rendered width < this
    "maxWidth": null,                 // Hide if rendered width > this
    "minHeight": null,
    "maxHeight": null
  },

  // ----- Display -----
  "fitMode": "fill",                  // 'fill' | 'contain' | 'cover' | 'stretch' | 'tile'
  "rotation": 0,                      // Degrees; positive = clockwise
  "locked": false,                    // Canvas safety lock; does NOT affect playback

  // ----- Drop shadow (applies to every element type) -----
  "shadowEnabled": false,             // Master toggle
  "shadowX": 0,                       // Horizontal offset (px against design canvas)
  "shadowY": 4,                       // Vertical offset (px)
  "shadowBlur": 16,                   // Blur radius in px (>=0)
  "shadowOpacity": 0.5,               // 0–1
  "shadowColor": "#000000",           // Hex; alpha taken from shadowOpacity

  // ----- Lifecycle -----
  "delayIn": 0,                       // Seconds before this element starts entering
  "transitionIn": "none",             // See TRANSITION_TYPES
  "transitionInDuration": 0,          // Seconds; ignored when transitionIn === 'none'
  "duration": null,                   // Seconds the element stays visible (null = full layout)
  "transitionOut": "none",            // See TRANSITION_TYPES
  "transitionOutDuration": 0,
  "easingType": "easeInOutCubic",     // See EASING_TYPES

  // ----- Keyframes (optional) -----
  "keyframes": [ /* Keyframe[] */ ]
}

Defaults

When a field is omitted, the renderer uses the defaults shown above. Notable defaults:

  • position: { top: 10, left: 10, width: 30, height: 30 } in %
  • positionUnits: all %
  • breakpoints: all null
  • fitMode: 'fill'
  • rotation: 0
  • shadowEnabled: false (when true, defaults: shadowX: 0, shadowY: 4, shadowBlur: 16, shadowOpacity: 0.5, shadowColor: '#000000')
  • delayIn: 0
  • transitionIn / transitionOut: 'none'
  • transitionInDuration / transitionOutDuration: 0
  • duration: null
  • easingType: 'easeInOutCubic'

TRANSITION_TYPES

'none' | 'fade' | 'slideLeft' | 'slideRight' | 'slideUp' | 'slideDown' | 'zoomFade' | 'scaleUp'

When transitionIn or transitionOut is 'none', the corresponding duration is ignored (treated as 0). The element is fully visible at t=0 of its lifecycle.

EASING_TYPES

'linear' | 'easeInQuad' | 'easeOutQuad' | 'easeInOutQuad' | 'easeInCubic' | 'easeOutCubic' | 'easeInOutCubic'

FIT_MODES

'fill'      // Crop to fill the box, preserving aspect ratio
'contain'   // Letterbox to fit inside the box, preserving aspect ratio
'cover'     // Same as fill in most cases (CSS object-fit semantics)
'stretch'   // Stretch to fill the box without preserving aspect ratio
'tile'      // Repeat to fill the box

Element types

Pick a type and add the type-specific fields below. Fields not listed for a type are ignored by the renderer for that type but are preserved when the element round-trips through the editor.

type: "asset"

Renders an image or video from the media library.

{
  "type": "asset",
  "assetId": "asset_xxxxx",    // Required for visible content
  "fitMode": "fill"            // See FIT_MODES
}

type: "widget"

Renders a built-in dynamic widget.

{
  "type": "widget",
  "widgetType": "clock" | "weather" | "countdown" | "qrcode" | "text",
  "widgetPreset": "default",         // ID of a preset for the chosen widgetType
  "widgetSettings": { /* widget-specific overrides on top of the preset */ }
}

Widget types and their widgetSettings keys

clock
{
  "format": "24h" | "12h",
  "showSeconds": true,
  "showDate": false,
  "dateFormat": "ddd, MMM D",
  "timezone": "Australia/Brisbane",  // Any IANA timezone, or null for display-local
  "font": "digital" | "modern" | "classic" | "monospace",
  "padding": 5,                      // Percentage 0–50
  "textColor": "#ffffff",
  "backgroundColor": "transparent",
  "textGlow": false,
  "glowColor": "#00ff66"
}
weather
{
  "location": "Brisbane",            // City name or "lat,lon"
  "units": "metric" | "imperial",
  "showCondition": true,
  "showHumidity": false,
  "showWind": false,
  "showIcon": true,
  "layout": "horizontal" | "vertical" | "compact",
  "refreshMinutes": 15,              // 5–60
  "padding": 0,
  "textColor": "#ffffff",
  "backgroundColor": "transparent"
}
countdown
{
  "target": "2026-12-31T23:59:00+10:00",  // ISO timestamp
  "title": "New Year",
  "showDays": true,
  "showHours": true,
  "showMinutes": true,
  "showSeconds": true,
  "showLabels": true,
  "completedMessage": "Happy New Year!",
  "font": "digital" | "modern" | "classic" | "monospace",
  "padding": 0,
  "textColor": "#ffffff",
  "backgroundColor": "transparent",
  "textGlow": false,
  "glowColor": "#ff3366"
}
qrcode
{
  "content": "https://example.com",
  "label": "Scan to view",
  "showLabel": true,
  "size": 80,                        // 40–100 (% of zone)
  "errorCorrection": "L" | "M" | "Q" | "H",
  "padding": 0,
  "qrColor": "#000000",
  "qrBackground": "#ffffff",
  "labelColor": "#ffffff",
  "labelBackground": "transparent"
}
text
{
  "text": "<p>Hello world</p>",      // Rich HTML allowed
  "fontSize": "auto" | 24 | "24px" | "32px" | "48px" | "64px" | "96px" | "128px" | "192px" | "256px",
  "fontFamily":
    "roboto" | "serif" | "monospace" | "segment"   // Legacy preset string
    | { "family": "Georgia" }                       // Real font family (system or web-available)
    | { "family": "Inter", "weight": "700",         // FontPicker object — when `files` is present
        "files": { "700": "https://..." } },        //   the widget injects @font-face automatically
  "fontWeight": "light" | "normal" | "bold",
  "fontStyle": "normal" | "italic",                 // SVG-import preserves italic
  "letterSpacing": 0,                               // Number → px, or CSS length string ('0.05em', '2px')
  "textAlign": "left" | "center" | "right",
  "lineHeight": 1.2,                                // 1.0–2.5
  "padding": 0,
  "textColor": "#ffffff",
  "backgroundColor": "transparent",
  "textShadow": false,                              // Drop shadow on glyphs
  "glowColor": "#3f8cff"
}

fontFamily forms. The legacy preset strings ('roboto', 'serif', 'monospace', 'segment') map to bundled stacks. To use a specific typeface, pass an object: { family: 'Georgia' } for system / web-safe fonts, or { family, weight, files: { <weight>: <url> } } to ship a webfont via @font-face. SVG import uses the object form automatically for any system family it recognises (Georgia, Times New Roman, Arial, Helvetica, Verdana, etc.).

type: "html"

Raw HTML/CSS rendered inside the element box.

{
  "type": "html",
  "html": "<div style=\"width:100%;height:100%;\">...</div>"
}

Use width:100%; height:100% on your root container to fill the box. Inline styles are recommended.

type: "url"

Embedded iframe.

{
  "type": "url",
  "url": "https://dashboard.example.com"
}

type: "shape"

Geometric primitive rendered as SVG (or, for rectangles, as a <div> with CSS borders so the corner radius stays circular under non-uniform stretching).

{
  "type": "shape",
  "shapeType": "rectangle" | "circle" | "triangle" | "diamond" | "pentagon" | "hexagon" | "star" | "arrow-right" | "arrow-left" | "heart" | "line" | "svg-path",
  "shapeFillColor": "#3f8cff",          // 6- or 8-char hex (`#RRGGBB` / `#RRGGBBAA`)
  "shapeBorderColor": "#ffffff",
  "shapeBorderWidth": 0,                // px against the design canvas
  "shapeBorderStyle": "solid",          // 'solid' | 'dashed' | 'dotted'
  "shapeBorderDashArray": "",           // Optional explicit pattern, e.g. "10 14"
                                        // (space-separated lengths in px). When non-empty
                                        // and `shapeBorderStyle !== 'solid'`, overrides
                                        // the default derived pattern. Set automatically
                                        // by SVG import to preserve `stroke-dasharray`.
  "shapeCornerRadius": 0,               // Rectangle only. 0–50, interpreted as a percentage
                                        // of the *shorter* rendered side, so pill buttons
                                        // (50) stay pill-shaped at any size.
  "shapePreserveAspect": "none",        // 'none' | 'contain' | 'cover'
                                        // 'none'    — stretch shape to the box (default, back-compat)
                                        // 'contain' — keep aspect, letterbox inside the box
                                        // 'cover'   — keep aspect, crop to fill the box
  "svgPath": null,                      // Required when shapeType === 'svg-path'; SVG path 'd' attribute
  "svgViewBox": "0 0 100 100",          // Optional viewBox for custom svg-path shapes
  "svgFillRule": "nonzero"              // 'nonzero' | 'evenodd' — for svg-path
}

Aspect preservation: Non-rectangular shapes (stars, arrows, hearts, custom SVG paths) distort when the element's aspect ratio changes between layouts (landscape vs portrait, different zone sizes, etc.). Set shapePreserveAspect: "contain" to keep them symmetrical; reserve "none" (the default) for shapes designed to span and stretch — wide diagonal stripes, full-width ribbons, etc.

Border style limitations: CSS border-style honours the keywords (dashed / dotted) but not arbitrary patterns — so for the rectangle case the renderer paints a CSS border for solid strokes and switches to an SVG overlay when shapeBorderDashArray is set. SVG-path / circle / polygon shapes always use stroke-dasharray directly.

type: "dynamicLayout" (Group)

A container element. Either embeds children inline OR references another saved dynamic layout.

{
  "type": "dynamicLayout",
  "dynamicLayoutId": null,           // If set, renders the referenced layout (children ignored)
  "data": {
    "canvasWidth": 1920,             // Design size of the inline children
    "canvasHeight": 1080,
    "duration": null,                // Optional duration override
    "elements": [ /* Element[] */ ]  // Inline children (recursive)
  }
}

Rules:

  • If dynamicLayoutId is non-null, the renderer fetches that layout; data.elements are ignored at playback.
  • The renderer detects and prevents circular references via dynamicLayoutId chains.
  • Inline children use the SAME element schema recursively. Nesting depth is unlimited but performance degrades with depth.

type: "playlist"

Embeds one or more device playlists inside the element box. Only valid when the dynamic layout is attached to a vvu-cs-app device.

{
  "type": "playlist",
  "playlistIds": ["layout_xxx", "layout_yyy"]   // Layouts where type === 'playlist'
}

Keyframes

A keyframe is a snapshot of animatable properties at a specific time. Keyframes are stored on the element under keyframes[] and interpolate between consecutive entries.

{
  "id": "kf_<timestamp>",            // Required, unique within the element
  "time": 2.5,                       // Seconds from the start of the layout timeline
  "easing": "easeInOutCubic",        // Easing INTO the next keyframe; see EASING_TYPES
  "props": {
    "position": {                    // Partial — only changed dimensions need to be present
      "top": 20,
      "left": 50,
      "width": 40,
      "height": 40
    },
    "rotation": 15,                  // Degrees
    "opacity": 1,                    // 0–1
    "scale": 1,                      // Optional uniform scale
    "filter": {                      // Optional CSS filter parts
      "blur": 0,                     // px
      "brightness": 1,               // 1 = unchanged
      "contrast": 1
    }
  }
}

Keyframe rules

  • time is relative to the layout timeline, not the element's lifecycle.
  • Keyframes must be sorted by time ascending at runtime; the editor sorts on save.
  • The element's base properties are used before the first keyframe and after the last keyframe unless those keyframes pin the values explicitly.
  • Easing easing lives on the LEFT keyframe in a pair (it controls the curve INTO the next keyframe).
  • props only needs to include the fields the keyframe changes — undefined fields fall back to the element's base values.

Element identity rules

  • id must be unique within its containing array. The editor uses the form el_<unix_ms>_<counter>; agents may emit any unique string.
  • Keyframe id must be unique within its element's keyframes[]. The editor uses the form kf_<unix_ms>.
  • When generating a new layout, pick IDs that won't collide between elements. A simple scheme: "el_g_001", "el_g_002", "kf_g_001_a", etc.

Validation rules an agent must follow

  1. Required fields: name, canvasWidth, canvasHeight, elements.
  2. Element required fields: id, type, position, positionUnits.
  3. Type values: Must be one of the listed type values. No custom types.
  4. positionUnits values: Each dimension must be '%' or 'px'. No other units.
  5. transitionIn / transitionOut: Must be one of TRANSITION_TYPES. When 'none', the corresponding duration MUST be 0 (renderer enforces this, but emit 0 for cleanliness).
  6. easingType: Must be one of EASING_TYPES.
  7. Shape shapeType: Must be one of the shape values listed.
  8. Widget widgetType: Must be one of 'clock' | 'weather' | 'countdown' | 'qrcode' | 'text'. Settings under widgetSettings MUST match the chosen widgetType's schema.
  9. Groups: If dynamicLayoutId is set, leave data.elements empty ([]). Do not set both with the intent of merging.
  10. Playlist: Only emit type: "playlist" elements when you have valid playlistIds. Do not invent IDs.
  11. Asset: Only emit assetId values that exist in the target organisation's media library. Leave null if creating a placeholder.
  12. No extra fields: Do not invent new top-level or element fields. The editor preserves unknown fields but the renderer ignores them, which is misleading.
  13. Numeric ranges:
    • rotation: any number (degrees)
    • position.*: % values typically 0–100 but may be negative (off-canvas for animations) or > 100
    • position.* with px: bounded only by sensible canvas size
    • delayIn, *Duration, duration, keyframe time: non-negative seconds
    • opacity: 0–1
    • breakpoints.*: positive px or null
  14. Sort order: Within elements[], you may rely on zIndex for stacking. Do not depend on array order for stacking — but DO rely on it for tiebreaks.
  15. Drop shadow: When shadowEnabled is false (or omitted), the renderer ignores all other shadow* fields. When true, all shadow* numeric fields must be finite and shadowOpacity must be in [0, 1]. Shadows compose on top of any keyframe filter animation.
  16. Shape aspect: shapePreserveAspect must be one of 'none' | 'contain' | 'cover'. Default 'none' preserves legacy stretch behaviour. For shapeType: 'svg-path', svgPath is required and svgViewBox defaults to '0 0 100 100'.
  17. Shape border: shapeBorderStyle must be one of 'solid' | 'dashed' | 'dotted'. shapeBorderDashArray, when supplied, must be a space- or comma-separated list of positive numbers (e.g. "10 14"). It only takes effect when shapeBorderStyle !== 'solid' and shapeBorderWidth > 0.
  18. Text widget fontFamily: May be a preset string ('roboto', 'serif', 'monospace', 'segment') OR a FontPicker object { family: string, weight?: string, files?: { [weight]: url } }. Do not emit other preset strings.
  19. Group children: Inside data.elements[], child positions and pixel-sized widget settings (e.g. text fontSize in px) are interpreted against data.canvasWidth × data.canvasHeight — NOT against the parent layout's canvas. Keep data.canvasWidth/Height in sync with the group element's intended pixel area for predictable rendering.

Minimal valid layout

{
  "name": "Hello Layout",
  "canvasWidth": 1920,
  "canvasHeight": 1080,
  "elements": [
    {
      "id": "el_g_001",
      "name": "Title",
      "type": "widget",
      "widgetType": "text",
      "widgetPreset": "default",
      "widgetSettings": {
        "text": "<p>Hello, world</p>",
        "fontSize": "auto",
        "textAlign": "center",
        "textColor": "#ffffff",
        "backgroundColor": "#000000"
      },
      "zIndex": 0,
      "position": { "top": 30, "left": 10, "width": 80, "height": 40 },
      "positionUnits": { "top": "%", "left": "%", "width": "%", "height": "%" },
      "breakpoints": { "minWidth": null, "maxWidth": null, "minHeight": null, "maxHeight": null },
      "fitMode": "fill",
      "rotation": 0,
      "locked": false,
      "delayIn": 0,
      "transitionIn": "none",
      "transitionInDuration": 0,
      "duration": null,
      "transitionOut": "none",
      "transitionOutDuration": 0,
      "easingType": "easeInOutCubic"
    }
  ]
}

Worked example — animated promo card

A title that fades in, a body that slides up, and a QR code that scales up — staggered. Total scene length 4s.

{
  "name": "Promo Card",
  "canvasWidth": 1920,
  "canvasHeight": 1080,
  "duration": 4,
  "elements": [
    {
      "id": "el_bg",
      "name": "Background",
      "type": "shape",
      "shapeType": "rectangle",
      "shapeFillColor": "#1e3a8a",
      "shapeCornerRadius": 24,
      "zIndex": 0,
      "position": { "top": 10, "left": 10, "width": 80, "height": 80 },
      "positionUnits": { "top": "%", "left": "%", "width": "%", "height": "%" },
      "transitionIn": "none",
      "transitionOut": "none"
    },
    {
      "id": "el_title",
      "name": "Title",
      "type": "widget",
      "widgetType": "text",
      "widgetPreset": "headline",
      "widgetSettings": {
        "text": "<p>Summer Sale</p>",
        "fontSize": "auto",
        "fontWeight": "bold",
        "textAlign": "center",
        "textColor": "#ffffff"
      },
      "zIndex": 10,
      "position": { "top": 18, "left": 15, "width": 70, "height": 20 },
      "positionUnits": { "top": "%", "left": "%", "width": "%", "height": "%" },
      "delayIn": 0.2,
      "transitionIn": "fade",
      "transitionInDuration": 0.6,
      "duration": 3.0,
      "transitionOut": "fade",
      "transitionOutDuration": 0.4,
      "easingType": "easeOutCubic"
    },
    {
      "id": "el_body",
      "name": "Body",
      "type": "widget",
      "widgetType": "text",
      "widgetPreset": "default",
      "widgetSettings": {
        "text": "<p>Up to 50% off all bottled wines this weekend.</p>",
        "fontSize": "48px",
        "textAlign": "center",
        "textColor": "#dbeafe"
      },
      "zIndex": 10,
      "position": { "top": 42, "left": 15, "width": 70, "height": 18 },
      "positionUnits": { "top": "%", "left": "%", "width": "%", "height": "%" },
      "delayIn": 0.6,
      "transitionIn": "slideUp",
      "transitionInDuration": 0.5,
      "duration": 2.7,
      "transitionOut": "fade",
      "transitionOutDuration": 0.4,
      "easingType": "easeOutCubic"
    },
    {
      "id": "el_qr",
      "name": "QR",
      "type": "widget",
      "widgetType": "qrcode",
      "widgetPreset": "default",
      "widgetSettings": {
        "content": "https://example.com/sale",
        "label": "Scan to view offers",
        "size": 90,
        "errorCorrection": "Q",
        "qrColor": "#000000",
        "qrBackground": "#ffffff"
      },
      "zIndex": 10,
      "position": { "top": 64, "left": 40, "width": 20, "height": 22 },
      "positionUnits": { "top": "%", "left": "%", "width": "%", "height": "%" },
      "delayIn": 1.0,
      "transitionIn": "scaleUp",
      "transitionInDuration": 0.6,
      "duration": 2.3,
      "transitionOut": "fade",
      "transitionOutDuration": 0.4,
      "easingType": "easeOutCubic"
    }
  ]
}

Worked example — keyframed motion

A logo that moves diagonally across the screen and rotates 360° over 5 seconds with two keyframes.

{
  "name": "Logo Sweep",
  "canvasWidth": 1920,
  "canvasHeight": 1080,
  "duration": 5,
  "elements": [
    {
      "id": "el_logo",
      "name": "Logo",
      "type": "asset",
      "assetId": "asset_logo_main",
      "fitMode": "contain",
      "zIndex": 1,
      "position": { "top": 10, "left": 5, "width": 15, "height": 15 },
      "positionUnits": { "top": "%", "left": "%", "width": "%", "height": "%" },
      "rotation": 0,
      "transitionIn": "none",
      "transitionOut": "none",
      "easingType": "easeInOutCubic",
      "keyframes": [
        {
          "id": "kf_a",
          "time": 0,
          "easing": "easeInOutCubic",
          "props": {
            "position": { "top": 10, "left": 5, "width": 15, "height": 15 },
            "rotation": 0,
            "opacity": 1
          }
        },
        {
          "id": "kf_b",
          "time": 5,
          "easing": "linear",
          "props": {
            "position": { "top": 75, "left": 80, "width": 15, "height": 15 },
            "rotation": 360,
            "opacity": 1
          }
        }
      ]
    }
  ]
}

Agent generation checklist

When asked to generate a layout from a natural-language brief, an agent should:

  1. Resolve canvas size from the brief (default 1920×1080 if unspecified).
  2. Plan a layout at the percentage level. Keep elements within 0–100 on each axis unless intentionally animating off-canvas.
  3. Choose element types strictly from the listed type values.
  4. Choose widgets strictly from the five widget types; fill widgetSettings with the documented keys for the chosen widget.
  5. Reference assets only by ID if the brief or context provides them. Otherwise leave assetId: null and put a shape or text placeholder.
  6. Stagger lifecycle for animated scenes by tuning delayIn per element.
  7. Use keyframes only when interpolation is needed — for simple in/out, prefer lifecycle transitions.
  8. Set transition* to 'none' with 0 duration when not animating; this matches the editor's defaults and avoids accidental fades.
  9. Emit complete element objects with all common fields, even if equal to defaults. This avoids ambiguity for the renderer.
  10. Output only the JSON object in the response, with no surrounding markdown unless explicitly requested.

Embedding in a Signage Campaign Component

A campaign component can host a dynamic layout inline (the elements are stored on the component itself, not in the device's dynamicLayouts collection). This is the path most agents will use when asked to "update the dynamic layout on this campaign component" — the data lives at well-known paths on the CampaignComponent document.

Campaign component shape

{
  "id": "cc_xxxxx",
  "type": "signage",
  "data": {
    "content": {
      // ===== When data.settings.contentMode === "asset" =====
      "assetId": "asset_xxxxx",        // Single media asset path

      // ===== When data.settings.contentMode === "dynamicLayout" =====
      "canvasWidth": 1920,             // Design canvas width (default 1920)
      "canvasHeight": 1080,            // Design canvas height (default 1080)
      "elements": [ /* Element[] */ ]  // Same Element schema as a top-level DynamicLayout
    },
    "settings": {
      "contentMode": "asset" | "dynamicLayout",  // Switches which `content` fields are used
      "duration": 10,                  // Seconds the component plays each time selected
      "frequency": 1,                  // Play 1 in every X rotations (>=2, or 0/null = once)
      "priority": false,               // Priority overrides non-priority components
      "fitMode": "fill",               // FIT_MODES
      "transitionType": "fade",        // TRANSITION_TYPES (component-level transition)
      "transitionDuration": 0.5,       // Seconds
      "easingType": "easeInOutCubic",  // EASING_TYPES
      "repeat": 0,                     // Extra repeats; 0 = play once
      "layouts": ["layout_xxx", ...]   // Device playlist layouts this component is eligible for
    }
  }
}

Where dynamic layout data lives on a component

FieldPath on the campaign componentNotes
Mode toggledata.settings.contentModeSet to "dynamicLayout" to enable inline-layout fields
Canvas widthdata.content.canvasWidthDefaults to 1920 if missing
Canvas heightdata.content.canvasHeightDefaults to 1080 if missing
Elementsdata.content.elementsArray of Elements, exactly as defined above
Component durationdata.settings.durationMUST be ≥ the dynamic layout's full timeline length
Component transitiondata.settings.transitionType + transitionDuration + easingTypePlays between this component and the previous one — separate from per-element transitions inside the layout
Component fitdata.settings.fitModeHow the layout output is fitted to the target zone
Eligible zonesdata.settings.layoutsRestricts which device playlist layouts may select this component

Inline layout vs linked dynamic layout

ApproachHowWhen to use
Inline (this section)Set data.settings.contentMode = "dynamicLayout" and write the elements straight into data.content.elementsOne-off creative tied to a specific campaign
LinkedBuild the layout under Marketing > Signage > Dynamic Layouts (a standalone DynamicLayout record), then reference it from a dynamicLayout-type element inside the inline data.content.elements ({ "type": "dynamicLayout", "dynamicLayoutId": "<id>" })Reusable scenes shared across multiple campaigns

Tip for agents: When asked to update a campaign component's creative, write into data.content.elements. When asked to update a reusable scene, write into the standalone DynamicLayout record's elements. Both arrays use the SAME element schema; nothing else changes.

Minimal "dynamic layout" campaign component

{
  "data": {
    "settings": {
      "contentMode": "dynamicLayout",
      "duration": 6,
      "frequency": 0,
      "priority": false,
      "fitMode": "fill",
      "transitionType": "fade",
      "transitionDuration": 0.4,
      "easingType": "easeInOutCubic",
      "repeat": 0,
      "layouts": ["layout_main_zone"]
    },
    "content": {
      "canvasWidth": 1920,
      "canvasHeight": 1080,
      "elements": [
        {
          "id": "el_g_001",
          "name": "Headline",
          "type": "widget",
          "widgetType": "text",
          "widgetPreset": "headline",
          "widgetSettings": {
            "text": "<p>Welcome</p>",
            "fontSize": "auto",
            "textAlign": "center",
            "textColor": "#ffffff"
          },
          "zIndex": 0,
          "position": { "top": 30, "left": 10, "width": 80, "height": 40 },
          "positionUnits": { "top": "%", "left": "%", "width": "%", "height": "%" },
          "transitionIn": "fade",
          "transitionInDuration": 0.5,
          "duration": 5,
          "transitionOut": "fade",
          "transitionOutDuration": 0.4,
          "easingType": "easeInOutCubic"
        }
      ]
    }
  }
}

Validation rules specific to campaign components

  1. When data.settings.contentMode === "dynamicLayout", data.content.elements MUST be an array (may be empty).
  2. When contentMode === "asset", data.content.assetId SHOULD be set; data.content.elements is ignored at playback.
  3. data.settings.duration MUST be ≥ the dynamic layout's full animation length (longest delayIn + transitionInDuration + duration + transitionOutDuration across elements). Otherwise the layout is cut off mid-animation.
  4. data.settings.frequency: 0 or null means play once; otherwise must be >= 2.
  5. data.settings.layouts is required for the component to be eligible for any zone.
  6. The element schema inside data.content.elements is identical to top-level DynamicLayout.elements — including all enum values, defaults, and validation rules from the rest of this document.