Skip to content

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
  2. Project Structure & Naming
  3. Core Concepts & SDK Details
  4. Deep Dive into Plugin Types
  5. Advanced Development Patterns
  6. Best Practices & Design Principles
  7. Workflow & Process
  8. 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) and README_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_status and _emit_notification.
  • Frontend Console Debugging: Highly recommended for real-time data flow viewing. Use _emit_debug_log to print structured debug logs in the browser console.
  • Server-side Logging: Use Python's standard logging module. Do not use print().

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 parameters
  • outlet: After response. Used for formatting output, logging
  • stream: 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 &quot;.

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_results and 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 ![description](data:image/svg+xml;base64,...) │
└──────────────────────────────────────────────────────────────┘

5.5 Agent File Delivery Standards (3-Step Delivery Protocol)

  1. Write Local: Create files in the current execution directory (.).
  2. Publish: Call publish_file_from_workspace(filename='name.ext').
  3. Display Link: Present the returned download_url as 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_SOURCE fallback 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: inlet summary injection + outlet background summary generation, with model-threshold override and system-message protection.
  • Workspace/tool hardening: explicit params_type schemas, strict path-boundary validation, and publish flow returning /api/v1/files/{id}/content with skip_rag=true metadata.
  • 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 main triggers 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

Additional Resources