This reference freezes the wrapper-facing contract proven so far from preserved xEdit automation artifacts. JSON examples are linked to their source captures in examples/ and are checked by the automation contract drift checker.
- Current
contractVersion:0.22, pending Phase 17 Starfield runtime verification. - Client rule: ignore unknown keys on objects and arrays unless a later contract version explicitly says otherwise.
supports.jobs.kindsis frozen byte-for-byte across the0.7to0.8delta. The script-execution descriptors are adjacent to the job-kind list; script execution is not added tojobs.*.
Call system.capabilities before assuming wrapper behavior. The fields to key on are under result.supports.scripts.execution:
synchronous: truecancelable: falseoverlapPolicy: "single-process-single-runner"busyHolders: ["daemon", "gui"]failureMessagesOnError: trueiKnowWhatImDoing: false|truereflecting daemon launch state
- Transport is a Windows named pipe.
- Pipe naming convention for a spawned daemon is
\\.\pipe\xedit-<pid>where<pid>is the xEdit process ID written by the launcher state file. - Requests and responses are UTF-8 JSON.
- One connection carries one request and one response; the client connects, writes a single JSON request object, reads one full JSON response message, and disconnects.
- Requests use the shape
{ "command": "<name>", "args": { ... } }. An empty arg object is still an object ({}), not an array.
The frozen supports.jobs.kinds list remains:
files.hygiene.batchplugin.esl.analyzeplugin.esl.applyplugin.formids.compact_for_eslvalidation.check_for_errorsvalidation.check_for_itmvalidation.check_for_deleted_refscleaning.quick_cleancleaning.quick_auto_cleancleaning.sort_and_clean_masters
See examples/01-capabilities.md for the source-linked response.
- Locator objects require
file. formIdis optional unless the addressed command requires a record identity.pathis optional. Missingpathdefaults to the record root and resolves the same way aspath:"".
When native wbCopyElementToFile returns nil, records.copy_into still returns
mutation_not_allowed. In 0.22 it forwards the native gate reason when it can
identify one:
Source contains Reflection and can not be copiedSource contains Unmapped FormID and can not be copied into a module which does not have the game master as its first master
Other nil-copy failures keep the existing generic message:
Automation records.copy_into could not identify the copied main record.
See docs/plans/2026-06-07-xedit-phase13-elements-mutation-surface-design.md §4 for canonical request/response shapes.
Typed NativeValue write. Optional kind enum: int, float, string, bool, formId, formIdArray. Without kind, JSON type drives marshaling.
Mutation verbs deferring to native IsEditable/IsClearable predicates. Response includes before and after element envelopes.
Locator-drift family. Response includes before.locator, after.locator, top-level locator (post-mutation, authoritative), and pathChanged: bool. Caller MUST continue from after.locator after a successful operation.
Read-only discovery. No consent gate. edit_capabilities returns predicates/operations/assign blocks; assign_templates returns a narrow template list.
Optional args:
targetIndex(defaultwbAssignAdd)templateIndex/templateName— must agree if both provided- Ambiguous multi-template targets without selector return
mutation_not_allowedwithdetails.availableTemplates. (Note: this refusal-with-details branch is implemented but unverified end-to-end against FO4 fixtures in 0.11 — no probed vanilla FO4 container exposedassign_templates count >= 2. Clients should prefer the two-callassign_templates+add_child{templateName}pattern.)
Optional args:
targetIndex(defaultwbAssignAdd)addRequiredMasters(defaultfalse)
Response includes masters.added/alreadyPresent/skipped and placement.sortOrderApplied (internal GUI-equivalent fixup).
No new error codes. Reuse invalid_request / invalid_target / read_only_target / mutation_not_allowed / consent_required / file_not_found / record_not_found / element_not_found.
Successful scripts.run stays synchronous. A success response has ok: true, command: "scripts.run", and result fields including:
idcompile.oklintBypassedlintWarningsranInitializeranFinalizeprocessedterminatedEarlyterminationCodetimedOutstatementBudgetExceededmessagesmessagesTruncateddirtyFiles
The success example in examples/02-scripts-run-success.md is a preserved real capture of the same success envelope shape; the docs-only contract pass did not change product code or rerun a new success fixture.
Failed scripts.run responses have ok: false, command: "scripts.run", and error with stable code, human-readable message, and per-code details. Clients must branch on error.code, not on error.message text.
For lifecycle-bearing script failures, the contract includes:
error.details.messages: ordered JSON array of captured script messages, present even when empty.error.details.messagesTruncated: boolean;truemeans one or more emitted messages were dropped or clipped before response emission.
See examples/03-scripts-run-failure.md for a runtime failure carrying messages and messagesTruncated.
Unknown future keys may appear. Clients should consume the documented keys below and ignore extra keys.
lintWarningslintScope
holder
sourceUnitlineranInitializeranFinalizeprocessedmessagesmessagesTruncated
errorLocationwhen availableranInitializeranFinalizeprocessedmessagesmessagesTruncated
timeoutMselapsedMsranInitializeranFinalizeprocessedmessagesmessagesTruncated
maxStatementsranInitializeranFinalizeprocessedmessagesmessagesTruncated
runtimeDenieddeniedIdentifierwhen denial was detectedtargetIndexwhen Process-phase failure indexing is meaningfulranInitializeranFinalizeprocessedmessagesmessagesTruncated
Runtime policy denials continue to normalize as script_runtime_error with runtimeDenied: true; this contract does not introduce script_policy_denied.
The contract advertises one process-wide script execution token shared by daemon script runs and GUI Apply Script. supports.scripts.execution.overlapPolicy is single-process-single-runner, and the possible holders are daemon and gui.
If the token is already held, clients should expect a failed scripts.run response with error.code: "script_busy" and error.details.holder naming the active holder. The GUI-held overlap is proven by manual GUI evidence: a daemon scripts.run was attempted while GUI Apply Script held the token, and the response returned script_busy with error.details.holder: "gui".
The daemon-held mirror case remains Not Covered for an architectural reason, not because the response shape is undefined: daemon scripts.run executes synchronously on the GUI thread, freezing the GUI message pump while the daemon holds the guard. A user click queued during that freeze cannot reach the GUI Apply Script guard-acquire boundary until after the daemon releases the token, so the normal operator path cannot witness a GUI-side script_busy refusal for holder daemon. This is covered by the architectural rationale in ARCHITECTURE.md, which also cites the shared guard call sites.
Record and element locators use the stable {file, formId, path} shape. The locator audit captured root-locator.json with file: "Fallout4.esm", formId: "0000003C", and path: "", and nested-path.json with a nested locator.path of "[0]" echoed as object.path: "[0]".
Load-order FormID lookup remains the primary identity rule; file-local lookup is a compatibility carve-out, not a second addressing model. The audit artifact load-order-lookup.json found six hits for FormID 0000003C, including the Fallout4.esm hit, and included both masterOrSelf and winningOverride readback fields.
Phase 15A adds a read-side navigation layer for xEdit ChildGroup-owned content without adding new verbs. Clients keep using elements.children, elements.get, and record locators; the only new locator vocabulary is the synthetic \Child Group prefix.
Locator paths starting with \Child Group engage ChildGroup-aware resolution. The full grammar:
\Child Group— the owning record's ChildGroup (a siblingIwbGroupRecord).\Child Group\<sub-label>— a named sub-group or named child inside the ChildGroup.\Child Group\<sub-label>\...— further descent (WRLD only, for Block / Sub-Block).
Prefix detection is tight: only the exact \Child Group path or a path beginning \Child Group\ enters the ChildGroup resolver. Other paths preserve the existing locator behavior.
| Parent | GroupType | Sub-labels |
|---|---|---|
| CELL | 6 | Persistent, Temporary, Visible when Distant |
| WRLD | 1 | Persistent (returns persistent worldspace CELL record), Block <X>, <Y>, Block <X>, <Y>\Sub-Block <X>, <Y> |
| DIAL | 7 | (none — contents are INFOs surfaced directly) |
| QUST | 10 | (none — contents are DLBR/SCEN/DIAL surfaced directly; only present when wbVWDAsQuestChildren=True) |
The WRLD walk uses ordinary elements.children breadcrumbs for every step:
records.get { file:"Fallout4.esm", formId:"0000003C", path:"" }returns the CommonwealthWRLDrecord.elements.childrenon that WRLD root appends a trailing child withobject.kind:"child_group"andlocator.path:"\\Child Group".elements.childrenonpath:"\\Child Group"returns the persistent worldspace CELL as a flat record locator pluschild_groupstubs such aspath:"\\Child Group\\Block 0, 0".elements.childrenon a Block stub returns Sub-Block stubs such aspath:"\\Child Group\\Block 0, 0\\Sub-Block 0, 0".elements.childrenon a Sub-Block stub returns exterior CELL records with flat FormID locators; a returned CELL record's ownelements.childrencan then expose itsTemporaryChildGroup and REFR/ACHR/etc. child records, again as flat record locators.
- READ-only: synthetic
\Child Grouppaths resolve throughelements.get,elements.children, and other read-side element lookup surfaces. - Mutation verbs (
elements.set_value,elements.set_native_value,elements.add_child,elements.remove_child,elements.copy_child_to, movement verbs, etc.) reject synthetic ChildGroup paths witherror.code:"invalid_target". To mutate a child record, use its flat FormID locator, which is what the breadcrumb emitted byelements.childrenalways uses for records.
ChildGroups with zero immediate children are suppressed; no stub appears. This is content-aware behavior, not a defect. Clients should treat the absence of a stub as "no ChildGroup content".
Block and Sub-Block labels use signed decimal coordinates matching xEdit GUI ShortName output, including the space after the comma: Block 0, 0, Block -3, 2, Sub-Block 0, 1.
Phase 15B extends the existing records.apply_filter discovery verb. Phase 15G
keeps the same verb and generalizes the identifier *Pattern / *Regex fields
to accept either a scalar string or an array of strings. Existing scalar glob
fields and response fields keep their previous meaning.
parentFormId: load-order FormID string. A candidate record matches when its xEdit container/ChildGroup ownership chain contains that MainRecord. This is an AND predicate with all other filters.- Regex alternatives to existing glob fields:
editorIdRegexdisplayNameRegexfullNameRegexbaseEditorIdRegexbaseDisplayNameRegex
As of contract 0.20, each of these ten identifier fields accepts either a
single string or an array of strings:
editorIdPattern,editorIdRegexdisplayNamePattern,displayNameRegexfullNamePattern,fullNameRegexbaseEditorIdPattern,baseEditorIdRegexbaseDisplayNamePattern,baseDisplayNameRegex
A scalar string is interpreted exactly as before. An array is evaluated with OR
semantics inside that field; predicates across different fields remain ANDed.
Arrays must contain 1..32 strings. Empty arrays, arrays longer than 32, and
non-string entries return invalid_request with error.details.invalidField
naming the malformed field.
Regex fields use System.RegularExpressions.TRegEx with roIgnoreCase and
roCompiled. Matching is partial by default; callers should add ^ / $ when
they want anchoring.
Each regex evaluation is bounded by a 100ms TTask.Wait wall-time check on the
daemon's main filter loop. The underlying TRegEx.IsMatch call is not
interruptible in this RTL, so a timed-out worker may finish later in the
background; the wait bounds daemon responsiveness, not worker lifetime. The daemon
treats that per-record wait expiry as a non-match and increments optional
result.regexTimeouts.
The regex worker pool is capped at four in-flight tasks. If all four slots are
already occupied by still-running workers, the candidate record is skipped without
starting another worker and optional result.regexSlotsExhausted is incremented
instead of result.regexTimeouts. Both fields are omitted when zero. Clients
should debug regexTimeouts as slow per-record evaluation and
regexSlotsExhausted as saturation under load or repeated slow patterns. Clients
should also avoid catastrophic-backtracking patterns; a future Phase 16+ Tier 2
slice is expected to move regex evaluation behind subprocess isolation for real
wall-time worker termination.
Do not provide both glob and regex for the same identifier family. For example,
editorIdPattern plus editorIdRegex returns invalid_request with
error.details.invalidField: "editorIdRegex".
Invalid regex syntax also returns invalid_request with invalidField naming the
bad regex argument.
This conflict rule applies regardless of scalar or array shape. For example,
editorIdPattern:["*Steel*"] plus editorIdRegex:"Steel" is rejected the
same way as two scalar fields.
{
"command": "records.apply_filter",
"args": {
"files": ["Fallout4.esm"],
"signatures": ["REFR"],
"parentFormId": "00000025",
"displayNameRegex": ["picket fence", "chair"],
"limit": 25
}
}Expected success shape is unchanged except for optional regex observability metadata:
{
"ok": true,
"command": "records.apply_filter",
"result": {
"truncated": false,
"hits": [],
"count": 0,
"regexTimeouts": 1,
"regexSlotsExhausted": 1
}
}Phase 15C extends records.references with one optional argument:
recursive(boolean, defaultfalse): when omitted or false, the command keeps the pre-0.15 shallow behavior and scans only the addressed record's own element tree. When true and the addressed record has a populated ChildGroup, xEdit walks ChildGroup-owned records withwbGetSiblingRecords, collects each child record's outgoing references, and unions them with the parent record's own hits.
The supports.referencesRecursive block advertises:
defaultRecursive: falseappliesTo: ["CELL", "WRLD", "DIAL", "QUST"]dedupBy: "loadOrderFormId"limitSemantics: "post-union-post-dedup"
Limit semantics are aggregate: deduplication happens across the parent and all
descended child-record hits, then the existing limit cap is applied to the final
hits array. Supplying recursive:true for a record with no populated ChildGroup
is a silent no-op and returns the same shape as recursive:false.
Example:
{
"command": "records.references",
"args": {
"file": "Fallout4.esm",
"formId": "00000025",
"path": "",
"recursive": true,
"limit": 100
}
}The success envelope remains { "truncated": bool, "hits": [...], "count": n }.
Phase 15C adds an optional result.childGroup sub-block to
records.conflict_status. It is omitted entirely when the addressed record has no
ChildGroup or when the ChildGroup is empty.
When present, the block contains:
count: number of ChildGroup-owned child main records included in the scan.hasConflict: true when any scanned child record reports a non-peaceful xEdit conflict state.signatures: object keyed by child record signature. Each value hastotalandconflictingcounts.conflictingHits: capped array of shallow record stubs for conflicting child records. Entries reuse the normal record summary/locator shape and include a shallowconflictobject.conflictingHitsTruncated: true when more than 20 conflicting child records were found.
The supports.conflictStatusChildGroup block advertises the sub-block key, field
names, omission rule, and conflictingHitsMax: 20. Existing main-record
record, conflict, and children fields are unchanged; clients that ignore the
new key keep pre-0.15 behavior.
Phase 15D extends records.create with an optional parent object for authoring
new records into ChildGroup-owning parents without introducing a new verb.
Existing top-level creation remains valid:
{
"command": "records.create",
"args": {
"targetFile": "patch.esp",
"signature": "KYWD"
}
}Parent-spec creation adds:
{
"command": "records.create",
"args": {
"targetFile": "patch.esp",
"signature": "REFR",
"parent": {
"file": "patch.esp",
"formId": "00000025",
"subGroup": "Temporary"
}
}
}parent.file and parent.formId address the parent main record. For writable
patch authoring, use a parent record owned by targetFile (for example, copy the
CELL/DIAL/QUST override first), then create the child record under that parent.
| Parent | Child target | subGroup |
|---|---|---|
CELL |
Cell ChildGroup sub-GRUP | Optional: Persistent, Temporary, Visible when Distant |
DIAL |
DIAL ChildGroup | Not allowed |
QUST |
QUST ChildGroup | Not allowed |
WRLD |
WRLD ChildGroup world CELL | Persistent or coords:[x,y] |
When CELL.subGroup is omitted, default selection is:
REFR,ACHR,PGRD,LAND,NAVM->Temporary- all other signatures ->
Persistent
For WRLD parents, signature must be CELL. Two mutually exclusive shapes are
supported: parent.subGroup:"Persistent" returns or creates the persistent
worldspace CELL, and parent.coords:[x,y] returns or creates the exterior CELL at
signed int16 grid coords through xEdit's native CELL[x,y] path. Supplying both
or neither shape returns invalid_request.
supports.createParentSpec advertises:
supportedParents: ["CELL", "DIAL", "QUST", "WRLD"]subGroupVocabulary.CELL: ["Persistent", "Temporary", "Visible when Distant"]subGroupVocabulary.WRLD: ["Persistent"]defaultSubGroup.CELLfor the default signature heuristicunsupportedParents: []in 0.18+ (retained for 0.16-era clients; no parent signatures are currently deferred)wrldDeferralReason: "superseded-by-0.18"in 0.18+ (sentinel retained so the former deferral field evolves additively instead of disappearing)wrldCoords: truewrldRequiresCellSignature: true
Signature validity is still native xEdit behavior. The CLI does not add a protocol-side allowlist for which signatures can be created under a parent.
Phase 15H extends the existing elements.children progressive-disclosure verb
with offset pagination so dense ChildGroups stay below the named-pipe response
ceiling without adding a streaming protocol or a new verb.
limit(integer, optional): default200; valid range1..1000inclusive.0, negative values, values above1000, and non-integers returninvalid_request.offset(integer, optional): default0; valid range>= 0. Negative values and non-integers returninvalid_request.
The existing children array remains. Four additive fields appear beside it:
{
"children": [
{ "locator": { "file": "Fallout4.esm", "formId": "00013A9D", "path": "" }, "object": { "kind": "record", "signature": "REFR" } }
],
"count": 1,
"total": 742,
"offset": 0,
"truncated": true
}count: number of entries returned in this response page. The Phase 15A synthetic ChildGroup stub counts as a returned entry when present.total: native immediate-child count for the addressed container. It does not include synthetic ChildGroup stubs.offset: the requested offset echoed back.truncated: true when more native children exist after the returned page.
For a main record with a non-empty IwbMainRecord.ChildGroup, the
object.kind:"child_group" navigation stub is appended only on the first page
(offset:0). Later pages do not repeat it. The stub is appended after the native
children in that page, counts toward count, and never counts toward total.
Single page:
{
"command": "elements.children",
"args": { "file": "Fallout4.esm", "formId": "000001F4", "path": "" }
}Returns count == total, offset == 0, and truncated == false.
Multiple pages:
{
"command": "elements.children",
"args": {
"file": "Fallout4.esm",
"formId": "00000025",
"path": "\\Child Group\\Temporary",
"limit": 200,
"offset": 200
}
}For the accepted FO4 fixture this returns count:200, total:742,
offset:200, and truncated:true.
Beyond total:
{
"command": "elements.children",
"args": {
"file": "Fallout4.esm",
"formId": "00000025",
"path": "\\Child Group\\Temporary",
"offset": 10000
}
}Returns children:[], count:0, total:742, offset:10000, and
truncated:false.
supports.elementsChildrenPagination advertises:
defaultLimit: 200maxLimit: 1000responseFields: ["count", "total", "offset", "truncated"]
Phase 15F extends existing read verbs with an optional includeParents boolean.
The default is false; omitting it preserves the pre-0.19 response shape.
includeParents:true is accepted by:
records.getrecords.find_by_form_idrecords.find_by_editor_idrecords.master_or_selfrecords.winning_overrideelements.getelements.children
When requested, the addressed record or returned entry includes
relations.parents. Entries reuse the standard shallow record summary shape:
{
"relations": {
"parents": [
{
"locator": { "file": "Fallout4.esm", "formId": "00000025", "path": "" },
"object": { "kind": "record", "signature": "CELL", "formId": "00000025" }
}
]
}
}Parent order is nearest-first (for example, REFR -> CELL -> WRLD). Top-level
records return an explicitly empty parents: [] when requested. If
includeParents is omitted or false, relations.parents is absent.
supports.reverseNavigation advertises:
optInArg: "includeParents"appliesTo: the seven verbs listed abovemaxAncestorDepth: 16ordering: "nearest-first"relationKey: "parents"
session.save is the explicit save seam. A successful response reports what xEdit did during that save operation:
savedFilesNow: files saved immediately in that call.savedFilesPendingShutdown: files whose save succeeded but whose final rename/durability is deferred until shutdown.savedNowCountandsavePendingShutdownCount: counts for the two arrays.dirtyState: dirty-state readback after the save operation.
A successful session.save response is not by itself a fresh-restart durability proof. Strong durability claims need a later restart/readback artifact. The durability audit captured:
audits\save-now.json:savedNowCount: 1,savePendingShutdownCount: 0, anddirtyState.dirty: falsefor a newly created first-save file.audits\save-pending.json:savedNowCount: 0,savePendingShutdownCount: 1, anddirtyState.dirty: falseafter reloading an already-on-disk target, mutating it again, and saving.audits\save-pending-after-restart.json: a fresh daemon readback found the second saved keyword inVT_AutomationAudit_Pending_rerun2_20260508.esp, proving the pending-shutdown response needed the explicit restart/readback step for a strong durability claim.
See examples/05-save-durability.md for the wrapper-facing save example and the locator/durability audit for the corresponding audit table.
- Do not parse prose in
error.messageor scriptmessagesto decide control flow. - Branch on
error.code. - Use documented
error.detailskeys only. - Treat
messagesas ordered observational output for operators and diagnostics. - Ignore unknown keys for forward compatibility.
- Ignore unknown
object.kindvalues for forward compatibility;child_groupis additive in 0.13 and future kinds may appear. - Keep
scripts.runoutsidejobs.*; it is synchronous and non-cancelable in this contract.
Mutating daemon commands now require the daemon to have been launched with -IKnowWhatImDoing.
When the flag is absent, mutating commands fail at the request boundary with:
error.code: "consent_required"error.details.deniedReason: "iknowwhatimdoing-required"error.details.commandName: <the rejected command>error.details.mutationCategory: <the policy classifier category>
This is a request-validation tier refusal. No script lifecycle has started yet, so no messages, messagesTruncated, ranInitialize, ranFinalize, or processed fields appear on this error code.
Read-only commands are unaffected.
script_runtime_error remains reserved for real script execution failures after execution starts; consent refusal is never reported through that lifecycle code.
The capability descriptor result.supports.scripts.execution.iKnowWhatImDoing reflects the daemon's current launch state.
See examples/06-consent-required.md for a source-linked example.