Skip to main content

🔔 Event Function: React to System Events

Event functions react to system events: auth.signup, chat.deleted, system.startup and config.updated flow into an Open WebUI Event functionEvent functions react to system events: auth.signup, chat.deleted, system.startup and config.updated flow into an Open WebUI Event function
New in 0.10.0

Event functions are a new plugin primitive introduced in Open WebUI 0.10.0.

⚠️ Critical Security Warning

Functions execute arbitrary Python code on your server, and Event functions run automatically in response to system activity, not just when a user acts. Function creation is restricted to administrators. Only install from trusted sources and review code before importing. See the Plugin Security Warning.

An Event function is the fourth function primitive, alongside Pipe, Filter, and Action. Where those shape an individual chat request, an Event function runs custom Python in response to events happening across Open WebUI: a user signs up, a chat is deleted, a model is created, the server starts up, configuration changes, and so on.

This turns Open WebUI from an app you configure into a platform you can program: lifecycle automation, audit logging, signup gating, provisioning, alerting, and integrations with external systems, all expressed as a single function.


Basic Structure

The type is auto-detected from a top-level class Event:

"""
title: Example Event
author: open-webui
version: 0.1
"""

from pydantic import BaseModel


class Event:
    class Valves(BaseModel):
        pass

    def __init__(self):
        self.valves = self.Valves()

    async def event(
        self,
        event: dict,
        __event_id__: str = None,
        __event_name__: str = None,
        __id__: str = None,
        __app__=None,
        __request__=None,
    ):
        print(f"event id:   {__event_id__}")
        print(f"event name: {__event_name__}")
        print(f"payload:    {event}")

Your event() handler is called for every system event. You decide what to act on by checking __event_name__.

The event() handler arguments

ArgumentDescription
eventThe event payload (a dict): what changed, plus an actor (the user who triggered it, when applicable). Sensitive fields are redacted before the payload reaches your function.
__event_name__The event's name, for example auth.signup or chat.deleted. Use this to filter.
__event_id__A unique id for this specific event occurrence.
__id__The id of your Event function.
__app__The Open WebUI FastAPI application instance. This lets you register new API routes, read or update application state, and call internal services (see Registering endpoints).
__request__The current request, when the event was triggered by one.

Filtering by event

async def event(self, event: dict, __event_name__: str = None, **kwargs):
    if __event_name__ == "auth.signup":
        await self.on_signup(event)
    elif __event_name__ == "system.startup.completed":
        await self.on_startup()

The Event Catalog

Open WebUI emits 170+ events spanning every subsystem. Each name is namespaced as <area>.<action>. A representative selection:

AreaExample events
Systemsystem.startup.started, system.startup.completed, system.shutdown.started, system.shutdown.completed
Authauth.signup, auth.login, auth.logout, auth.password_changed, auth.api_key.created
Users & groupsuser.created, user.deleted, user.role_updated, user.permissions_updated, group.member_added
Chats & messageschat.created, chat.deleted, chat.shared, chat.archived, chat.compacted, message.created, message.reaction_added
Models & promptsmodel.created, model.updated, model.access_updated, prompt.created
Knowledge & filesknowledge.created, knowledge.file.added, knowledge.reindexed, file.uploaded, file.deleted
Pluginstool.created, function.enabled, function.valves_updated, tool.valves_updated, skill.created
Configconfig.updated, config.models.updated, config.connections.updated, config.webhook.updated
Otherchannel.*, calendar.*, automation.*, memory.*, note.*, image.*, audio.*, terminal.*, feedback.*

The authoritative, complete list is exposed by the backend event catalog and shown in Admin Settings > Events when configuring webhooks.


Lifecycle Hooks (Startup & Shutdown)

The system.startup.completed event fires once when the server is ready, and system.shutdown.started fires when it begins to stop. Use them for setup and teardown:

async def event(self, event: dict, __event_name__: str = None, __app__=None, **kwargs):
    if __event_name__ == "system.startup.completed":
        # Apply a saved configuration, warm a cache, register routes, etc.
        ...
    elif __event_name__ == "system.shutdown.started":
        # Flush buffers, close connections, etc.
        ...

This is what makes Event functions a good fit for self-applying configuration: run the same setup on every startup using values your admin set in Valves, so the whole configuration lives inside Open WebUI rather than in external scripts.


Concurrency, multiple replicas, and exactly-once

You are responsible for deduplication

