feat: split and improve widget renderer guidance for CLT skill

Split widget-rendition.md: extract meta directives (forEach/forItem/if)
into a focused references/widget-meta-directives.md so the iteration and
conditional rules can grow independently.

widget-rendition.md now owns workflow, layout, and styling. Layout
primitives are anchored on the base-lightning-types tile/* schemas.
Styling principles are anchored on the AXL playground tutorial 02
(button variants), Salesforce HIG conventions, and the schemas'
own enums.

Add three widget renderer examples adapted from playground tutorials
04/05/07: single-object profile (root binding), list-of-products
(root forEach + nested forEach over a per-item array), and
conditional-rendering (if on the meta object, including if + forEach
combined). Tutorials are wired into workflow step 5 and the
Reference File Index.

Slim SKILL.md: collapse five overlapping widget-routing instructions
into a single Step 4 fork; standardize trigger keywords on widget /
mosaic / fragment; drop the structurally-wrong "STOP before step 1"
header (widget needs schema.json from steps 1-2 first); add a
Reference File Index pointing at widget-rendition.md.

Tighten the conditional-rendering rule to require a strict
lightning__booleanType schema property. Strings, numbers, and nullable
fields are not supported as if expressions.

gitignore: ignore .agents/, .claude/, skills-lock.json (skills CLI
install artifacts) and .claude/ (per-user Claude Code state).
This commit is contained in:
sowmya.sriram 2026-05-21 13:40:00 +05:30
parent 2d26b24d0e
commit 9a0494073d
No known key found for this signature in database
GPG Key ID: 50B1C900A9E2B573
7 changed files with 617 additions and 290 deletions

7
.gitignore vendored
View File

@ -245,3 +245,10 @@ service-account.json
deploy-*.log
deployment_log.txt
validation_results.json
# Skills CLI install artifacts (regenerated by `npx skills add`)
.agents/
skills-lock.json
# Claude Code local state (per-user)
.claude/

View File

@ -1,6 +1,6 @@
---
name: generating-custom-lightning-type
description: "Use this skill when users need to create Custom Lightning Types (CLTs) for Einstein Agent actions or structured input/output schemas. Trigger when users mention CLT, Custom Lightning Types, custom lightning type with widget renderer, custom lightning type with widget, CLT with widget renderer, CLT with widget, JSON schemas for agents, type definitions, lightning__objectType, or editor/renderer configurations. When a widget renderer is requested, you MUST first read the widget-rendition.md reference file in this skill's references/ directory and follow its complete workflow. This is complex - always use this skill for CLT work."
description: "Use this skill when users need to create Custom Lightning Types (CLTs) for Einstein Agent actions or structured input/output schemas, and their editor/renderer configurations. TRIGGER when: users mention CLT, Custom Lightning Types, JSON schemas for agents, type definitions, lightning__objectType, editor/renderer configurations; users ask for a CLT with a 'widget', 'mosaic' or 'fragment' renderer (synonymous — all route to the widget rendition path)."
metadata:
version: "1.0"
---
@ -26,8 +26,7 @@ Custom Lightning Types (CLTs) are JSON Schema-based type definitions used by the
- **Choose referenced CLT pattern for nested objects** - When you need a **reusable** or **separately deployed** nested type, create a CLT for that shape and reference it with `"lightning:type": "c__<CLTName>"`. That string is the referenced types **`lightning:type` value / FQN / registered identifier** — not the JSON Schema `title`.
- **Choose standard Lightning types** when the structure is simple and can be expressed with properties and supported primitive `lightning:type` identifiers.
- **Choose Apex class types** (`@apexClassType/...`) when the structure already exists server-side and you want the Apex class to define the shape.
- **Include editor/renderer config** only when you need custom UI behavior (custom LWC input/output components). Otherwise, omit.
- **Widget rendition**: After `schema.json` is drafted, you **must read references/widget-rendition.md and follow the "Widget renderer pattern"** section below for complete guidance. Do NOT attempt widget generation without reading the reference file first.
- **Include editor/renderer config** only when you need custom UI behavior (custom LWC input/output components, or widget renderer). Otherwise, omit.
## Critical Rules (Read First)
- **Root object schemas MUST include**:
@ -96,9 +95,6 @@ When strict validation is enabled (`unevaluatedProperties: false`), keep each pr
## Generation Workflow
> **⚠️ Widget renderer requested? STOP before step 1.**
> If the user requested a "widget renderer", "mosaic", "widget", or "fragment": you MUST read `references/widget-rendition.md` NOW before doing anything else — before drafting schema, before creating any files, before planning steps. The API context examples show `c/<componentName>` renderers — those are the custom LWC pattern, NOT the widget pattern. Do not follow them for widget rendition.
1. **Confirm the CLT approach**
- If referencing Apex: capture the exact class reference (`@apexClassType/namespace__ClassName$InnerClass`).
- If using standard primitives: list the fields, their Lightning primitive types, and which fields are required.
@ -133,25 +129,13 @@ When strict validation is enabled (`unevaluatedProperties: false`), keep each pr
- **Avoid known-invalid patterns**:
- Do not use `es_property_editors/inputList`.
- Do not use `itemSchema` attributes.
4. **(Optional) Draft `renderer.json`** (only if custom UI or mosaic rendition is required)
- **⚠️ DECIDE FIRST — widget or custom LWC?** Before writing any renderer code or planning any steps:
- If the user requested "widget", "mosaic", "fragment", or "cross-platform rendering": you MUST use the **Widget renderer pattern** below. Do NOT create a custom LWC. Do NOT use the root override pattern with `c/<componentName>`. Skip directly to the Widget renderer pattern now.
- Otherwise: use the root override pattern or property-level overrides described below.
- **Supported shape:** Top-level `renderer` object with `renderer.componentOverrides` and `renderer.layout`.
- Top-level `renderer` object.
- Use `renderer.componentOverrides` for component overrides.
- Use `renderer.layout` for layout.
- **DEPRECATED**: Do NOT use `propertyRenderers` or `view` — these are legacy keys. Always use `componentOverrides` and `layout` instead.
- **Widget renderer pattern** (for widget rendition — use when user requests "widget", "mosaic", "fragment", or "cross-platform rendering"):
- **Structure:** `renderer.componentOverrides["$"] = { "type": "mosaic", "definition": "tile/mosaic", "children": [ /* UEM tree of blocks and regions */ ] }`
- **CRITICAL**: Do NOT create a custom LWC component (e.g. `c/<componentName>`) for widget rendering. Widget rendition uses a `tile/mosaic` UEM block tree — not a custom component. Creating a custom LWC here is always wrong.
- **REQUIRED workflow:**
- **STOP**: Do NOT attempt to build the UEM tree yourself.
- **MANDATORY FIRST STEP**: You MUST fetch the reference file `references/widget-rendition.md` located in this skill's directory before proceeding.
- Follow the complete workflow documented in `widget-rendition.md` using the generated CLT schema as the grounding schema.
- The `widget-rendition.md` reference contains the full widget generation workflow: discovering UEM blocks via discoverUiComponents, calling getUiComponentSchemas, building the UEM tree, and writing renderer.json.
- **Do not** attempt to generate widget rendition without first fetching the `widget-rendition.md` reference file.
- **Root override pattern** (for custom LWC rendering — only when widget/mosaic was NOT requested):
4. **(Optional) Draft `renderer.json`** (only if custom UI or widget rendition is required)
- **Decide first — widget or custom LWC?**
- If the user requested `widget`, `mosaic`, or `fragment`: read `references/widget-rendition.md` and follow its workflow. The reference file owns the entire widget path — discovering blocks, fetching schemas, building the UEM tree, and writing `renderer.json`. Do not create a custom LWC for this path.
- Otherwise: use the custom LWC patterns below.
- **Supported shape (custom LWC path):** Top-level `renderer` object with `renderer.componentOverrides` and `renderer.layout`.
- **DEPRECATED**: Do NOT use `propertyRenderers` or `view`. Use `componentOverrides` and `layout`.
- **Root override pattern**:
- `renderer.componentOverrides["$"] = { "definition": "c/<yourRendererComponent>", "attributes": { ... } }`
- Use `{!$attrs.<name>}` in attribute mappings when binding schema data to custom renderer component attributes.
- **CRITICAL**: Attribute mappings like `{!$attrs.propertyName}` must reference properties that **actually exist** in your type schema. Referencing non-existent properties will fail validation.
@ -219,3 +203,11 @@ When strict validation is enabled (`unevaluatedProperties: false`), keep each pr
- [ ] Layout configurations use `lightning/propertyLayout` with ONLY the `property` attribute (no `label`, `title`, or other attributes)
- [ ] All attribute mappings (`{!$attrs.propertyName}`) reference properties that exist in the type schema
- [ ] Custom LWC components have correct targets in `-meta.xml`: `lightning__AgentforceInput` for editors, `lightning__AgentforceOutput` for renderers
---
## Reference File Index
| File | When to read |
|------|--------------|
| `references/widget-rendition.md` | When the user requests a CLT with a 'widget', 'mosaic', or 'fragment' renderer — read at workflow step 4 to follow the widget rendition path (block discovery, schema fetching, UEM tree, `renderer.json`). |

View File

@ -0,0 +1,113 @@
{
"renderer": {
"componentOverrides": {
"$": {
"type": "mosaic",
"definition": "tile/mosaic",
"children": [
{
"definition": "tile/column",
"attributes": {
"gap": "md",
"padding": "md"
},
"children": [
{
"definition": "tile/callout",
"meta": {
"if": "{!$attrs.showBanner}"
},
"attributes": {
"variant": "warning",
"title": "Announcement"
},
"children": [
{
"definition": "tile/text",
"attributes": {
"text": "This banner is conditionally hidden.",
"variant": "body"
}
}
]
},
{
"definition": "tile/card",
"meta": {
"if": "{!$attrs.isVerified}"
},
"attributes": {
"padding": "md",
"variant": "outlined"
},
"children": [
{
"definition": "tile/row",
"attributes": {
"gap": "sm",
"align": "center"
},
"children": [
{
"definition": "tile/icon",
"attributes": {
"name": "check-circle",
"color": "success"
}
},
{
"definition": "tile/text",
"attributes": {
"text": "Welcome back, {!$attrs.name}! Your account is verified.",
"variant": "body"
}
}
]
}
]
},
{
"definition": "tile/card",
"meta": {
"forEach": "{!$attrs.tasks}",
"forItem": "$task",
"if": "{!$task.isUrgent}"
},
"attributes": {
"padding": "md",
"variant": "outlined"
},
"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": "{!IF($task.priority = 'high', 'error', 'secondary')}"
}
}
]
}
]
}
]
}
]
}
}
}
}

View File

@ -0,0 +1,95 @@
{
"renderer": {
"componentOverrides": {
"$": {
"type": "mosaic",
"definition": "tile/mosaic",
"children": [
{
"definition": "tile/card",
"meta": {
"forEach": "{!$attrs.products}",
"forItem": "$product"
},
"attributes": {
"padding": "md",
"variant": "outlined"
},
"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"
}
}
]
}
]
}
]
}
}
}
}

View File

@ -0,0 +1,131 @@
{
"renderer": {
"componentOverrides": {
"$": {
"type": "mosaic",
"definition": "tile/mosaic",
"children": [
{
"definition": "tile/card",
"attributes": {
"padding": "lg",
"variant": "elevated"
},
"children": [
{
"definition": "tile/text",
"attributes": {
"text": "User Profile",
"variant": "h2"
}
},
{
"definition": "tile/divider"
},
{
"definition": "tile/column",
"attributes": {
"gap": "sm"
},
"children": [
{
"definition": "tile/row",
"attributes": {
"gap": "sm"
},
"children": [
{
"definition": "tile/text",
"attributes": {
"text": "Name:",
"variant": "body",
"weight": "semibold"
}
},
{
"definition": "tile/text",
"attributes": {
"text": "{!$attrs.name}",
"variant": "body"
}
}
]
},
{
"definition": "tile/row",
"attributes": {
"gap": "sm"
},
"children": [
{
"definition": "tile/text",
"attributes": {
"text": "Email:",
"variant": "body",
"weight": "semibold"
}
},
{
"definition": "tile/text",
"attributes": {
"text": "{!$attrs.email}",
"variant": "body"
}
}
]
},
{
"definition": "tile/row",
"attributes": {
"gap": "sm"
},
"children": [
{
"definition": "tile/text",
"attributes": {
"text": "Role:",
"variant": "body",
"weight": "semibold"
}
},
{
"definition": "tile/text",
"attributes": {
"text": "{!$attrs.role}",
"variant": "body"
}
}
]
},
{
"definition": "tile/row",
"attributes": {
"gap": "sm"
},
"children": [
{
"definition": "tile/text",
"attributes": {
"text": "Location:",
"variant": "body",
"weight": "semibold"
}
},
{
"definition": "tile/text",
"attributes": {
"text": "{!$attrs.location}",
"variant": "body"
}
}
]
}
]
}
]
}
]
}
}
}
}

View File

@ -0,0 +1,130 @@
# 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. For the widget workflow, layout, and metadata-action API, see `widget-rendition.md`.
---
## 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` for that condition — 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", // rendered only when $task.completed is true
"meta": { "if": "{!$task.completed}" },
"attributes": { "label": "Done" }
}
]
}
```
---
## Gotchas
| Issue | Resolution |
|-------|-----------|
| `if` bound to a non-boolean (e.g. a string or number) does not behave as expected | Use `if` only when the schema has a `lightning__booleanType` property suited to the condition; 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 |

View File

@ -1,305 +1,164 @@
# Widget Generation Guide
# Widget Rendition Reference
## 📋 Overview
Widgets are reusable pieces of UI similar to templates, with placeholders for actual data values. The purpose of this file is to assist developers in creating mosaic renditions for CLTs.
How to author a UEM widget tree that renders a Custom Lightning Type's grounding schema as a `tile/mosaic` widget across surfaces.
## 🎯 Purpose
Widgets render data in a structured and unified way across various Salesforce experiences like Slack, Mobile, LEX etc.
## When to read this file
## Schema Grounding
Widget generation is **always schema-grounded** using a CLT's schema. The schema describes the data shape the widget should render. Extract property names, types, required vs optional, and nesting from the schema; then follow the full **Workflow** below, using this extracted structure to guide every step. Do not add or remove properties relative to the schema.
Read this file when the parent skill routes to widget rendition (user requested a "widget", "mosaic", or "fragment"). Do not use these patterns for custom-LWC renderers.
## ⚙️ Composition
A widget is a UEM (Unified Experience Model) tree of blocks and regions. The widget you return must follow the Typescript interfaces below:
## Scope
- **In scope**: building the UEM tree under `renderer.componentOverrides["$"].children`, including composition, attribute binding, layout, and writing the final `renderer.json`.
- **Out of scope**: runtime meta directives (`forEach`, `forItem`, `if`) — read `references/widget-meta-directives.md`.
---
## Composition
A widget is a UEM(Unified Experience Model) tree of blocks. Every block follows this shape:
```ts
interface BlockType {
type: 'block'
interface Block {
definition: string // {namespace}/{blockName}
attributes?: Record<string, any>
meta?: {
forEach?: string // expression resolving to an array, e.g. "{!$attrs.items}"
forItem?: string // loop variable (must start with $), e.g. "$item"
if?: string // boolean expression; block is omitted when falsy
forEach?: string // see widget-meta-directives.md
forItem?: string // see widget-meta-directives.md
if?: string // see widget-meta-directives.md
}
children?: (BlockType | RegionType)[]
}
interface RegionType {
type: 'region'
name: string
children: BlockType[]
children?: Block[]
}
```
---
## 🔧 Available Metadata Actions
### When to Use Each Action
#### discoverUiComponents
**Purpose:** Discover the palette of available blocks that can be used in widget composition.
**Use for:** Finding available blocks before building your widget structure.
**Input Parameters:**
- `actionName` (**required***): "discoverUiComponents"
- `metadataType` (**required**): "FRAGMENT"
- `parameters` (**required**): JSON object with the below fields
- `pageType` (**required**): "FRAGMENT"
- `pageContext` (optional): JSON object - not required for FRAGMENT type
- `searchQuery` (optional): String to filter components by name or description
**Returns:** List of components with:
- `definition`: Fully qualified name (e.g., "namespace/definiton")
- `description`: Component description
- `label`: Human-readable label
- `attributes`: Optional attribute metadata
#### getUiComponentSchemas
**Purpose:** Get detailed JSON schemas for component configuration, including property types, required vs optional fields, and validation rules.
**Use for:** You know which components you want but need to understand their properties before adding them to your widget.
**Input Parameters:**
- `actionName` (**required***): "getUiComponentSchemas"
- `metadataType` (**required**): "FRAGMENT"
- `parameters` (**required**): JSON object with the below fields
- `pageType` (**required**): "FRAGMENT"
- `componentDefinitions` (**required**): List of fully qualified names (e.g., ["namespace/definition"])
- **CRITICAL**: NEVER include "tile/mosaic" in this list. "tile/mosaic" is a container component used in renderer.json structure and **should not** be passed to getUiComponentSchemas
- `pageContext` (optional): JSON object - not required for FRAGMENT type
- `includeKnowledge` (optional): Boolean, defaults to true - includes additional component-specific guidance
**Returns:**
- `componentSchemas`: List of results (supports partial failures)
- **Success entries**: Contains JSON schema with property definitions, types, constraints
- **Failure entries**: Contains error message explaining why schema couldn't be retrieved
- `$defs`: Schema definitions and references (if schema transformation applied)
**Key Feature:** Supports partial failures - if some components can't be found, you still get schemas for the successful ones.
---
## Attribute binding using placeholder syntax
## Available metadata actions
- **Where to use:** When block properties must display or pass runtime data from the grounding schema, use the **Placeholder Syntax** below so that the runtime binds values into the widget. Check each block's schema (from `getUiComponentSchemas`) for the correct property name (e.g. `value`, `text`, `label`).
- **Placeholder Syntax:** Use `{!$attrs.<attrName>}` as the placeholder for each block property that should receive data.
`<attrName>` **must** match the property name from the grounding schema so that the runtime can resolve its value.
Example: for a schema property `title`, set the block property to `{!$attrs.title}`.
- **List / iterative data:** Only the children (list items) hold bound values; the parent list block does not. For each item inside a list (e.g. `tile/listItem`), use `{!$attrs.<listAttrName>.item}` so the runtime binds the current item. `<listAttrName>` MUST match the schema property name of the list. Example: for `icons`, use `"{!$attrs.icons.item}"` on the list item.
### 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).
---
## 🔁 Iteration with forEach / forItem
## Attribute binding
Use `forEach`/`forItem` when a block repeats over an array. Whether that applies at the root level depends on the schema shape — see step 5 of the Workflow for the decision.
- Bind a block property to schema data with `{!$attrs.<attrName>}`. `<attrName>` MUST match the property name in the grounding schema. Example: `"text": "{!$attrs.title}"`.
- When the block is inside a `forEach`, reference the loop variable instead — e.g. `"text": "{!$item.name}"`. See `references/widget-meta-directives.md`.
### Rules
- Place `forEach` on the `meta` object of the **repeating** block (a row, card, list item, etc.), **not** on the container block. When a container holds repeating children (e.g. a list wrapper and its item rows), `forEach`/`forItem` goes on the child, not the container — otherwise a new container is created per item instead of one container with all items inside it.
- The value must be an expression resolving to an array: `"{!$attrs.<arrayAttrName>}"`, e.g. `"{!$attrs.items}"`.
- `forItem` (required with `forEach`) names the loop variable for the current item. **Must start with `$`**, e.g. `"$item"`.
- All children of the `forEach` block reference the loop variable (e.g. `{!$item.id}`, **not** `{!$attrs.items.id}`).
- For **nested lists** (inner arrays on each item), add another `forEach` block inside the repeating block's children, using a distinct `forItem` name (e.g. `"$subItem"`).
---
### Example — top-level list
## Layout best practices
These conventions cover widget *structure* — how blocks are grouped and stacked. For visual style choices, see *Styling best practices* below.
The first child inside `tile/mosaic.children` should be a single `tile/column`. All widget content goes inside that column for predictable vertical structure across surfaces.
| Primitive | Purpose | When to use |
|-----------|---------|-------------|
| `tile/column` | Vertical stack of children | The 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/card` | Visually-boxed group | A bounded section that should read as one unit |
| `tile/divider` | Visual rule between sections | Separating major content groups (header / body / footer) |
| `tile/spacer` | Whitespace without a visible line | When extra space is needed but a divider would be too heavy; set `flex: true` to fill remaining space in a row |
Use the five primitives above for structure; all other (content) blocks come from `discoverUiComponents`.
- **Sectioning**: Place a `tile/divider` *or* a fresh `tile/card`-bounded section between major content groups (header → body, body → footer). Pick the divider for lightweight rules between sections of the same widget; pick a card when the section is its own logical unit. Do not use either inside a single section.
- **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.
---
## Styling best practices
Widgets express *intent*, not pixels. Each surface (Slack, ChatGPT, ACC, Mobile, etc.) provides a default native look and feel, and brand/theme overrides apply automatically. Principles the widget author owns:
- **Style semantically.** Use `variant`, `size`, and other enum-typed attributes (e.g. `primary`, `destructive`, `success`, `warning`) to express intent. Do not pin literal colors, fonts, or pixel values — that fights the surface's rendering and breaks brand/theme application.
- **One primary action per visible group.** At most one `tile/button` with `variant: primary` per visible action group. Use `secondary`, `outline`, or `ghost` for additional actions. Reserve `destructive` for genuinely destructive operations (delete, cancel a paid order, etc.).
- **One `h1` per widget.** The `h1` is the widget title. Use `h2`/`h3` for sub-section headings (skipping levels is fine if the hierarchy is shallow), `body` for prose, and `caption` for helper text.
- **Use semantic state variants on state-bearing blocks.** For `tile/alert`, `tile/badge`, `tile/callout`, and `tile/chip`, set `variant` to the semantic state (`success`, `warning`, `error`, `info`, etc.). Do not express state by overriding `text.color` on a generic block — the dedicated blocks render the correct iconography and accessibility cues for free.
- **Accept schema defaults for `gap`, `padding`, and `size`** unless there's a specific reason to override. When you do override, always pass the enum-defined token (the schema rejects pixel values and freeform strings).
- **Don't pin `width`, `height`, or `maxWidth`** unless a content constraint genuinely requires it — the surface owns layout sizing. For long text, use `truncate: true` rather than capping `maxWidth`.
- **Use the Lucide icon set.** Every `icon` attribute resolves to a name in the Lucide icon set. Pass the Lucide name (e.g. `"check"`, `"alert-circle"`); other icon libraries are not supported.
---
## Workflow
1. **Parse the schema** — extract property names, types, required vs optional, and nested structure from the CLT's grounding schema. This is the **widget spec**.
2. **Discover blocks** — call `discoverUiComponents`. Use property types from the widget spec to inform `searchQuery` (e.g. text → `"text"`, number → `"number"`).
3. **Select blocks** — choose one block per widget-spec property, plus structural primitives from *Layout best practices*.
4. **Get block schemas** — call `getUiComponentSchemas` for the selected blocks and review their property metadata.
5. **Read every matching example** — before authoring, identify which patterns the widget spec needs and read **all** matching renderer examples in `examples/widgets/`. A complex widget combines multiple patterns; read all that apply.
| Pattern in the widget spec | Example to read |
|---|---|
| Single object (no root iteration) | `examples/widgets/single-object-profile.renderer.json` |
| Collection (root-level array iterated with `forEach`), including a nested `forEach` over an array on each item | `examples/widgets/list-of-products.renderer.json` |
| Conditional rendering (`if` bound to a boolean), including `if` + `forEach` on the same `meta` | `examples/widgets/conditional-rendering.renderer.json` |
Use these as structural starting points, not literal templates. Compose the patterns the widget actually needs.
6. **Build the UEM tree**
- Map each widget-spec property to a block property; preserve widget-spec order.
- **Decide root iteration**: if the schema is a single object, render its properties directly under the root `tile/column`. If the schema is a collection (top-level array), wrap the repeating block in `forEach` / `forItem`. For placement rules and example shapes, read `references/widget-meta-directives.md`.
- Bind values with placeholder syntax (see *Attribute binding* above).
- For any block that should render conditionally, add `"if"` on its `meta` — read `references/widget-meta-directives.md`.
7. **Write to the CLT bundle** — output to `lightningTypes/<TypeName>/lightningDesktopGenAi/renderer.json` (or the surface-specific subfolder: `experienceBuilder/`, `lightningMobileGenAi/`, `enhancedWebChat/`). The renderer's root override is:
```json
{
"type": "block",
"definition": "namespace/arrayBlockDefiniton",
"meta": {
"forEach": "{!$attrs.items}",
"forItem": "$item"
},
"children": [
{
"type": "block",
"definition": "namespace/blockDefinition1",
"attributes": { "content": "{!$item.id}" }
},
{
"type": "block",
"definition": "namespace/blockDefinition2",
"attributes": { "content": "{!$item.total}" }
"renderer": {
"componentOverrides": {
"$": {
"type": "mosaic",
"definition": "tile/mosaic",
"children": [ /* the UEM tree built in step 5 — root is tile/column */ ]
}
}
]
}
```
### Example — container block with repeating child
When a container block holds repeating items (e.g. a list wrapper and its item rows), place `forEach`/`forItem` on the **child** (the repeating element), not on the container.
```json
{
"type": "block",
"definition": "namespace/containerBlockDefinition",
"attributes": { "variant": "default" },
"children": [
{
"type": "block",
"definition": "namespace/itemBlockDefinition",
"meta": {
"forEach": "{!$attrs.items}",
"forItem": "$item"
},
"attributes": { "title": "{!$item.name}" }
}
]
}
```
### Example — nested list (inner items)
```json
{
"type": "block",
"definition": "namespace/arrayBlockDefiniton",
"meta": { "forEach": "{!$attrs.items}", "forItem": "$item" },
"children": [
{
"type": "block",
"definition": "namespace/blockDefinition1",
"attributes": { "content": "{!$item.id}" }
},
{
"type": "block",
"definition": "namespace/blockDefinition1",
"meta": { "forEach": "{!$item.lineItems}", "forItem": "$lineItem" },
"children": [
{
"type": "block",
"definition": "namespace/blockDefinition2",
"attributes": { "content": "{!$lineItem.name}" }
},
{
"type": "block",
"definition": "namespace/blockDefinition2",
"attributes": { "content": "{!$lineItem.count}" }
}
]
}
]
}
}
```
---
## 🔀 Conditional rendering with if
## Rules / constraints
- Place `"if"` on the `meta` object of a block to conditionally include it.
- The expression must resolve to a truthy/falsy value. When falsy, the block **and all its children** are excluded from the rendered output.
- `if` can be combined with `forEach` on the same `meta` object.
### Example
```json
{
"type": "block",
"definition": "namespace/blockDefinition",
"meta": { "if": "{!$attrs.isTrue}" },
"attributes": { "label": "Label"}
}
```
### Example — if inside a forEach loop
```json
{
"type": "block",
"definition": "namespace/blockDefinition1",
"meta": { "forEach": "{!$attrs.items}", "forItem": "$item" },
"children": [
{
"type": "block",
"definition": "namespace/blockDefinition2",
"attributes": { "content": "{!$item.id}" }
},
{
"type": "block",
"definition": "namespace/blockDefinition3",
"meta": { "if": "{!$item.isTrue}" },
"attributes": { "label": "Label"}
}
]
}
```
### Example — forEach and if on the same block
`forEach` and `if` can be placed together on the same `meta` object. The block is first evaluated against `if` — if falsy, the entire loop is skipped. If truthy, the block repeats for every item in the array.
```json
{
"type": "block",
"definition": "namespace/arrayBlockDefinition",
"meta": {
"forEach": "{!$attrs.items}",
"forItem": "$item",
"if": "{!$attrs.showItems}"
},
"children": [
{
"type": "block",
"definition": "namespace/blockDefinition2",
"attributes": { "content": "{!$item.id}" }
},
{
"type": "block",
"definition": "namespace/blockDefinition3",
"attributes": { "content": "{!$item.total}" }
}
]
}
```
| Constraint | Rationale |
|-----------|-----------|
| Block definitions follow `{namespace}/{blockName}` and must match the form returned by `discoverUiComponents` | The runtime resolves blocks by exact definition string |
| Never pass `tile/mosaic` 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 a hard failure, not a partial result |
| The first child of `tile/mosaic.children` is a single `tile/column` | Predictable widget structure across surfaces |
---
## 💡Workflow
## Gotchas
1. **Schema Parsing**
- Parse the schema and extract: property names, types, required vs optional, and nested structure. Use this as the **widget spec**.
2. **Discover Available Blocks** (**REQUIRED** - do NOT skip)
- Use **discoverUiComponents metadata action** above to explore what blocks are available.
- Use property types from the **widget spec** to inform `searchQuery` (e.g. text → "text", number → "number").
3. **Select Components**
- Choose blocks that can represent each property in the **widget spec** from the results of step 2.
4. **Get Component Schemas** (**REQUIRED** - do NOT skip)
- Use **getUiComponentSchemas metadata action** with the selected block definitions from step 3 and review block properties' metadata.
5. **Build Widget**
- Construct the UEM tree. Map each property in the **widget spec** to block properties and preserve order of the **widget spec**.
- **Decide whether to iterate at the root level:**
- If the schema represents a **single object** (e.g. one item — root `type: object` with scalar/list properties) — do NOT add `forEach` on the top-level block. Render its properties directly. Use `forEach`/`forItem` only on blocks that repeat over an array property within it (e.g. the list of cart items).
- If the schema represents a **collection** (e.g. a list of items) — wrap the repeating block in a `forEach` / `forItem` meta directive so the widget renders one row/card per item. See **Iteration with forEach / forItem** above.
- For block properties that must show or pass runtime data, use the placeholder syntax (see **Attribute binding using placeholder syntax** above). Inside a `forEach` block, reference the loop variable (e.g. `{!$item.attrName}`) instead of `{!$attrs.items.attrName}`.
- Add `"if"` on the `meta` object of any block that should render conditionally (see **Conditional rendering with if** above).
- For inner arrays on each item, add a nested `forEach` block with a distinct `forItem` name.
- Use block properties from the schemas retrieved in step 4.
6. **Write output to CLT Bundle**
- Always write to `lightningTypes/<TypeName>/lightningDesktopGenAi/renderer.json` (or the correct target subfolder for the product surface, e.g. `experienceBuilder/`, `lightningMobileGenAi/`, `enhancedWebChat/` when applicable).
Check **required root override pattern** below -
`renderer.componentOverrides["$"] = { "type": "mosaic", "definition": "tile/mosaic", "children": [ ... ] — array of UEM nodes - contains the widget UEM generated using the **Workflow** steps 1-5 above }`
| Issue | Resolution |
|-------|-----------|
| `getUiComponentSchemas` returns a partial-failure entry for a block | Pick a different block from `discoverUiComponents` results; do not silently continue without a schema |
| Output written to the wrong surface subfolder | Default to `lightningDesktopGenAi/`; switch only when the user names the surface |
---
## ⚠️ Important Notes
## Reference File Index
- **widget spec** includes both required and optional attributes - review carefully to ensure valid configuration.
- When using **`execute_metadata_action`** tool, always supply **`parameters`** with the required fields above; missing `parameters` or required keys causes hard failures, not partial results.
- Block definitions always follow the `{namespace}/{blockName}` convention.
- Use the same definition format returned by `discoverUiComponents` when calling `getUiComponentSchemas`
- Placeholder syntax for non-list properties is `{!$attrs.<attrName>}` and for list properties is `{!$attrs.<listAttrName>.item}`.
- **`forEach` + `forItem` are required together.** Never use one without the other.
- **`forItem` values must start with `$`** (e.g. `"$item"`, `"$order"`, `"$line"`).
- Inside a `forEach` block, children **must** reference the loop variable (`{!$item.attrName}`), not the original array path.
- Nested `forEach` blocks must use a **different** `forItem` name from the outer loop.
- `"if"` expressions must be boolean. Use comparison or logic expressions (e.g. `{!$item.isActive}`).
- `"if"` and `"forEach"` can coexist on the same `meta` object.
| File | When to read |
|------|--------------|
| `references/widget-meta-directives.md` | For the `forEach` / `forItem` (iteration) and `if` (conditional rendering) directives — including patterns not shown in examples (nested `forEach`, container-vs-child placement) |
| `examples/widgets/single-object-profile.renderer.json` | For the single-object pattern (root binding via `{!$attrs.X}`, no iteration) |
| `examples/widgets/list-of-products.renderer.json` | For the collection pattern (root-level `lightning__listType` iterated with `forEach`); also covers a nested `forEach` over an array per item |
| `examples/widgets/conditional-rendering.renderer.json` | For the conditional pattern (`if` on a `meta` object, including `if` + `forEach` on the same block) |
A complex widget often combines several of these patterns (e.g. collection + nested `forEach` + conditional). Read every applicable row.