Custom Scripts

Run any code when movie events happen. Forward to Home Assistant, post to Slack, trigger backups, update databases — using any language.

Why Custom Scripts

Luminarr ships with 9 built-in notification channels — Discord, Slack, email, webhooks, and more. But sometimes you need something specific: a Home Assistant scene, a custom database write, a Plex scan triggered only on certain imports. Custom scripts close that gap without waiting for a plugin.

Any executable file works. Bash, Python, Ruby, Node.js, a compiled binary — if your container can run it, Luminarr can call it.

Home automation

Trigger Hue scenes, ring a doorbell chime, announce imports on a speaker — anything Home Assistant or Matter can do.

External APIs

Post to services Luminarr doesn't natively support — custom Slack bots, ntfy topics, PagerDuty, or internal tooling.

Data pipelines

Log events to a file, append rows to a spreadsheet, sync import history with an external database, or trigger backups.

Getting Started

  1. 1

    Create your script

    Write a script in any language. Place it in /config/scripts/ inside the container — this directory persists across restarts via the named volume.

  2. 2

    Make it executable

    shell
    chmod +x /config/scripts/my-script.sh
  3. 3

    Configure in Luminarr

    Go to Settings → Notifications → Add Notification → Command. Enter just the filename — not a full path.

  4. 4

    Choose events

    Select which events trigger the script: grabs, imports, download completion, health changes, or all of the above.

  5. 5

    Test it

    Click Test to verify the script exists, is executable, and exits 0. stdout and stderr are captured and shown in the test result.

How Scripts Are Called

When a subscribed event fires, Luminarr resolves the script path, spawns a subprocess, and waits for it to finish within the configured timeout.

Event fires
Resolve script path
Spawn subprocess
Pipe JSON to stdin
Set env vars
Capture stdout/stderr
Check exit code

Exit 0 is treated as success. Any non-zero exit code is logged as an error. The process is killed if it runs longer than the configured timeout (default: 30 seconds).

stdout and stderr are both captured and written to Luminarr's log at INFO level, so you can see your script's output without logging in to the container.

Environment Variables

These variables are set for every script invocation. They're useful for quick one-liners that don't need to parse JSON.

Variable Description Example
LUMINARR_EVENT_TYPE Event type identifier grab_started
LUMINARR_MOVIE_ID Movie UUID — empty for non-movie events a1b2c3d4-...
LUMINARR_MESSAGE Human-readable event summary Grabbed: Inception (2010)
LUMINARR_TIMESTAMP RFC 3339 timestamp 2026-03-05T14:30:00Z

The script also inherits the full environment of the Luminarr process, so any variables you set in docker-compose.yml or at the OS level are visible too.

JSON Payload

Luminarr pipes a JSON object to the script's stdin. This is the same for every event type; only the Data field varies.

stdin (JSON)
{
  "Type":      "import_done",                          // event type
  "Timestamp": "2026-03-05T14:30:00Z",                 // RFC 3339
  "MovieID":   "a1b2c3d4-e5f6-7890-abcd-ef1234567890", // UUID, may be empty
  "Message":   "Imported: Inception (2010) — 1080p BluRay x265",
  "Data": {
    "movie_title": "Inception",
    "movie_year":  2010,
    "tmdb_id":     27205
  }
}

The Data object contains event-specific fields and may be null for events that aren't tied to a movie. Use stdin for the full payload when you need structured data; use env vars for simple scripts that only need the event type or message.

Event Types

Type When it fires
movie_added A movie is added to a library
movie_deleted A movie is removed from Luminarr
grab_started A release is grabbed from an indexer
download_done The download client reports the file is complete
import_done File successfully imported into the library
import_failed Import attempt failed (wrong quality, corrupt file, etc.)
health_issue A health check detected a problem
health_ok A previously failing health check recovered

Examples

Copy any of these into /config/scripts/, make it executable, and wire it up in Settings. Each example is self-contained.

