Configure widgets
Read the widget catalog: configSchema as JSON Schema, required fields, examples, and a ready-made widget instance for each built-in building block.
10 minute readApps SDKUpdated: 10/25/2026
On this page
- Fetch the catalog
- What is in the configSchema
- Four rules you cannot see in the schema
- Two values with a fixed shape: place and surface
- The six built-in building blocks
- A ready-made widget instance per building block
- Check a configuration before you write it
- The form in the editor
- The order of precedence
- Custom fields: registerConfigField
- Field kinds with a control of their own
- Quick fields: quickFields
- The fit report: keys are field names
- Related
Before you start
- For
- Anyone who creates or changes scenes through the API — a tool of your own, an assistant, an import.
- Prerequisite
- a credential from your organization for
GET /v1/widget-catalogand a JSON Schema validator that understands Draft 2020-12.
The widget catalog delivers a complete JSON Schema of its configuration for every widget: each field with a heading and an explanation, the required fields named, and at least one ready-made example configuration. Anyone who builds a scene without the editor reads the rules from it instead of guessing them — and checks their configuration against it before writing it.
Fetch the catalog
curl -sS "$SZENERA_API/v1/widget-catalog" \
-H "Authorization: Bearer $TOKEN" | jq '.entries | length'
The response carries one entry per widget under entries:
| Field | Meaning |
|---|---|
appId |
The app that the widget belongs to, or core for the six built-in building blocks. |
version |
The version of the app that the definition comes from. Fixed at 0.0.0 for the built-in building blocks. |
definition.id |
The widget type. This exact value later appears in widgetType of the widget instance. |
definition.name, definition.description |
Name and short text, as the selection in the editor also shows them. |
definition.configSchema |
The JSON Schema of the configuration — the subject of this page. |
definition.size.min |
The smallest frame, in reference pixels of a 1920 × 1080 scene, in which the widget still shows something useful. |
definition.preview.defaultConfig |
The configuration with which a freshly inserted instance starts. |
definition.preview.variants |
The presentations that the widget supports, each with a partial configuration and, as a template, a starting size (size). |
definition.preview.states |
Named sample payloads for the preview in the editor (rain, night, no data). Of no significance for a configuration. |
The catalog mirrors the latest released state of an app. An organization that has installed an older version may differ slightly from it, and the version that counts is then the one in the instance’s appVersionPin.
What is in the configSchema
The configSchema is a JSON Schema document according to Draft 2020-12. It describes exactly the object that later appears as config in the widget instance.
| Key | Meaning |
|---|---|
$schema |
The version of the specification: https://json-schema.org/draft/2020-12/schema. |
type |
Always object — a widget configuration is never a list and never a bare value. |
title, description |
Name and short text of the widget, so that the document stays understandable on its own. |
properties |
The fields of the configuration, one node per field. |
required |
The field names without which the configuration is incomplete. Often empty: fields with a default are not required. |
examples |
At least one complete configuration that you can adopt unchanged. |
Each node under properties carries plain text and, where they exist, the limits of the value:
| Key | Meaning |
|---|---|
title |
Short heading of the field, as it also appears above the control in the editor. |
description |
One sentence on what the field does. |
type |
string, number, integer, boolean, array, or object. |
enum |
The permitted values of a selection field — the check rejects any others. |
default |
The value that applies if the field is missing. |
minimum, maximum, minLength, maxLength, pattern |
The limits of the value. pattern usually appears for colors (#rrggbb) and identifiers. |
properties, items |
For nested objects and lists: the same rules one level deeper. |
An excerpt from the “Text” building block:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"title": "Text",
"description": "Statischer Text-Block mit voller Kontrolle über Schrift, Ausrichtung und Farben.",
"properties": {
"content": {
"type": "string",
"minLength": 1,
"maxLength": 500,
"title": "Text",
"description": "Der angezeigte Text. Zeilenumbrüche bleiben erhalten; Markup gibt es hier nicht."
},
"textAlign": {
"type": "string",
"enum": ["left", "center", "right"],
"default": "center",
"title": "Ausrichtung",
"description": "Waagerechte Ausrichtung des Textes."
}
},
"required": ["content"],
"examples": [{ "content": "Ihr Text", "textAlign": "center", "fontSizeRem": 2 }]
}
Four rules you cannot see in the schema
- Required means: there is no default.
requiredlists only what the platform cannot fill in itself, usually the content. Everything else carries adefault, and an omitted field takes exactly that value. - Unknown fields are discarded, not rejected. A field that the schema does not know does not make a configuration invalid, and it disappears on saving. That keeps scenes created before a field restructuring readable, but do not count on a typo being noticed.
- The examples are complete and verified. Every entry in
examplesis a configuration that satisfies the schema: the starting configuration and one more per preview variant. An app build aborts if an example breaks its own rule. - A configuration survives new versions. An app adds fields only with a default, and none disappears or changes its type. A stored configuration therefore stays valid when the organization updates the app, and an older version that a display still plays also reads a configuration with fields from a newer one.
Two values with a fixed shape: place and surface
Two field kinds of the editor forms write values whose shape is the same everywhere. Anyone who builds a configuration themselves finds them in the configSchema as an object or a string, respectively:
| Value | Shape | Meaning |
|---|---|---|
Place (placeSelection()) |
{ "mode": "display" | "fixed", "place": Place | null, "fallback": Place | null }, a place is { "displayName", "countryCode", "latitude", "longitude" } |
display takes the location of the display that plays the scene, and fixed takes the place under place. fallback is the substitute place for displays whose location has no known position. Default: display, both places null. |
| Surface | an identifier such as light, dark, glass, transparent, or one of the app’s own |
The background of the widget. The permitted values are named by the field’s schema. |
How the editor shows these and all other fields, organizes them, and places them on the widget toolbar is described by the app’s form description (config-ui.ts, type WidgetConfigUI). It belongs to the editor, not to the catalog: the configSchema remains the single rule for everyone who writes a configuration.
The six built-in building blocks
Text, image, video, shape, graphic, and document are part of the core setup: they are not installed, need no consent, and are open to every organization. In the catalog they appear like the widgets of an app, with the reserved identifier core and the fixed version pin 0.0.0.
widgetType |
Name | Purpose |
|---|---|---|
text |
Text | A text from the configuration, without markup and without network access. |
image |
Image | An image from the media library, optionally with slow image motion. |
video |
Video | A video from the media library, muted and looping. |
shape |
Shape | Rectangles, circles, stars, arrows, and lines, drawn from numbers. |
graphic |
Graphic | A motif from the built-in library, tintable and flippable. |
document |
Document | A PDF from the media library as page images: one page, in sequence, or two side by side. |
Image, video, and document refer to the media library instead of an address: assetRef carries the identifier of the file and the file variants that the display software needs. Without assetRef, the widget shows a designed placeholder — that is a valid state and not an error.
For the document, assetRef.variantSet stays empty: its page images always travel along, one row page-<n> per page in the snapshot’s media list. Which pages the widget shows is stated in pages: numbers are the chosen pages, and of is the file’s page count at the time of choosing. If pages is missing, the widget shows all pages except empty ones. If the file later has a different page count than of, the scene check reports DocumentPagesChanged (a notice, not a blocker). The document declares three templates (page, spread, full) and the minimum size 200 × 200.
A ready-made widget instance per building block
This is what an entry in a scene’s document.widgets looks like. id is a new identifier in the format wgi_<ULID>, frame is the box in pixels of the scene surface, and zIndex is the stacking order.
Text
{
"id": "wgi_01ARZ3NDEKTSV4RRFFQ69G5FAV",
"appId": "core",
"widgetType": "text",
"appVersionPin": "0.0.0",
"frame": { "x": 120, "y": 120, "w": 1680, "h": 400 },
"zIndex": 10,
"config": {
"content": "Willkommen in Ihrer Apotheke",
"fontFamily": "inter",
"fontStyle": "sans",
"fontWeight": "bold",
"fontSizeRem": 3,
"lineHeight": "tight",
"textTransform": "none",
"textAlign": "center",
"verticalAlign": "middle",
"padding": "md",
"textColor": "#0f172a",
"backgroundStyle": "transparent",
"backgroundColor": "#ffffff"
}
}
Image
{
"id": "wgi_01ARZ3NDEKTSV4RRFFQ69G5FAW",
"appId": "core",
"widgetType": "image",
"appVersionPin": "0.0.0",
"frame": { "x": 0, "y": 0, "w": 1920, "h": 1080 },
"zIndex": 0,
"config": {
"assetRef": { "assetId": "ast_01ARZ3NDEKTSV4RRFFQ69G5FAV", "variantSet": ["webp", "original"] },
"fit": "cover",
"position": { "x": "center", "y": "center" },
"borderRadius": "none",
"backgroundMode": "color",
"backgroundColor": "#0f172a",
"motionEffect": "kenburns",
"kenBurns": { "zoom": 1.15, "durationSec": 24, "direction": "in" }
}
}
Video
{
"id": "wgi_01ARZ3NDEKTSV4RRFFQ69G5FAX",
"appId": "core",
"widgetType": "video",
"appVersionPin": "0.0.0",
"frame": { "x": 0, "y": 0, "w": 1920, "h": 1080 },
"zIndex": 0,
"config": {
"assetRef": { "assetId": "ast_01ARZ3NDEKTSV4RRFFQ69G5FAV", "variantSet": ["original"] },
"loop": true,
"muted": true,
"autoplay": true,
"fit": "cover",
"position": { "x": "center", "y": "center" },
"borderRadius": "none",
"backgroundMode": "color",
"backgroundColor": "#000000",
"startAtSeconds": 0
}
}
Shape
{
"id": "wgi_01ARZ3NDEKTSV4RRFFQ69G5FAY",
"appId": "core",
"widgetType": "shape",
"appVersionPin": "0.0.0",
"frame": { "x": 120, "y": 560, "w": 480, "h": 80 },
"zIndex": 5,
"config": {
"variant": "rectangle",
"fillColor": "#0f766e",
"fillTransparent": false,
"fillOpacity": 1,
"strokeColor": "#0f172a",
"strokeWidth": 0,
"cornerRadius": 0,
"innerRadius": 60,
"pointDepth": 60,
"headSize": 36,
"shaftWidth": 28,
"lineCap": "butt",
"dash": "none",
"rotationDeg": 0
}
}
Graphic
{
"id": "wgi_01ARZ3NDEKTSV4RRFFQ69G5FAZ",
"appId": "core",
"widgetType": "graphic",
"appVersionPin": "0.0.0",
"frame": { "x": 1500, "y": 760, "w": 320, "h": 320 },
"zIndex": 20,
"config": {
"graphicId": "pfeil-rechts",
"tintColor": "#0f172a",
"accentColor": "#0f766e",
"flipH": false,
"flipV": false,
"opacity": 1
}
}
Document
{
"id": "wgi_01ARZ3NDEKTSV4RRFFQ69G5FB0",
"appId": "core",
"widgetType": "document",
"appVersionPin": "0.0.0",
"frame": { "x": 0, "y": 0, "w": 1920, "h": 1080 },
"zIndex": 0,
"config": {
"assetRef": { "assetId": "ast_01ARZ3NDEKTSV4RRFFQ69G5FAV", "variantSet": [] },
"pages": { "numbers": [1, 2, 3], "of": 3 },
"display": "sequence",
"fit": "page",
"interval": 10,
"corners": "round",
"surface": "dark"
}
}
A shorter form works too: because every field except content carries a default, { "content": "Willkommen" } is already a valid text configuration. The spelled-out form above is the one that the editor saves.
Check a configuration before you write it
Take the configSchema of the widget you want and give it to a validator for Draft 2020-12. The result is a field-level message instead of a rejected request.
import Ajv2020 from 'ajv/dist/2020.js';
const katalog = await (await fetch(`${api}/v1/widget-catalog`, { headers })).json();
const eintrag = katalog.entries.find(
(e) => e.appId === 'core' && e.definition.id === 'text',
);
const pruefe = new Ajv2020({ allErrors: true }).compile(eintrag.definition.configSchema);
const config = { content: 'Willkommen in Ihrer Apotheke', fontSizeRem: 3 };
if (!pruefe(config)) {
for (const fehler of pruefe.errors) {
console.error(`${fehler.instancePath || '(Wurzel)'}: ${fehler.message}`);
}
}
The validator does not cover three things:
- Whether the file that
assetRefpoints to exists. The media library answers that. - Whether the widget type belongs to an installed app. The platform rejects widgets of an app that is not installed when publishing, and the built-in building blocks are always open.
- Whether the configuration makes sense. A QR code with an empty address is schema-valid and still shows only a placeholder.
The form in the editor
This section is aimed at app authors: it describes how the scene editor builds the settings window and the widget toolbar from an app’s form description (config-ui.ts). It changes nothing for a configuration written through the API.
The order of precedence
The settings window takes the app’s form description if it has one registered (registry.registerConfigUI(widgetType, ui)). Only without it does an app’s own form apply (registerConfigForm), and without both, one derived from the schema. Declarative comes first so that shared improvements (structure, collapsed groups, hints, place field) reach every app without it having to update its form. An app in the new standard therefore no longer registers a whole form, only individual fields at most.
Custom fields: registerConfigField
What no field kind covers (a city search, a selection from its own data), the app brings along as an individual control:
import type { AuthoringRegistry, ConfigFieldProps } from '@szenera/widget-editor/registry';
import type { GeoPlace } from '@szenera/apps-sdk/config';
function CityPicker({ value, onChange, apiFetch, context }: ConfigFieldProps<GeoPlace | null>) {
// searches the app's cities through apiFetch, context.locale for the spelling
}
export function registerAuthoring(registry: AuthoringRegistry): void {
registry.registerConfigUI('weather', weatherConfigUI);
registry.registerConfigField('weather', 'location.place', CityPicker as never);
}
The key is the dot path of the field (city) or of a part of it (location.place is the place selection of the place field location). The form renders label, help text, required asterisk, and error around the control. It receives:
| Prop | Content |
|---|---|
value, onChange |
The value at this key and the way to write it. |
field |
The declared description of the field. |
config |
The whole config of the draft, read-only. |
error |
The message if the draft violates the schema here. |
context |
Frame of the widget in stage pixels, its shape and size class (band, wide, square, tall, the same classification as useWidgetFormat), language and time zone of the organization, and its displays (name, location name, whether the location has coordinates). |
apiFetch |
The app’s transport for its own routes, as with a form of its own. |
Field kinds with a control of their own
| Field kind | In the window | On the toolbar |
|---|---|---|
location |
Choice “Display location” or “Fixed place” (without a display reference using allowDisplay: false, only the fixed place). With the display reference, a sentence on which displays have no location with an address, and with fallback: true, a substitute place. The place selection is the app’s own (<field>.place), otherwise a simple input of name, country, latitude, and longitude. |
not compact, window only |
surface |
Tiles with a color swatch and label, one active. | Selection with swatch |
Group with collapsed: true |
Accordion, closed on opening, and open as long as an error is inside. | – |
required: true |
Required asterisk on the label. The schema decides whether a value is missing. | – |
Quick fields: quickFields
quickFields names at most two fields of a compact kind (color, segmented, select, slider, toggle, surface), in the order of the toolbar. The toolbar leaves out other kinds and more than two fields, and defineWidgetConfigUI() reports this at build time. Besides the quick fields, the toolbar shows the template selection for a widget with several templates (Widget, configuration, and preview).
The fit report: keys are field names
If the runtime hides enabled content because the frame is too small, it reports it with useFitReport().reportHidden(parts). The parts are named like the config fields that turn the content on, for example ['showDaily']. The editor then shows “Hidden in this frame: Next days.” beneath the preview, using the field’s label from the form description, and beneath the toggle a sentence on why it has no effect. A part without a matching field appears with its key.
Related
- Widget, configuration, and preview — how an app writes its widget definition, from which this schema arises.
- Signing and the path into the catalog — the two catalogs across all apps and their queries.
- Data access, permissions, and terms — what an app discloses before installation.