Custom Rules Vs Sonarr Custom Formats
Sonarr custom formats and Scryer custom rules answer the same question — how much do I want this release? — but they answer it in different shapes. A custom format is a declarative matcher: you pick from a fixed set of specification types, and the score lives somewhere else. A Scryer rule is a policy: a small piece of Rego that looks at a release and emits named score entries.
If you are coming from Sonarr, most of your custom formats port over directly. The parts that do not port are usually the parts worth revisiting anyway.
The Shape Of Each
Section titled “The Shape Of Each”| Sonarr custom format | Scryer custom rule | |
|---|---|---|
| Written as | A list of specifications in the UI | Rego policy source |
| Condition vocabulary | 8 fixed specification types | Any field on the rule input document |
| Combination | Same type ORs, different types AND | Ordinary boolean logic |
| Where the score lives | In each quality profile, per format | In the rule itself |
| Scoping | Per quality profile | Per facet (movie, series, anime) |
| Blocking | Emergent from the score total | An explicit veto sentinel |
| Sees the actual file | No stream-level detail | Yes, at import |
| Reusable across profiles | Score re-entered per profile | Rule carries its own score |
Sonarr’s eight specification types are Release Title, Release Group, Language, Indexer Flag, Source, Resolution, Size, and Release Type. That set is the ceiling: if the thing you care about is not one of those eight, there is no custom format for it. A Scryer rule reads a structured input document instead, so its ceiling is the document.
Where The Score Lives
Section titled “Where The Score Lives”This is the first thing that surprises people porting a setup.
In Sonarr, a custom format carries no score. It is purely a matcher. The score is stored in each quality profile’s format items, which is why the same custom format can be worth +500 in one profile and -10000 in another, and why importing a TRaSH custom format is only half the job — you still have to set its score in every profile that should care.
In Scryer, the number is in the rule:
score_entry["x265_bonus"] := 100 if { codec := scryer.normalize_codec(input.release.video_codec) codec == "H.265"}score_entry is a map of code → delta. The code (x265_bonus) is what shows up in the scoring breakdown for a release, so it is worth naming precisely. One rule can emit several entries, and each lands in the breakdown on its own line.
The tradeoff is real in both directions. Sonarr’s split lets one matcher mean different things to different profiles. Scryer’s rules are scoped by facet rather than by profile, so a rule says one thing everywhere it applies — easier to reason about, less flexible if you genuinely wanted per-profile weights.
How Blocking Differs
Section titled “How Blocking Differs”Both systems use -10000 as the “never grab this” number, which makes them look more alike than they are.
In Sonarr, blocking is arithmetic. A release is rejected when its total custom format score falls below the profile’s Minimum Custom Format Score (default 0). The -10000 convention works because -10000 is far below zero — but it is a convention, not a mechanism. It can be outvoted:
| Release | Matched formats | Score | Rejected by min score? |
|---|---|---|---|
...1080p.BluRay.x264-YIFY | Blocked Groups | −10000 | Yes |
...1080p.BluRay.x265-YIFY | Blocked Groups, Prefer x265 (+100) | −9900 | Yes |
...1080p.BluRay.x265-YIFY | Blocked Groups, Prefer x265 (+10100) | +100 | No |
That last row is not hypothetical — it is what Sonarr does when a positive format is scored high enough to cancel a -10000. The “must not have” format still matched; it just lost the arithmetic.
In Scryer, a veto is not a number. A rule blocks by emitting scryer.block_score(), and the decision’s allowed flag is set by the presence of that entry, not by the sum. No stack of bonuses can revive a blocked release.
score_entry["blocked_group"] := scryer.block_score() if { input.release.release_group != null input.release.release_group != "" group := lower(input.release.release_group) blocked_groups[group]}Scryer does also have Sonarr’s threshold behavior, as a separate profile setting — a minimum score to grab, which produces a score_below_minimum block. That is a grab floor, deliberately not an import gate: a file already on disk and correct is not improved by refusing it.
What A Rule Can See
Section titled “What A Rule Can See”A rule is evaluated against one input document per release. The top-level sections:
| Path | What it holds |
|---|---|
input.release | Everything parsed from the release name, plus size, age, and indexer extras |
input.profile | The active quality profile’s criteria |
input.context | Title, library, facet, tags, original language, runtime, search mode |
input.builtin_score | What Scryer’s own scoring already decided, including its codes |
input.file | Media-analysis facts about the actual file — null before download |
input.builtin_score has no Sonarr equivalent: a rule can read the built-in decision and react to it, rather than only adding to it.
Useful input.release fields include quality, source, video_codec, audio_codecs, audio_channels, languages_audio, release_group, size_bytes, age_days, is_remux, is_atmos, is_dolby_vision, is_hdr10plus, is_10bit, is_dual_audio, is_repack, is_proper_upload, is_season_pack, is_multi_episode, is_obfuscated, is_retagged, has_release_group, edition, streaming_service, and extra for indexer-supplied values.
Five builtins do the normalization you would otherwise write by hand:
| Builtin | Returns |
|---|---|
scryer.block_score() | The veto sentinel |
scryer.size_gib(bytes) | Size in GiB |
scryer.lang_matches(code, pattern) | Language match across ISO 639 aliases (ja, jpn, japanese) |
scryer.normalize_source(raw) | Canonical source (WEB-DL, BLURAY, …) |
scryer.normalize_codec(raw) | Canonical codec (H.264, H.265, …) |
Side-By-Side Examples
Section titled “Side-By-Side Examples”The Sonarr JSON below is the shape the v3 API accepts: fields is an array of {name, value} objects. Exports from TRaSH Guides are written for the UI’s import box and use an object instead — worth knowing when a paste is rejected.
1. Prefer x265
Section titled “1. Prefer x265”Sonarr matches the release title with a regex. Scryer normalizes the parsed codec, so it does not need to enumerate the ways a group might spell HEVC.
{ "name": "Prefer x265", "includeCustomFormatWhenRenaming": false, "specifications": [ { "name": "x265", "implementation": "ReleaseTitleSpecification", "negate": false, "required": true, "fields": [{ "name": "value", "value": "\\b(x265|h265|hevc)\\b" }] } ]}score_entry["x265_bonus"] := 100 if { codec := scryer.normalize_codec(input.release.video_codec) codec == "H.265"}Then, in Sonarr, set the score to 100 in each profile. In Scryer the 100 above is the whole story.
2. Preferred Release Groups
Section titled “2. Preferred Release Groups”{ "name": "Preferred Anime Groups", "includeCustomFormatWhenRenaming": false, "specifications": [ { "name": "Groups", "implementation": "ReleaseGroupSpecification", "negate": false, "required": true, "fields": [{ "name": "value", "value": "^(SubsPlease|Erai-raws|EMBER|Yameii)$" }] } ]}preferred_groups := {"subsplease", "erai-raws", "ember", "yameii"}
score_entry["preferred_anime_group"] := 400 if { input.release.release_group != null input.release.release_group != "" group := lower(input.release.release_group) preferred_groups[group]}A set with a case-folded lookup is easier to extend than a regex alternation, and it cannot accidentally match mid-token. This rule would be scoped to the anime facet, so it never touches Movie or Series decisions — in Sonarr the equivalent isolation means keeping separate profiles.
Before writing group rules by hand, check what is already there: Scryer ships TRaSH Guides release-group tiers built in, scored through your scoring persona. Many group-preference custom formats are redundant on arrival.
3. Two Conditions At Once
Section titled “3. Two Conditions At Once”Penalize x264 at 4K. In Sonarr this is two specifications of different types, which AND together.
{ "name": "x264 at 2160p", "includeCustomFormatWhenRenaming": false, "specifications": [ { "name": "2160p", "implementation": "ResolutionSpecification", "negate": false, "required": true, "fields": [{ "name": "value", "value": 2160 }] }, { "name": "x264", "implementation": "ReleaseTitleSpecification", "negate": false, "required": true, "fields": [{ "name": "value", "value": "\\b(x264|h264|avc)\\b" }] } ]}score_entry["x264_4k_penalty"] := -200 if { input.release.quality == "2160P" scryer.normalize_codec(input.release.video_codec) == "H.264"}The combination rule is the thing to internalize. Sonarr groups specifications by type, then requires every group to match. Within a group, a required specification must match, and at least one specification must match. In practice:
| Specifications | Behavior |
|---|---|
| Two of the same type, neither required | OR — either one matching is enough |
| Two of the same type, both required | AND |
| Two of different types | AND, always |
In Rego there is no such table. Conditions in one body are AND; write a second body for the same key to get OR.
4. Freeleech
Section titled “4. Freeleech”{ "name": "Freeleech", "includeCustomFormatWhenRenaming": false, "specifications": [ { "name": "Freeleech", "implementation": "IndexerFlagSpecification", "negate": false, "required": true, "fields": [{ "name": "value", "value": 1 }] } ]}score_entry["freeleech_bonus"] := 500 if { input.release.extra.freeleech == true}
score_entry["halfleech_bonus"] := 200 if { input.release.extra.downloadvolumefactor == 0.5}Sonarr’s indexer flags are a fixed enum — Freeleech, Halfleech, DoubleUpload, Internal, Scene, Freeleech75, Freeleech25, Nuked — and they exist only on torrent releases; a Usenet release never carries one. Scryer reads input.release.extra, which is whatever the indexer plugin supplied, so anything the indexer publishes is available without waiting for a new specification type.
5. Block Oversized Releases
Section titled “5. Block Oversized Releases”{ "name": "Oversized", "includeCustomFormatWhenRenaming": false, "specifications": [ { "name": "Over 100GB", "implementation": "SizeSpecification", "negate": false, "required": true, "fields": [ { "name": "min", "value": 100 }, { "name": "max", "value": 10000 } ] } ]}score_entry["too_large"] := scryer.block_score() if { scryer.size_gib(input.release.size_bytes) > 100}Two gotchas here. Sonarr’s size field is a range, not a threshold — matching “bigger than 100” means setting a maximum you never expect to hit. And the unit labeled GB is computed as 1024³, so it is really GiB; Scryer’s scryer.size_gib() at least says so in the name.
The blocking difference from earlier applies directly: the Sonarr version rejects only while nothing else outscores it, and only for profiles where you set -10000. The Scryer version is decisive in every facet the rule applies to.
6. Require Japanese Audio
Section titled “6. Require Japanese Audio”This is the one that does not really port.
{ "name": "Japanese Audio", "includeCustomFormatWhenRenaming": false, "specifications": [ { "name": "Japanese", "implementation": "LanguageSpecification", "negate": false, "required": true, "fields": [ { "name": "value", "value": 8 }, { "name": "exceptLanguage", "value": false } ] } ]}score_entry["no_japanese_audio"] := scryer.block_score() if { input.file != null not has_japanese_audio}
has_japanese_audio if { some lang in input.file.audio_languages scryer.lang_matches(lang, "ja")}The Sonarr format matches a release whose name claims Japanese. The Scryer rule inspects the audio tracks of the downloaded file and refuses the import when no Japanese track is actually present. One trusts the announcement; the other checks.
A Sonarr custom format’s input is limited to parsed release info, quality, size, indexer flags, languages, filename, and release type. There is no stream-level detail available to it — no per-stream codecs, channel counts, bit depth, or HDR format. Scryer’s input.file carries all of that at import time, which is why rules like this exist at all.
Because input.file is null before the download, guarding on input.file != null is what makes a rule import-only. A rule without that guard runs at search time too, where file fields simply read as undefined.
What Does Not Port Cleanly
Section titled “What Does Not Port Cleanly”negate. Sonarr inverts one specification with a flag. In Rego you writenot, but watch the interaction withnull:not input.release.is_atmosis true when the field is absent, which is usually what you want, but be deliberate about it.- Per-profile scores. If you relied on one custom format scoring differently across profiles, a single rule cannot express that. Split it into differently-scoped rules.
- Source and Resolution formats. These are usually better expressed as quality-profile criteria in Scryer —
source_allowlist,quality_tiers, and friends — rather than as rules. Reach for a rule when you want a preference, not an eligibility boundary. - Large TRaSH bundles. Check what is already built in before porting dozens of formats by hand. Release-group tiers, source scoring, audio and HDR weighting, and size shaping are handled by scoring personas.
Porting Checklist
Section titled “Porting Checklist”- Write down each custom format and its score in each profile — half the information is in the profile, not the format.
- Drop anything the built-in TRaSH data and personas already cover.
- Convert the rest, one rule per intent, with a precise
score_entrycode so the breakdown stays readable. - Replace
-10000scores withscryer.block_score(), never a hand-written number. - Scope each rule to the facets it belongs to instead of duplicating it per profile.
- Reconsider anything language- or codec-related — a name-matching format may be better as a post-download rule against
input.file.
Scryer ships templates for most of the patterns above; start from Settings → Rules rather than from a blank editor.
Related Concepts
Section titled “Related Concepts”- Custom Rules — the rules engine, authoring guidance, templates, and rule packs
- Quality Profiles — eligibility criteria, cutoffs, and minimum score to grab
- TRaSH Guides And Scoring Personas — the built-in scoring your rules add to
- Context-Free Vs Context-Aware Release Parsing — where the parsed fields a rule reads come from
- Migrate From Sonarr/Radarr — moving an existing setup across