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, read the EDS developer tutorial and Exploring blocks before customizing a widget. These resources cover blocks, the decorate function, and the EDS project structure.

After successful generation, the platform provides an EDS widget linked to the action in LLM Apps. The widget renders representative data for preview and reads the handler result when connected to a supported LLM platform. Review the generated design and behavior before customizing the widget.

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

In this guide: Learn how to find the generated widget and adapt it to your action’s data and your brand’s design. Then preview your changes locally and test them in the deployed app.

Find the generated widget

Open the EDS repository connected during infrastructure setup. If you use Adobe managed infrastructure, request code access before continuing. Each generated widget is an EDS block:

blocks/
└── <action-slug>/
    ├── <action-slug>.js
    └── <action-slug>.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 slug uses hyphens: an action with code identifier search_products uses a block such as blocks/search-products/. The generated widget page is typically /eds-widgets/search-products-demo; use the actual Widget URL in the action metadata.

The platform also configures the widget URLs and supporting SDK files. You do not need to create a second EDS project or re-enter the widget URLs 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. Its entry points in the generated EDS repository are:

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.
}

The embed fetches the widget page’s .plain.html content and imports its blocks. You do not import the SDK in the block. The embed automatically provides bridge; normal EDS page loading does not supply it. The bridge 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 ${records.length} products.` }],
  structuredContent: { arguments: { query: 'coffee' }, records }
};
// EDS block
const PREVIEW_RESULT = {
  records: [{ id: 'P-100', name: 'Frescopa House Blend' }]
};

export default async function decorate(block, bridge) {
  const live = bridge?.isConnected && bridge.hostContext?.preview !== true;
  const result = live ? await bridge.toolResult : null;
  const data = live ? result?.structuredContent : PREVIEW_RESULT;
  const records = Array.isArray(data?.records) ? data.records : [];
  // Render records or an error state when result?.isError is true.
}

In this example, PREVIEW_RESULT is the block’s local sample-data object. Current generated widgets use structuredContent.arguments and structuredContent.records; inspect your generated files for the exact shape. Never replace an empty live result with preview records.

bridge.toolResult resolves with the first result only. Use bridge.onToolResult(callback) if a widget remains mounted and must handle later results.

When you change structuredContent, update the handler and widget together. Do not assume that structured data is hidden from the model; it is part of the host’s action result. 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.disabled = !bridge?.isConnected;
  const status = document.createElement('p');
  status.setAttribute('role', 'status');
  button.addEventListener('click', async () => {
    if (bridge?.isConnected && typeof product.id === 'string') {
      try {
        await bridge.sendMessage(`Show me details for product ${product.id}`);
      } catch {
        status.textContent = 'Unable to send the request. Try again.';
      }
    }
  });

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

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

Use the host bridge

The embed passes a bridge to decorate(block, bridge). Guard interactive calls with bridge?.isConnected so the block also renders during direct EDS preview. A bridge object can exist in standalone mode without being connected; awaiting toolResult in standalone mode never resolves. The LLM Apps preview also sets bridge.hostContext.preview to let the block choose sample data.

Apply host styles

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

The method applies available host CSS variables and font styles. Applying host styles is optional: generated widgets can use their own design tokens instead. The embed maintains .light and .dark classes on its shadow body. Use bridge.hostContext.theme and bridge.onContextChange(callback) when your block also needs to react to theme changes.

Send a follow-up message

if (bridge?.isConnected) {
  await bridge.sendMessage('Show me similar products.');
}

Use sendMessage to continue the conversation after an interaction.

Call an action directly

if (bridge?.isConnected) {
  const result = await bridge.callTool('search_products', { query: 'coffee' });
  // Re-render this widget from result.structuredContent, or show result.isError.
}

Use callTool for an explicit interaction that needs an action result. Pass the actual target action’s code identifier, not its block folder name or display name; search_products is an example. The host must support action calls, and the target action must allow app visibility. Calling an action does not automatically re-render the current widget. Pass only validated values and handle rejected calls without exposing internal details.

If the target action’s visibility needs to change, edit only that action under Action details, save the change, and redeploy to the environment you are testing.

Keep the widget size synchronized

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

To let the host respond to content changes, call autoResize after the initial render. The method returns a cleanup function that stops observation when the widget is removed.

Preview your changes

Generated blocks include sample data for direct preview when the bridge is missing, disconnected, or marked as preview. Keep the sample-data fallback separate from live empty and error states.

To preview the EDS project locally:

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

Open the path from the action’s Widget URL on the local server, for example http://localhost:3000/eds-widgets/search-products-demo, rather than only the site root. 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.

Direct EDS preview does not test the host handshake or live action data. Publish the EDS changes and test with live structuredContent in the supported LLM platform.

Publish the customization

  1. Commit and push the EDS changes.
  2. Verify that the widget page and changed scripts and styles are available at the configured EDS URLs. Publish content changes through your EDS publishing workflow.
  3. If you changed the handler or action metadata, push those changes and deploy the app to Stage.
  4. Test the action and widget in a supported LLM platform. See Test in ChatGPT or Test in Claude.
  5. After testing succeeds, deploy handler and metadata changes to Production as needed.

EDS publication is separate from app deployment. If Stage and Production use the same EDS URLs, widget changes at those URLs affect both environments; deploying to Stage does not create an isolated widget copy.

Other EDS setups

If you created actions yourself or want to integrate a compatible existing EDS site, see Bring your own EDS project.

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