RobutlerRobutler
Agent

Widgets

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

Overview

The WebAgents widget system supports two distinct widget types:

TypeDescriptionCreationRenderingUse Cases
WebAgents WidgetsCustom HTML/JavaScript componentsCode with @widget decoratorSandboxed iframesInteractive UIs, media players, forms, visualizations
OpenAI ChatKit WidgetsWidgets from OpenAI's toolsOpenAI Widget Builder / Agent BuilderDirect React renderingStructured layouts, data displays, simple interactions
  • WebAgents HTML/JS Widgets
    Custom HTML/JavaScript components for maximum flexibility.
  • OpenAI ChatKit Widgets
    Created in OpenAI's Widget Builder, rendered by WebAgents.

Widget Decorator

The @widget decorator is used for both widget types. It accepts the following parameters:

  • name (optional): Override for widget name (defaults to function name)
  • description (optional): Widget description for LLM awareness (defaults to docstring)
  • template (optional): Path to Jinja2 template file (WebAgents only)
  • scope (optional): Access control - "all", "owner", "admin", or list of scopes
  • auto_escape (optional, default=True): Automatically HTML-escape string arguments (WebAgents only)

WebAgents Widgets

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

Basic Example

from webagents import Skill, widget
from webagents.server.context.context_vars import Context

class MusicPlayerSkill(Skill):
    @widget(
        name="play_music",
        description="Display an interactive music player for a given track",
        scope="all"
        # auto_escape=True by default - arguments are automatically escaped!
    )
    async def play_music(
        self,
        song_url: str,
        title: str,
        artist: str = "Unknown Artist",
        context: Context = None
    ) -> str:
        """Create an interactive music player widget
        
        All string arguments are automatically HTML-escaped for security.
        No need for manual html.escape() calls!
        """
        
        # Create HTML content - arguments are already escaped!
        html_content = f"""<!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="{song_url}" type="audio/mpeg">
        </audio>
        <button 
            onclick="sendMessage('Play next song')" 
            class="bg-blue-600 text-white px-4 py-2 rounded mt-4 w-full">
            Next Song
        </button>
    </div>
    <script>
        function sendMessage(text) {{
            window.parent.postMessage({{
                type: 'widget_message',
                content: text
            }}, '*');
        }}
    </script>
</body>
</html>"""
        
        # Return wrapped in <widget> tags with kind="webagents"
        return f'<widget kind="webagents" id="music_player">{html_content}</widget>'

Widget Format

WebAgents widgets must follow this format:

return f'<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:

from webagents import WidgetTemplateRenderer

renderer = WidgetTemplateRenderer(template_dir="widgets")

@widget(name="complex_widget", template="complex.html")
async def complex_widget(self, data: dict) -> str:
    html = renderer.render("complex.html", data)
    return f'<widget kind="webagents" id="complex">{html}</widget>'

For simple widgets, use inline HTML:

@widget
async def simple_widget(self, text: str) -> str:
    html = f"<div class='p-4'>{text}</div>"  # Auto-escaped
    return f'<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:

from webagents import WidgetTemplateRenderer

html = WidgetTemplateRenderer.inject_tailwind_cdn(my_html)

Advanced: Widget Data Attribute

The optional data attribute allows you to pass structured metadata. Use cases:

  • State restoration (playback position, volume, etc.)
  • Analytics and logging
  • Error recovery (fallback URLs)
  • Dynamic configuration

Backend - Adding Data:

import html
import json

@widget(name="music_player")
async def play_music(self, song_url: str, title: str, artist: str) -> str:
    html_content = f"""..."""  # Your widget HTML
    
    # Optional: Add metadata for state restoration/analytics
    widget_data = {
        'song_url': song_url,
        'title': title,
        'artist': artist,
        'timestamp': time.time()
    }
    
    # Escape JSON for HTML attribute (must use html.escape with quote=True)
    escaped_data = html.escape(json.dumps(widget_data), quote=True)
    
    return f'<widget kind="webagents" id="music_player" data="{escaped_data}">{html_content}</widget>'

Frontend - Accessing Data:

The widget receives data via postMessage (recommended) or iframe attribute (fallback).

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

Example:

@widget(name="stateful_player")
async def stateful_player(self, song_url: str, last_position: float = 0.0) -> str:
    widget_data = {
        'song_url': song_url,
        'last_position': last_position  # Resume from where user left off
    }
    
    html_content = f"""
    <audio id="audio" src="{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>
    """
    
    escaped_data = html.escape(json.dumps(widget_data), quote=True)
    return f'<widget id="player" data="{escaped_data}">{html_content}</widget>'

