Robutler

Widgets

Widgets are interactive components that can be rendered in the chat interface, providing rich user experiences beyond text and images.

TypeScript: Coming soon. The @widget decorator and WidgetTemplateRenderer ship in the Python SDK only. In the TypeScript SDK, you can return widget-formatted HTML strings from a regular @tool, and chat clients that recognize the <widget> envelope will render them. Track parity in the Python ↔ TypeScript Parity Matrix.

Overview

The WebAgents widget system supports two distinct widget types:

TypeDescriptionCreationRenderingUse Cases
WebAgents WidgetsCustom HTML/JavaScript componentsCode with @widget decorator (Python) or tool returning <widget kind="webagents"> HTML (both)Sandboxed iframesInteractive UIs, media players, forms, visualizations
OpenAI ChatKit WidgetsWidgets from OpenAI's toolsOpenAI Widget Builder / Agent BuilderDirect React renderingStructured layouts, data displays, simple interactions

Widget Decorator

The @widget decorator (Python) accepts:

  • name (optional) — override widget name (defaults to function name)
  • description (optional) — widget description for LLM awareness (defaults to docstring)
  • template (optional) — path to Jinja2 template file (WebAgents widgets only)
  • scope (optional) — access control: "all", "owner", "admin", or list of scopes
  • auto_escape (optional, default True) — automatically HTML-escape string arguments

WebAgents Widgets

WebAgents widgets are custom HTML/JavaScript components rendered in sandboxed iframes. They provide maximum flexibility for interactive user interfaces.

Basic Example

// @widget is Python-only today. The TypeScript equivalent is a regular @tool
// that returns a string wrapped in <widget kind="webagents"> tags. Chat clients
// that support widget rendering will pick it up automatically.

import { Skill, tool } from 'webagents';

class MusicPlayerSkill extends Skill {
  readonly name = 'music-player';

  @tool({
    description: 'Display an interactive music player for a given track',
    scopes: ['all'],
  })
  async playMusic(params: { song_url: string; title: string; artist?: string }): Promise<string> {
    const escape = (s: string) => s.replace(/[&<>"']/g, (c) => (
      { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]!
    ));
    const title = escape(params.title);
    const artist = escape(params.artist ?? 'Unknown Artist');
    const songUrl = escape(params.song_url);

    const html = `<!DOCTYPE html>
<html>
<head><script src="https://cdn.tailwindcss.com"></script></head>
<body class="bg-gray-900 p-4">
  <div class="max-w-md mx-auto bg-gray-800 rounded-lg p-6">
    <h2 class="text-white text-xl font-bold">${title}</h2>
    <p class="text-gray-400">${artist}</p>
    <audio controls class="w-full mt-4"><source src="${songUrl}" type="audio/mpeg"></audio>
    <button onclick="window.parent.postMessage({type:'widget_message',content:'Play next song'},'*')"
            class="bg-blue-600 text-white px-4 py-2 rounded mt-4 w-full">Next Song</button>
  </div>
</body>
</html>`;

    return `<widget kind="webagents" id="music_player">${html}</widget>`;
  }
}

Widget Format

WebAgents widgets must follow this format:

<widget kind="webagents" id="<widget_id>">{html_content}</widget>

Required attributes:

  • kind="webagents" — identifies this as a WebAgents widget.
  • id — unique identifier for the widget.

Optional attributes:

  • data — JSON metadata for state restoration (see Advanced Usage).

Template Rendering

For complex widgets, use Jinja2 templates (Python) or template literals (TypeScript):

import { Skill, tool } from 'webagents';

class ComplexWidgetSkill extends Skill {
  readonly name = 'complex-widget';

  @tool({ description: 'Render a complex widget' })
  async complexWidget(params: { data: Record<string, string> }): Promise<string> {
    const html = renderTemplate('complex.html', params.data);
    return `<widget kind="webagents" id="complex">${html}</widget>`;
  }
}

function renderTemplate(_name: string, _data: Record<string, string>): string {
  return '<div>complex widget</div>';
}

For simple widgets, return inline HTML:

@tool({ description: 'Render a simple widget' })
async simpleWidget(params: { text: string }): Promise<string> {
  const escape = (s: string) => s.replace(/[&<>"']/g, (c) => (
    { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]!
  ));
  const html = `<div class='p-4'>${escape(params.text)}</div>`;
  return `<widget kind="webagents" id="simple">${html}</widget>`;
}

Styling

WebAgents widgets should use Tailwind CSS. Include the CDN in your HTML:

<script src="https://cdn.tailwindcss.com"></script>

Or use the helper method (Python):

from webagents import WidgetTemplateRenderer

html = WidgetTemplateRenderer.inject_tailwind_cdn(my_html)

Advanced: Widget data attribute

The optional data attribute carries structured metadata for state restoration, analytics, error recovery, or dynamic configuration.

Backend — adding data:

@tool({ description: 'Stateful music player' })
async statefulPlayer(params: { song_url: string; last_position?: number }): Promise<string> {
  const widgetData = {
    song_url: params.song_url,
    last_position: params.last_position ?? 0,
  };

  const html = `<audio id="audio" src="${params.song_url}"></audio>
<script>
  window.addEventListener('message', (e) => {
    if (e.data.type === 'widget_init') {
      const audio = document.getElementById('audio');
      audio.currentTime = e.data.data.last_position || 0;
      audio.play();
    }
  });
</script>`;

  const escapeAttr = (s: string) => s.replace(/[&<>"]/g, (c) => (
    { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c]!
  ));
  const escapedData = escapeAttr(JSON.stringify(widgetData));
  return `<widget kind="webagents" id="player" data="${escapedData}">${html}</widget>`;
}

