Defines the export file formats, import validation, template-based field mapping, and the asynchronous import-job contracts.
Data Export & Import Protocol
Defines the export file formats, import validation, template-based field
mapping, and the asynchronous import-job contracts.
Industry alignment: Salesforce Data Export, Airtable CSV Export,
Dynamics 365 Data Management.
The export the platform serves is the synchronous streaming door
GET /api/v1/data/:object/export, which answers the file itself as CSV,
JSON or XLSX. The asynchronous export-job API that used to be
declared here (export jobs, their progress / download / list shapes,
scheduled exports and ExportApiContracts) was never served by any route and
was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove); a recurring
export is a Job whose handler you write.
Name of a registered mapping metadata artifact to apply; the server resolves it (org-scoped rows first, then env-wide) and projects columns through it. Mutually exclusive with an inline mapping — supplying both is refused (400 CONFLICTING_MAPPING).
dryRun
boolean
optional (default: false)
Validate + coerce every row without persisting. The verdict is the engine's own write-path validation, with one boundary an author should know: a preview runs NO automations. Hooks never fire in a dry run — a preview that executed user-authored side effects (mail, outbound calls, writes to other objects) would be the retired validateOnly defect in a new spelling. So a dry run with runAutomations: true can report required for a field a beforeInsert hook would populate during the real import; for hook-derived fields the real write is authoritative.
writeMode
Enum<'insert' | 'update' | 'upsert'>
optional (default: "insert")
insert / update / upsert semantics
matchFields
string[]
optional
Fields that identify an existing record (required for update/upsert)
runAutomations
boolean
optional (default: true)
Fire triggers/hooks for each imported row. ON by default, and opting out must be explicit: automations always ran on import historically (the engine ignored this flag until this flag was honoured), so a caller that wants a silent bulk load sends runAutomations: false — omitting the key runs them. This matches platform convention (Salesforce fires triggers on import by default). One boundary: a dryRun preview runs NO automations whatever this flag says.
treatAsHistorical
boolean
optional (default: false)
Import as established historical facts. Two effects, both off by default so a normal import is unchanged: (1) skip the state_machine rule so mid-lifecycle rows (e.g. already-closed tickets, closed_won deals) are not rejected by initialStates; and (2) preserve the original audit timeline — keep the supplied created_at / updated_at / updated_by and author-declared business readonly fields (e.g. closed_at, resolved_by) instead of stamping-now / stripping them. Undoing a historical import mirrors (2): the captured pre-import values are restored verbatim rather than re-stamped.
trimWhitespace
boolean
optional (default: true)
Trim leading/trailing whitespace from string cells
nullValues
string[]
optional
Strings treated as null/blank (besides empty string)
createMissingOptions
boolean
optional (default: false)
Keep a select / radio / multiselect cell that matches none of the field's options instead of failing the row. The cell is stored as written (trimmed; for a multi-value cell, each unmatched item), and the engine's option check admits exactly the values this import kept, on this import's writes (dry run included). The field's option list is not changed: a later write that sends the field is judged against the options again, while one that leaves the field out is not affected. A field bound to a shared picklist that did not resolve is still refused.
Capped sample of per-row outcomes, failures first. An ok row, and any droppedFields or warnings it carries, reaches this reader only if it falls inside the sample; resultsTruncated says whether the sample is partial, and the job counters count every row.
resultsTruncated
boolean
✅
Whether results is a capped sample of a larger set
Findings this deployment ADMITS rather than rejects (ADR-0104 value shapes under a warn-first posture), in the same { field, code, message } shape the validate verdict carries. Set only on a dry-run row the verdict accepted: the row is ok because the write would store it and log the same complaint. A committed row never carries it, since the write path has no channel to report admitted findings on.
Write-observability: caller-supplied fields the engine LEGALLY strips from THIS row, one event per reason, in the engine's own droppedFields shape and reason vocabulary. A committed row reports the strips its write made. A dry-run row reports the strips its preview runs, which for a row the import would update excludes readonlyWhen and primary-key strips, so that row can name fewer fields than its commit. The row still succeeds: ok and action are unchanged. Present only when at least one field was dropped. A server or write path that does not produce this report omits the key too, so an absent key alone does not prove nothing was dropped. In an async job's results, an ok row's drops reach the reader only if the row falls inside that capped, failures-first sample.
Name of a registered mapping metadata artifact to apply; the server resolves it (org-scoped rows first, then env-wide) and projects columns through it. Mutually exclusive with an inline mapping — supplying both is refused (400 CONFLICTING_MAPPING).
dryRun
boolean
optional (default: false)
Validate + coerce every row without persisting. The verdict is the engine's own write-path validation, with one boundary an author should know: a preview runs NO automations. Hooks never fire in a dry run — a preview that executed user-authored side effects (mail, outbound calls, writes to other objects) would be the retired validateOnly defect in a new spelling. So a dry run with runAutomations: true can report required for a field a beforeInsert hook would populate during the real import; for hook-derived fields the real write is authoritative.
writeMode
Enum<'insert' | 'update' | 'upsert'>
optional (default: "insert")
insert / update / upsert semantics
matchFields
string[]
optional
Fields that identify an existing record (required for update/upsert)
runAutomations
boolean
optional (default: true)
Fire triggers/hooks for each imported row. ON by default, and opting out must be explicit: automations always ran on import historically (the engine ignored this flag until this flag was honoured), so a caller that wants a silent bulk load sends runAutomations: false — omitting the key runs them. This matches platform convention (Salesforce fires triggers on import by default). One boundary: a dryRun preview runs NO automations whatever this flag says.
treatAsHistorical
boolean
optional (default: false)
Import as established historical facts. Two effects, both off by default so a normal import is unchanged: (1) skip the state_machine rule so mid-lifecycle rows (e.g. already-closed tickets, closed_won deals) are not rejected by initialStates; and (2) preserve the original audit timeline — keep the supplied created_at / updated_at / updated_by and author-declared business readonly fields (e.g. closed_at, resolved_by) instead of stamping-now / stripping them. Undoing a historical import mirrors (2): the captured pre-import values are restored verbatim rather than re-stamped.
trimWhitespace
boolean
optional (default: true)
Trim leading/trailing whitespace from string cells
nullValues
string[]
optional
Strings treated as null/blank (besides empty string)
createMissingOptions
boolean
optional (default: false)
Keep a select / radio / multiselect cell that matches none of the field's options instead of failing the row. The cell is stored as written (trimmed; for a multi-value cell, each unmatched item), and the engine's option check admits exactly the values this import kept, on this import's writes (dry run included). The field's option list is not changed: a later write that sends the field is judged against the options again, while one that leaves the field out is not affected. A field bound to a shared picklist that did not resolve is still refused.
Findings this deployment ADMITS rather than rejects (ADR-0104 value shapes under a warn-first posture), in the same { field, code, message } shape the validate verdict carries. Set only on a dry-run row the verdict accepted: the row is ok because the write would store it and log the same complaint. A committed row never carries it, since the write path has no channel to report admitted findings on.
Write-observability: caller-supplied fields the engine LEGALLY strips from THIS row, one event per reason, in the engine's own droppedFields shape and reason vocabulary. A committed row reports the strips its write made. A dry-run row reports the strips its preview runs, which for a row the import would update excludes readonlyWhen and primary-key strips, so that row can name fewer fields than its commit. The row still succeeds: ok and action are unchanged. Present only when at least one field was dropped. A server or write path that does not produce this report omits the key too, so an absent key alone does not prove nothing was dropped. In an async job's results, an ok row's drops reach the reader only if the row falls inside that capped, failures-first sample.
Findings this deployment ADMITS rather than rejects (ADR-0104 value shapes under a warn-first posture), in the same { field, code, message } shape the validate verdict carries. Set only on a dry-run row the verdict accepted: the row is ok because the write would store it and log the same complaint. A committed row never carries it, since the write path has no channel to report admitted findings on.
Write-observability: caller-supplied fields the engine LEGALLY strips from THIS row, one event per reason, in the engine's own droppedFields shape and reason vocabulary. A committed row reports the strips its write made. A dry-run row reports the strips its preview runs, which for a row the import would update excludes readonlyWhen and primary-key strips, so that row can name fewer fields than its commit. The row still succeeds: ok and action are unchanged. Present only when at least one field was dropped. A server or write path that does not produce this report omits the key too, so an absent key alone does not prove nothing was dropped. In an async job's results, an ok row's drops reach the reader only if the row falls inside that capped, failures-first sample.
Why the fields were dropped: static readonly, a TRUE readonlyWhen predicate, the primary-key strip of a payload id the engine ruled is not an identifier, or a computed (formula) field no driver has a column for
Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages)
declaredCode
string
optional
The producer-declared code, verbatim, when it is not a member of the closed code vocabulary — the open, author-authored channel (app-specific spellings; ADR-0112)
message
string
✅
Readable error message
userMessage
string
optional
Producer-marked user-facing refusal text, verbatim. Present exactly when the producer opted in at throw time; consumers render it to end users and keep their generic substitution for anything unmarked. Status-agnostic; never replaces message.
refusal
true
optional
Producer-declared: the 5xx this envelope carries is a deliberate refusal whose message is authored for the caller, so a boundary that reads the declaration keeps it verbatim (until the withhold arms read it, a declared refusal is still withheld). Absent (the default) on a declared fault, whose message is withheld from the body and logged for the operator; redundant on a 4xx. Presence is the declaration — true is the only value.
category
string
optional
Error category (e.g. validation, authorization)
httpStatus
integer
optional
HTTP status of the response carrying this error
details
any
optional
Additional error context (e.g. field validation errors)