Security

Sandboxing

Widgets render in sandboxed iframes:

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

Security features:

  • Isolated execution - no access to 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:

  • Auto-escaping enabled by default (auto_escape=True)
  • No boilerplate needed - variables are automatically safe
  • Blob URL rendering prevents inline XSS
  • Escape hatch: auto_escape=False (use with caution)

Secure by default:

@widget(name="safe_widget")
async def safe_widget(self, user_input: str) -> str:
    return f'<widget><div>{user_input}</div></widget>'  # Auto-escaped

Communication

Widgets communicate with the chat interface via postMessage API:

window.parent.postMessage({
    type: 'widget_message',
    content: 'User message text'
}, '*');

How it works:

  1. Widget sends widget_message event
  2. Frontend validates message structure
  3. Content appended as 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.

How it works:

  1. Create widgets using OpenAI's Widget Builder
  2. Build agents with OpenAI's Agent Builder
  3. WebAgents renders the widget JSON structures from OpenAI's tools
  4. Full compatibility with all OpenAI ChatKit components

Current Status: WebAgents provides full rendering support for all OpenAI ChatKit components. See component support below.

Rendering OpenAI Widgets

WebAgents can render widgets created in OpenAI's tools by returning the widget JSON:

import json
from webagents import widget

@widget(name="openai_widget")
async def render_openai_widget(self, widget_json: str, context: Context = None) -> str:
    """Render a widget created in OpenAI's Widget Builder
    
    Args:
        widget_json: JSON structure from OpenAI's Widget Builder
    """
    # Widget JSON from OpenAI's tools can be passed through directly
    return f'<widget kind="openai">{widget_json}</widget>'

Example Widget Structure

If you need to generate widget JSON programmatically (compatible with OpenAI's format):

import json
from webagents import widget

@widget(name="info_card")
async def info_card(self, title: str, description: str) -> str:
    """Display an informational card (OpenAI ChatKit compatible)"""
    
    widget_structure = {
        "$kind": "card",
        "content": [
            {
                "$kind": "text",
                "content": title,
                "size": "lg",
                "weight": "bold"
            },
            {
                "$kind": "text",
                "content": description
            }
        ]
    }
    
    return f'<widget kind="openai">{json.dumps(widget_structure)}</widget>'

Widget Format

OpenAI ChatKit widgets use this format:

return f'<widget kind="openai">{widget_json}</widget>'

Required attributes:

  • kind="openai" - Identifies this as an OpenAI ChatKit widget

Sources:

Supported Components

WebAgents renders all components from OpenAI's Widget Builder:

CategoryComponentStatusDescription
LayoutCard✅ SupportedContainer with optional styling
Box✅ SupportedFlexible container
Row✅ SupportedHorizontal layout
Col✅ SupportedVertical layout
Spacer✅ SupportedFlexible space
Divider✅ SupportedVisual separator
TypographyText✅ SupportedText content with formatting
Caption✅ SupportedSmall text captions
Title✅ SupportedLarge heading text
Label✅ SupportedLabel text
Markdown✅ SupportedMarkdown content
ContentImage✅ SupportedDisplay images
Icon✅ SupportedDisplay icons (emoji/unicode)
Chart✅ SupportedBar and line chart visualizations
Badge✅ SupportedStatus badges with variants
ControlsButton✅ SupportedInteractive buttons
DatePicker✅ SupportedDate selection input
Select✅ SupportedDropdown selection
Checkbox✅ SupportedCheckbox input
RadioGroup✅ SupportedRadio button group
Form✅ SupportedForm container
OtherTransition✅ SupportedAnimated transitions

Create Widgets: Use OpenAI's Widget Builder to create widgets visually, then WebAgents will render them. For detailed component specifications, see the OpenAI Widget Builder and OpenAI Agent Builder.

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 User-Agent is from a browser
  • Check <widget> tags are properly formatted
  • Ensure kind attribute is correct

postMessage Not Working

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

Styling Issues

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

Debug Logging

Backend:

self.logger.debug(f"🎨 Widget registered: {widget_name}")

Frontend:

console.log('[SkillWidget] Message from widget:', message);

API Reference

Decorator

@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

BaseAgent Methods

def get_all_widgets(self) -> List[Dict[str, Any]]
def register_widget(self, widget_func: Callable, source: str = "manual", scope: Union[str, List[str]] = None)
def _is_browser_request(self, context=None) -> bool
  • Tools - Simple tool-based interactions
  • Handoffs - Agent delegation
  • Skills - Skill development

On this page