Skip to content

Custom Notification Decorator

Apprise supports lightweight, file-based notification handlers via the @notify() decorator. This is ideal when you want a quick integration without creating and publishing a full Apprise plugin.

The decorator system works by importing a Python module and collecting any functions registered with @notify(). Those functions are then exposed as Apprise schemas.

Use a decorator-based handler when:

  • You need a quick, local integration for personal or controlled environments.
  • You want to glue Apprise to local automation, scripts, or a private service.
  • You do not want to maintain a full plugin lifecycle (tests, packaging, release cadence).

Avoid a decorator-based handler when:

  • You want to ship a service to Apprise itself (upstream contribution).
  • You need strict parsing, templates, native URL reversal, batching, or robust retry logic.
  • You require long-term stability, public distribution, or third-party usage.

The decorator system requires importing a Python module to discover the decorated functions. Importing a module executes code at import time.

That means:

  • Any code at module scope will run, not just the decorated function.
  • Any imports inside that module will execute.
  • Any side effects, file access, network calls, or subprocess calls can occur during load.

Only use decorator-based handlers for trusted code in controlled environments. Do not point Apprise at a shared or untrusted filesystem location.

Here is the practical decision rule.

Capability@notify() DecoratorFull Apprise Plugin
Time to implementMinutesHours to days
Packaging and distributionLocal filePyPI, distro packages, upstream
URL templates and validationMinimalFirst-class (tokens, args, strict parsing)
Privacy handling in url()ManualStandardized (PrivacyMode.Secret)
Native URL reversalRareExpected when feasible
Attachments supportYou implementBuilt-in patterns (AppriseAttachment)
Throttling, retry patternsYou implementStandardized (throttle(), request helpers)
TestabilityAd-hocFirst-class (AppriseURLTester, error paths)
Best forPrivate automationUpstream services and public use

If your handler is likely to be reused by other people, becomes business-critical, or needs strong URL parsing, an Apprise plugin is usually more appropriate.

At minimum, you must define a unique schema and a Python function.

from apprise.decorators import notify
# Maps 'foobar://' to this function and prints to stdout
@notify(on="foobar")
def my_wrapper(body, title, notify_type, *args, **kwargs):
print(f"{notify_type}: {title} - {body}")

Once loaded, Apprise can trigger this function using:

Terminal window
apprise -b "Hello world" foobar://

A decorated function should return:

  • True to indicate success
  • False to indicate failure
  • None (or no return), which is treated as success

Failures propagate back to Apprise and affect overall notification status.

Your wrapper function may accept the following parameters.

ParameterRequiredDescription
bodyYesNotification body
titleNoNotification title
notify_typeNoOne of info, success, warning, failure
body_formatNotext, html, or markdown
metaNoParsed and merged URL metadata
attachNoList of AppriseAttachment objects
*argsYesRequired for forward compatibility
**kwargsYesRequired for forward compatibility

**Always include *args and **kwargs** to remain compatible with future Apprise releases.

A minimal wrapper may look like:

from apprise.decorators import notify
@notify(on="foobar")
def my_wrapper(body, *args, **kwargs):
print(body)

The meta parameter provides a fully parsed and merged view of:

  1. The decorator declaration URL
  2. The user-supplied initialization URL

Example structure:

{
"schema": "foobar",
"url": "foobar://user:pass@host:80/path?key=value",
"host": "host",
"user": "user",
"password": "pass",
"port": 80,
"path": "/",
"fullpath": "/path",
"query": "key=value",
"qsd": {"key": "value"},
"asset": AppriseAsset(),
"tag": set(),
}

Only fields present in the URL are included. At minimum, schema, url, asset, and tag are always present.

The decorator can preload defaults by specifying a full URL.

@notify(on="foobar://localhost:234?notify_on_complete=0")
def my_wrapper(body, meta, *args, **kwargs):
pass

Users may override these values at runtime:

Terminal window
apprise -b "override" foobar://example.com?notify_on_complete=1

Merged result:

{
"schema": "foobar",
"url": "foobar://example.com:234?notify_on_complete=1",
"host": "example.com",
"port": 234,
"qsd": {
"notify_on_complete": "1"
}
}

This example appends a time to the end of a file when called Usage: demo://

apprise/plugins/mylogger.py
from apprise.decorators import notify
import logging
@notify(on="demo")
def my_wrapper(body, title, notify_type, *args, **kwargs):
# Configure the logging system
logging.basicConfig(
filename='/tmp/my-demo.log',
level=logging.DEBUG, filemode='w',
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
# Create a logger instance
logger = logging.getLogger(__name__)
# Log a simple message at the INFO level
logger.info("f{title} - {body}")

Defaults defined in the decorator persist unless explicitly overridden.

Internally, the decorator:

  • Registers a dynamic NotifyBase wrapper
  • Binds your function to the schema
  • Enforces schema uniqueness
  • Supports attachments and persistent storage automatically

If a schema already exists, Apprise logs a warning and skips registration.

By default, Apprise scans:

  • Linux and macOS:
    • ~/.apprise/plugins
    • ~/.config/apprise/plugins
  • Windows:
    • %APPDATA%/Apprise/plugins
    • %LOCALAPPDATA%/Apprise/plugins

Override using:

Terminal window
apprise -P /custom/plugin/path -b "test" foobar://

Provide plugin paths through AppriseAsset:

from apprise import Apprise, AppriseAsset
asset = AppriseAsset(plugin_paths=[
"/path/to/plugins",
"/path/to/plugin.py",
])
aobj = Apprise(asset=asset)
aobj.add("foobar://")
aobj.notify("Hello")

Directory scanning rules:

  • Hidden files and directories are ignored
  • A directory with __init__.py loads only that file
  • Directories without __init__.py load all .py files at that level
  • Scanning is non-recursive

  • Schemas must be unique
  • Keep modules import-safe. Do not perform network calls or long-running work at import time.
  • Validate and sanitize meta values before using them.
  • Prefer allowlists over dynamic execution.
  • Put secrets in environment variables or secret stores, not in URLs.
  • If you need strong parsing, batching, attachments, or upstream contribution, write a full plugin instead.

The Apprise decorator API provides a fast, flexible way to extend Apprise with custom logic while preserving the familiar schema:// interface. It is ideal for internal workflows, automation hooks, and tightly integrated systems.