---
sidebar_position: 3
title: "Events"
---

# 🔔 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](#-external-tool-events) below for details.

---

## 🧰 Basic Usage

### Sending an Event

You can trigger an event anywhere inside your Tool, or Function by calling:

```python
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__`:

```python
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 value
```

:::tip Configurable Timeout
By 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`](/reference/env-configuration#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:

```json
{
  "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`,<br/>`message`          | Append content to the current message                | `{content: "text to append"}`                                                                        |
| `chat:message`,<br/>`replace`                | Replace current message content completely           | `{content: "replacement text"}`                                                                      |
| `chat:message:files`,<br/>`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`,<br/>`citation`                     | Add a source/citation, or code execution result      | For code: See [below.](/features/extensibility/plugin/development/events#source-or-citation-and-code-execution) |
| `notification`                               | Show a notification ("toast") in the UI              | `{type: "info" or "success" or "error" or "warning", content: "..."}`                                |
| `confirmation` <br/>(needs `__event_call__`) | Ask for confirmation (OK/Cancel dialog)              | `{title: "...", message: "..."}`                                                                     |
| `input` <br/>(needs `__event_call__`)        | Request simple user input ("input box" dialog)       | `{title: "...", message: "...", placeholder: "...", value: ..., type: "password"}` (type is optional) |
| `execute` <br/>(`__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:

```python
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.

:::warning Always Emit a Final `done: True`
If 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.

```python
# ✅ 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: True
```
:::

#### The `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):

```python
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:**

```python
await __event_emitter__(
    {
        "type": "chat:message",  # or "replace"
        "data": {
            "content": "Final, complete response."
        },
    }
)
```

---

### `files` or `chat:message:files`

**Attach or update files:**

```python
await __event_emitter__(
    {
        "type": "files",  # or "chat:message:files"
        "data": {
            "files": [
               # Open WebUI File Objects
            ]
        },
    }
)
```

---

### `chat:title`

**Update the chat's title:**

```python
await __event_emitter__(
    {
        "type": "chat:title",
        "data": {
            "title": "Market Analysis Bot Session"
        },
    }
)
```

---

### `chat:tags`

**Update the chat's tags:**

```python
await __event_emitter__(
    {
        "type": "chat:tags",
        "data": {
            "tags": ["finance", "AI", "daily-report"]
        },
    }
)
```

---

### `source` or `citation` (and code execution)

**Add a reference/citation:**

```python
await __event_emitter__(
    {
        "type": "source",  # or "citation"
        "data": {
            # Open WebUI Source (Citation) Object
        }
    }
)
```

**For code execution (track execution state):**

```python
await __event_emitter__(
    {
        "type": "source",
        "data": {
            # Open WebUI Code Source (Citation) Object
        }
    }
)
```

---

### `notification`

**Show a toast notification:**

```python
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:**

```python
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.

:::note Designed for Actions
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](https://openwebui.com/posts/pin_message_action_143594d1)**. 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:**

```python
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:**

```python
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:

```python
result = await __event_call__(
    {
        "type": "input",
        "data": {
            "title": "Enter API Key",
            "message": "Your API key is required for this integration.",
            "placeholder": "sk-...",
            "type": "password"
        }
    }
)
```

:::tip
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) |

#### Two-way example (with `__event_call__`)

```python
result = await __event_call__(
    {
        "type": "execute",
        "data": {
            "code": "return document.title;",
        }
    }
)

await __event_emitter__(
    {
        "type": "notification",
        "data": {
            "type": "info",
            "content": f"Page title: {result}"
        }
    }
)
```

#### Fire-and-forget example (with `__event_emitter__`)

```python
# Trigger a blob download — no return value needed
try:
    await __event_emitter__(
        {
            "type": "execute",
            "data": {
                "code": """
                    (function() {
                        const blob = new Blob([data], {type: 'application/octet-stream'});
                        const url = URL.createObjectURL(blob);
                        const a = document.createElement('a');
                        a.href = url;
                        a.download = 'file.bin';
                        document.body.appendChild(a);
                        a.click();
                        URL.revokeObjectURL(url);
                        a.remove();
                    })();
                """
            }
        }
    )
except Exception:
    pass
```

:::tip iOS PWA compatibility
On iOS Safari (especially in PWA / standalone mode), using `__event_call__` for blob downloads can fail with a `"TypeError: Load failed"` error — the two-way response channel breaks when the browser processes the download. Using `__event_emitter__` (fire-and-forget) avoids this issue entirely, since no response channel is needed.

If your `execute` code triggers a file download and you don't need a return value, prefer `__event_emitter__` for maximum cross-platform compatibility.
:::

#### How It Works

The `execute` event runs JavaScript **directly in the main page context** using `new Function()`. This means:

- It runs with **full access** to the page's DOM, cookies, localStorage, and session
- It is **not sandboxed** — there are no iframe restrictions
- It can manipulate the Open WebUI interface directly (show/hide elements, read form data, trigger downloads)
- The code runs as an async function, so you can use `await` and `return` a value back to the backend (when using `__event_call__`)

:::tip Frontend Automation
Because `execute` runs in the main page context with full DOM access, you can use it to **automate virtually anything on the Open WebUI frontend**: click buttons, fill input fields, navigate between pages, read page state, trigger downloads, interact with the model selector, submit messages on behalf of the user, and more. Think of it as a remote control for the browser UI — if a user can do it manually, your function can do it programmatically via `execute`.
:::

#### Example: Display a Custom Form

```python
result = await __event_call__(
    {
        "type": "execute",
        "data": {
            "code": """
                return new Promise((resolve) => {
                    const overlay = document.createElement('div');
                    overlay.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center;z-index:9999';
                    overlay.innerHTML = `
                        <div style="background:white;padding:24px;border-radius:12px;min-width:300px">
                            <h3 style="margin:0 0 12px">Enter Details</h3>
                            <input id="exec-name" placeholder="Name" style="width:100%;padding:8px;margin:4px 0;border:1px solid #ccc;border-radius:6px"/>
                            <input id="exec-email" placeholder="Email" style="width:100%;padding:8px;margin:4px 0;border:1px solid #ccc;border-radius:6px"/>
                            <button id="exec-submit" style="margin-top:12px;padding:8px 16px;background:#333;color:white;border:none;border-radius:6px;cursor:pointer">Submit</button>
                        </div>
                    `;
                    document.body.appendChild(overlay);
                    document.getElementById('exec-submit').onclick = () => {
                        const name = document.getElementById('exec-name').value;
                        const email = document.getElementById('exec-email').value;
                        overlay.remove();
                        resolve({ name, email });
                    };
                });
            """
        }
    }
)
# result will be {"name": "...", "email": "..."}
```

#### Execute vs Rich UI Embeds

The `execute` event and [Rich UI Embeds](/features/extensibility/plugin/development/rich-ui) are complementary ways to create interactive experiences:

| | `execute` Event | Rich UI Embed |
|---|---|---|
| **Runs in** | Main page context (no sandbox) | Sandboxed iframe |
| **Persistence** | Ephemeral — gone on reload/navigate | Persistent — saved in chat history |
| **Page access** | Full (DOM, cookies, localStorage) | Isolated from parent by default |
| **Forms** | Always works (no sandbox) | Requires `allowForms` setting enabled |
| **Best for** | Transient interactions, side effects, downloads, DOM manipulation | Persistent visual content, dashboards, charts |

Use `execute` for transient interactions (confirmations, custom dialogs, triggering downloads, reading page state) and Rich UI embeds for persistent visual content you want to stay in the conversation.

:::warning
Because `execute` runs unsandboxed JavaScript in the user's browser session, it has full access to the Open WebUI page context. Only use this in trusted functions — never execute untrusted or user-provided code through this event.
:::

---

## 🏗️ When & Where to Use Events

- **From any Tool, or Function** in Open WebUI.
- To **stream responses**, show progress, request user data, update the UI, or display supplementary info/files.
- `await __event_emitter__` is for one-way messages (fire and forget).
- `await __event_call__` is for when you need a response from the user (input, confirmation) or a return value from client-side code (execute).
- The `execute` event is unique: it works with **both** helpers. Use `__event_call__` when you need the JS return value, or `__event_emitter__` for fire-and-forget execution (e.g., triggering downloads).

:::warning Pipes: Return Value vs Events
For Pipes, be careful about mixing content delivery methods. If your `pipe()` method **returns a string**, that string becomes the final message content. If it **yields** (generator), the yielded chunks are streamed. If you also emit `chat:message:delta` events during execution, both the return/yield content and the event-based content are processed and can conflict.

**Recommendation**: Use return/yield as your primary content delivery mechanism. Events like `status`, `source`, `files`, and `notification` work well alongside return/yield, but avoid using `chat:message:delta` or `chat:message` events as your **sole** way to deliver message content from a pipe.

**Why event-only content delivery is fragile for pipes**: When a pipe completes, the frontend saves the entire chat history (including all message content from its local state) to the database. This full-history save can **overwrite** content that was previously persisted by the backend event emitter. If the pipe returns `None` or an empty string and relies solely on `type: "message"` events for content, the final save may write empty content to the database — erasing what the event emitter had written.

```python
# ❌ Fragile: relies only on events for content — can be overwritten on save
async def pipe(self, body: dict, __event_emitter__=None):
    await __event_emitter__({"type": "message", "data": {"content": "Hello!"}})
    # Returns None — frontend may save empty content, overwriting the emitted content

# ✅ Correct: return content directly, use events for supplementary data
async def pipe(self, body: dict, __event_emitter__=None):
    await __event_emitter__({"type": "status", "data": {"description": "Working...", "done": False}})
    result = "Hello!"
    await __event_emitter__({"type": "status", "data": {"description": "Done", "done": True}})
    return result

# ✅ Also correct: yield for streaming, use events for supplementary data
async def pipe(self, body: dict, __event_emitter__=None):
    await __event_emitter__({"type": "status", "data": {"description": "Streaming...", "done": False}})
    for chunk in ["Hello", ", ", "world", "!"]:
        yield chunk
    await __event_emitter__({"type": "status", "data": {"description": "Done", "done": True}})
```
:::

---

## 💡 Tips & Advanced Notes

- **Multiple types per message:** You can emit several events of different types for one message—for example, show `status` updates, then stream with `chat:message:delta`, then complete with a `chat:message`.
- **Custom event types:** While the above list is the standard, you may use your own types and detect/handle them in custom UI code.
- **Extensibility:** The event system is designed to evolve—always check the [Open WebUI documentation](https://github.com/open-webui/open-webui) for the most current list and advanced usage.

---

## 🧐 FAQ

### Q: How do I trigger a notification for the user?
Use `notification` type:
```python
await __event_emitter__({
    "type": "notification",
    "data": {"type": "success", "content": "Task complete"}
})
```

### Q: How do I prompt the user for input and get their answer?
Use:
```python
response = await __event_call__({
    "type": "input",
    "data": {
        "title": "What's your name?",
        "message": "Please enter your preferred name:",
        "placeholder": "Name"
    }
})

# response will be: {"value": "user's answer"}
```

### Q: What event types are available for `__event_call__`?
- `"input"`: Input box dialog
- `"confirmation"`: Yes/No, OK/Cancel dialog
- `"execute"`: Run provided code on client and return result (also works with `__event_emitter__` for fire-and-forget — see [execute](#execute-works-with-both-__event_call__-and-__event_emitter__) above)

### Q: Can I update files attached to a message?
Yes—use the `"files"` or `"chat:message:files"` event type with a `{files: [...]}` payload.

### Q: Can I update the conversation title or tags?
Absolutely: use `"chat:title"` or `"chat:tags"` accordingly.

### Q: Can I stream responses (partial tokens) to the user?
Yes—emit `"chat:message:delta"` events in a loop, then finish with `"chat:message"`.

---

## 🌐 External Tool Events

External tools (OpenAPI and MCP servers) can emit events to the Open WebUI UI via a REST endpoint. This enables features like status updates, notifications, and streaming content from tools running on external servers.

### Prerequisites

To receive the chat and message ID headers, you must enable header forwarding by setting the following environment variable on your Open WebUI instance:

```
ENABLE_FORWARD_USER_INFO_HEADERS=True
```

Without this, Open WebUI will not include the identification headers in requests to external tools, and event emitting will not work.

### Headers Provided by Open WebUI

When Open WebUI calls your external tool (with header forwarding enabled), it includes these headers:

| Header | Description | Env Var Override |
|--------|-------------|------------------|
| `X-Open-WebUI-Chat-Id` | The chat ID where the tool was invoked | `FORWARD_SESSION_INFO_HEADER_CHAT_ID` |
| `X-Open-WebUI-Message-Id` | The message ID associated with the tool call | `FORWARD_SESSION_INFO_HEADER_MESSAGE_ID` |

### Event Endpoint

**Endpoint:** `POST /api/v1/chats/{chat_id}/messages/{message_id}/event`

**Authentication:** Requires a valid Open WebUI API key or session token.

**Request Body:**

```json
{
  "type": "status",
  "data": {
    "description": "Processing your request...",
    "done": false
  }
}
```

### Supported Event Types

External tools can emit the same event types as native tools:
- `status` – Show progress/status updates
- `notification` – Display toast notifications
- `chat:message:delta` / `message` – Append content to the message
- `chat:message` / `replace` – Replace message content
- `files` / `chat:message:files` – Attach files
- `source` / `citation` – Add citations

:::note
Interactive events (`input`, `confirmation`) require `__event_call__` and are **not supported** for external tools as they need bidirectional WebSocket communication. `execute` via `__event_call__` is similarly unsupported for external tools; however, fire-and-forget `execute` via `__event_emitter__` does not require a return channel and may work depending on your setup.
:::

### Example: Python External Tool

```python
import httpx

def my_tool_handler(request):
    # Extract headers from incoming request
    chat_id = request.headers.get("X-Open-WebUI-Chat-Id")
    message_id = request.headers.get("X-Open-WebUI-Message-Id")
    api_key = "your-open-webui-api-key"
    
    # Emit a status event
    httpx.post(
        f"http://your-open-webui-host/api/v1/chats/{chat_id}/messages/{message_id}/event",
        headers={"Authorization": f"Bearer {api_key}"},
        json={
            "type": "status",
            "data": {"description": "Working on it...", "done": False}
        }
    )
    
    # ... do work ...
    
    # Emit completion status
    httpx.post(
        f"http://your-open-webui-host/api/v1/chats/{chat_id}/messages/{message_id}/event",
        headers={"Authorization": f"Bearer {api_key}"},
        json={
            "type": "status",
            "data": {"description": "Complete!", "done": True}
        }
    )
    
    return {"result": "success"}
```

### Example: JavaScript/Node.js External Tool

```javascript
async function myToolHandler(req) {
  const chatId = req.headers['x-open-webui-chat-id'];
  const messageId = req.headers['x-open-webui-message-id'];
  const apiKey = 'your-open-webui-api-key';
  
  // Emit a notification
  await fetch(
    `http://your-open-webui-host/api/v1/chats/${chatId}/messages/${messageId}/event`,
    {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${apiKey}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        type: 'notification',
        data: { type: 'info', content: 'Tool is processing...' }
      })
    }
  );
  
  return { result: 'success' };
}
```

---

## 🔒 Persistence & Browser Disconnection

A common question is: **what happens if the browser tab is closed while a tool, action, or pipe is still running?**

### Server-Side Execution Continues

When you send a chat request, Open WebUI creates a background `asyncio` task that is **not tied to your HTTP connection or Socket.IO session**. If you close the tab:

1. The WebSocket disconnects and the Socket.IO disconnect handler fires
2. The disconnect handler cleans up session data but **does not cancel any running tasks**
3. The background task continues running to completion on the server
4. `sio.emit()` calls succeed silently — events are sent to an empty room and discarded
5. **Database writes still happen** for persisted event types (see below)
6. The task runs until the function returns, raises an error, or is manually cancelled

:::info No Execution Timeout
There is **no timeout** on pipe, tool, or action execution. Your code can run for minutes or hours — nothing in Open WebUI will kill it automatically. The only things that can stop a running task are:
- The function itself returning or raising an exception
- Manual cancellation via `POST /api/tasks/stop/{task_id}` (the stop button in the UI)
- The Open WebUI server process restarting
:::

### Which Event Types Are Persisted to the Database?

The event emitter writes certain event types directly to the database **regardless of whether a browser is connected**. These writes are independent of the `ENABLE_REALTIME_CHAT_SAVE` setting.

#### ✅ Persisted (survive tab close)

| Type | What's saved |
|------|-------------|
| `status` | Appended to the message's `statusHistory` array |
| `message` | Appended to the message's `content` field |
| `replace` | Overwrites the message's `content` field |
| `embeds` | Appended to the message's `embeds` array (Rich UI HTML) |
| `files` | Appended to the message's `files` array |
| `source` / `citation` | Appended to the message's `sources` array |

These 6 types always write to the database inside the event emitter function itself, completely independent of `ENABLE_REALTIME_CHAT_SAVE`.

:::warning Use Short Names for Persistence
The backend event emitter only recognizes the short names above for DB writes. If you emit `"chat:message:embeds"` instead of `"embeds"`, the frontend handles it identically, but the **backend won't persist it**. Always use the short names (`"status"`, `"message"`, `"replace"`, `"embeds"`, `"files"`, `"source"`) if you need persistence.
:::

:::caution Pipes: Backend Persistence Can Be Overwritten
For **Pipes specifically**, the backend-persisted content from `"message"` and `"replace"` events can be **overwritten** by the frontend after the pipe completes. When a pipe's `pipe()` method returns, the frontend saves the entire local chat history to the database. If the pipe returned `None` or empty content and relied solely on `"message"` events, the frontend's local state may still have empty content for the assistant message — causing it to overwrite the event-emitter-written content with an empty string.

This does **not** affect Tools, Actions, or Filters, where events supplement the return value rather than replace it. It also does not affect `"status"`, `"files"`, `"source"`, or `"embeds"` events, which update separate fields that aren't overwritten by the content save.

**Bottom line for Pipes**: Use return/yield for message content. Use events for status updates, sources, files, embeds, and notifications.
:::

#### ❌ Not persisted (lost on tab close)

| Type | Why it's lost |
|------|--------------|
| `chat:completion` | Streaming LLM deltas — Socket.IO only |
| `chat:message:delta` | Frontend alias, backend doesn't persist |
| `chat:message` | Frontend alias, backend doesn't persist |
| `chat:message:files` | Frontend alias, backend doesn't persist |
| `chat:message:embeds` | Frontend alias, backend doesn't persist |
| `chat:message:error` | Socket.IO only |
| `chat:message:follow_ups` | Socket.IO only |
| `chat:message:favorite` | Socket.IO only (updates frontend state) |
| `chat:title` | Socket.IO only |
| `chat:tags` | Socket.IO only |
| `notification` | Toast popup — Socket.IO only |

:::tip Alternative for Streaming LLM Output
If your pipe or tool needs to call an LLM and have the result persist even when the browser is closed, you can import and use `generate_chat_completion` from Open WebUI's internals instead of emitting `chat:completion` events. The completion flows through the normal chat pipeline and its result is saved to the database like any other assistant message.
:::

#### ⚠️ Requires live connection (will error on tab close)

| Type | Why |
|------|-----|
| `confirmation` | Uses `sio.call()` — waits for client response, will timeout |
| `input` | Uses `sio.call()` — waits for client response, will timeout |
| `execute` via `__event_call__` | Uses `sio.call()` — waits for client response, will timeout |
| `execute` via `__event_emitter__` | Fires and forgets — **will not error**, but JS may not run if no browser is connected |

`confirmation` and `input` fundamentally require a live browser connection via `__event_call__`. If the tab is closed, `sio.call()` will timeout and raise an exception in your function code. The timeout is configurable via the [`WEBSOCKET_EVENT_CALLER_TIMEOUT`](/reference/env-configuration#websocket_event_caller_timeout) environment variable (default: 300 seconds).

`execute` is more flexible: when used via `__event_emitter__`, it fires without waiting for a response, so it won't error on tab close (though the JS won't execute if no browser is listening). This makes `__event_emitter__` the safer choice for `execute` calls where you don't need the return value — particularly for file downloads on iOS PWA, where the two-way channel can fail with `"TypeError: Load failed"`.

### Return Value Persistence

The final return value of your function is **always saved to the database** when the task completes, regardless of browser state.

#### Pipes

When a pipe's `pipe()` method returns (or its generator finishes yielding), the streaming handler saves the final result at completion:
- If `ENABLE_REALTIME_CHAT_SAVE` is **on**: intermediate chunks are saved during streaming
- If `ENABLE_REALTIME_CHAT_SAVE` is **off**: the full final content is saved in one write at completion

Either way, the final assistant message is always persisted. When you reopen the chat, it will be there.

:::caution
The return value takes precedence over event-emitted content. If your pipe emits `"message"` events but returns `None`, the saved content will be empty — the frontend's final save overwrites whatever the event emitter wrote to the database. Always return or yield your content directly from the `pipe()` method.
:::

#### Tools

| Return type | What happens | Persisted? |
|-------------|-------------|-----------|
| `HTMLResponse` (with `Content-Disposition: inline`) | HTML body extracted → added to `embeds` → emitted as `"embeds"` event | ✅ Yes |
| `HTMLResponse` (without inline) | Body decoded as plain text tool result | ✅ Yes |
| `str` / `dict` | Used as tool result text | ✅ Yes |
| `list` (MCP) | Text items joined, images converted to files | ✅ Yes |

#### Actions

Actions go through the same return handling as tools. The same persistence rules apply:

| Return type | What happens | Persisted? |
|-------------|-------------|-----------|
| `HTMLResponse` (with `Content-Disposition: inline`) | HTML body extracted → added to `embeds` → emitted as `"embeds"` event | ✅ Yes |
| `HTMLResponse` (without inline) | Body decoded as plain text result | ✅ Yes |
| `str` / `dict` | Used as action result text | ✅ Yes |

#### Filters

Filters transform `form_data` in the pipeline — they don't return results to the user directly. However, filters **do receive `__event_emitter__`** and can emit persisted event types like `"status"`, `"embeds"`, `"message"`, etc.

### Function Type Capabilities Matrix

| Capability | Tools | Actions | Pipes | Filters |
|-----------|-------|---------|-------|---------|
| `__event_emitter__` | ✅ | ✅ | ✅ | ✅ |
| `__event_call__` | ✅ | ✅ | ✅ | ✅ |
| Return value → user response | ✅ | ✅ | ✅ | ❌ (modifies `form_data`) |
| `HTMLResponse` → Rich UI embed | ✅ | ✅ | ❌ | ❌ |

### Practical Summary

If you want your function's output to **survive a closed browser tab**, follow these rules:

1. **Always return your final answer** from your `pipe()`, tool, or action function — the return value is always saved
2. **Use short event type names** (`"status"`, `"message"`, `"embeds"`, `"files"`, `"source"`) for DB persistence
3. **Avoid relying on** `"notification"`, `"confirmation"`, `"input"`, or `"execute"` for critical workflows — these require a live browser connection
4. Rich UI HTML embeds (`"embeds"` type or `HTMLResponse` return) **are persisted** and will render when the user reopens the chat

---

## 📝 Conclusion

**Events** give you real-time, interactive superpowers inside Open WebUI. They let your code update content, trigger notifications, request user input, stream results, handle code, and much more—seamlessly plugging your backend intelligence into the chat UI.

- Use `__event_emitter__` for one-way status/content updates.
- Use `__event_call__` for interactions that require user follow-up (input, confirmation, execution).

Refer to this document for common event types and structures, and explore Open WebUI source code or docs for breaking updates or custom events!

---

**Happy event-driven coding in Open WebUI! 🚀**
