Development
Writing A Custom Toolkit
Toolkits are defined in a single Python file, with a top level docstring with metadata and a Tools class.
Tool methods should generally be defined as async to ensure compatibility with future Open WebUI versions. The backend is progressively moving toward fully async execution, and synchronous functions may block execution or cause issues in future releases.
Example Top-Level Docstring
"""
title: String Inverse
author: Your Name
author_url: https://website.com
git_url: https://github.com/username/string-reverse.git
description: This tool calculates the inverse of a string
required_open_webui_version: 0.4.0
requirements: langchain-openai, langgraph, ollama, langchain_ollama
version: 0.4.0
licence: MIT
"""
Tools Class
Tools have to be defined as methods within a class called Tools, with optional subclasses called Valves and UserValves, for example:
class Tools:
def __init__(self):
"""Initialize the Tool."""
self.valves = self.Valves()
class Valves(BaseModel):
api_key: str = Field("", description="Your API key here")
def reverse_string(self, string: str) -> str:
"""
Reverses the input string.
:param string: The string to reverse
"""
# example usage of valves
if self.valves.api_key != "42":
return "Wrong API key"
return string[::-1]
Type Hints
Each tool must have type hints for arguments. The types may also be nested, such as queries_and_docs: list[tuple[str, int]]. Those type hints are used to generate the JSON schema that is sent to the model. Tools without type hints will work with a lot less consistency.
Valves and UserValves - (optional, but HIGHLY encouraged)
Valves and UserValves are used for specifying customizable settings of the Tool, you can read more on the dedicated Valves & UserValves page.
Optional Arguments
Below is a list of optional arguments your tools can depend on:
__event_emitter__: Emit events (see following section)__event_call__: Same as event emitter but can be used for user interactions__user__: A dictionary with user information. It also contains theUserValvesobject in__user__["valves"].__metadata__: Dictionary with chat metadata__messages__: List of previous messages__files__: Attached files__model__: A dictionary with model information__oauth_token__: A dictionary containing the user's valid, automatically refreshed OAuth token payload. This is the new, recommended, and secure way to access user tokens for making authenticated API calls. The dictionary typically containsaccess_token,id_token, and other provider-specific data.
For more information about __oauth_token__ and how to configure this token to be sent to tools, check out the OAuth section in the environment variable docs page and the SSO documentation.
Just add them as argument to any method of your Tool class just like __user__ in the example above.
Using the OAuth Token in a Tool
When building tools that need to interact with external APIs on the user's behalf, you can now directly access their OAuth token. This removes the need for fragile cookie scraping and ensures the token is always valid.
Example: A tool that calls an external API using the user's access token.
import httpx
from typing import Optional
class Tools:
# ... other class setup ...
async def get_user_profile_from_external_api(self, __oauth_token__: Optional[dict] = None) -> str:
"""
Fetches user profile data from a secure external API using their OAuth access token.
:param __oauth_token__: Injected by Open WebUI, contains the user's token data.
"""
if not __oauth_token__ or "access_token" not in __oauth_token__:
return "Error: User is not authenticated via OAuth or token is unavailable."
access_token = __oauth_token__["access_token"]
headers = {
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json"
}
try:
async with httpx.AsyncClient() as client:
response = await client.get("https://api.my-service.com/v1/profile", headers=headers)
response.raise_for_status() # Raise an exception for bad status codes
return f"API Response: {response.json()}"
except httpx.HTTPStatusError as e:
return f"Error: Failed to fetch data from API. Status: {e.response.status_code}"
except Exception as e:
return f"An unexpected error occurred: {e}"
Event Emitters
Event Emitters are used to add additional information to the chat interface. Similarly to Filter Outlets, Event Emitters are capable of appending content to the chat. Unlike Filter Outlets, they are not capable of stripping information. Additionally, emitters can be activated at any stage during the Tool.
⚠️ CRITICAL: Function Calling Mode Compatibility
Event Emitter behavior is significantly different depending on your function calling mode. The function calling mode is controlled by the function_calling parameter:
- Default Mode: Uses traditional function calling approach with wider model compatibility
- Native Mode (Agentic Mode): Leverages model's built-in tool-calling capabilities for reduced latency and autonomous behavior
Before using event emitters, you must understand these critical limitations:
- Default Mode (
function_calling = "default"): Full event emitter support with all event types working as expected - Native Mode (Agentic Mode) (
function_calling = "native"): Limited event emitter support - many event types don't work properly due to native function calling bypassing Open WebUI's custom tool processing pipeline
When to Use Each Mode: For a comprehensive guide on choosing a function calling mode, including model requirements and administrator setup, refer to the Central Tool Calling Guide.
In general:
- Use Default Mode when you need full event emitter functionality, complex tool interactions, or real-time UI updates.
- Use Native Mode (Agentic Mode) when you have a quality model and need reduced latency, autonomous tool selection, and system-level tools (Agentic Research, Knowledge Base exploration, Memory) without complex custom emitter requirements.
Function Calling Mode Configuration
You can configure the function calling mode in two places:
- Administrator Level: Go to Admin Panel > Settings > Models > Model Specific Settings > Advanced Parameters > Function Calling (set to "Default" or "Native").
- Per-request basis: Set
params.function_calling = "native"or"default"in Chat Controls > Advanced Params.
If the model seems to be unable to call the tool, make sure it is enabled (either via the Model page or via the + sign next to the chat input field).
When writing custom tools, be aware that Open WebUI also provides built-in system tools when Native Mode is enabled. For details on built-in tools, function calling modes, and model requirements, see the Tool Calling Modes Guide.
Complete Event Type Compatibility Matrix
Here's the comprehensive breakdown of how each event type behaves across function calling modes:
| Event Type | Default Mode Functionality | Native Mode Functionality | Status |
|---|---|---|---|
status | ✅ Full support - Updates status history during tool execution | ✅ Identical - Tracks function execution status | COMPATIBLE |
message | ✅ Full support - Appends incremental content during streaming | ❌ BROKEN - Gets overwritten by native completion snapshots | INCOMPATIBLE |
chat:completion | ✅ Full support - Handles streaming responses and completion data | ⚠️ LIMITED - Carries function results but may overwrite tool updates | PARTIALLY COMPATIBLE |
chat:message:delta | ✅ Full support - Streams delta content during execution | ❌ BROKEN - Content gets replaced by native function snapshots | INCOMPATIBLE |
chat:message | ✅ Full support - Replaces entire message content cleanly | ❌ BROKEN - Gets overwritten by subsequent native completions | INCOMPATIBLE |
replace | ✅ Full support - Replaces content with precise control | ❌ BROKEN - Replaced content gets overwritten immediately | INCOMPATIBLE |
chat:message:files / files | ✅ Full support - Handles file attachments in messages | ✅ Identical - Processes files from function outputs | COMPATIBLE |
chat:message:error | ✅ Full support - Displays error notifications | ✅ Identical - Shows function call errors | COMPATIBLE |
chat:message:follow_ups | ✅ Full support - Shows follow-up suggestions | ✅ Identical - Displays function-generated follow-ups | COMPATIBLE |
chat:title | ✅ Full support - Updates chat title dynamically | ✅ Identical - Updates title based on function interactions | COMPATIBLE |
chat:tags | ✅ Full support - Modifies chat tags | ✅ Identical - Manages tags from function outputs | COMPATIBLE |
chat:tasks:cancel | ✅ Full support - Cancels ongoing tasks | ✅ Identical - Cancels native function executions | COMPATIBLE |
citation / source | ✅ Full support - Handles citations with full metadata | ✅ Identical - Processes function-generated citations | COMPATIBLE |
notification | ✅ Full support - Shows toast notifications | ✅ Identical - Displays function execution notifications | COMPATIBLE |
confirmation | ✅ Full support - Requests user confirmations | ✅ Identical - Confirms function executions | COMPATIBLE |
execute | ✅ Full support - Executes code dynamically | ✅ Identical - Runs function-generated code | COMPATIBLE |
input | ✅ Full support - Requests user input with full UI | ✅ Identical - Collects input for functions | COMPATIBLE |
Why Native Mode Breaks Certain Event Types
In Native Mode, the server constructs content blocks from streaming model output and repeatedly emits "chat:completion" events with full serialized content snapshots. The client treats these snapshots as authoritative and completely replaces message content, effectively overwriting any prior tool-emitted updates like message, chat:message, or replace events.
Technical Details:
middleware.pyadds tools directly to form data for native model handling- Streaming handler emits repeated content snapshots via
chat:completionevents - Client's
chatCompletionEventHandlertreats snapshots as complete replacements:message.content = content - This causes tool-emitted content updates to flicker and disappear
Best Practices and Recommendations
For Tools Requiring Real-time UI Updates:
class Tools:
def __init__(self):
# Add a note about function calling mode requirements
self.description = "This tool requires Default function calling mode for full functionality"
async def interactive_tool(self, prompt: str, __event_emitter__=None) -> str:
"""
⚠️ This tool requires function_calling = "default" for proper event emission
"""
if not __event_emitter__:
return "Event emitter not available - ensure Default function calling mode is enabled"
# Safe to use message events in Default mode
await __event_emitter__({
"type": "message",
"data": {"content": "Processing step 1..."}
})
# ... rest of tool logic
For Tools That Must Work in Both Modes:
async def universal_tool(self, prompt: str, __event_emitter__=None, __metadata__=None) -> str:
"""
Tool designed to work in both Default and Native function calling modes
"""
# Check if we're in native mode (this is a rough heuristic)
is_native_mode = __metadata__ and __metadata__.get("params", {}).get("function_calling") == "native"
if __event_emitter__:
if is_native_mode:
# Use only compatible event types in native mode
await __event_emitter__({
"type": "status",
"data": {"description": "Processing in native mode...", "done": False}
})
else:
# Full event functionality in default mode
await __event_emitter__({
"type": "message",
"data": {"content": "Processing with full event support..."}
})
# ... tool logic here
if __event_emitter__:
await __event_emitter__({
"type": "status",
"data": {"description": "Completed successfully", "done": True}
})
return "Tool execution completed"
Troubleshooting Event Emitter Issues
Symptoms of Native Mode Conflicts:
- Tool-emitted messages appear briefly then disappear
- Content flickers during tool execution
messageorreplaceevents seem to be ignored- Status updates work but content updates don't persist
Solutions:
- Switch to Default Mode: Change
function_callingfrom"native"to"default"in model settings - Use Compatible Event Types: Stick to
status,citation,notification, and other compatible event types in native mode - Implement Mode Detection: Add logic to detect function calling mode and adjust event usage accordingly
- Consider Hybrid Approaches: Use compatible events for core functionality and degrade gracefully
Debugging Your Event Emitters:
async def debug_events_tool(self, __event_emitter__=None, __metadata__=None) -> str:
"""Debug tool to test event emitter functionality"""
if not __event_emitter__:
return "No event emitter available"
# Test various event types
test_events = [
{"type": "status", "data": {"description": "Testing status events", "done": False}},
{"type": "message", "data": {"content": "Testing message events (may not work in native mode)"}},
{"type": "notification", "data": {"content": "Testing notification events"}},
]
mode_info = "Unknown"
if __metadata__:
mode_info = __metadata__.get("params", {}).get("function_calling", "default")
await __event_emitter__({
"type": "status",
"data": {"description": f"Function calling mode: {mode_info}", "done": False}
})
for i, event in enumerate(test_events):
await asyncio.sleep(1) # Space out events
await __event_emitter__(event)
await __event_emitter__({
"type": "status",
"data": {"description": f"Sent event {i+1}/{len(test_events)}", "done": False}
})
await __event_emitter__({
"type": "status",
"data": {"description": "Event testing complete", "done": True}
})
return f"Event testing completed in {mode_info} mode. Check for missing or flickering content."
There are several specific event types with different behaviors:
Status Events ✅ FULLY COMPATIBLE
Status events work identically in both Default and Native function calling modes. This is the most reliable event type for providing real-time feedback during tool execution.
Status events add live status updates to a message while it's performing steps. These can be emitted at any stage during tool execution. Status messages appear right above the message content and are essential for tools that delay the LLM response or process large amounts of information.
Basic Status Event Structure:
await __event_emitter__({
"type": "status",
"data": {
"description": "Message that shows up in the chat",
"done": False, # False = still processing, True = completed
"hidden": False # False = visible, True = auto-hide when done
}
})
Status Event Parameters:
description: The status message text shown to usersdone: Boolean indicating if this status represents completionhidden: Boolean to auto-hide the status oncedone: Trueis set
Basic Status Example
Advanced Status with Error Handling
Multi-Step Progress Status
Message Events ⚠️ DEFAULT MODE ONLY
🚨 CRITICAL WARNING: Message events are INCOMPATIBLE with Native function calling mode!
Message events (message, chat:message, chat:message:delta, replace) allow you to append or modify message content at any stage during tool execution. This enables embedding images, rendering web pages, streaming content updates, and creating rich interactive experiences.
However, these event types have major compatibility issues:
- ✅ Default Mode: Full functionality - content persists and displays properly
- ❌ Native Mode: BROKEN - content gets overwritten by completion snapshots and disappears
Why Message Events Break in Native Mode:
Native function calling emits repeated chat:completion events with full content snapshots that completely replace message content, causing any tool-emitted message updates to flicker and disappear.
Safe Message Event Structure (Default Mode Only):
await __event_emitter__({
"type": "message", # Also: "chat:message", "chat:message:delta", "replace"
"data": {"content": "This content will be appended/replaced in the chat"},
# Note: message types do NOT require a "done" condition
})
Message Event Types:
message/chat:message:delta: Appends content to existing messagechat:message/replace: Replaces entire message content- Both types will be overwritten in Native mode
Safe Message Streaming (Default Mode)
Dynamic Content Replacement (Default Mode)
Mode-Safe Message Tool
Citations ✅ FULLY COMPATIBLE
Citation events work identically in both Default and Native function calling modes. This event type provides source references and citations in the chat interface, allowing users to click and view source materials.
Citations are essential for tools that retrieve information from external sources, databases, or documents. They provide transparency and allow users to verify information sources.
Citation Event Structure:
await __event_emitter__({
"type": "citation",
"data": {
"document": [content], # Array of content strings
"metadata": [ # Array of metadata objects
{
"date_accessed": datetime.now().isoformat(),
"source": title,
"author": "Author Name", # Optional
"publication_date": "2024-01-01", # Optional
"url": "https://source-url.com" # Optional
}
],
"source": {"name": title, "url": url} # Primary source info
}
})
Important Citation Setup:
When implementing custom citations, you must disable automatic citations in your Tools class:
def __init__(self):
self.citation = False # REQUIRED - prevents automatic citations from overriding custom ones
⚠️ Critical Citation Warning:
If you set self.citation = True (or don't set it to False), automatic citations will replace any custom citations you send. Always disable automatic citations when using custom citation events.
Basic Citation Example
Advanced Multi-Source Citations
Database Citation Tool
Additional Compatible Event Types ✅
The following event types work identically in both Default and Native function calling modes:
Notification Events
await __event_emitter__({
"type": "notification",
"data": {"content": "Toast notification message"}
})
File Events
await __event_emitter__({
"type": "files", # or "chat:message:files"
"data": {"files": [{"name": "report.pdf", "url": "/files/report.pdf"}]}
})
Follow-up Events
await __event_emitter__({
"type": "chat:message:follow_ups",
"data": {"follow_ups": ["What about X?", "Tell me more about Y"]}
})
Title Update Events
await __event_emitter__({
"type": "chat:title",
"data": {"title": "New Chat Title"}
})
Tag Events
await __event_emitter__({
"type": "chat:tags",
"data": {"tags": ["research", "analysis", "completed"]}
})
Error Events
await __event_emitter__({
"type": "chat:message:error",
"data": {"content": "Error message to display"}
})
Confirmation Events
await __event_emitter__({
"type": "confirmation",
"data": {"message": "Are you sure you want to continue?"}
})
Input Request Events
await __event_emitter__({
"type": "input",
"data": {"prompt": "Please enter additional information:"}
})
Code Execution Events
await __event_emitter__({
"type": "execute",
"data": {"code": "print('Hello from tool-generated code!')"}
})
Comprehensive Function Calling Mode Guide
Choosing the right function calling mode is crucial for your tool's functionality. This guide helps you make an informed decision based on your specific requirements.
Mode Comparison Overview:
| Aspect | Default Mode | Native Mode |
|---|---|---|
| Latency | Higher - processes through Open WebUI pipeline | Lower - direct model handling |
| Event Support | ✅ Full - all event types work perfectly | ⚠️ Limited - many event types broken |
| Complexity | Handles complex tool interactions well | Best for simple tool calls |
| Compatibility | Works with all models | Requires models with native tool calling |
| Streaming | Perfect for real-time updates | Poor - content gets overwritten |
| Citations | ✅ Full support | ✅ Full support |
| Status Updates | ✅ Full support | ✅ Full support |
| Message Events | ✅ Full support | ❌ Broken - content disappears |
Decision Framework:
-
Do you need real-time content streaming, live updates, or dynamic message modification?
- Yes → Use Default Mode (Native mode will break these features)
- No → Either mode works
-
Is your tool primarily for simple data retrieval or computation?
- Yes → Native Mode is fine (lower latency)
- No → Consider Default Mode for complex interactions
-
Do you need maximum performance and minimal latency?
- Yes → Native Mode (if compatible with your features)
- No → Default Mode provides more features
-
Are you building interactive experiences, dashboards, or multi-step workflows?
- Yes → Default Mode required
- No → Either mode works
Recommended Usage Patterns:
🏆 Best Practices for Mode Selection
🔧 Debugging Event Emitter Issues
📚 Event Emitter Quick Reference
Rich UI Element Embedding
Tools and Actions can return HTML content that renders as interactive iframes directly in the chat. For full documentation, examples, security considerations, and CORS configuration, see the dedicated Rich UI Embedding guide.
External packages
In the Tools definition metadata you can specify custom packages. When you click Save the line will be parsed and pip install will be run on all requirements at once.
🚨 CRITICAL WARNING: Potential for Package Version Conflicts
When multiple tools define different versions of the same package (e.g., Tool A requires pandas==1.5.0 and Tool B requires pandas==2.0.0), Open WebUI installs them in a non-deterministic order. This can lead to unpredictable behavior and break one or more of your tools.
The only robust solution to this problem is to use an OpenAPI tool server.
We strongly recommend using an OpenAPI tool server to avoid these dependency conflicts.
Do not rely on runtime pip installation in production environments. When running with UVICORN_WORKERS > 1 or multiple replicas, each worker/replica attempts to install packages independently on startup. This causes race conditions where concurrent pip processes crash with AssertionError because pip's internal locking detects the simultaneous installs.
Set ENABLE_PIP_INSTALL_FRONTMATTER_REQUIREMENTS=False in production to disable runtime pip installs entirely. Then pre-install all required packages at image build time using a custom Dockerfile:
FROM ghcr.io/open-webui/open-webui:main
RUN pip install --no-cache-dir python-docx requests beautifulsoup4
Runtime installation is only suitable for single-worker development or homelab environments where you're experimenting with new functions and tools. For any deployment serving multiple users, bake your dependencies into the container image.
Keep in mind that pip runs in the same process as Open WebUI, so the UI will be completely unresponsive during installation.
No measures are taken to handle package conflicts with Open WebUI's own dependencies. Specifying requirements can break Open WebUI if you're not careful. You might be able to work around this by specifying open-webui itself as a requirement.