Log events to a file

log-events.sh
#!/bin/sh
echo "$(date): $LUMINARR_EVENT_TYPE — $LUMINARR_MESSAGE" >> /config/luminarr-events.log

Forward to Home Assistant

Pipe the full JSON payload to a Home Assistant webhook. HA can then trigger automations based on the event type.

forward-ha.sh
#!/bin/sh
cat | curl -s -X POST -H "Content-Type: application/json" \
  -d @- http://homeassistant.local:8123/api/webhook/luminarr

Desktop notification on import

Use notify-send to show a desktop popup when a movie is imported. Works on any Linux host running a notification daemon.

notify-import.py
#!/usr/bin/env python3
import json, sys, subprocess

event = json.load(sys.stdin)
if event["Type"] == "import_done":
    title = event.get("Data", {}).get("movie_title", "Unknown")
    subprocess.run(["notify-send", f"Luminarr: {title} imported"])

Post to Slack with movie details

Send a formatted Slack message using the Incoming Webhooks API. No dependencies beyond Python's standard library.

slack-notify.py
#!/usr/bin/env python3
import json, sys, urllib.request

event = json.load(sys.stdin)
data  = event.get("Data", {})
payload = {
    "text": (
        f":movie_camera: *{data.get('movie_title', 'Unknown')}*"
        f" ({data.get('movie_year', '')}) — {event['Type']}"
    )
}
req = urllib.request.Request(
    "https://hooks.slack.com/services/YOUR/WEBHOOK/URL",
    data=json.dumps(payload).encode(),
    headers={"Content-Type": "application/json"},
)
urllib.request.urlopen(req)

Trigger a Plex library scan after import

Tell Plex to refresh a section whenever Luminarr imports a new file. Set PLEX_TOKEN in your environment or docker-compose.

plex-scan.sh
#!/bin/sh
if [ "$LUMINARR_EVENT_TYPE" = "import_done" ]; then
  curl -s -X POST \
    "http://plex.local:32400/library/sections/1/refresh?X-Plex-Token=$PLEX_TOKEN"
fi

Back up the database after import

Create a timestamped copy of luminarr.db on every successful import. Rotate old backups separately with a cron job.

backup-on-import.sh
#!/bin/sh
if [ "$LUMINARR_EVENT_TYPE" = "import_done" ]; then
  cp /config/luminarr.db \
    "/config/backups/luminarr-$(date +%Y%m%d-%H%M%S).db"
fi

Security

Script execution is intentionally constrained. Luminarr only allows plain filenames with no path components — no /, no ... The resolved path is verified with filepath.Join, filepath.Clean, and filepath.Rel to ensure it stays inside /config/scripts/.

Scripts run with the full permissions of the Luminarr process. Only add scripts you trust. If Luminarr runs as root inside a container, your scripts do too.

Additional constraints:

  • Scripts must be plain filenames — slashes and .. are rejected before path resolution
  • The script file must exist and be executable before it will run
  • Scripts inherit all environment variables from the Luminarr process
  • The process is killed after the configured timeout (default 30 seconds) — no runaway processes

For the full security model including API key auth and network isolation, see the Security page.

Troubleshooting

Problem Fix
"script not found" Verify the file exists at /config/scripts/<name> inside the container. Use docker exec to check.
"script is not executable" Run chmod +x /config/scripts/<name> inside the container.
Timeout errors Increase the timeout in notification settings, or background long work with & and exit immediately.
Can't see script output stdout and stderr are logged at INFO level. Check Luminarr's logs: docker logs luminarr.
Script runs but nothing happens Confirm the event type matches — log $LUMINARR_EVENT_TYPE to verify which event is firing.

Configuration

Both settings are per-notification. You can have multiple script notifications with different scripts, different events, and different timeouts.

Setting Default Description
script_name required Filename only — no paths. Must exist in /config/scripts/.
timeout 30 Maximum execution time in seconds. Process is killed if exceeded.