Open WebUI dispatches events in-process on whichever instance handled the activity, fire-and-forget: a failed handler is logged, not retried, and there is no exactly-once guarantee and no coordination across replicas. If your handler has a side effect that must happen once, you have to enforce that yourself.

How often your event() runs depends on the event:

  • Request-scoped events (auth.signup, chat.deleted, model.created, …) fire on the single instance that handled that request. One signup, one handler call.
  • Lifecycle events (system.startup.completed, system.shutdown.started) fire on every replica, because every replica boots and shuts down independently. In a 4-replica deployment your startup handler runs 4 times.

So the classic footgun: an Event function that logs signups, sends a "server is up" alert, or provisions a resource on startup will do it once per replica, not once globally, the moment you scale past a single instance.

The fix is a shared lock or idempotency key. In a correctly configured multi-instance deployment REDIS_URL is already set (websockets and shared caches require it), so use Redis to claim the work for exactly one replica:

from open_webui.utils.redis import get_redis_connection
from open_webui.env import REDIS_URL, REDIS_KEY_PREFIX

_redis = get_redis_connection(REDIS_URL, decode_responses=True) if REDIS_URL else None


def run_once(key: str, ttl: int = 300) -> bool:
    # Redis SET NX EX: atomic "set if not exists" with expiry. Exactly one
    # caller across all replicas gets True within the ttl window.
    if _redis is None:
        return True  # single process: nothing to coordinate with
    return bool(_redis.set(f"{REDIS_KEY_PREFIX}:event:{key}", "1", nx=True, ex=ttl))


class Event:
    async def event(self, event: dict, __event_name__: str = None, **kwargs):
        if __event_name__ == "system.startup.completed":
            if run_once("startup-provision"):
                await self.provision()          # runs on one replica only
        elif __event_name__ == "auth.signup":
            actor_id = (event.get("actor") or {}).get("id", "unknown")
            if run_once(f"signup-log:{actor_id}"):
                await self.log_signup(event)     # logged once, even if delivered twice

This is the same pattern, and the same caveats (key choice, ttl, the in-tree RedisLock helper for long-running work), documented for all plugin types under Under the Hood → Concurrency and multiple instances.


Registering Endpoints and Self-Installing Plugins

Because the handler receives __app__ (the FastAPI application), an Event function can register its own API routes at startup and reach internal services. This unlocks patterns that previously needed an external service:

  • Self-installing / self-configuring plugins. On system.startup.completed, check whether a component is installed and configured, and if not, provision it by calling the admin endpoints, using values from your Valves. Admins configure everything from inside Open WebUI instead of fiddling with manual setup.
  • Custom flows that span a request and an event. For example, an email-verification flow: on system.startup.completed register a /verify endpoint; on auth.signup email the new user a tokenized link; when they click it and hit your endpoint, flip their role from pending to user. A complete activation flow with no external service, as one function.
  • Server-rendered pages, not just JSON. A registered route can return HTML, JavaScript, CSS, or a whole bundled SPA (FastAPI HTMLResponse / Response), so a function can host its own consent page, status dashboard, approval screen, or installer UI at its own URL without a frontend rebuild. Example, a versioned ToS gate: hold new accounts as pending, serve the agreement at /tos, flip the role to user on acceptance, and store the accepted version in the user's settings so bumping the ToS re-gates everyone automatically. Note the boundary: this serves your own page, it does not inject into the Open WebUI chat UI itself, see Serve a page or asset from a route.
warning

Registering routes and calling admin endpoints from a function is powerful and effectively unrestricted. Treat any Event function as trusted, server-level code.


Configuration with Valves

Like other functions, Event functions support Valves for admin-configurable settings (and UserValves where relevant). Put your endpoints, allowlists, schedules, API keys, and toggles in Valves so the behavior is configured from Admin Panel → Functions → ⚙️, not hard-coded.

The function.valves_updated event also fires when an admin saves new Valve values, so a function can react to its own configuration changing and apply it live.


Example Use Cases

Event functions turn one-off admin chores and external glue scripts into configuration that lives inside Open WebUI. Because they receive __app__, they can read and write application state, call internal services, register routes, and run scheduled work, all gated by Valves. The families below are a map of what that unlocks.

React, don't intercept

An Event function runs after the activity it reacts to (the user is already created, the message already exists). It can respond, remediate, or notify, but it cannot block or rewrite the triggering action in-band. To validate or transform a chat request as it flows, use a Filter; to gate a signup, hold the freshly created account as pending rather than trying to stop the request.

