Customize a Generated Widget customize-generated-widget

IMPORTANT
Adobe LLM Apps is currently in Beta.
Features, workflows, and UI shown here do not necessarily represent the final state of the product. To join the Beta, send an email to llm-apps-beta@adobe.com.
NOTE
This guide assumes basic familiarity with Adobe Edge Delivery Services (EDS). If you are new to EDS, first read the EDS developer tutorial and Exploring blocks to learn the essentials — blocks, the decorate function, and the EDS project structure — before customizing a widget.

The platform creates an EDS widget for every generated action. The widget already receives the action result, renders sample data, applies host styling, and is linked to the action in LLM Apps.

Start by testing the generated widget. Then customize its data contract, interaction, and visual design.

Journey: Find the generated block → align its data contract → customize safely → preview locally → deploy and test.

Find the generated widget

Open the EDS repository selected when you created the app. Each generated widget is an EDS block:

blocks/
└── <action-name>/
    ├── <action-name>.js
    └── <action-name>.css
  • The JavaScript file reads the action result and builds the interface.
  • The CSS file controls layout, responsive behavior, and visual design.
  • The generated pull request shows the exact files created for the action.

The platform also configures the widget URLs and supporting SDK files. You do not need to create a second EDS project or re-enter those values to customize a generated widget.

How the LLM Apps SDK connects the widget

The @adobe/llmapps-sdk package connects the EDS widget to the LLM host. The generated EDS repository includes:

scripts/
├── aem-embed.js
└── llmapps-sdk.js

aem-embed.js establishes the host connection, loads the EDS page, and calls your block:

export default async function decorate(block, bridge) {
  // Customize the widget here.
}

You do not import the SDK in the block. The connected bridge is provided automatically. It lets the widget:

  • Read the handler result with bridge.toolResult.
  • Apply host styling with bridge.applyHostStyles().
  • Continue the conversation with bridge.sendMessage().
  • Invoke another action with bridge.callTool().
  • Keep its size synchronized with bridge.autoResize().

This guide covers the common bridge methods. See the @adobe/llmapps-sdk package for the complete API.

Understand the data contract

The action handler returns structuredContent, and the block reads it from bridge.toolResult.

// Handler result
return {
  content: [{ type: 'text', text: `Found ${products.length} products.` }],
  structuredContent: { products, total: products.length }
};
// EDS block
export default async function decorate(block, bridge) {
  const result = bridge ? await bridge.toolResult : null;
  const products = result?.structuredContent?.products ?? [];
  // Render products.
}

When you change structuredContent, update the handler and widget together. See Customize a generated handler for the complete return contract.

Render external data safely

Treat handler output as untrusted data. Prefer DOM APIs such as textContent instead of inserting response values into innerHTML.

function createProductCard(product, bridge) {
  const card = document.createElement('article');
  card.className = 'product-card';

  const title = document.createElement('h3');
  title.textContent = String(product.name ?? 'Product');

  const button = document.createElement('button');
  button.type = 'button';
  button.textContent = 'Tell me more';
  button.addEventListener('click', () => {
    if (bridge && product.id) {
      bridge.sendMessage(`Show me details for product ${String(product.id)}`);
    }
  });

  card.append(title, button);
  return card;
}

Validate URLs before assigning them to href or src, and allow only the protocols required by the experience.

Use the host bridge

EDS passes a connected bridge to decorate(block, bridge). Guard bridge calls so the block also renders during direct EDS preview.

Apply host styles

if (bridge) {
  bridge.applyHostStyles();
}

This applies host typography and theme variables. Your widget CSS should support both light and dark host themes.

Send a follow-up message

await bridge.sendMessage('Show me similar products.');

Use sendMessage when an interaction should continue the conversation.

Call another action

const result = await bridge.callTool('get-product-details', {
  id: product.id
});

Use callTool for an explicit interaction that needs another action result. Pass only validated values and handle failures without exposing internal details.

Keep the widget size synchronized

if (bridge) {
  bridge.autoResize(block);
}

Call autoResize after the initial render so the host can respond to content changes.

Preview your changes

Generated blocks should include sample data for direct preview when bridge is unavailable.

To preview the EDS project locally:

npm install -g @adobe/aem-cli
aem up

Open the generated widget page at http://localhost:3000. Verify:

  • Empty, loading, success, and error states.
  • Long text and missing optional fields.
  • Keyboard navigation and visible focus.
  • Light and dark themes.
  • Narrow and wide layouts.

Then deploy the app to staging and test with live structuredContent in the LLM platform.

Publish the customization

  1. Commit and push the EDS changes.
  2. If you changed the data shape, commit and push the matching handler changes.
  3. Deploy the app to staging.
  4. Test the action and widget in ChatGPT.
  5. Promote the verified version to production.

Other EDS setups

If you did not build the app automatically or want to integrate an existing EDS site, see Bring your own EDS project.

recommendation-more-help
llm-apps-help-main-toc