# Auto-Tagger Guide The Auto-Tagger applies tags to your Sonarr and Radarr items based on rule criteria — the same 50+ rule type DSL used by [[Library Cleanup]], plus composite (AND/OR) rules and real-time tagging via Sonarr/Radarr Connect webhooks. Pair with [[Label Sync]] to mirror those tags onto Plex, Jellyfin, or Emby labels. ## Overview Auto-Tagger automates what you would otherwise do manually: walking your library to apply consistent tags to items that match a definition (genre + year, watch state, custom format score, list membership, etc.). The workflow follows three stages: 1. **Configure** — Define an Auto-Tag rule with a single rule type or a composite (AND/OR) of multiple conditions, and the tag name to apply 2. **Match** — Either the 5-minute scheduler tick walks your `LibraryCache` or a Sonarr/Radarr Connect webhook fires within seconds of an import event 3. **Apply** — The rule walks matched items and merges the tag into the source-side *arr (creating it if absent) Tags written by Auto-Tagger are real *arr tags. They appear in Sonarr/Radarr, can be filtered on in those tools, and can drive other Sonarr/Radarr automations (custom format profile assignment, release profiles, notifications) just like any manually-applied tag. --- ## Getting Started ### Prerequisites 1. At least one **Sonarr** or **Radarr** instance configured in Settings (Lidarr is not yet supported) 2. (Optional) **Plex**, **Jellyfin/Emby**, **Tautulli**, or **Seerr** for cross-service rule criteria (e.g., "tag items that have Plex label X" or "tag items requested in Seerr by user Y") 3. (Optional, for TMDb list-membership rules) A **TMDb v3 read-access token** in Settings → Account 4. (Optional, for Trakt list-membership rules) A **Trakt personal access token** in Settings → Account, plus the operator-side `TRAKT_CLIENT_ID` env var ### Access the Auto-Tagger 1. Log in to Arr Dashboard 2. Click **Auto-Tag** in the sidebar (Maintenance section) 3. The page lists your existing rules with last-run status and a "Run now" button per rule, plus the webhook configuration panel --- ## How It Works ### Two Trigger Paths Auto-Tagger rules can match items in two ways: | Trigger | When | Latency | |---------|------|---------| | **Scheduler tick** | Every 5 minutes | Up to 5 minutes for new rules; subsequent passes respect a 60-minute per-rule cooldown | | **Connect webhook** | Sonarr/Radarr fires on import-related events | Sub-second | Both triggers use the same evaluation engine and apply the same tag merge logic. Webhooks are optional — without them, the scheduler still tags imports within ~5 minutes when the next library cache refresh runs. ### Scheduler Tick A scheduler runs every 5 minutes and selects rules that are either: - Never run (no `lastRunAt`), or - Last run more than 60 minutes ago For each due rule, the scheduler walks every item in the user's `LibraryCache` and evaluates the rule. Items that match get the rule's tag merged in via a single `series.update` or `movie.update` call per item. ### Per-Rule Lock A per-rule mutex prevents two concurrent executions from racing. If you click "Run now" while the scheduler tick is mid-execution for the same rule, the second request returns HTTP 409 with the message "Rule is currently running — try again in a moment." This matters because two concurrent `series.update` or `movie.update` calls perform a read-modify-write on the same `tags: number[]` array; without a lock, one rule's tag merge can clobber another's. ### Sonarr/Radarr Connect Webhook Sonarr and Radarr can fire HTTP webhooks via their **Connect → Webhook** notification setting. Auto-Tagger exposes a public endpoint that receives those events and tags the imported item within seconds. **Allowed event types** (anything else is acknowledged with HTTP 202 and ignored): - `Download`, `Grab`, `Rename`, `Upgrade` - `MovieFile.Import`, `MovieFile.Download` - `ManualInteraction`, `ManualInteractionRequired` - `Test` — acknowledged with no-op so you can verify the wiring The webhook handler: 1. Authenticates via Bearer token (the user's hashed webhook secret) 2. Verifies the instance ID in the URL belongs to the authenticated user 3. Live-fetches the imported item from the *arr API (the LibraryCache row may not exist yet — sync runs every 6h, but Connect fires within seconds) 4. Evaluates every enabled rule whose scope includes the instance 5. Merges all matching rule tags into the item #### One-click auto-install (2.18.2+) You no longer need to hand-paste the URL and Bearer header into each *arr's Connect settings. Inside Settings → Auto-Tagger → **Real-time webhook**, the **Auto-install** sub-panel discovers your enabled Sonarr/Radarr instances and installs the canonical `arr-dashboard auto-tagger` Connect notification on selected instances in a single click. **Prerequisites** 1. Click **Rotate secret** in the Real-time webhook panel — the plaintext is only displayed at generation time and is required for the install button to be enabled. 2. The instances you target must be **enabled** in Settings → Services. Disabled instances are excluded from the discovery list. **What gets installed** For each selected instance, the dashboard creates (or updates, on re-run) a Connect notification named `arr-dashboard auto-tagger` with: - `implementation: Webhook` - `url`: the auto-tag webhook URL with the instance ID baked in - `headers`: `Authorization: Bearer ` - Event flags from the **Events** checkboxes (default: `onDownload` + `onUpgrade`, no `onGrab`) **Idempotency** The installer matches by notification name, so re-running it updates the existing entry instead of creating duplicates. Rotating the webhook secret invalidates the old Bearer header — re-run the install on every instance after rotation. **Per-instance failures** If an *arr is offline, has a stale API key, or otherwise rejects the create call, that instance reports its error inline (e.g. `401 Invalid API key`) without aborting the rest of the batch. The dashboard's Bearer secret is never sent for failed instances. **URL resolution** The installer prefers `SystemSettings.externalUrl` when set (System → Settings → External URL). If unset, it falls back to the request's protocol + hostname as observed by Fastify's `trustProxy`-aware request fields. If your dashboard is behind a reverse proxy, set `externalUrl` explicitly to avoid relying on `X-Forwarded-*` headers. ### Library Cache Dependency Scheduled rule evaluation reads from `LibraryCache`. The cache is populated by background sync; rules will not match items until the cache contains them. Webhook-driven evaluation does not depend on the cache — it live-fetches the item directly. --- ## Configuration ### Creating a Rule 1. Click **Add Rule** on the Auto-Tag page 2. Give the rule a **Name** (shown in the rules list and in scheduler logs) 3. Pick a **Tag Name** — the tag to apply to matched items. Created on the *arr side automatically if it does not exist. 4. Configure a **Single criterion** OR a **Composite (AND/OR)** of conditions (see Rule Types below) 5. Optionally set **Scope filters** to narrow which items the rule sees: - **Service Filter** — Limit to Sonarr or Radarr only - **Instance Filter** — Limit to specific instances - **Exclude Tags** — Skip items that already carry one of these *arr tag IDs - **Exclude Titles** — Skip items whose title matches any of these regex patterns - **Plex Library Filter** — Limit to items in specific Plex sections (only meaningful for rules that depend on Plex data) 6. Toggle **Enabled** — disabled rules are skipped by both the scheduler and the webhook 7. Save the rule ### Composite (AND/OR) Rules Composite rules combine multiple conditions with a single boolean operator. Examples: - `Genre is "Anime" AND Audio Channels >= 6 AND Custom Format Score > 1000` → tag "Premium Anime" - `Year >= 2020 AND HDR Type is HDR10 AND Plex Watch Count = 0` → tag "New 4K Unwatched" - `tmdb_list_member is_in 8068 OR Plex Label is "Award Winner"` → tag "Awards" Each condition uses the same parameter schema as its standalone rule type. Composite rules and single-criterion rules cannot be mixed in the same rule — switch the rule's mode in the dialog. ### Run Now Each rule has a **Run now** button. This: - Acquires the per-rule lock; returns HTTP 409 if a scheduler tick or another run is already executing this rule - Walks every cached library item once - Persists `lastRunAt`, `lastRunStatus` (`success` / `partial` / `failed`), and `lastRunMessage` (count summary or failure reason) --- ## Rule Types The Auto-Tagger uses the same rule criteria DSL as [[Library Cleanup]]. See the [[Library Cleanup#Rule Types|Library Cleanup rule types reference]] for the full table covering Content Attributes, File Metadata, Seerr/Plex/Jellyfin/Tautulli integration rules, and Behavior-Aware rules. In addition, Auto-Tagger ships with two **list-membership** rule types not yet exposed in Library Cleanup: ### List-Membership Rules | Rule Type | Description | Key Parameters | |-----------|-------------|----------------| | **TMDb List Member** | Match items in a TMDb curated list | List ID (e.g., `8068`), Operator (`is_in` / `not_in`) | | **Trakt List Member** | Match items in a Trakt curated list | List slug (e.g., `trakt-official/oscar-winners`), Operator (`is_in` / `not_in`) | Both use cached membership tables refreshed every 4 hours by background schedulers. Lists referenced by enabled rules get refreshed; cache rows for unreferenced lists are garbage-collected. **TMDb requirements:** A TMDb v3 read-access token in Settings → Account (the same token Discover uses). Public lists only — private list support would need TMDb v4 OAuth. **Trakt requirements:** A per-user Trakt personal access token in Settings → Account, plus the operator-side `TRAKT_CLIENT_ID` env var (issued from `trakt.tv/oauth/applications`). Without `TRAKT_CLIENT_ID` set, the Trakt scheduler logs `"skipped — TRAKT_CLIENT_ID not configured"` and trakt rules will not match. --- ## Connect Webhook Setup The webhook gives you sub-second tagging on import, instead of waiting up to 5 minutes for the scheduler tick. ### Generate the Webhook Secret 1. Open the **Webhook Configuration** panel on the Auto-Tag page 2. Click **Generate Secret** if no secret exists yet, or **Rotate Secret** to invalidate the old one 3. **Copy the plaintext secret immediately** — Arr Dashboard hashes it before storing, so the plaintext is shown only at generation or rotation. If you lose it, you must rotate to get a new one. (This is intentional: a database compromise no longer yields a usable webhook credential.) ### Configure Sonarr or Radarr In your Sonarr or Radarr instance: 1. Go to **Settings → Connect → Add → Webhook** 2. Set **URL** to: `https://your-arr-dashboard-host/api/auto-tag/webhook/` - Replace `` with the instance's UUID from Arr Dashboard's Settings → Services page (shown next to each instance) 3. Set **Method** to `POST` 4. Add a custom header `Authorization: Bearer ` 5. Enable the events you want to react to: **On Grab**, **On Import / On Download**, **On Upgrade**, **On Rename**, **On Manual Interaction Required** are all supported 6. Click **Test** — the webhook should return a 200 status with `{"status": "test", "message": "Test webhook received."}` 7. Save ### Webhook Response Codes | Code | Meaning | |------|---------| | `200` | Event processed; check response body for tag application count | | `202` | Event ignored (unsupported event type) | | `400` | Bad payload, instance/payload type mismatch, or tag-update failure | | `401` | Missing or invalid Bearer token | | `404` | Instance not found, disabled, or not owned by the token's user | Failed webhooks do not retry. If a Connect call fails, the next scheduler tick (within 5 minutes) will catch the item. --- ## Composition with Label Sync Auto-Tagger writes tags to the source-side *arr (Sonarr or Radarr). To mirror those tags into Plex, Jellyfin, or Emby labels, pair with a [[Label Sync]] rule: ``` [Auto-Tagger] [Label Sync] Sonarr "Anime" tag ──→ Plex "Anime" label Radarr "Premium" tag ──→ Jellyfin "Premium" label ``` This split keeps the two engines simple. Auto-Tagger seeds the source-of-truth tag based on criteria. Label Sync propagates that tag to other services on its own cadence. You can also use Auto-Tagger and Label Sync independently. Auto-Tagger is useful on its own if you only need *arr tags. Label Sync is useful on its own if you maintain tags manually in Sonarr/Radarr. --- ## Operations ### Status and Telemetry Each rule row on the Auto-Tag page shows: | Field | Meaning | |-------|---------| | **Last Run At** | Timestamp of the last execution (scheduled or on-demand) | | **Last Run Status** | `success` (all matched items tagged), `partial` (some items failed), or `failed` (rule errored before any application) | | **Last Run Message** | Count summary like `"Applied tag to 12 items across 2 instances"` or a failure reason | ### Operator Environment Variables | Variable | Purpose | Required For | |----------|---------|--------------| | `TRAKT_CLIENT_ID` | Trakt API app credential | `trakt_list_member` rules | TMDb list-membership has no operator-side env requirement — it uses the user's TMDb v3 token from Settings → Account. ### Public Routes The inbound Connect webhook (`POST /api/auto-tag/webhook/:instanceId`) is mounted as a **public route** (no session cookie required). Authentication is the per-user Bearer token. All other Auto-Tagger endpoints require a session cookie like the rest of the app. --- ## Best Practices ### Start Small, Measure, Then Expand Create one or two rules first, click **Run now**, and check the resulting tag count and `lastRunMessage`. Once you trust the rule's match set, enable additional rules. ### Use Composite Rules to Avoid Tag Storms A single broad rule like `genre includes "Action"` may match thousands of items on the first run. Composite rules (`Genre is "Action" AND Year >= 2020 AND Custom Format Score > 500`) keep the match set targeted. ### Pair with Label Sync for Cross-Service Tagging Don't write two rules — one in Auto-Tagger applying the Sonarr tag, one in Label Sync mirroring it to Plex — for two separate match sets. Use a single Auto-Tagger rule as the source of truth and let Label Sync propagate. ### Rotate the Webhook Secret if Logs Show Unexpected Calls Bearer tokens in Connect webhook headers are visible to anyone with access to the Sonarr/Radarr config. If you suspect leakage, rotate via the Webhook Configuration panel — the old secret stops authenticating immediately. ### Use Exclude Tags as a Safety Net Tag certain items in Sonarr/Radarr with a "no-auto-tag" tag and add that tag ID to every Auto-Tagger rule's **Exclude Tags** list. This gives you an opt-out per item without disabling the whole rule. --- ## Troubleshooting ### "Rule shows last_run_status: success but no tag was applied" **Causes:** - Items already carried the tag (the tag is merged additively, so re-running on items already tagged is a no-op) - Rule criteria do not match any cached items - Rule's scope filters exclude the instances containing matching items **Solutions:** 1. Check `lastRunMessage` — it reports the application count, not just the match count 2. Use the [[Library Cleanup#Explain Feature|Explain]] feature on a known item to see whether the rule would match 3. Verify scope filters (service, instance, library) include the instances you expect ### "Webhook returns 401 Invalid or missing webhook secret" **Causes:** - The Bearer token in the Connect header is wrong or has been rotated - The Authorization header is missing or malformed (must be exactly `Authorization: Bearer `) - The token is shorter than 16 characters (rejected outright as malformed) **Solutions:** 1. Open the Webhook Configuration panel and rotate the secret to get a fresh one 2. Copy the plaintext value directly into the Sonarr/Radarr Webhook custom header 3. Test with the Sonarr/Radarr "Test" button before saving ### "Webhook returns 404 Instance not found or disabled" **Causes:** - The `` in the URL is wrong - The instance is disabled in Settings → Services - The instance belongs to a different user **Solutions:** 1. Check Settings → Services and copy the exact instance UUID 2. Confirm the instance is enabled 3. Confirm you generated the webhook secret as the same user that owns the instance ### "Webhook returns 400 with payload type mismatch" **Cause:** The Sonarr Connect webhook is pointing at a Radarr instance ID (or vice versa). The webhook payload contains a `series` object but the URL targets a Radarr instance. **Solution:** Each instance gets its own webhook URL. Configure Sonarr Connect to target a Sonarr instance ID and Radarr Connect to target a Radarr instance ID. ### "Trakt rules show no matches" **Causes:** - `TRAKT_CLIENT_ID` env var not set in the API process — Trakt scheduler skips with a one-line log notice - Trakt PAT not configured in Settings → Account - List slug uses a wrong format (must be `username/list-slug`, e.g., `trakt-official/oscar-winners`) - Cache has not refreshed yet — first refresh takes up to 4 hours after the rule is created **Solutions:** 1. Confirm `TRAKT_CLIENT_ID` is in the API container's environment 2. Add the Trakt PAT in Settings → Account 3. Verify the list slug is in `username/list-slug` form 4. Wait for the next 4-hour refresh, or restart the API to fire the startup tick ### "TMDb rules show no matches" **Causes:** - TMDb v3 token not configured in Settings → Account - List ID is wrong (TMDb list IDs are integers like `8068`, not URL slugs) - The TMDb list is private (only public lists work in v1) **Solutions:** 1. Confirm the TMDb v3 token in Settings → Account is the same one used by Discover 2. Use the numeric list ID from the TMDb URL 3. Use a public list ### "Rule is currently running" (HTTP 409 on Run Now) **Cause:** A scheduler tick or a concurrent on-demand run is already executing this rule. The per-rule lock prevents racing. **Solution:** Wait a moment and retry. Scheduled ticks complete in seconds for most rules. --- ## API Reference All endpoints require a session cookie except `POST /api/auto-tag/webhook/:instanceId`, which uses Bearer-token authentication. ### List Rules ``` GET /api/auto-tag/rules ``` Returns the user's rules sorted by `createdAt` descending. ### Create Rule ``` POST /api/auto-tag/rules Content-Type: application/json { "name": "Premium Anime", "enabled": true, "ruleType": "composite", "operator": "AND", "conditions": [ { "ruleType": "genre", "parameters": { "operator": "includes_any", "genres": ["Anime"] } }, { "ruleType": "audio_channels", "parameters": { "operator": "greater_than_or_equal", "channels": 6 } } ], "tagName": "premium-anime", "serviceFilter": ["sonarr"] } ``` For single-criterion rules, set `ruleType` to the rule type and `parameters` to its parameters; omit `operator` and `conditions`. ### Update Rule ``` PATCH /api/auto-tag/rules/:id Content-Type: application/json { "enabled": false } ``` Any subset of fields can be updated. The validator re-evaluates the post-PATCH composite-vs-leaf shape and rejects mode mismatches. ### Delete Rule ``` DELETE /api/auto-tag/rules/:id ``` ### Run Rule On Demand ``` POST /api/auto-tag/rules/:id/run ``` Returns the updated rule with `lastRunAt`, `lastRunStatus`, `lastRunMessage`. Returns HTTP 409 if the rule is already executing. ### Get Webhook Configuration ``` GET /api/auto-tag/webhook-config ``` Returns `{ secret: "...", configured: true, freshlyGenerated: true }` on the first-ever access (the secret is generated lazily). On subsequent accesses returns `{ secret: null, configured: true, freshlyGenerated: false }` — the plaintext is not stored, only its SHA-256 hash. ### Rotate Webhook Secret ``` POST /api/auto-tag/webhook-config/regenerate ``` Generates a new 256-bit secret, stores its hash, and returns the plaintext once. Invalidates the old secret immediately. ### Inbound Connect Webhook ``` POST /api/auto-tag/webhook/:instanceId Authorization: Bearer Content-Type: application/json (Sonarr or Radarr Connect payload) ``` Public endpoint — no session cookie required. The Bearer token resolves to the user; the path's `instanceId` must reference an enabled instance owned by that user. Returns: ```json { "status": "ok", "message": "Applied 2 tags from 2 rules.", "tagsApplied": 2, "rulesEvaluated": 2 } ```