Subreddit Config Schema¶
The per-subreddit toolbox config (removal reasons, mod macros, ban macros) is stored as JSON on a wiki page. There are two schema versions, split across two wiki pages:
Schema |
Page |
Written for |
|---|---|---|
v1 |
|
6.x clients (legacy / compatibility mirror) |
v2 |
|
7.x clients (NXG layout) |
The in-memory model is always v2. normalizeConfig up-converts anything it reads (v1 pages, hand-edited pages, cached configs) and encodeClassicConfig (extension/data/modules/config/codec.ts) down-converts on every write to the legacy page. Unmigrated subs read and write only the legacy page, but still in v1 on the wire; migrated subs with 6.x compatibility enabled double-write.
Schema reference¶
Top-level config object¶
{
"ver": 2,
"removalReasons": { ... },
"modMacros": [ ... ],
"banMacros": { ... },
"showRetiredUsernoteShards": false,
"requireUsernoteType": false,
"requireUsernoteText": true,
"requireUsernoteLink": false,
"usernoteRequirementOption": "leave",
"trainingMods": [],
"guardedActions": ["approve", "remove"],
"proposalRetentionDays": 14
}
Field |
Type |
Description |
|---|---|---|
|
integer |
Schema version; |
|
|
Removal reasons configuration block; always present |
|
|
Mod macro entries; empty array when none configured |
|
|
Ban form defaults; |
|
boolean |
NXG-only. When |
|
boolean |
NXG-only. When |
|
boolean |
NXG-only. When |
|
boolean |
NXG-only. When |
|
string |
NXG-only. How the three |
|
|
NXG-only. Usernames of moderators in training mode for this subreddit; their in-scope moderation actions are captured as proposals for review instead of being performed. Compared case-insensitively. Defaults to |
|
|
NXG-only. Optional allowlist narrowing which captured action types are guarded for this subreddit’s trainees. Absent (the default) guards every action type; a present array guards only the listed types (trainees take any other action directly); an empty array guards nothing. Recognized proposal action discriminants are |
|
integer |
NXG-only. How many days a resolved proposal is retained before pruning, unless its proposer dismisses it sooner. Clamped to an integer in |
Note
Domain tags and usernote type colors were previously stored here as domainTags: DomainTag[] and usernoteColors: UserNoteColor[]. Both now live on dedicated wiki pages — domain tags on toolbox-nxg/domain-tags (with a richer schema including approval/removal counts, glob patterns, notes, and alert thresholds; see Domain Tags Schema), and usernote types in the usernotes manifest’s types array (see Usernotes Schema). NXG strips both legacy fields from the config page on load so they don’t round-trip back.
RemovalReasonsConfig¶
{
"reasons": [ ... ],
"header": "---\n\n*I am a bot...*",
"footer": "",
"pmsubject": "Your {kind} was removed from /r/{subreddit}",
"logsub": "moderationlog",
"logtitle": "Removed: {kind} by /u/{author}",
"logreason": "",
"removalOption": "suggest",
"typeReply": "reply",
"typeStickied": false,
"typeLockComment": false,
"typeCommentAsSubreddit": false,
"typeAsSub": false,
"autoArchive": false,
"typeLockThread": false,
"editableReasonsEnabled": false
}
Field |
Required |
Type |
Description |
|---|---|---|---|
|
yes |
array |
List of configured removal reasons |
|
no |
string |
Markdown prepended to every removal message |
|
no |
string |
Markdown appended to every removal message |
|
no |
string |
Name of another subreddit whose removal reasons to use instead |
|
no |
string |
Subject line template for removal PMs; supports substitution tokens |
|
no |
string |
Subreddit to post the removal log to |
|
no |
string |
Title template for the removal log post; supports substitution tokens |
|
no |
string |
Default reason text pre-filled in the log post; supports substitution tokens |
|
no |
string |
How delivery settings apply to other mods: |
|
no |
string |
Default reply type: |
|
no |
boolean |
Whether the reply is stickied by default |
|
no |
boolean |
Whether the reply locks the removed comment by default |
|
no |
boolean |
Whether the reply is sent as the subreddit by default |
|
no |
boolean |
Whether the removal message is sent via modmail as the subreddit by default |
|
no |
boolean |
Whether modmail threads are auto-archived after sending by default |
|
no |
boolean |
Whether the target thread is locked after removal by default |
|
no |
boolean |
When true, moderators may edit reason text before sending |
|
no |
|
NXG-only. Maps report text to removal reasons that are pre-selected in the removal overlay when a queue item’s report matches. Dropped entirely when empty; stripped from the v1 mirror. |
RemovalReason¶
{
"id": "abc12345",
"title": "Rule 1: No spam",
"text": "Your post has been removed for {select:rule}.\n\nPlease review our rules.",
"selects": [
{
"name": "rule",
"prompt": "Which rule was broken?",
"options": ["Rule 1: No spam", "Rule 2: Be civil"]
}
],
"removePosts": true,
"flairText": "Removed",
"flairCSS": "",
"flairTemplateID": ""
}
Field |
Required |
Type |
Description |
|---|---|---|---|
|
no |
string |
Stable 8-character base-36 identifier; assigned by NXG, absent in v1 mirrors |
|
yes |
string |
Display title shown in the removal overlay |
|
yes |
string |
Markdown body of the removal message; may contain substitution and interactive tokens (v2) |
|
no |
|
Named pick-one choice definitions referenced from |
|
no |
boolean |
When |
|
no |
boolean |
|
|
yes |
string |
Post flair text to apply after removal; empty string for none |
|
yes |
string |
Post flair CSS class to apply; empty string for none |
|
yes |
string |
Post flair template ID to apply; empty string for none |
|
no |
boolean |
When true, the moderator may edit this reason’s text before sending |
|
no |
string |
Default usernote text pre-filled when this reason is selected |
|
no |
string |
Key of the usernote type ( |
SelectDefinition¶
Stored in RemovalReason.selects; referenced from reason text as {select:name}.
Field |
Required |
Type |
Description |
|---|---|---|---|
|
yes |
string |
Slug-safe name ( |
|
no |
string |
Optional label shown above the choices; omitted (never |
|
yes |
|
Choice texts; each is both the visible label and the value inserted into the message |
SuggestedReasonMapping¶
NXG-only. Stored in RemovalReasonsConfig.suggestedReasons; stripped from the v1 mirror. Each mapping links report text to one or more removal reasons. When a queue item carries a report whose text contains the mapping’s pattern (case-insensitive substring), the referenced reasons are pre-selected when the moderator opens the removal overlay. Reports filed by any moderator or bot are matched by default; user reports are matched only when includeUserReports is set. See Removal Reasons → Suggested removal reasons.
{
"id": "sug00001",
"pattern": "low effort post",
"includeUserReports": true,
"reasonIds": ["abc12345"]
}
Field |
Required |
Type |
Description |
|---|---|---|---|
|
no |
string |
Stable 8-character base-36 identifier; assigned by NXG, may be absent in hand-edited configs |
|
yes |
string |
Report text to look for, matched as a case-insensitive substring; an entry with an empty pattern is dropped |
|
no |
boolean |
When |
|
yes |
|
Ids of the |
MacroConfig¶
Mod macros are stored in modMacros as an array of objects. All fields except text are optional.
{
"id": "xyz98765",
"title": "Lock and warn",
"text": "This thread has been locked for {select:reason}.",
"remove": false,
"lockthread": true,
"distinguish": true,
"sticky": true,
"contextpost": true,
"contextcomment": false,
"contextmodmail": false
}
Field |
Required |
Type |
Description |
|---|---|---|---|
|
no |
string |
Stable 8-character base-36 identifier; absent in v1 mirrors |
|
yes |
string |
Macro reply text; supports markdown and substitution tokens |
|
no |
string |
Display title shown in the macro picker |
|
no |
boolean |
Remove the target post or comment |
|
no |
boolean |
Approve the target post or comment |
|
no |
boolean |
Mark the target as spam |
|
no |
boolean |
Ban the target post/comment author |
|
no |
boolean |
Unban the target post/comment author |
|
no |
boolean |
Mute the target author in the subreddit |
|
no |
string |
Flair template ID to apply to the author |
|
no |
string |
Display text for the flair template |
|
no |
boolean |
Lock the target post or comment thread |
|
no |
boolean |
Lock the reply posted by this macro |
|
no |
boolean |
Sticky the macro reply (only effective on top-level comments) |
|
no |
boolean |
Archive the modmail thread after sending |
|
no |
boolean |
Highlight the modmail thread after sending |
|
no |
boolean |
Distinguish the macro reply as a moderator comment |
|
no |
boolean |
Post the reply as the subreddit ModTeam account via official removal message |
|
no |
boolean |
Show this macro in post contexts; defaults to |
|
no |
boolean |
Show this macro in comment contexts; defaults to |
|
no |
boolean |
Show this macro in modmail contexts; defaults to |
BanMacros¶
Ban form defaults stored in banMacros. The field is null when not configured.
{
"banNote": "Permanent ban: repeated rule violations",
"banMessage": "You have been permanently banned for repeatedly violating our rules.",
"defaultBanPermanent": true,
"defaultBanDuration": 0,
"banDurationPresets": [3, 7, 30]
}
Field |
Required |
Type |
Description |
|---|---|---|---|
|
yes |
string |
Internal mod note pre-filled into the ban form |
|
yes |
string |
Ban message pre-filled into the ban form (sent to the banned user) |
|
yes |
boolean |
Whether the ban defaults to permanent |
|
yes |
integer |
Default temporary ban duration in days; |
|
yes |
|
Quick-select duration buttons in the ban form (days, 1–999) |
Substitution tokens¶
Removal reason text, headers, footers, log titles, and macro text support substitution tokens. These are bare {word} tokens with no colon — distinct from the interactive brace tokens described in v2 vs v1.
Token |
Replaced with |
|---|---|
|
Username of the post or comment author |
|
Name of the subreddit |
|
|
|
Title of the post |
|
Permalink to the removed post or comment |
|
Domain the post links to |
|
Username of the moderator sending the removal |
|
Body of the removed item, quoted as markdown (lines prefixed with |
|
Reddit fullname of the removed item (e.g. |
|
Short base-36 id of the removed item |
|
URL the post links to |
|
Body without markdown quoting |
|
URL-encoded body, for use inside markdown links |
|
URL-encoded title, for use inside markdown links |
Unknown {…} content (no colon, no match) is left in the text untouched.
v2 vs v1¶
No encoding. v1 stores removal reason text, the removal header/footer, and macro text escape()-encoded because 6.x unescape()s them unconditionally on read. v2 stores every string as plain text. (normalizeConfig only URI-decodes configs with ver < 2, so a literal %20 in v2 text survives.)
Interactive tokens instead of limited HTML. v1 reason text could embed literal <input>, <textarea>, and <select>/<option> elements that the removal dialog turns into fill-in fields. v2 replaces them with brace tokens, which can’t collide with reddit markdown and sit naturally beside the existing substitution tokens ({subreddit}, {author}, …, which are unchanged):
{input: placeholder text}
{textarea: placeholder text}
{select:rule}
Select options live in the reason’s selects array, not inline in the reason text. A select token references one named definition:
{
"text": "Please review {select:rule}.",
"selects": [
{
"name": "rule",
"prompt": "Pick the rule that applies",
"options": [
"Your post breaks rule 1 | see [the rules](https://example.com/rules)",
"Your post breaks rule 2"
]
}
]
}
Each token may carry an optional stable id used to persist the entered value between overlay opens: {input#flightnum: Flight number}. For selects, the definition name is the stable id and maps to the HTML id attribute on the v1 mirror; a select prompt rides along as a label attribute there (invisible in 6.x, recovered on up-convert). Token content cannot contain braces (the serializer substitutes parens), and option line breaks collapse to spaces on the v1 mirror because legacy options are single-line.
The codec for both directions lives in extension/data/modules/shared/removalReasons/tokens.ts. Conversion notes:
<br>becomes a paragraph break (\n\n); the v1 mirror keeps plain newlines, which 6.x handles fine.A legacy
<option value="x">label</option>keepsx(the value is what 6.x inserted into the removal message); the label is dropped.
Stable entry ids. Every removal reason and mod macro carries an id (eight base-36 characters). Ids are assigned by the editing UI on create and backfilled by ensureStableIds during normalization; they are stripped from the v1 mirror (6.x rebuilds entries wholesale on save, so they wouldn’t survive there anyway — they’re re-backfilled on the next normalize). They exist so reordering and cross-references don’t have to rely on array indexes.
Versioning¶
ver on the page selects the schema. configSchema / configMinSchema / configMaxSchema in extension/data/util/wiki/schemas/config/schema.ts define what a build writes and accepts. configMigrations is a registry of in-place upgrade steps keyed by the version each upgrades from; it is currently empty because the v1 → v2 upgrade has no discrete migration — its string decode and HTML-to-token conversion run unconditionally in normalizeConfig (the conversion doubles as self-healing for v2 pages). To bump the schema again, add a migration keyed by the version it upgrades from, raise configSchema/configMaxSchema, and extend encodeClassicConfig if the new fields need a v1 representation.