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
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
Make it executable
shellchmod +x /config/scripts/my-script.sh
-
3
Configure in Luminarr
Go to Settings → Notifications → Add Notification → Command. Enter just the filename — not a full path.
-
4
Choose events
Select which events trigger the script: grabs, imports, download completion, health changes, or all of the above.
-
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.
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.
{ "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
#!/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.
#!/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.
#!/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.
#!/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.
#!/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.
#!/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. |