# 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 1. [Quick Start](#1-quick-start) 2. [Core Concepts & SDK Details](#2-core-concepts--sdk-details) 3. [Deep Dive into Plugin Types](#3-deep-dive-into-plugin-types) * [Action](#31-action) * [Filter](#32-filter) * [Pipe](#33-pipe) 4. [Advanced Development Patterns](#4-advanced-development-patterns) 5. [Best Practices & Design Principles](#5-best-practices--design-principles) 6. [Troubleshooting](#6-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 (e.g., inject context, filter sensitive words) 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: ```python """ 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. Core Concepts & SDK Details ### 2.1 ⚠️ Important: Sync vs Async OpenWebUI plugins run within an `asyncio` event loop. * **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, ...)`. ### 2.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 like `{{USER_NAME}}`, `{{CURRENT_TIME}}`. | | `__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 on the frontend. | ### 2.3 Configuration System (Valves) * **`Valves`**: Global admin configuration. * **`UserValves`**: User-level configuration (higher priority, overrides global). ```python class Filter: class Valves(BaseModel): API_KEY: str = Field(default="", description="Global API Key") class UserValves(BaseModel): API_KEY: str = Field(default="", description="User Private API Key") def inlet(self, body, __user__): # Prioritize user's Key user_valves = __user__.get("valves", self.UserValves()) api_key = user_valves.API_KEY or self.valves.API_KEY ``` --- ## 3. Deep Dive into Plugin Types ### 3.1 Action **Role**: Adds buttons below messages that trigger upon user click. **Advanced Usage: Execute JavaScript on Frontend (File Download Example)** ```python 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}}) ``` ### 3.2 Filter **Role**: Middleware that intercepts and modifies requests/responses. * **`inlet`**: Before request. Used for injecting context, modifying model parameters. * **`outlet`**: After response. Used for formatting output, logging. * **`stream`**: During streaming. Used for real-time sensitive word filtering. **Example: Injecting Environment Variables** ```python 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 ``` ### 3.3 Pipe **Role**: Custom Model/Agent. **Example: Simple OpenAI Wrapper** ```python 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. Advanced Development Patterns ### 4.1 Pipe & Filter Collaboration Use `__request__.app.state` to share data between plugins. * **Pipe**: `__request__.app.state.search_results = [...]` * **Filter (Outlet)**: Read `search_results` and format them as citation links appended to the response. ### 4.2 Async Background Tasks Execute time-consuming operations (e.g., summarization, database storage) in the background without blocking the user response. ```python 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. Best Practices & Design Principles ### 5.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. ### 5.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". ### 5.3 Error Handling Never let a plugin fail silently. Catch exceptions and inform the user via `__event_emitter__`. ```python try: # Business logic except Exception as e: await __event_emitter__({ "type": "notification", "data": {"type": "error", "content": f"Processing failed: {str(e)}"} }) ``` --- ## 6. 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`.