π 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
]
},
}
)chat:titleβ
Update the chat's title:
await __event_emitter__(
{
"type": "chat:title",
"data": {
"title": "Market Analysis Bot Session"
},
}
)chat:tagsβ
Update the chat's tags:
await __event_emitter__(
{
"type": "chat:tags",
"data": {
"tags": ["finance", "AI", "daily-report"]
},
}
)source or citation (and code execution)β
Add a reference/citation:
await __event_emitter__(
{
"type": "source", # or "citation"
"data": {
# Open WebUI Source (Citation) Object
}
}
)For code execution (track execution state):
await __event_emitter__(
{
"type": "source",
"data": {
# Open WebUI Code Source (Citation) Object
}
}
)notificationβ
Show a toast notification:
await __event_emitter__(
{
"type": "notification",
"data": {
"type": "info", # "success", "warning", "error"
"content": "The operation completed successfully!"
}
}
)chat:message:favoriteβ
Update the favorite/pin status of a message:
await __event_emitter__(
{
"type": "chat:message:favorite",
"data": {
"favorite": True # or False to unpin
}
}
)What this does exactly:
This event forces the Open WebUI frontend to update the "favorite" state of a message in its local cache. Without this emitter, if an Action Function modifies the message.favorite field in the database directly, the frontend (which maintains its own state) might overwrite your change during its next auto-save cycle. This emitter ensures the UI and database stay perfectly in sync.
While this event can technically be emitted from any plugin type (tools, pipes, filters), it is designed for and meaningful in Actions. Actions operate on existing messages and can modify the database directly. From a pipe or tool, emitting this event would update the frontend state temporarily, but unless the plugin also wrote to the database, the change would be lost on the next chat auto-save.
Where it appears:
- Message Toolbar: When set to
True, the "Heart" icon beneath the message will fill in, indicating it is favorited. - Chat Overview: Favorited messages (pins) are highlighted in the conversation overview, making it easier for users to locate key information later.
Example: "Pin Message" Actionβ
For a practical implementation of this event in a real-world plugin, see the Pin Message Action on Open WebUI Community. This action demonstrates how to toggle the favorite status in the database and immediately sync the UI using the chat:message:favorite event.
confirmation (requires __event_call__)β
Show a confirm dialog and get user response:
result = await __event_call__(
{
"type": "confirmation",
"data": {
"title": "Are you sure?",
"message": "Do you really want to proceed?"
}
}
)
if result: # or check result contents
await __event_emitter__({
"type": "notification",
"data": {"type": "success", "content": "User confirmed operation."}
})
else:
await __event_emitter__({
"type": "notification",
"data": {"type": "warning", "content": "User cancelled."}
})input (requires __event_call__)β
Prompt user for text input:
result = await __event_call__(
{
"type": "input",
"data": {
"title": "Enter your name",
"message": "We need your name to proceed.",
"placeholder": "Your full name"
}
}
)
user_input = result
await __event_emitter__(
{
"type": "notification",
"data": {"type": "info", "content": f"You entered: {user_input}"}
}
)Masked / Password Inputβ
To hide sensitive input (e.g., API keys, passwords), set type to "password" in the data payload. The input field will be rendered as a masked password input with a show/hide toggle:
result = await __event_call__(
{
"type": "input",
"data": {
"title": "Enter API Key",
"message": "Your API key is required for this integration.",
"placeholder": "sk-...",
"type": "password"
}
}
)This uses the same SensitiveInput component used for user valve password fields, providing a familiar "eye" icon toggle for showing/hiding the value.
execute (works with both __event_call__ and __event_emitter__)β
Run JavaScript directly in the user's browser.
Unlike confirmation and input, the execute event works with both helpers:
| Helper | Behavior | Use when |
|---|---|---|
__event_call__ | Runs JS and waits for the return value (two-way) | You need the result back in Python (e.g., reading localStorage, detecting browser state) |
__event_emitter__ | Runs JS fire-and-forget (one-way) | You don't need the result (e.g., triggering a file download, manipulating the DOM) |