Frontend — accessing data:

window.addEventListener('message', (event) => {
  if (event.data.type === 'widget_init') {
    const widgetData = event.data.data;
    // Use widgetData for state restoration, analytics, etc.
  }
});

Security

Sandboxing

Widgets render in sandboxed iframes:

<iframe sandbox="allow-scripts allow-same-origin" />

Security features:

  • Isolated execution — no access to the parent window.
  • No cookies/storage access.
  • Blob URLs — content served from memory.
  • Script execution allowed for interactivity.
  • Same-origin policy for styling and APIs.

XSS Prevention

Widgets are secure by default with automatic HTML escaping (Python auto_escape=True).

In TypeScript, escape user-provided strings yourself before interpolating into HTML — there is no auto_escape runtime hook today.

Communication

Widgets communicate with the chat interface via the postMessage API:

window.parent.postMessage({
  type: 'widget_message',
  content: 'User message text',
}, '*');
  1. Widget sends a widget_message event.
  2. Frontend validates the message structure.
  3. Content is appended as a user message.
  4. Agent processes the new message.

Browser Detection

WebAgents widgets are only available to browser clients. The system detects browsers via User-Agent headers (Mozilla, Chrome, Safari, Firefox, Edge). OpenAI ChatKit widgets may work in more contexts since they don't require iframe support.

OpenAI ChatKit Widgets

ChatKit widgets are created using OpenAI's Widget Builder and Agent Builder. WebAgents provides full rendering support for widgets created in these tools.

import { Skill, tool } from 'webagents';

class OpenAIWidgetSkill extends Skill {
  readonly name = 'openai-widgets';

  @tool({ description: "Render a widget created in OpenAI's Widget Builder" })
  async renderOpenAIWidget(params: { widget_json: string }): Promise<string> {
    return `<widget kind="openai">${params.widget_json}</widget>`;
  }
}

Generating Widget JSON

@tool({ description: 'Display an informational card (OpenAI ChatKit compatible)' })
async infoCard(params: { title: string; description: string }): Promise<string> {
  const widgetStructure = {
    $kind: 'card',
    content: [
      { $kind: 'text', content: params.title, size: 'lg', weight: 'bold' },
      { $kind: 'text', content: params.description },
    ],
  };
  return `<widget kind="openai">${JSON.stringify(widgetStructure)}</widget>`;
}

Widget Format

OpenAI ChatKit widgets use this format:

<widget kind="openai">{widget_json}</widget>

Sources:

Supported Components

WebAgents renders all components from OpenAI's Widget Builder:

CategoryComponentStatusDescription
LayoutCardSupportedContainer with optional styling
BoxSupportedFlexible container
RowSupportedHorizontal layout
ColSupportedVertical layout
SpacerSupportedFlexible space
DividerSupportedVisual separator
TypographyTextSupportedText content with formatting
CaptionSupportedSmall text captions
TitleSupportedLarge heading text
LabelSupportedLabel text
MarkdownSupportedMarkdown content
ContentImageSupportedDisplay images
IconSupportedDisplay icons (emoji/unicode)
ChartSupportedBar and line chart visualizations
BadgeSupportedStatus badges with variants
ControlsButtonSupportedInteractive buttons
DatePickerSupportedDate selection input
SelectSupportedDropdown selection
CheckboxSupportedCheckbox input
RadioGroupSupportedRadio button group
FormSupportedForm container
OtherTransitionSupportedAnimated transitions

Choosing Between Widget Types

ChooseWhen You Need
WebAgents WidgetsCustom HTML/JS interactivity, complex layouts, full control over rendering
OpenAI ChatKit WidgetsVisual widget creation with OpenAI's tools, standard UI patterns, compatibility with OpenAI agents
ToolsData fetching/processing, no UI, text-based output

Troubleshooting

Widget Not Appearing

  • Verify the User-Agent is from a browser.
  • Check that <widget> tags are properly formatted.
  • Ensure the kind attribute is correct.

postMessage Not Working

  • Verify type: 'widget_message' is set.
  • Check that the iframe sandbox allows allow-scripts.
  • Ensure the target is '*' for blob URLs.

Styling Issues

  • Confirm the Tailwind CDN is included.
  • Check for conflicting styles.
  • Test with colorScheme: 'normal' on the iframe.

API Reference (Python)

@widget(
    name: Optional[str] = None,
    description: Optional[str] = None,
    template: Optional[str] = None,
    scope: Union[str, List[str]] = "all",
    auto_escape: bool = True,
)

WidgetTemplateRenderer — Jinja2 template renderer for WebAgents HTML widgets.

class WidgetTemplateRenderer:
    def __init__(self, template_dir: Optional[str] = None): ...
    def render(self, template_name: str, context: Dict[str, Any]) -> str: ...
    def render_inline(self, html_string: str, context: Dict[str, Any]) -> str: ...

    @staticmethod
    def escape_data(data: Any) -> str: ...

    @staticmethod
    def inject_tailwind_cdn(html_content: str) -> str: ...
  • Tools — Simple tool-based interactions
  • Handoffs — Agent delegation
  • Skills — Skill development

On this page