Onboarding and provisioning

Fire on auth.signup / user.created to shape a new user's first five minutes, and on system.startup.completed to provision the instance itself.

  • Welcome chat. Drop a pre-populated getting-started chat into the new user's sidebar, with an optional notification pointing at it.
  • Auto-group assignment. Add new users to default groups so they inherit the right models, channels, and permissions on day one.
  • Apply interface defaults. Push admin-chosen UI defaults onto new users, and re-apply to everyone when you change them.
  • Email verification. Email a tokenized link on signup; clicking it flips the account from pending to user. No external service (how).
  • Seed starter content. Give each new user a starter knowledge base, prompt set, or personal channel.
  • Progressive onboarding drip. Send a scheduled series (day 1 welcome, day 3 tips, day 7 advanced) based on each user's account age.
  • Feature-launch announcer. Push a one-time announcement chat or notification to all existing users when you ship something new.

Access control and signup gating

Because the account already exists when the event fires, gating means holding or removing it, not rejecting the request.

  • Email-domain policy. Allowlist trusted domains, hold the rest as pending, reject disposable or throwaway domains.
  • Burst defense. Auto-hold new accounts during a signup spike (bot defense).
  • Approval queue. Post "User X wants access [Approve] / [Deny]" to Slack or Discord, approve from your phone.

Lifecycle and housekeeping

Register a periodic background task on system.startup.completed (see Spawn a background task) and let it run on a schedule.

  • Self-cleaning chats. Archive or delete chats past an age cutoff. Use chat.created as a throttled heartbeat instead of a cron box.
  • Orphaned-knowledge GC. Periodically prune knowledge files no model references anymore.
  • Offboarding cascade. On user.deleted, revoke API keys, purge shared resources, disable automations. Clean teardown.

Compliance and audit

  • Event-driven alerting. Fan out a loud multi-sink alert (channel + email + SIEM) on a high-signal event. Flagship: scream when someone becomes admin; same pattern for config.* changes.
  • Immutable audit sink. Ship every event to an append-only store or SIEM. Tamper-evident audit logging, for free.
  • Content flagging. Scan message.created for secrets and PII and flag it. (To block in-band, use a Filter.)
  • Access recertification. Periodically ask resource owners to re-approve who can access their stuff. Automated access reviews.

Instance-as-code and fleet ops

  • Bootstrap from manifest. Provision a whole instance to spec from a Valve manifest. New deployment, paste manifest, done.
  • GitOps sync. Pull prompts, models, groups, and config from a central git repo so a fleet stays in lockstep.
  • Backup on change. Snapshot config on every config.updated, to external storage or the persisted /data directory.
  • Model retirement with reassignment. Point a retiring model at a destination, and auto-rewrite every model and automation that referenced it.

Operations and integrations

  • Self-healing connectors. Health-check connectors, auto-disable a dead one and alert, so the model picker stays clean.
  • Notify-anything bridge. Forward selected events to external services, making Open WebUI a node in an automation graph.
  • Usage metering. Accumulate per-user token usage and export it. (A real currency budget also needs an external price source.)
  • Honeypot / intrusion detection. Register decoy admin routes, then log and auto-deactivate anyone who probes them.
One-time work runs once per replica

Several of these (provisioning, welcome chat, scheduled jobs) must happen once globally, but system.startup.completed fires on every replica and request-scoped handlers can run concurrently. Guard at-most-once side effects with a distributed lock, see Concurrency, multiple replicas, and exactly-once.


Event Functions vs Event Webhooks

There are two ways to react to events, for different needs:

  • Event functions (this page) run Python in-process. Use them when you need real logic: branching, calling internal services, registering endpoints, transforming data.
  • Event webhooks (configured in Admin Settings > Events) send selected events to external HTTP endpoints as JSON, or to chat destinations (Slack, Discord) as readable messages, filtered by event pattern and by user or group. Use them for simple "notify an external system" needs without writing code.
Breaking change in 0.10.0

The single global notification webhook is being replaced by this per-event webhook system. Existing webhook configuration is migrated automatically into Admin Settings > Events. If you previously received a notification on every action (including page refreshes), you now choose exactly which events fire a webhook and where they go.

This content is for informational purposes only and does not constitute a warranty, guarantee, or contractual commitment. Open WebUI is provided "as is." See your license for applicable terms.