examples/conditional.json
{
"type": "lightning__agentforceWidget",
"contentBody": {
"widgetBody": {
"definition": "tile/widget",
"children": [
{
"definition": "tile/column",
"attributes": { "gap": "md" },
"children": [
{
"definition": "tile/callout",
"meta": { "if": "{!$attrs.showBanner}" },
"attributes": { "variant": "warning", "title": "Announcement" },
"children": [
{
"definition": "tile/text",
"attributes": { "text": "{!$attrs.bannerMessage}", "variant": "body" }
}
]
},
{
"definition": "tile/column",
"attributes": { "gap": "md" },
"meta": { "if": "{!$attrs.isVerified}"},
"children": [
{
"definition": "tile/row",
"attributes": { "gap": "sm", "align": "center" },
"children": [
{
"definition": "tile/icon",
"attributes": { "name": "check-circle", "color": "success" }
},
{
"definition": "tile/text",
"attributes": { "text": "{!$attrs.welcomeMessage}", "variant": "body" }
}
]
}
]
},
{
"definition": "tile/container",
"meta": {
"forEach": "{!$attrs.tasks}",
"forItem": "$task",
"if": "{!$task.isUrgent}"
},
"children": [
{
"definition": "tile/column",
"attributes": { "gap": "md" },
"children": [
{
"definition": "tile/row",
"attributes": { "gap": "md", "justify": "between", "align": "center" },
"children": [
{
"definition": "tile/text",
"attributes": { "text": "{!$task.title}", "variant": "h4" }
},
{
"definition": "tile/badge",
"attributes": { "label": "{!$task.priority}", "variant": "secondary" }
}
]
}
]
}
]
}
]
}
]
}
}
}
examples/list-with-foreach.json
{
"type": "lightning__agentforceWidget",
"contentBody": {
"widgetBody": {
"definition": "tile/widget",
"children": [
{
"definition": "tile/column",
"attributes": { "gap": "md" },
"children": [
{
"definition": "tile/text",
"attributes": { "text": "{!$attrs.title}", "variant": "h1" }
},
{
"definition": "tile/column",
"attributes": { "gap": "sm" },
"meta": { "forEach": "{!$attrs.products}", "forItem": "$product" },
"children": [
{
"definition": "tile/row",
"attributes": { "gap": "md", "justify": "between", "align": "center" },
"children": [
{
"definition": "tile/column",
"attributes": { "gap": "xs" },
"children": [
{
"definition": "tile/text",
"attributes": { "text": "{!$product.name}", "variant": "h3" }
},
{
"definition": "tile/text",
"attributes": { "text": "{!$product.category}", "variant": "caption", "color": "muted" }
}
]
},
{
"definition": "tile/text",
"attributes": { "text": "{!$product.price}", "variant": "h4", "color": "primary" }
}
]
},
{
"definition": "tile/row",
"attributes": { "gap": "sm", "align": "center" },
"meta": { "forEach": "{!$product.features}", "forItem": "$feature" },
"children": [
{
"definition": "tile/icon",
"attributes": { "name": "{!$feature.icon}", "size": "sm", "color": "success" }
},
{
"definition": "tile/text",
"attributes": { "text": "{!$feature.label}", "variant": "caption", "color": "muted" }
}
]
}
]
}
]
}
]
}
}
}
examples/single-object.json
{
"type": "lightning__agentforceWidget",
"contentBody": {
"widgetBody": {
"definition": "tile/widget",
"children": [
{
"definition": "tile/column",
"attributes": { "gap": "md" },
"children": [
{
"definition": "tile/column",
"attributes": { "gap": "md" },
"children": [
{
"definition": "tile/text",
"attributes": { "text": "{!$attrs.title}", "variant": "h2" }
},
{
"definition": "tile/text",
"attributes": { "text": "{!$attrs.subtitle}", "variant": "caption", "color": "muted" }
},
{
"definition": "tile/column",
"attributes": { "gap": "sm" },
"children": [
{
"definition": "tile/row",
"attributes": { "gap": "sm" },
"children": [
{
"definition": "tile/text",
"attributes": { "text": "{!$attrs.field1Label}", "variant": "body", "weight": "semibold" }
},
{
"definition": "tile/text",
"attributes": { "text": "{!$attrs.field1Value}", "variant": "body" }
}
]
},
{
"definition": "tile/row",
"attributes": { "gap": "sm" },
"children": [
{
"definition": "tile/text",
"attributes": { "text": "{!$attrs.field2Label}", "variant": "body", "weight": "semibold" }
},
{
"definition": "tile/text",
"attributes": { "text": "{!$attrs.field2Value}", "variant": "body" }
}
]
}
]
}
]
}
]
}
]
}
}
}
references/schema-from-lightning-type.md
# Deriving Widget schema.json From a Lightning Type
When the orchestrator passes a `lightningTypeSchema` (with `path` and `apexClassFqn`), the widget bundle's `schema.json` is derived from the Apex class the Lightning Type references — not invented. The Lightning Type root points at the **outer Apex class** (`@apexClassType/<namespace>__<ClassName>`); that outer class's `@AuraEnabled` fields define the payload shape. Inner classes appear either as a **singular field** (e.g. `InnerClass field` on the outer class) or as a **list-element type** (e.g. `List<InnerClass>` on the outer class). In either case, the widget schema references the nested type by its own inner-class FQN (`@apexClassType/<namespace>__<OuterClass>$<InnerClass>`) — see step 4 below. This file documents the derivation rule.
> **Scope:** This guide covers Apex-backed Lightning Types only. The orchestrator (`platform-lightning-type-widget-coordinate`) only routes Apex-backed types into this skill. Object/JSON-based Lightning Types (Lightning Type root `lightning:type: "lightning__objectType"` with primitive `properties`) are out of scope here.
---
## Guidance
- The widget aligns to the Lightning Type's shape — it grounds on the `@AuraEnabled` fields of the Apex class the type references.
- The widget MUST NOT introduce properties the Apex class does not expose.
- **Default to including every `@AuraEnabled` field.** Omission is the exception, not the rule. Before dropping any field, confirm with the user — print the field, its Apex type, and the omission rationale (e.g. "audit timestamp, not user-facing"), and ASK before continuing. Silent omission is a hard violation; the orchestrator's P1.1 gate flags it.
- Fields that are typically safe to propose for omission *with user confirmation*: audit timestamps (`createdDate`, `lastModifiedDate`), system IDs that duplicate a primary key, and internal flags. **Domain-meaningful fields — including `List<InnerClass>` collections — are NEVER omitted silently.**
---
## Deriving from an Apex-backed Lightning Type
The Lightning Type root is minimal and points at the **outer Apex class**:
```json
{
"title": "<TypeName>",
"lightning:type": "@apexClassType/<namespace>__<ClassName>"
}
```
The widget cannot mirror this directly — the widget root is a plain `type: "object"` whose `properties.attributes` wrapper carries `lightning:type: "lightning__objectType"` and the actual field map. Derive the widget shape from the Apex class's `@AuraEnabled` fields:
1. Read the Apex class file (path provided by the orchestrator's Phase 4 output, or located via `<pkgDir>/classes/<ClassName>.cls`). The `apexClassFqn` from the orchestrator names the outer class.
2. **Enumerate every `@AuraEnabled` field on the outer class AND every field on each inner class referenced by a `List<Inner>` field or a singular inner-class field.** Default disposition is **include**. If you propose to drop any field, ASK the user first and record the rationale in the build plan's `Properties omitted:` section. Do not silently drop.
3. For each retained field, map the Apex type to the matching `lightning:type`:
| Apex type | `lightning:type` (in widget schema) |
|---|---|
| `String`, `Id` | `lightning__textType` |
| `Decimal`, `Double`, `Integer`, `Long` | `lightning__numberType` |
| `Boolean` | `lightning__booleanType` |
| `Date`, `Datetime` | `lightning__dateTimeType` |
| `List<Primitive>` (e.g. `List<String>`) | `lightning__listType`, with `items.lightning:type` set to the matching primitive `lightning:type` from this table (e.g. `items.lightning:type: "lightning__textType"` for `List<String>`). |
| `List<InnerClass>` | `lightning__listType`, with `items.lightning:type` set to the inner Apex class reference (see step 4). The widget body MUST also iterate this list with `forEach`/`forItem` and bind every `@AuraEnabled` field on `InnerClass` via `{!$item.<innerField>}`. See `references/widget-meta-directives.md`. |
| `InnerClass` (singular field) | `lightning:type` set directly to the inner Apex class reference (see step 4). Bind its fields via `{!$attrs.<outerField>.<innerField>}`. |
4. Build the widget `schema.json`. **Every `@AuraEnabled` field on the outer class** that survived step 2 MUST appear as an entry under `properties.attributes.properties`.
For any field whose type is an inner Apex class — whether a singular field or a `List<InnerClass>` — the property (or, for a list, its `items`) MUST reference the inner Apex class type directly, using the same `@apexClassType/<namespace>__<OuterClass>$<InnerClass>` form as the Lightning Type root, with `<InnerClass>` naming the nested class as declared inside the outer class. Do not redeclare the inner class's fields inline:
```json
"<listFieldName>": {
"title": "<List Field Label>",
"description": "<short description>",
"lightning:type": "lightning__listType",
"items": {
"lightning:type": "@apexClassType/<namespace>__<OuterClass>$<InnerClass>"
}
},
"<singularInnerFieldName>": {
"title": "<Field Label>",
"description": "<short description>",
"lightning:type": "@apexClassType/<namespace>__<OuterClass>$<InnerClass>"
}
```
This is the only case where a widget schema property carries an `@apexClassType/...` reference instead of a primitive `lightning:type` — it lets the runtime resolve the inner class's own `@AuraEnabled` fields without the widget schema duplicating them. For a list, inner-class fields are reached in the widget body via the `forEach`/`forItem` loop variable (`{!$item.<innerField>}`); for a singular field, via `{!$attrs.<outerField>.<innerField>}` — neither case gets a separate nested `properties` entry in the schema.
```json
{
"title": "<WidgetDisplayName>",
"description": "<one line about what the widget renders>",
"type": "object",
"properties": {
"attributes": {
"lightning:type": "lightning__objectType",
"properties": {
"<textFieldName>": { "title": "<Text Field Label>", "lightning:type": "lightning__textType" },
"<numberFieldName>": { "title": "<Number Field Label>", "lightning:type": "lightning__numberType" },
"<dateTimeFieldName>": { "title": "<Date/Time Field Label>", "lightning:type": "lightning__dateTimeType" },
"<listFieldName>": { "title": "<List Field Label>", "lightning:type": "lightning__listType", "items": { "lightning:type": "<primitive lightning:type, or an inner-class @apexClassType/... reference>" } }
}
}
}
}
```
---
## Reachability check
Before authoring the widget body, confirm every property you plan to bind via `{!$attrs.X}` exists in the derived widget `schema.json`. The orchestrator's P0.4 gate enforces this; the leaf skill must self-check first to avoid round-trips.
---
## Out of scope
- Renaming Apex class fields for display. The widget schema must use the same names as the Apex class. Display labels are carried in `title` only.
- Synthesizing properties not present on the resolved payload class. This includes computed/derived fields and any field the Apex class does not currently expose. **`platform-widget-generate` MUST NOT edit `.cls` files.** If the widget body needs a field that is missing (for example, a `lightning__booleanType` to drive `meta.if`), STOP and surface the gap to the orchestrator — name the missing field, its expected type, and why the widget needs it. The orchestrator decides whether to amend the Apex class (via `platform-apex-generate`) or revise the widget plan.
references/widget-bundle-layout.md
# WidgetBundle Layout
The widget skill writes three files into one directory under the project's package directory.
---
## Folder layout
```text
<pkgDir>/uiWidgets/<widgetName>/
<widgetName>.json # widget envelope + UEM body
schema.json # JSON Schema
<widgetName>.uiwidget-meta.xml # UiWidgetBundle registration
```
`<widgetName>` is `camelCase` and matches the directory name and both `<widgetName>.json` / `<widgetName>.uiwidget-meta.xml` filenames exactly.
---
## Resolving `<pkgDir>`
`<pkgDir>` resolves to `<packageDirectories[].path>/main/default` — the SFDX source-format default, where `<packageDirectories[].path>` is the `default: true` entry in `sfdx-project.json` (or the first entry if none is marked default).
---
## `<widgetName>.json` — Widget envelope + UEM body
The envelope has exactly two top-level keys — `type` and `contentBody`. The UEM tree lives at `contentBody.widgetBody`. Display metadata (label / description) lives in `<widgetName>.uiwidget-meta.xml`, not in the envelope.
```json
{
"type": "lightning__agentforceWidget",
"contentBody": {
"widgetBody": {
"definition": "tile/widget",
"children": [
/* every block — root and non-root — carries "definition", optional "attributes", optional "meta", optional "children". No "type" key on any node. */
]
}
}
}
```
Envelope keys:
- `type` — always `"lightning__agentforceWidget"`.
- `contentBody.widgetBody` — the root UEM node; this is `tile/widget`. It carries `definition` and `children` only — no `type` key on the root, no `type` key on any child.
Tree composition (everything inside `widgetBody.children`) is owned by `SKILL.md` *Composition*. No node — root or non-root — carries a `type` key.
---
## `schema.json` — Input contract
JSON Schema describing what data the widget accepts at runtime. Fields are wrapped one level deep under `properties.attributes`.
**Required root keys:**
- `title` (string) — display name
- `type` (string) — must equal `"object"`
- `properties.attributes` (object) — must carry `lightning:type: "lightning__objectType"` and a nested `properties` map whose leaves each carry `lightning:type`
**Optional root keys:**
- `description` (string)
**Each leaf under `properties.attributes.properties` MUST have `lightning:type`.** Optional per-leaf: `title`, `description`. **A `lightning__listType` leaf MUST also carry `items.lightning:type`** — set to the matching primitive `lightning:type` for a list of primitives, or to an `@apexClassType/...` reference for a list of Apex objects (see `references/schema-from-lightning-type.md`). **For a list of plain objects with no Apex-backed type**, `items.lightning:type` is `lightning__objectType` and `items` MUST also carry a nested `properties` map inlining every field the list items expose (same shape as the outer `attributes.properties` map).
Example:
```json
{
"title": "Order Summary Widget",
"description": "Displays an order's id, customer, total, and tags.",
"type": "object",
"properties": {
"attributes": {
"lightning:type": "lightning__objectType",
"properties": {
"orderId": { "title": "Order ID", "description": "Stable identifier shown in the header.", "lightning:type": "lightning__textType" },
"customer": { "title": "Customer", "description": "Display name of the customer on the order.", "lightning:type": "lightning__textType" },
"total": { "title": "Total", "description": "Order total in the order's currency.", "lightning:type": "lightning__numberType" },
"tags": { "title": "Tags", "description": "Labels attached to the order.", "lightning:type": "lightning__listType", "items": { "lightning:type": "lightning__textType" } }
}
}
}
}
```
`{!$attrs.X}` in the body resolves to `properties.attributes.properties.X` in the schema.
---
## `<widgetName>.uiwidget-meta.xml` — Registration
```xml
<?xml version="1.0" encoding="UTF-8"?>
<UiWidgetBundle xmlns="http://soap.sforce.com/2006/04/metadata">
<masterLabel><Human display label></masterLabel>
<description><One-line description of what the widget renders></description>
<widgetType>JSON</widgetType>
</UiWidgetBundle>
```
Elements:
- `<masterLabel>` — required. Human-readable label shown in the runtime UI. Prefer title-case (e.g. `Account Summary Card`) — not the `camelCase` `<widgetName>`.
- `<description>` — required. One-line description of what the widget renders. Populates the display metadata previously carried in the envelope.
- `<widgetType>` — required. Only the `JSON` variant is supported. The `FUNCTION` variant is out of scope.
---
## Validation reminders
- Every `{!$attrs.X}` in `<widgetName>.json` MUST resolve to a property under `schema.json` `properties.attributes.properties` (or to a `forItem` loop variable defined upstream).
- `<widgetName>.uiwidget-meta.xml` MUST parse as well-formed XML, have root element `<UiWidgetBundle>`, and carry non-empty `<masterLabel>`, `<description>`, and `<widgetType>JSON</widgetType>` elements.
- The three files MUST be co-located in the same `<widgetName>/` directory under `uiWidgets/`.
references/widget-meta-directives.md
# Widget Meta Directives Reference
The `meta` object on a UEM block carries runtime directives for iteration (`forEach` / `forItem`) and conditional rendering (`if`). Read this file when a widget needs either.
---
## Iteration with forEach / forItem
`forEach` iterates over an array; the block and ALL its children repeat for each item.
### Rules
- Place `forEach` on the `meta` object of the REPEATING block (e.g. a row or card).
- The value is an expression referencing an array: `{!$attrs.<arrayAttr>}` (or `{!$<outerForItem>.<arrayField>}` for nested loops).
- `forItem` is required alongside `forEach`. It names the variable bound to the current item and must start with `$`.
- Inside the `forEach` block, reach the **current item** through the loop variable (`{!$item.X}`) — not by traversing the array path (`{!$attrs.items.X}`, which does not unfold to the current iteration). Top-level references for values that don't change across iterations (`{!$attrs.<unrelatedField>}`) are still valid.
- `forEach` blocks can be nested — inner loops use their own `forItem` name.
### Example — top-level list
```json
{
"definition": "namespace/repeatingBlock",
"meta": { "forEach": "{!$attrs.items}", "forItem": "$item" },
"children": [
{ "definition": "namespace/childBlock1", "attributes": { "content": "{!$item.id}" } },
{ "definition": "namespace/childBlock2", "attributes": { "content": "{!$item.total}" } }
]
}
```
### Example — container holds repeating child
When a container holds repeating items, `forEach` goes on the child — not on the container.
```json
{
"definition": "namespace/block",
"children": [
{
"definition": "namespace/repeatingBlock",
"meta": { "forEach": "{!$attrs.items}", "forItem": "$item" },
"children": [
{ "definition": "namespace/childBlock", "attributes": { "content": "{!$item.name}" } }
]
}
]
}
```
### Example — nested loops
The inner `forEach` references an array on the outer loop variable and uses a distinct `forItem` name.
```json
{
"definition": "namespace/repeatingBlock",
"meta": { "forEach": "{!$attrs.orders}", "forItem": "$order" },
"children": [
{ "definition": "namespace/childBlock", "attributes": { "content": "{!$order.id}" } },
{
"definition": "namespace/repeatingChildBlock",
"meta": { "forEach": "{!$order.lineItems}", "forItem": "$line" },
"children": [
{ "definition": "namespace/childBlock1", "attributes": { "content": "{!$line.name}" } },
{ "definition": "namespace/childBlock2", "attributes": { "content": "{!$line.count}" } }
]
}
]
}
```
---
## Conditional rendering with if
`if` conditionally renders a block. When the expression is `false`, the block and all its children are excluded from the rendered output.
### Rules
- Place `if` on the `meta` object of the block.
- Use `if` only when the schema has a `lightning__booleanType` property suited to the condition. Bind directly to that property (or to a loop variable holding such a value). If no suitable boolean exists in the schema, do not use `if` — render the block unconditionally instead.
- Do not lean on the truthiness of strings (`""` vs `"value"`), numbers (`0` vs `1`), or nullable fields — that may render today but is not guaranteed across surfaces. Comparisons, arithmetic, and string operations are not supported.
- `if` may coexist with `forEach` on the same `meta`. `if` is evaluated first — if `false`, the loop is skipped entirely.
### Example — top-level boolean
```json
{
"definition": "namespace/block",
"meta": { "if": "{!$attrs.isVerified}" },
"attributes": { "label": "Verified user" }
}
```
### Example — boolean nested inside a schema object
```json
{
"definition": "namespace/block",
"meta": { "if": "{!$attrs.features.showBanner}" },
"attributes": { "text": "Promo banner" }
}
```
### Example — boolean inside a forEach loop
```json
{
"definition": "namespace/repeatingBlock",
"meta": { "forEach": "{!$attrs.tasks}", "forItem": "$task" },
"children": [
{ "definition": "namespace/block1", "attributes": { "content": "{!$task.title}" } },
{
"definition": "namespace/block2",
"meta": { "if": "{!$task.completed}" },
"attributes": { "label": "Done" }
}
]
}
```
---
## Gotchas
| Issue | Resolution |
|---|---|
| `if` bound to a non-boolean (string or number) does not behave as expected | Use `if` only when the schema has a `lightning__booleanType` property; otherwise render the block unconditionally |
| Nested loops share the same `forItem` name | Pick distinct names (e.g. `$item` outer, `$line` inner) — there is no validation error on collision |
SKILL.md
---
name: platform-widget-generate
description: "Use this skill to author a complete HXL WidgetBundle (UEM body + schema.json + -meta.xml). TRIGGER when: user asks for a widget, mosaic, fragment, card, or rich UI surface for any subject, domain, feature, or entity noun; the prompt names only an entity or data shape without invoking Lightning Types, CLTs, or Apex-backed types. DO NOT TRIGGER when: the prompt explicitly says 'Lightning Type', 'CLT', 'Custom Lightning Type', 'Apex-backed type', or references '@apexClassType/...' (use platform-lightning-type-widget-coordinate); authoring a custom-LWC renderer for a Custom Lightning Type (use platform-custom-lightning-type-generate); or editing only an LWC component."
metadata:
version: "1.3"
domains: ["Platform", "Agentforce"]
minApiVersion: "68.0"
relatedSkills:
- "platform-apex-generate"
- "platform-custom-lightning-type-generate"
- "platform-lightning-type-widget-coordinate"
mcpTools:
metadata-experts:
tools: ["execute_metadata_action"]
semver: ">=1.0.0"
---
# Generating a Widget Bundle
Author a complete WidgetBundle: a UEM tree (`tile/widget`), a JSON Schema describing the widget's input contract, and the `.uiwidget-meta.xml` that registers the bundle.
## When to Use This Skill
Use when the user asks for a widget, mosaic, fragment, or card-style rich UI surface. Do not use this skill for custom-LWC renderers or for `renderer.json` files inside a Custom Lightning Type bundle — those belong to `platform-custom-lightning-type-generate`.
## Inputs
- **`widgetName`** (required) — `camelCase` identifier; becomes the directory name under `uiWidgets/`.
- A **shape** — what data the widget renders. The widget cannot be generated without it. The shape arrives one of two ways, in priority order:
1. **`lightningTypeSchema`** — `{ path, apexClassFqn }` for an existing Apex-backed Lightning Type. The FQN takes one of two forms: outer-class (`<namespace>__<ClassName>`) where the outer class is the payload, or inner-class (`<namespace>__<ClassName>$<InnerClass>`) where the named inner class is the payload. Passed in by the `platform-lightning-type-widget-coordinate` orchestrator. When present, derive per `references/schema-from-lightning-type.md`.
2. **Extracted from the user's prompt** — when no `lightningTypeSchema` is passed, infer the shape directly from what the user wrote: a pasted JSON payload, an enumerated field list ("id as string, total as number"), or descriptive prose. The output is the same ordered list of `{ name, type, required }` either way.
If neither source yields a shape, STOP and ask the user before proceeding.
## Output
Three files in `<pkgDir>/uiWidgets/<widgetName>/`:
| File | Content |
|---|---|
| `<widgetName>.json` | Widget envelope — `{ "type": "lightning__agentforceWidget", "contentBody": { "widgetBody": { UEM tree rooted at tile/widget } } }` |
| `schema.json` | JSON Schema — root has `type: "object"` + `properties.attributes` wrapper carrying `lightning:type: "lightning__objectType"` and the field `properties` |
| `<widgetName>.uiwidget-meta.xml` | `<UiWidgetBundle>` element with `<masterLabel>`, `<description>`, and `<widgetType>JSON</widgetType>` |
See `references/widget-bundle-layout.md` for the `<pkgDir>` resolution procedure and the exact `<widgetName>.uiwidget-meta.xml` shape.
---
## Composition
A widget body is a UEM tree of blocks nested under `contentBody.widgetBody`. The root node is `tile/widget`. Every node — root and non-root — has the same shape: no `type` key; just `definition`, optional `attributes`, optional `meta`, and optional `children`. Block shape:
```ts
interface Block {
definition: string // {namespace}/{blockName} — root is "tile/widget"
attributes?: Record<string, any>
meta?: { // see references/widget-meta-directives.md
forEach?: string
forItem?: string
if?: string
}
children?: Block[]
}
```
The first child of `tile/widget.children` SHOULD be a single `tile/column`. All widget content typically goes inside that first child for predictable vertical structure across surfaces.
---
## Available Metadata Actions
### discoverUiComponents
**Purpose:** Discover the palette of blocks available for composition.
**Required parameters:** `actionName: "discoverUiComponents"`, `metadataType: "FRAGMENT"`, `parameters.pageType: "FRAGMENT"`. Optional: `searchQuery` to filter by name/description.
**Returns:** list of `{ definition, description, label, attributes? }`.
### getUiComponentSchemas
**Purpose:** Fetch JSON schemas (property types, required vs optional, validation) for selected blocks.
**Required parameters:** `actionName: "getUiComponentSchemas"`, `metadataType: "FRAGMENT"`, `parameters.pageType: "FRAGMENT"`, `parameters.componentDefinitions: ["namespace/definition", ...]`. Optional: `includeKnowledge` (default `true`).
**Returns:** `componentSchemas[]` — success entries carry the JSON schema, failure entries carry an error message. Partial failures are supported.
> Never pass `tile/widget` to `getUiComponentSchemas` — it is a fixed wrapper, not a queryable component.
---
## Attribute Binding
- Bind a block property to runtime data with `{!$attrs.<attrName>}`. `<attrName>` MUST match a property name in `schema.json`.
- Inside a `forEach`, reference the loop variable instead — e.g. `"text": "{!$item.name}"`. See `references/widget-meta-directives.md`.
---
## Actions
`tile/button` supports an `actions` attribute that dispatches an action node on an event. Two action definitions are supported:
| Action | Timing | Required attributes | Effect |
|---|---|---|---|
| `action/openLink` | synchronous | `url` (string). Optional `target` (`_blank`\|`_self`, default `_blank`) | Opens a URL |
| `action/sendMessage` | asynchronous | `content` (string) | Posts a new user message back to the agent and earns a fresh turn |
```json
{
"definition": "tile/button",
"attributes": {
"label": "Button Label",
"variant": "primary",
"actions": { "click": [ { "definition": "action/sendMessage", "attributes": { "content": "content" } } ] }
}
}
```
```json
{
"definition": "tile/button",
"attributes": {
"label": "Button Label",
"variant": "primary",
"actions": { "click": [ { "definition": "action/openLink", "attributes": { "url": "https://www.example.com", "target": "_blank" } } ] }
}
}
```
**A `tile/button` with no `actions` attribute renders disabled** — a button exists to trigger an action, so always attach a `click` action to a button meant to be interactive.
---
## Layout Best Practices
These conventions cover widget *structure* — how blocks are grouped and stacked.
| Primitive | Purpose | When to use |
|---|---|---|
| `tile/column` | Vertical stack of children | Root wrapper, and any group of blocks that should stack |
| `tile/row` | Horizontal stack of children | Two or more blocks that belong on the same line |
| `tile/spacer` | Whitespace between blocks | When extra space is needed between content groups |
- **Nesting:** Prefer flat layouts. Only nest a `tile/column` inside a `tile/row` (or vice versa) when the visual orientation actually changes for that subgroup.
- **Authoritative palette:** the table above lists *typical* layout primitives. Always confirm a block exists by inspecting `discoverUiComponents` output — do not assume a block name from this table without seeing it in the discovery response.
---
## Styling Best Practices
Widgets express *intent*, not pixels. Each surface provides a default look and feel; brand/theme overrides apply automatically.
- **Style semantically.** Use `variant`, `size`, and other enum-typed attributes (`primary`, `destructive`, `success`, `warning`). Do not pin literal colors or pixel values.
- **One primary action per visible group.** At most one `tile/button` with `variant: primary`. Use `secondary` or `destructive` for additional actions (see the `tile/button` schema for the full variant enum).
- **Every `tile/button` needs a `click` action.** See *Actions* above — an action-less button renders disabled.
- **One `h1` per widget.** Use `h2`/`h3` for sub-section headings, `body` for prose, `caption` for helper text.
- **Use semantic state variants on state-bearing blocks** (`tile/badge`, `tile/callout`).
- **Accept schema defaults for `gap`, `size`** unless there is a specific reason to override.
- **Don't pin `width`** unless a content constraint requires it.
- **Use the Lucide icon set.** Pass the Lucide name (`"check"`, `"alert-circle"`); other icon libraries are not supported.
---
## Workflow
1. **Resolve the widget spec** — an ordered list of `{ name, type, required }`. Source depends on which input was provided (see *Inputs*):
- If `lightningTypeSchema` was passed by the orchestrator → derive per `references/schema-from-lightning-type.md`.
- Otherwise → infer the list directly from the user prompt (pasted JSON payload, enumerated field list, or descriptive prose).
2. **Discover blocks (REQUIRED — do NOT skip).** Call the `discoverUiComponents` metadata action via `execute_metadata_action`. Use property types from the widget spec to seed `searchQuery` (text → `"text"`, number → `"number"`). **If `discoverUiComponents` returns `success: false`, an error, or an empty list, STOP and surface the error verbatim — do not improvise block names from memory, prior runs, or training data. Re-run discover with a different `searchQuery` only if the failure is search-query-specific.**
3. **Select blocks.** Choose one block per widget-spec property, plus structural primitives from *Layout Best Practices*.
4. **Get block schemas (REQUIRED — do NOT skip).** Call the `getUiComponentSchemas` metadata action via `execute_metadata_action` for the selected blocks. Review property metadata. **If `componentSchemas` returns all-failure or empty, STOP and surface the error — do not improvise from existing widgets in the project.**
5. **Build the UEM tree (example reads REQUIRED — do NOT skip).** First, identify which patterns match the widget spec and read each matching example file from this skill's own `examples/` directory (`<skill-root>/examples/`):
| Pattern in the spec | Example to read |
|---|---|
| Single object (no iteration) | `<skill-root>/examples/single-object.json` |
| Any list iteration (root-level array, nested list, or list embedded in a single-object widget) | `<skill-root>/examples/list-with-foreach.json` |
| Conditional rendering (`if` bound to a boolean) | `<skill-root>/examples/conditional.json` |
A spec may match multiple patterns (e.g. a list of items where some items render conditionally reads both `list-with-foreach.json` and `conditional.json`). **Read every matching example, and only those — do not skip the read because the pattern feels familiar.**
Then:
- Map each widget-spec property to a block property; preserve spec order.
- **Decide root iteration:** single object → properties directly under root `tile/column`. Collection → wrap repeating block in `forEach`/`forItem`. See `references/widget-meta-directives.md`.
- Bind values with `{!$attrs.X}` (or `{!$item.X}` inside `forEach`).
- For conditional blocks, add `"if"` on `meta` — only when the schema has a matching `lightning__booleanType` property.
6. **Author `schema.json`.** Build the JSON Schema from the widget spec. Fields live one level deep under an `attributes` wrapper:
```json
{
"title": "<Widget Display Name>",
"description": "<one line about what the widget shows>",
"type": "object",
"properties": {
"attributes": {
"lightning:type": "lightning__objectType",
"properties": {
"<propertyName>": {
"title": "<label>",
"description": "<short description>",
"lightning:type": "<lightning__textType | lightning__numberType | ...>"
}
}
}
}
}
```
**Required root keys:** `title`, `type: "object"`, `properties.attributes` (with `lightning:type: "lightning__objectType"` and a nested `properties` map). See `references/schema-from-lightning-type.md` for full primitive type guidance.
7. **Author `<widgetName>.uiwidget-meta.xml`.** See `references/widget-bundle-layout.md` for the exact shape.
8. **Resolve `<pkgDir>` and write the bundle.** Follow the procedure in `references/widget-bundle-layout.md` (`## Resolving <pkgDir>`). A widget bundle is a **three-file set** — all three files must be written in the same step; a bundle with fewer than three files is incomplete and will not deploy.
```text
<pkgDir>/uiWidgets/<widgetName>/<widgetName>.json # widget envelope — UEM tree (primary artifact)
<pkgDir>/uiWidgets/<widgetName>/schema.json # attribute contract for the envelope
<pkgDir>/uiWidgets/<widgetName>/<widgetName>.uiwidget-meta.xml # UiWidgetBundle registration
```
Each file has a distinct role:
- `<widgetName>.json` — the widget envelope with the `tile/widget` UEM tree. This is the primary artifact; `schema.json` is its companion contract, not a substitute.
- `schema.json` — the JSON Schema for the attributes referenced by `{!$attrs.X}` bindings in the envelope.
- `<widgetName>.uiwidget-meta.xml` — the `UiWidgetBundle` element that registers the bundle for source tracking and deployment.
Write all three before proceeding to self-validation.
9. **Self-validate.** Before reporting, confirm each check below and report each result individually (`pass` or `fail (<reason>)`). Do **not** summarize as a single "all passed" line — list every check so a reviewer can spot a silent skip.
- **`schema-parses`** — `<pkgDir>/uiWidgets/<widgetName>/schema.json` parses as JSON.
- **`schema-root-keys`** — root has `title` (string), `type: "object"`, and `properties.attributes` (object) — where `properties.attributes` carries `lightning:type: "lightning__objectType"` and a nested `properties` map. No `unevaluatedProperties: false`.
- **`schema-leaf-types`** — every leaf under `properties.attributes.properties` carries a `lightning:type`. Singular nested inner-class fields carry `lightning:type` set to the inner Apex class reference (`@apexClassType/<namespace>__<OuterClass>$<InnerClass>`); the nested shape is not redeclared. `List<InnerClass>` fields carry `lightning:type: "lightning__listType"` with `items.lightning:type` set to the inner Apex class reference (`@apexClassType/<namespace>__<OuterClass>$<InnerClass>`), not a redeclared field map — see `references/schema-from-lightning-type.md`. **When the list has no Apex-backed type** (schema inferred from the prompt), `items.lightning:type: "lightning__objectType"` MUST carry an inline nested `properties` map for every item field the body binds via `{!$item.X}`.
- **`bindings-resolve`** — every `{!$attrs.X}` (or `{!$attrs.<outerField>.<innerField>}` for nested objects) in `<widgetName>.json` resolves to a property under `schema.json` `properties.attributes.properties`, and every `{!$item.X}` resolves to a `forItem` loop variable defined upstream.
- **`body-envelope`** — `<widgetName>.json` root has `type: "lightning__agentforceWidget"` and a `contentBody` object whose `widgetBody` carries the UEM tree rooted at `tile/widget`. No node in the tree — root or non-root — carries a `type` key.
- **`metaxml-wellformed`** — `<widgetName>.uiwidget-meta.xml` parses as well-formed XML.
- **`metaxml-elements`** — `<widgetName>.uiwidget-meta.xml` has root `<UiWidgetBundle>` and contains `<masterLabel>` (non-empty), `<description>` (non-empty), and `<widgetType>JSON</widgetType>`.
- **`files-present`** — all three files exist at the resolved `<pkgDir>/uiWidgets/<widgetName>/` path.
- **`button-actions-present`** — every `tile/button` in `<widgetName>.json` has an `actions.click` action node whose `definition` is `action/openLink` or `action/sendMessage`.
---
## Rules / Constraints
| Constraint | Rationale |
|---|---|
| Block definitions follow `{namespace}/{blockName}` and must match `discoverUiComponents` output | Runtime resolves blocks by exact definition string |
| Never pass `tile/widget` to `getUiComponentSchemas` | It is a fixed wrapper, not a queryable component |
| Always supply `parameters` (with required keys) when calling `execute_metadata_action` | Missing parameters cause hard failure, not partial result |
| Every `{!$attrs.X}` in the body resolves to a property in the widget `schema.json` | No invented fields |
| Every `tile/button` carries an `actions.click` entry using `action/openLink` or `action/sendMessage` only | These are the only two supported tile action definitions; an action-less button renders disabled |
| No `$(…)`, backticks, `<(…)`, brace expansion `{a,b,c}`, or `eval`/`exec` in any Bash tool call | Vibes' safe-shell filter forces manual approval on these patterns even in Bypass mode. Emit separate commands (`mkdir -p a && mkdir -p b`) or print each value with its own command and reason about the output — do not capture into a shell variable |
---
## Gotchas
| Issue | Resolution |
|---|---|
| `getUiComponentSchemas` returns a partial-failure entry | Pick a different block from `discoverUiComponents`; do not silently continue without a schema |
| Body references `{!$attrs.foo}` but `foo` is not under `schema.json` `properties.attributes.properties` | Add `foo` to `schema.json` `properties.attributes.properties` OR remove the body reference |
| Output written outside `<pkgDir>/uiWidgets/<widgetName>/` | `<pkgDir>` = `<packageDirectories[].path>/main/default` (see `references/widget-bundle-layout.md`). Dropping the `main/default/` segment is the common cause of widgets landing at `force-app/uiWidgets/...` instead of `force-app/main/default/uiWidgets/...` |
| `if` bound to a non-boolean | Use `if` only when the schema has a `lightning__booleanType` property |
| `tile/button` renders but does nothing when clicked | No `actions.click` entry was set — an action-less button renders disabled by design. Add one (see *Actions* above) |
| Using `action/sendMessage` for pure navigation, or `action/openLink` when the agent should respond | `action/openLink` is synchronous and does not consume a turn; `action/sendMessage` is asynchronous and earns a fresh turn. Pick the one matching the intended UX |
---
## Reference File Index
| File | When to read |
|---|---|
| `references/widget-meta-directives.md` | For `forEach` / `forItem` (iteration) and `if` (conditional rendering), including nested loops |
| `references/schema-from-lightning-type.md` | When `lightningTypeSchema` is provided; how to derive the widget `schema.json` from an Apex-backed Lightning Type |
| `references/widget-bundle-layout.md` | Folder layout, `-meta.xml` shape, `<pkgDir>` resolution rules |
| `examples/single-object.json` | Single-object pattern (root binding via `{!$attrs.X}`, no iteration) |
| `examples/list-with-foreach.json` | Any list-iteration case — root-level collections, nested lists, and lists embedded inside a single-object widget (e.g. iterating a `List<InnerClass>` inside an outer Apex payload) |
| `examples/conditional.json` | Conditional pattern (`if` on `meta`, including `if` + `forEach` together) |