OpenWebUI Plugin Development Guide¶
This guide consolidates official documentation, SDK details, and best practices to provide a systematic tutorial for developers, from beginner to expert.
đ Table of Contents¶
- Quick Start
- Project Structure & Naming
- Core Concepts & SDK Details
- Deep Dive into Plugin Types
- Advanced Development Patterns
- Best Practices & Design Principles
- Workflow & Process
- Troubleshooting
1. Quick Start¶
1.1 What are OpenWebUI Plugins?¶
OpenWebUI Plugins (officially called "Functions") are the primary way to extend the platform's capabilities. Running in a backend Python environment, they allow you to:
- Integrate New Models: Connect to Claude, Gemini, or custom RAGs via Pipes
- Enhance Interaction: Add buttons (e.g., "Export", "Generate Chart") next to messages via Actions
- Intervene in Processes: Modify data before requests or after responses via Filters
1.2 Your First Plugin (Hello World)¶
Save the following code as hello.py and upload it to the Functions panel in OpenWebUI:
"""
title: Hello World Action
author: Demo
version: 1.0.0
"""
from pydantic import BaseModel, Field
from typing import Optional
class Action:
class Valves(BaseModel):
greeting: str = Field(default="Hello", description="Greeting message")
def __init__(self):
self.valves = self.Valves()
async def action(
self,
body: dict,
__event_emitter__=None,
__user__=None
) -> Optional[dict]:
user_name = __user__.get("name", "Friend") if __user__ else "Friend"
if __event_emitter__:
await __event_emitter__({
"type": "notification",
"data": {"type": "success", "content": f"{self.valves.greeting}, {user_name}!"}
})
return body
2. Project Structure & Naming¶
2.1 Language & Code Requirements¶
- Single Code File:
plugins/{type}/{name}/{name}.py. Never create separate source files for different languages. - Built-in i18n: Must dynamically switch UI, prompts, and logs based on user language.
- Documentation: Must include both
README.md(English) andREADME_CN.md(Chinese).
2.2 Docstring Standard¶
Each plugin file must start with a standardized docstring:
"""
title: Plugin Name
author: Fu-Jie
author_url: https://github.com/Fu-Jie/openwebui-extensions
funding_url: https://github.com/open-webui
version: 0.1.0
icon_url: data:image/svg+xml;base64,<base64-encoded-svg>
requirements: dependency1==1.0.0, dependency2>=2.0.0
description: Brief description of plugin functionality.
"""
- icon_url: Required for Action plugins. Must be Base64 encoded SVG from Lucide Icons.
- requirements: Only list dependencies not installed in the OpenWebUI environment.
3. Core Concepts & SDK Details¶
3.1 â ī¸ Important: Sync vs Async¶
OpenWebUI plugins run within an asyncio event loop.
Critical
- Principle: All I/O operations (database, file, network) must be non-blocking
- Pitfall: Calling synchronous methods directly (e.g.,
time.sleep,requests.get) will freeze the entire server - Solution: Wrap synchronous calls using
await asyncio.to_thread(sync_func, ...)
3.2 Core Parameters¶
All plugin methods (inlet, outlet, pipe, action) support injecting the following special parameters:
| Parameter | Type | Description |
|---|---|---|
body | dict | Core Data. Contains request info like messages, model, stream |
__user__ | dict | Current User. Contains id, name, role, valves (user config), etc. |
__metadata__ | dict | Metadata. Contains chat_id, message_id. The variables field holds preset variables |
__request__ | Request | FastAPI Request Object. Access app.state for cross-plugin communication |
__event_emitter__ | func | One-way Notification. Used to send Toast notifications or status bar updates |
__event_call__ | func | Two-way Interaction. Used to execute JS code, show confirmation dialogs, or input boxes |
3.3 Configuration System (Valves)¶
Use Pydantic BaseModel to define configurable parameters. All Valves fields must use UPPER_SNAKE_CASE.
from pydantic import BaseModel, Field
class Action:
class Valves(BaseModel):
SHOW_STATUS: bool = Field(default=True, description="Whether to show operation status updates.")
# ...
3.4 Context Access¶
All plugins must use _get_user_context and _get_chat_context methods to safely extract information, rather than accessing __user__ or body directly.
3.5 Event Emission & Logging¶
- Event Emission: Implement helper methods
_emit_statusand_emit_notification. - Frontend Console Debugging: Highly recommended for real-time data flow viewing. Use
_emit_debug_logto print structured debug logs in the browser console. - Server-side Logging: Use Python's standard
loggingmodule. Do not useprint().
3.6 Database & File Storage¶
- Database: Re-use Open WebUI's internal database connection (
open_webui.internal.db). - File Storage: Implement multi-level fallback mechanisms (DB -> S3 -> Local -> URL -> API) to ensure compatibility across all storage configurations.
3.7 Internationalization (i18n)¶
Define a TRANSLATIONS dictionary and use a robust language detection mechanism (Multi-level Fallback: JS localStorage -> HTTP Accept-Language -> User Profile -> en-US).
4. Deep Dive into Plugin Types¶
4.1 Action¶
Role: Adds buttons below messages that trigger upon user click.
Advanced Usage: Execute JavaScript on Frontend¶
import base64
async def action(self, body, __event_call__):
# 1. Generate content on backend
content = "Hello OpenWebUI".encode()
b64 = base64.b64encode(content).decode()
# 2. Send JS to frontend for execution
js = f"""
const blob = new Blob([atob('{b64}')], {{type: 'text/plain'}});
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = 'hello.txt';
a.click();
"""
await __event_call__({"type": "execute", "data": {"code": js}})
4.2 Filter¶
Role: Middleware that intercepts and modifies requests/responses.
inlet: Before request. Used for injecting context, modifying model parametersoutlet: After response. Used for formatting output, loggingstream: During streaming. Used for real-time sensitive word filtering
Example: Injecting Environment Variables¶
async def inlet(self, body, __metadata__):
vars = __metadata__.get("variables", {})
context = f"Current Time: {vars.get('{{CURRENT_DATETIME}}')}"
# Inject into System Prompt or first message
if body.get("messages"):
body["messages"][0]["content"] += f"\n\n{context}"
return body
4.3 Pipe¶
Role: Custom Model/Agent.
Example: Simple OpenAI Wrapper¶
import requests
class Pipe:
def pipes(self):
return [{"id": "my-gpt", "name": "My GPT Wrapper"}]
def pipe(self, body):
# Modify body here, e.g., force add prompt
headers = {"Authorization": f"Bearer {self.valves.API_KEY}"}
r = requests.post(
"https://api.openai.com/v1/chat/completions",
json=body,
headers=headers,
stream=True
)
return r.iter_lines()
4.4 Copilot SDK Tool Definition Standards¶
When developing custom tools for GitHub Copilot SDK, you must define a Pydantic BaseModel for parameters and explicitly reference it using params_type in define_tool.
4.5 Copilot SDK Streaming & Tool Card Standards¶
- Reasoning Streaming: Must use native
<think>tags and ensure proper closure (\n</think>\n) before outputting main content or tool calls. - Native Tool Calls Block: Output strictly formatted HTML
<details type="tool_calls"...>blocks. Ensure all double quotes in attributes are escaped as".
5. Advanced Development Patterns¶
5.1 Pipe & Filter Collaboration¶
Use __request__.app.state to share data between plugins:
- Pipe:
__request__.app.state.search_results = [...] - Filter (Outlet): Read
search_resultsand format them as citation links
5.2 Async Background Tasks¶
Execute time-consuming operations without blocking the user response:
import asyncio
async def outlet(self, body, __metadata__):
asyncio.create_task(self.background_job(__metadata__["chat_id"]))
return body
async def background_job(self, chat_id):
# Execute time-consuming operation...
pass
5.3 Calling Built-in LLM¶
from open_webui.utils.chat import generate_chat_completion
from open_webui.models.users import Users
# Get user object
user_obj = Users.get_user_by_id(user_id)
# Build LLM request
llm_payload = {
"model": "model-id",
"messages": [
{"role": "system", "content": "System prompt"},
{"role": "user", "content": "User input"}
],
"temperature": 0.7,
"stream": False
}
# Call LLM
llm_response = await generate_chat_completion(
__request__, llm_payload, user_obj
)
5.4 JS Render to Markdown (Data URL Embedding)¶
For scenarios requiring complex frontend rendering (e.g., AntV charts, Mermaid diagrams) but wanting persistent pure Markdown output, use the Data URL embedding pattern:
Workflow¶
ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
â 1. Python Action â
â âââ Analyze message content â
â âââ Call LLM to generate structured data (optional) â
â âââ Send JS code to frontend via __event_call__ â
ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ¤
â 2. Browser JS (via __event_call__) â
â âââ Dynamically load visualization library â
â âââ Render SVG/Canvas offscreen â
â âââ Export to Base64 Data URL via toDataURL() â
â âââ Update message content via REST API â
ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ¤
â 3. Markdown Rendering â
â âââ Display  â
ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
5.5 Agent File Delivery Standards (3-Step Delivery Protocol)¶
- Write Local: Create files in the current execution directory (
.). - Publish: Call
publish_file_from_workspace(filename='name.ext'). - Display Link: Present the returned
download_urlas a Markdown link.
6. Best Practices & Design Principles¶
6.1 Naming & Positioning¶
- Short & Punchy: e.g., "FlashCard", "DeepRead". Avoid generic terms like "Text Analysis Assistant"
- Complementary: Don't reinvent the wheel; clarify what specific problem your plugin solves
6.2 User Experience (UX)¶
- Timely Feedback: Send a
notification("Generating...") before time-consuming operations - Visual Appeal: When Action outputs HTML, use modern CSS (rounded corners, shadows, gradients)
- Smart Guidance: If text is too short, prompt the user: "Suggest entering more content for better results"
6.3 Error Handling¶
Never fail silently
Always catch exceptions and inform the user via __event_emitter__.
try:
# Business logic
pass
except Exception as e:
await __event_emitter__({
"type": "notification",
"data": {"type": "error", "content": f"Processing failed: {str(e)}"}
})
6.4 Long-running Task Notifications¶
If a foreground task is expected to take more than 3 seconds, implement a user notification mechanism (e.g., sending a notification every 5 seconds).
7. Workflow & Process¶
7.1 Source-derived Knowledge (from plugins/)¶
- Input/context safety: normalize multimodal text extraction, use
_get_user_context/_get_chat_context, and protect frontend language detection with timeout guards. - Long task UX: emit immediate
status/notification, then staged progress updates; keep full exception detail in backend logs. - HTML merge strategy: use stable wrapper markers (
OPENWEBUI_PLUGIN_OUTPUT) and support both overwrite and merge modes. - Theme consistency: detect parent/system theme and apply theme-aware rendering/export styles for iframe-based outputs.
- Render-export-persist loop: offscreen render (SVG/PNG) -> upload
/api/v1/files/-> event update + persistence update to avoid refresh loss. - DOCX production path:
TITLE_SOURCEfallback naming, reasoning-block stripping, native Word math (latex2mathml + mathml2omml), and citation/reference anchoring. - File retrieval fallback chain: DB inline -> S3 direct -> local path variants -> public URL -> internal API -> raw fields, with max-byte guards on each stage.
- Filter singleton discipline: do not store request-scoped mutable state on
self; compute from request context each run. - Async compression pattern:
inletsummary injection +outletbackground summary generation, with model-threshold override and system-message protection. - Workspace/tool hardening: explicit
params_typeschemas, strict path-boundary validation, and publish flow returning/api/v1/files/{id}/contentwithskip_rag=truemetadata. - MoE refinement pipeline: detect aggregation prompts, parse segmented responses, and rewrite to synthesis-oriented master prompt with optional reroute model.
7.2 Copilot Engineering Configuration¶
- For repository-wide AI-assisted engineering setup (GitHub Copilot + Gemini CLI + antigravity mode), follow
docs/development/copilot-engineering-plan.md. -
This plan defines the shared contract for tool parameter schema/routing, file creation/publish protocol, rollback-safe delivery patterns, and streaming/tool-card compatibility.
-
Consistency Maintenance: Any addition, modification, or removal of a plugin must simultaneously update the plugin code, READMEs, project docs, doc indexes, and the root README.
- Release Workflow: Pushing to
maintriggers automatic release. Ensure version numbers are updated and follow SemVer. Use Conventional Commits.
8. Troubleshooting¶
HTML not showing?
Ensure it's wrapped in a ```html ... ``` code block.
Database error?
Check if you called synchronous DB methods directly in an async function; use asyncio.to_thread.
Parameters not working?
Check if Valves are defined correctly and if they are being overridden by UserValves.
Plugin not loading?
- Check for syntax errors in the Python file
- Verify the metadata docstring is correctly formatted
- Check OpenWebUI logs for error messages