| name | mutagen-reference |
| description | Look up the schema of any Skyrim record type โ its fields, their types, which are writable, the legal enum values, and the arms a polymorphic field accepts โ from the bundled record-schema reference before reading or editing that record type. Use when the user wants to edit, patch, or override a record (ARMO, WEAP, MGEF, NPC_, CELL, โฆ), asks what fields a record type has or what an xEdit signature maps to, whether a field is writable, or what values an enum field allows (legal ActorValue, Aggression, or CastType values), or needs a field's name, type, or cardinality before composing a record change. Load before answering any "what fields does X have" or "can I set Y on Z" question and before composing any record write โ the reference is by construction from the game's full record library, so a type that's absent is a real coverage gap to surface, never something to guess. |
Mutagen Reference
Overview
This skill provides offline lookup of record-type schemas for Skyrim plugins: for any record type, the fields it has, each field's type and cardinality, whether each field is writable, the legal values of an enum field, and the arms a polymorphic field can take. It is a reference-lookup skill: it carries no procedure, write action, or routing โ just a single lookup, what is the shape of this record type? (papyrus-reference is the sibling that does the same for Papyrus function signatures.)
The reference is generated by construction by reflecting over the game's record library (Mutagen) and ships in this skill's references/ tree โ so the set of record types it knows is the set the library models, not a hand-maintained subset. That property is what makes the bundled-or-warn fallback trustworthy (below).
It serves both reading and writing. Consult the schema to understand a record you're about to read (what fields exist), and โ equally โ to compose a legal change before a write: is the field writable? what type must the value be? what enum values or polymorphic arms are legal? Schema, not instance: "what fields does ARMO have and which are editable" is this skill; "what is the armor rating of this steel armor in this plugin" is a record read laid against this schema, not a schema lookup.
First step
When you need the shape of a record type โ to answer a question about it, or to compose a read or write โ open the index at references/index.jsonl. It is JSONL (one entry per line) and maps every type's name and xEdit signature to the shard file and line where its schema block lives. The index is grep-friendly: a "name":"Armor" or "sig":"ARMO" scan narrows in one step.
Do not bulk-load the index or a shard. Grep the index for the one matching line, then read the single schema line it points to โ each block is exactly one line, so the read is one line. Both reads are tiny; whole-loading defeats the token economics and, worse, invites answering from a stale memory of the file instead of ground truth (see "Always fetch fresh").
Lookup procedure
-
Identify the record type. From the user's words or the task: a type name (Armor, MagicEffect), an xEdit signature (ARMO, MGEF), or a record you're reading or patching. Modders usually think in signatures; the library names types in full words โ and the two often differ (ALCH is Ingestible, ENCH is ObjectEffect, CLFM is ColorRecord). The index carries both, so either resolves.
-
Grep references/index.jsonl. Match the full quoted token โ "name":"<Type>" or "sig":"<SIG>", closing quote included โ so the hit is exact and field-scoped. A partial or fuzzy hit (a 3-letter "sig":"WEA" brushing WEAP, or a substring landing inside another entry) is not a match: re-check the spelling and route it to "Bundled-or-warn", never substitute the nearest record. Three result shapes:
- Single exact match โ take its
file and line, read that one line (step 3).
- Several matches for one signature โ the library splits some signatures into typed variants:
GMST โ GameSettingBool / Float / Int / String, GLOB โ GlobalFloat / Int / Short / Unknown. Pick the variant whose data type matches the value in hand (a float game setting โ GameSettingFloat); read more than one block if you must to disambiguate.
- No match โ go to "Bundled-or-warn". Do not invent a schema, and do not substitute the nearest-looking record.
-
Block-read the schema. Read exactly the line the index named (offset = line, limit = 1) from the shard. That one line is the whole schema for the type. Do not read the rest of the shard.
-
Resolve references on demand. A field that points at another modeled type carries a ref (a substruct or enum it points to), arms (a polymorphic field's permitted types), or target (the record a FormLink points at). To learn that referenced type's own shape โ an enum's legal values, a substruct's fields, an arm's shape โ grep the index for its name and block-read it the same way. This is how enums resolve: a field reads "c":"enum","ref":"ActorValue", and the legal ActorValue names live once on the ActorValue entry in enums.jsonl, fetched only when you need them.
Worked example. "Can I set the armor rating on armor?" โ grep the index for "sig":"ARMO" โ records.jsonl line 9 โ block-read โ ArmorRating is {"c":"scalar","w":true,"t":"float"} โ yes: writable, a float.
Always fetch fresh โ never answer a schema question from memory
Every lookup reads the entry fresh. Do not answer "what fields does X have" or "is Y writable" from a schema you think you remember from earlier in the session. Two reasons, both load-bearing:
- A schema half-remembered is a schema mis-stated, and a confidently wrong field name or writability flag is exactly the silent-wrong-answer failure this project exists to prevent. The file is ground truth; your memory of it is not.
- Long sessions get compacted. A schema you "saw" earlier may have been summarized to a stub, and reconstructing it from that stub invents fields and line numbers. Re-grep, re-read.
- This applies to presence as much as content: never assert from memory that a type exists or is absent. The "no schema for X" warning below must follow a fresh grep that just missed โ not a recollection that X wasn't there earlier.
The reads are cheap by design โ one index line plus one block line. There is no economy worth buying with a guess.
Bundled-or-warn โ never invent a schema
The reference covers exactly the record types the game's record library models. By construction the generator walks the whole library, so coverage is the library's coverage โ which means a type being absent from the index means one specific thing: the library doesn't model it. That is the known, documented gap between the library and xEdit, not a houseCARL bug, and not license to guess.
When the index has no match for a requested record type or signature:
-
Emit an explicit warning. Use this shape:
No schema for record type `XXXX` โ the bundled reference doesn't include it.
The reference is generated from the full record library by construction, so an
absent type means the pinned Mutagen version (see this skill's Notes) doesn't
model it โ the documented library-vs-xEdit coverage gap, not a houseCARL bug.
I won't invent a schema.
To proceed:
- Double-check the type name / signature spelling (the index carries both forms)
- If you can confirm in xEdit that the type exists there but not in the library, that confirms the known gap
- For a write, stop here: composing against a guessed schema risks a malformed record
-
Never invent fields, types, writability, or enum values. A guessed schema can produce a record that looks valid and corrupts on load, or send the user chasing a "bug" that is really an authoring error. A clear non-answer beats a confident wrong one โ and here the non-answer is informative: it pinpoints a real coverage boundary.
The index and schema shapes
references/index.jsonl โ one entry per line:
{"name":"Armor","sig":"ARMO","kind":"record","file":"references/records.jsonl","line":9}
{"name":"ActorValue","kind":"enum","file":"references/enums.jsonl","line":4}
name โ the library's type name; the primary lookup key.
sig โ the xEdit 4-char signature (records only: ARMO, MGEF). One signature can map to several names (the typed-variant case above).
kind โ record / header / struct / arm / polymorphic-base / enum.
file + line โ the shard and 1-indexed line of the schema block; block-read it directly.
Schema blocks live in per-kind shards (records.jsonl, structs.jsonl, arms.jsonl, polymorphic.jsonl, enums.jsonl), one compact JSON object per line. A record / struct block:
{"name":"Armor","kind":"record","sig":"ARMO","getter":"...IArmorGetter","mutable":"...IArmor","writable":"32/32","fields":[{"n":"ArmorRating","t":"float","c":"scalar","w":true},{"n":"Keywords","t":"List<FormLink<IKeywordGetter>>","c":"list","w":true,"target":"IKeywordGetter"},{"n":"MajorFlags","t":"MajorFlag","c":"enum","w":true,"ref":"MajorFlag"}]}
Field keys are terse to stay light:
n name ยท t type (display) ยท c cardinality (scalar / enum / formlink / list / dict / substruct / polymorphic / value) ยท w writable (true/false).
- Sparse keys, present only when they apply:
ref (the substruct or enum this field points to โ grep the index for it), arms (a polymorphic field's permitted types), elem / elemRef / elemArms (a list/dict element's type, modeled-type ref, or polymorphic arms), key (a dict's key type), target (the record a FormLink points at), null (the field is nullable), id (a record-identity field โ FormKey/ModKey, not free-edit content).
Each block also carries provenance keys you can ignore for a lookup: getter / mutable (the type's interface names) and, on an arm, base (its polymorphic parent).
An enum block carries its legal values:
{"name":"ActorValue","kind":"enum","values":["Aggression","Confidence", "...", "None"]}
A type's writable is its writable/total field count; a field's own w is what governs whether you can set that field.
Addressing a field & what you can write
The schema tells you a field's c (cardinality) and w (writable). That same c determines how you name the field in a houseCARL write tool's path and which verbs the write tool accepts โ so once you've read the schema, you can compose a legal housecarl_set_field / housecarl_bulk_apply / housecarl_create_record edit without guessing. This table maps each cardinality to its path-form + legal verbs (it mirrors the write tools' own pre-flight rules โ that enforcement is the source of truth, this is the reading view of it):
c (cardinality) | How to address it in the path | Verb(s) the write tool accepts | Notes |
|---|
scalar / enum / formlink / value | the dotted name, e.g. ArmorRating, BasicStats.Damage, MajorFlags | Set (the default) | The value is coerced to the field's t: a number, an enum name (one of the referenced enum's values), or a FormID XXXXXX:Plugin.esp for a formlink. To clear a nullable field, use Remove. |
substruct | descend by name to the sub-field you want โ WorldModel.Male.Model.File, not WorldModel | Set on the leaf sub-field | A direct Set on the substruct itself is refused โ navigate into it and set a sub-field. The schema's ref names the substruct's own type; grep + block-read it to learn its fields. |
list | [N] mid-path to step into an element (Effects[0].Data.Magnitude); at the leaf, target the list field itself and use verb + key (the index) โ not a leaf bracket | Add, Remove, SetAtIndex, ReplaceAll (not Set) | To add a modeled element (a leveled-list entry, an effect), Add with compose:{type:'<ElementType>', ...}. For a coercible-element list (e.g. a list of FormLinks), Add/ReplaceAll take plain value(s). |
dict | [key] to step into an element mid-path; at the leaf use verb + key | Set (with key), Add, Remove, Merge, ReplaceAll | Merge / ReplaceAll take an entries keyโvalue map. The key schema key gives the dict's key type. |
polymorphic as a list element | step into the element (Scripts[0].Properties[0].Object) and address the field on the element's concrete arm | element-level verbs (above) | The library models the list as the polymorphic base, but each real element is a concrete arm. A field that lives on an arm (ScriptObjectProperty.Object) is still addressable by name; the write tool resolves which arm at apply time. To Add an element, compose:{type:'<concrete arm>', ...} (e.g. 'ScriptObjectProperty'). |
polymorphic as a standalone field | descend by name (Configuration.Level.Level), and to set the arm itself use a compose:{type:'<arm>', ...} on a Set of the polymorphic field | Set carrying a compose arm; or descend and Set a sub-field of the live arm | See the standalone-polymorphic note below โ never index a standalone polymorphic field with a bracket. |
Standalone polymorphic fields (NpcConfiguration.Level, ConditionFloat.Data, โฆ). You can now both descend one (Configuration.Level.Level resolves the sub-field across the base's arms) and set its arm with a nested compose: a Set whose compose:{type:'<arm name>', sets:[...]} selects which arm sits there (e.g. composing a Condition's Data as compose:{type:'GetActorValueConditionData', sets:[...]}). The legal arms are the field's arms (or the referenced polymorphic-base's arms) โ block-read them from arms.jsonl rather than guessing; this reference does not inline an arm list per type (coverage is by construction, so the arm set lives once on each base/field). A standalone polymorphic field is never addressed with a bracket (Data[...] is wrong) โ brackets are for list/dict elements only.
Two honesty boundaries the write tool enforces, worth stating when you compose against this schema:
- The arm is resolved at apply time, not from the schema. The static schema can't know which arm currently sits at a polymorphic field, so a path like
Configuration.Level.Level is accepted whenever some arm declares Level โ and if the live arm is a different one, the write fails loud and writes nothing, never silently. (When a name lives on several arms with disagreeing shapes, the tool refuses up front and names the conflict rather than guessing.)
- A composed record missing a required arm fails loud. If you
compose or Add a modeled element and leave a required polymorphic sub-field unset (a Condition composed without its Data arm, a leveled-list entry missing required data), the write is refused at serialize time with a named error and nothing is written โ all-or-nothing, never a half-written patch. So when you compose an element, set its required sub-arm in the same compose.
Condition (CTDA) form-link targets are FormLinkOrIndex, shown here as FormLink<T>. A form-link parameter on a *ConditionData arm โ GetEquipped.ItemOrList, GetGlobalValue.Global, GetIsID.Object, GetStage.Quest, HasPerk.Perk, and the rest โ is really a Mutagen FormLinkOrIndex<T>, but this reference (and housecarl_read_record) normalize it to FormLink<T> in the displayed t/c, because a condition target can hold either a real FormID or a numeric quest-alias / package-data index. So the schema understates the type: do not read FormLink<IItemOrListGetter> on a condition param as a plain link. When you compose one, give it a FormID XXXXXX:Plugin.esp (form mode) or alias N / packdata N / a bare integer (index mode); houseCARL routes it through the parent-aware setter and sets the arm's UseAliases/UsePackageData mode for you, so both the flat compose.fields shorthand and the nested compose.sets path accept it and produce the identical write. The reference can't expose the FormLinkOrIndex distinction (it's a by-construction normalization of the underlying type) โ when the form-vs-index nature matters, confirm it at the engine, not from the displayed t.
Common mistakes
- Answering a schema question from memory. Re-grep and re-read every time. A remembered schema is the silent-wrong-answer trap (see "Always fetch fresh").
- Inventing a schema for an absent type. The bundled-or-warn path is the correct response โ an absent type is a real library-coverage boundary, not a prompt to guess.
- Picking the first hit when a signature has several variants. A
sig grep that returns GameSettingBool/Float/Int/String needs disambiguation by the data type, not first-match.
- Reading a whole shard instead of the one line. Every index entry gives an exact
line; a whole-shard read pulls in hundreds of unrelated types. Only widen the read if a block looks malformed (a generation bug worth reporting).
- Treating a
ref / arms / target as the answer. Those are pointers. To learn the referenced enum's legal values or the substruct's fields, grep the index for that name and block-read it too.
- Reading
w:false as "broken". Some fields are genuinely read-only in the library (computed, or no mutable accessor). That is the real schema, not a gap โ compose writes only against w:true fields.
- Bracketing the wrong cardinality.
[N]/[key] step into a list/dict element only โ a substruct is descended by name and a standalone polymorphic field is set by compose (see "Addressing a field"). A bracket on a substruct or a standalone polymorphic field is refused.
- Reading a condition param's displayed
FormLink<T> as a plain link. On a *ConditionData arm it's a normalized FormLinkOrIndex<T> โ it also accepts an alias N / packdata N index, and houseCARL composes it through the parent-aware setter from either the compose.fields shorthand or the compose.sets path (see the condition-target note under "Addressing a field").
Notes
- Provenance. The
references/ tree is generated by construction by reflecting over Mutagen.Bethesda.Skyrim (0.53.1) โ the same walk that produces houseCARL's write-surface rulebook, so the skill's read view and houseCARL's write tools can't disagree about field names or types. It refreshes by regenerating from the library on a version bump.
- Coverage is the library's coverage. Every record type, sub-struct, polymorphic arm, and enum the library models is here, at full depth. The only thing not here is what the library itself doesn't model (the documented xEdit-delta), which the bundled-or-warn path surfaces explicitly rather than papering over.