๐ Events: Using __event_emitter__ and __event_call__ in Open WebUI
Open WebUI's plugin architecture is not just about processing input and producing outputโit's about real-time, interactive communication with the UI and users. To make your Tools, Functions, and Pipes more dynamic, Open WebUI provides a built-in event system via the __event_emitter__ and __event_call__ helpers.
This guide explains what events are, how you can trigger them from your code, and the full catalog of event types you can use (including much more than just "input").
๐ What Are Events?โ
Events are real-time notifications or interactive requests sent from your backend code (Tool, or Function) to the web UI. They allow you to update the chat, display notifications, request confirmation, run UI flows, and more.
- Events are sent using the
__event_emitter__helper for one-way updates, or__event_call__when you need user input or a response (e.g., confirmation, input, etc.).
Metaphor: Think of Events like push notifications and modal dialogs that your plugin can trigger, making the chat experience richer and more interactive.
๐ Availabilityโ
Native Python Tools & Functionsโ
Events are fully available for native Python Tools and Functions defined directly in Open WebUI using the __event_emitter__ and __event_call__ helpers.
External Tools (OpenAPI & MCP)โ
External tools can emit events via a dedicated REST endpoint. Open WebUI passes the following headers to all external tool requests when ENABLE_FORWARD_USER_INFO_HEADERS=True is set:
| Header | Description |
|---|---|
X-Open-WebUI-Chat-Id | The chat ID where the tool was invoked |
X-Open-WebUI-Message-Id | The message ID associated with the tool call |
Your external tool can use these headers to emit events back to the UI via:
POST /api/v1/chats/{chat_id}/messages/{message_id}/event
See External Tool Events below for details.
๐งฐ Basic Usageโ
Sending an Eventโ
You can trigger an event anywhere inside your Tool, or Function by calling:
await __event_emitter__(
{
"type": "status", # See the event types list below
"data": {
"description": "Processing started!",
"done": False,
"hidden": False,
},
}
)You do not need to manually add fields like chat_id or message_idโthese are handled automatically by Open WebUI.
Interactive Eventsโ
When you need to pause execution until the user responds (e.g., confirm/cancel dialogs, code execution, or input), use __event_call__:
result = await __event_call__(
{
"type": "input", # Or "confirmation", "execute"
"data": {
"title": "Please enter your password",
"message": "Password is required for this action",
"placeholder": "Your password here",
},
}
)
# result will contain the user's input valueBy default, __event_call__ waits up to 300 seconds (5 minutes) for a user response before timing out with an exception. This timeout is configurable via the WEBSOCKET_EVENT_CALLER_TIMEOUT environment variable. Increase this value if your users need more time to fill out forms, make decisions, or complete complex interactions.
๐ Event Payload Structureโ
When you emit or call an event, the basic structure is:
{
"type": "event_type", // See full list below
"data": { ... } // Event-specific payload
}Most of the time, you only set "type" and "data". Open WebUI fills in the routing automatically.
๐ Full List of Event Typesโ
Below is a comprehensive table of all supported type values for events, along with their intended effect and data structure. (This is based on up-to-date analysis of Open WebUI event handling logic.)
| type | When to use | Data payload structure (examples) |
|---|---|---|
status | Show a status update/history for a message | {description: ..., done: bool, hidden: bool} |
chat:completion | Provide a chat completion result | (Custom, see Open WebUI internals) |
chat:message:delta,message | Append content to the current message | {content: "text to append"} |
chat:message,replace | Replace current message content completely | {content: "replacement text"} |
chat:message:files,files | Set or overwrite message files (for uploads, output) | {files: [...]} |
chat:title | Set (or update) the chat conversation title | Topic string OR {title: ...} |
chat:tags | Update the set of tags for a chat | Tag array or object |
source,citation | Add a source/citation, or code execution result | For code: See below. |
notification | Show a notification ("toast") in the UI | {type: "info" or "success" or "error" or "warning", content: "..."} |
confirmation (needs __event_call__) | Ask for confirmation (OK/Cancel dialog) | {title: "...", message: "..."} |
input (needs __event_call__) | Request simple user input ("input box" dialog) | {title: "...", message: "...", placeholder: "...", value: ..., type: "password"} (type is optional) |
execute ( __event_call__ or __event_emitter__) | Run JavaScript in the user's browser. Use __event_call__ to get a return value, or __event_emitter__ for fire-and-forget | {code: "...javascript code..."} |
chat:message:favorite | Update the favorite/pin status of a message | {"favorite": bool} |
Other/Advanced types:
- You can define your own types and handle them at the UI layer (or use upcoming event-extension mechanisms).
โ Details on Specific Event Typesโ
statusโ
Show a status/progress update in the UI:
await __event_emitter__(
{
"type": "status",
"data": {
"description": "Step 1/3: Fetching data...",
"done": False,
"hidden": False,
},
}
)The done Fieldโ
The done field controls the shimmer animation on the status text in the UI:
done value | Visual effect |
|---|---|
false (or omitted) | Status text has a shimmer/loading animation โ indicates ongoing processing |
true | Status text appears static โ indicates the step is complete |
The backend does not inspect done at all โ it simply saves the value and forwards it to the frontend. The shimmer effect is purely a frontend visual cue.
done: TrueIf you emit status events, always send at least one with done: True at the end of your status sequence. Without it, the last status item keeps its shimmer animation indefinitely, making it look like processing never finished โ even after the response is complete.
# โ
Correct pattern
await __event_emitter__({"type": "status", "data": {"description": "Fetching data...", "done": False}})
# ... do work ...
await __event_emitter__({"type": "status", "data": {"description": "Complete!", "done": True}})
# โ ๏ธ Broken pattern โ shimmer never stops
await __event_emitter__({"type": "status", "data": {"description": "Fetching data...", "done": False}})
# ... do work, return result, but never sent done: TrueThe hidden Fieldโ
When hidden is true, the status is saved to statusHistory but not shown in the current status display. This is useful for internal status tracking that shouldn't be visible to the user.
Additionally, when message.content is empty and the last status has hidden: true (or no status exists at all), the frontend shows a skeleton loader instead of the status bar โ so hidden statuses don't replace the loading indicator.
chat:message:delta or messageโ
Streaming output (append text):
await __event_emitter__(
{
"type": "chat:message:delta", # or simply "message"
"data": {
"content": "Partial text, "
},
}
)
# Later, as you generate more:
await __event_emitter__(
{
"type": "chat:message:delta",
"data": {
"content": "next chunk of response."
},
}
)chat:message or replaceโ
Set (or replace) the entire message content:
await __event_emitter__(
{
"type": "chat:message", # or "replace"
"data": {
"content": "Final, complete response."
},
}
)files or chat:message:filesโ
Attach or update files:
await __event_emitter__(
{
"type": "files", # or "chat:message:files"
"data": {
"files": [
# Open WebUI File Objects
]
},
}
)embeds or chat:message:embedsโ
Attach Rich UI iframes to the assistant message. Each entry in the embeds list is a string that is fed directly to the embed iframe's src/srcdoc:
- A
http://,https://, or//-prefixed value is loaded as a URL. - Anything else is treated as raw HTML and rendered inline (Open WebUI auto-detects the two cases).
The full payload shape is:
await __event_emitter__(
{
"type": "embeds", # short name; persists to the DB
"data": {
"embeds": ["<html>โฆ</html>", "https://example.com/widget"],
"replace": False, # optional, default False
},
}
)Append vs. replaceโ
By default the new entries are appended to whatever is already on the message โ repeated emits stack up. For a single live widget you want to update in place, set data.replace: True; the entire embeds array on the message is overwritten with the payload's array. (Pass an empty list with replace: True to clear all embeds for the message.)
| Mode | Behavior | Typical use |
|---|---|---|
replace omitted / False | Each new payload's items are appended to message.embeds. | One-shot tools / actions that emit a single embed at the end. |
replace: True | The message's embeds array is replaced with the payload's array. | Long-running pipes that keep refreshing one widget (progress bar, live dashboard, polling status card). |
The replace flag was added so that re-emitting an embed doesn't leave stale copies stacked in the conversation when the user reloads โ only the latest version is persisted.
Example: append a single embed at the end of a toolโ
async def get_weather_dashboard(self, city: str, __event_emitter__) -> str:
html = build_dashboard_html(city) # your renderer
await __event_emitter__(
{
"type": "embeds",
"data": {"embeds": [html]},
}
)
return f"Rendered weather dashboard for {city}."This is equivalent to returning an HTMLResponse from a tool โ the Rich UI Embedding doc covers that path and is the recommended option whenever a single embed is enough.
Example: replace a live progress widget from a pipeโ
async def pipe(self, body: dict, __event_emitter__):
for step in range(1, 6):
progress_html = f"""
<div style="font-family:system-ui;padding:1rem;">
<h3>Indexing your library</h3>
<progress value="{step}" max="5" style="width:100%;"></progress>
<p>Step {step} of 5โฆ</p>
</div>
"""
await __event_emitter__(
{
"type": "embeds",
"data": {
"embeds": [progress_html],
"replace": True, # overwrite, don't stack
},
}
)
await asyncio.sleep(2)
# Final state โ also replaces, leaving exactly one embed on the message
await __event_emitter__(
{
"type": "embeds",
"data": {"embeds": ["<div>โ
Library indexed.</div>"], "replace": True},
}
)
yield "Indexing complete."When the user reloads the chat, message.embeds contains exactly one entry โ the most recent one โ instead of five stacked progress cards.
Example: clear all embedsโ
await __event_emitter__(
{
"type": "embeds",
"data": {"embeds": [], "replace": True},
}
)