ObjectStackObjectStack

Batch schema — API Protocol property reference

Batch Operations API — Provides efficient bulk data operations with transaction support.

Batch Operations API

Provides efficient bulk data operations with transaction support. Implements P0/P1 requirements for ObjectStack kernel.

Features:

  • Batch create/update/delete operations
  • Atomic transaction support (all-or-none)
  • Partial success handling
  • Detailed error reporting per record

Industry alignment: Salesforce Bulk API, Microsoft Dynamics Bulk Operations

Source: packages/spec/src/api/batch.zod.ts

TypeScript Usage

import { BatchConfigSchema, BatchOperationResultSchema, BatchOperationType, BatchOptionsSchema, BatchRecordSchema, BatchUpdateRequestSchema, BatchUpdateResponseSchema, CrossObjectBatchDroppedFieldsSchema, CrossObjectBatchOperationSchema, CrossObjectBatchRequestSchema, CrossObjectBatchResponseSchema, DeleteManyRequestSchema, UpdateManyRecordSchema, UpdateManyRequestSchema } from '@objectstack/spec/api';
import type { BatchConfig, BatchOperationResult, BatchOperationType, BatchOptions, BatchRecord, BatchUpdateRequest, BatchUpdateResponse, CrossObjectBatchDroppedFields, CrossObjectBatchOperation, CrossObjectBatchRequest, CrossObjectBatchResponse, DeleteManyRequest, UpdateManyRecord, UpdateManyRequest } from '@objectstack/spec/api';

// Validate data
const result = BatchConfigSchema.parse(data);

BatchConfig

Properties

PropertyTypeRequiredDescription
enabledbooleanoptional (default: true)Enable batch operations
maxRecordsPerBatchintegeroptional (default: 200)Maximum records per batch
defaultOptions{ atomic?: boolean; returnRecords?: boolean; continueOnError?: boolean }optionalDefault batch options

Nested Shape: BatchConfig.defaultOptions

PropertyTypeRequiredDescription
atomicbooleanoptional (default: false)Opt-in all-or-nothing. When explicitly true the whole batch runs inside ONE engine transaction: the first failure rolls back every prior write, and the response reports zero successes — each row carries errors[0].code ROLLED_BACK (written, then undone), the causal row its own error, and rows never reached NOT_ATTEMPTED. A runtime that cannot roll back REFUSES the request (501 NOT_IMPLEMENTED) rather than silently degrading to best-effort — probe capabilities.transactionalBatch on /discovery first. Takes precedence over continueOnError. Default false: sequential best-effort.
returnRecordsbooleanoptional (default: false)If true, return full record data in response
continueOnErrorbooleanoptional (default: false)If true (and atomic=false), continue processing remaining records after errors. Default false: the first failure ENDS the run — records before it stay written (nothing is rolled back on this arm), and every record after it is reported errors[0].code NOT_ATTEMPTED rather than omitted, so results always covers all total records and succeeded + failed === total.
validateOnlyneveroptional[REMOVED] options.validateOnly was removed from BatchOptions in @objectstack/spec 17.0.0. It was never implemented: the batch surfaces persisted regardless, so a "dry-run" would have silently executed. There is no dry-run today — drop the key. If you need to preview a batch without writing, open an issue so it can be designed (no-commit cascade / constraint semantics) and reintroduced as a flag that actually holds.

BatchOperationResult

Properties

PropertyTypeRequiredDescription
idstringoptionalRecord ID if operation succeeded
successboolean✅Whether this record was processed successfully
errors{ code: Enum<'VALIDATION_ERROR' | 'INVALID_FIELD' | 'MISSING_REQUIRED_FIELD' | …>; declaredCode?: string; message: string; userMessage?: string; … }[]optionalArray of errors if operation failed. Branch on errors[0].code — an atomic batch that rolled back marks rows that were written then undone with code ROLLED_BACK and rows never reached with NOT_ATTEMPTED, while the causal row keeps its own error. A NON-atomic batch that stopped (the continueOnError: false default) marks its un-attempted tail with the same NOT_ATTEMPTED code — rows before the failure stay written and keep reporting success, since nothing was rolled back.
dataRecord<string, any>optionalFull record data (if returnRecords=true)
indexnumberoptionalIndex of the record in the request array
droppedFields{ object: string; fields: string[]; reason: Enum<'readonly' | 'readonly_when' | 'primary_key' | 'computed'> }[]optionalWrite-observability: caller-supplied fields LEGALLY stripped from THIS row before it was written — static readonly / TRUE readonlyWhen on update, or the in-engine static readonly strip on create. Per-row because a batch can drop different fields on different rows (readonlyWhen is record-state-dependent). Present ONLY when ≥1 field was dropped for this row; the row still succeeded (success unchanged). A single response header cannot express per-row drops, so this body field is the canonical bulk channel — REST does not emit X-ObjectStack-Dropped-Fields for batches. Optional — omit-when-empty keeps the shape backward-compatible.

Nested Shape: BatchOperationResult.errors[number]

PropertyTypeRequiredDescription
codeEnum<'VALIDATION_ERROR' | 'INVALID_FIELD' | 'MISSING_REQUIRED_FIELD' | …>✅Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages)
declaredCodestringoptionalThe 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)
messagestring✅Readable error message
userMessagestringoptionalProducer-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.
refusaltrueoptionalProducer-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.
categorystringoptionalError category (e.g. validation, authorization)
httpStatusintegeroptionalHTTP status of the response carrying this error
detailsanyoptionalAdditional error context (e.g. field validation errors)
requestIdstringoptionalRequest ID for tracking

Nested Shape: BatchOperationResult.droppedFields[number]

A write-path strip event: caller-supplied fields legally dropped from the payload

PropertyTypeRequiredDescription
objectstring✅Object the write targeted (resolved object name)
fieldsstring[]✅Caller-supplied field names the engine removed from the write payload
reasonEnum<'readonly' | 'readonly_when' | 'primary_key' | 'computed'>✅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

BatchOperationType

Allowed Values

  • create
  • update
  • upsert
  • delete

BatchOptions

Properties

PropertyTypeRequiredDescription
atomicbooleanoptional (default: false)Opt-in all-or-nothing. When explicitly true the whole batch runs inside ONE engine transaction: the first failure rolls back every prior write, and the response reports zero successes — each row carries errors[0].code ROLLED_BACK (written, then undone), the causal row its own error, and rows never reached NOT_ATTEMPTED. A runtime that cannot roll back REFUSES the request (501 NOT_IMPLEMENTED) rather than silently degrading to best-effort — probe capabilities.transactionalBatch on /discovery first. Takes precedence over continueOnError. Default false: sequential best-effort.
returnRecordsbooleanoptional (default: false)If true, return full record data in response
continueOnErrorbooleanoptional (default: false)If true (and atomic=false), continue processing remaining records after errors. Default false: the first failure ENDS the run — records before it stay written (nothing is rolled back on this arm), and every record after it is reported errors[0].code NOT_ATTEMPTED rather than omitted, so results always covers all total records and succeeded + failed === total.
validateOnlyneveroptional[REMOVED] options.validateOnly was removed from BatchOptions in @objectstack/spec 17.0.0. It was never implemented: the batch surfaces persisted regardless, so a "dry-run" would have silently executed. There is no dry-run today — drop the key. If you need to preview a batch without writing, open an issue so it can be designed (no-commit cascade / constraint semantics) and reintroduced as a flag that actually holds.

BatchRecord

Properties

PropertyTypeRequiredDescription
idstringoptionalRecord ID (required for update/delete)
dataRecord<string, any>optionalRecord data (required for create/update/upsert)
externalIdstringoptionalExternal ID for upsert matching

BatchUpdateRequest

Properties

PropertyTypeRequiredDescription
operationEnum<'create' | 'update' | 'upsert' | 'delete'>✅Type of batch operation
records{ id?: string; data?: Record<string, any>; externalId?: string }[]✅Array of records to process (server caps the count — see batch.maxBatchSize)
options{ atomic?: boolean; returnRecords?: boolean; continueOnError?: boolean }optionalBatch operation options

Nested Shape: BatchUpdateRequest.records[number]

PropertyTypeRequiredDescription
idstringoptionalRecord ID (required for update/delete)
dataRecord<string, any>optionalRecord data (required for create/update/upsert)
externalIdstringoptionalExternal ID for upsert matching

Nested Shape: BatchUpdateRequest.options

PropertyTypeRequiredDescription
atomicbooleanoptional (default: false)Opt-in all-or-nothing. When explicitly true the whole batch runs inside ONE engine transaction: the first failure rolls back every prior write, and the response reports zero successes — each row carries errors[0].code ROLLED_BACK (written, then undone), the causal row its own error, and rows never reached NOT_ATTEMPTED. A runtime that cannot roll back REFUSES the request (501 NOT_IMPLEMENTED) rather than silently degrading to best-effort — probe capabilities.transactionalBatch on /discovery first. Takes precedence over continueOnError. Default false: sequential best-effort.
returnRecordsbooleanoptional (default: false)If true, return full record data in response
continueOnErrorbooleanoptional (default: false)If true (and atomic=false), continue processing remaining records after errors. Default false: the first failure ENDS the run — records before it stay written (nothing is rolled back on this arm), and every record after it is reported errors[0].code NOT_ATTEMPTED rather than omitted, so results always covers all total records and succeeded + failed === total.
validateOnlyneveroptional[REMOVED] options.validateOnly was removed from BatchOptions in @objectstack/spec 17.0.0. It was never implemented: the batch surfaces persisted regardless, so a "dry-run" would have silently executed. There is no dry-run today — drop the key. If you need to preview a batch without writing, open an issue so it can be designed (no-commit cascade / constraint semantics) and reintroduced as a flag that actually holds.

BatchUpdateResponse

Properties

PropertyTypeRequiredDescription
successboolean✅Operation success status
error{ code: Enum<'VALIDATION_ERROR' | 'INVALID_FIELD' | 'MISSING_REQUIRED_FIELD' | …>; declaredCode?: string; message: string; userMessage?: string; … }optionalError details if success is false
meta{ timestamp: string; duration?: integer; requestId?: string; traceId?: string }optionalResponse metadata
operationEnum<'create' | 'update' | 'upsert' | 'delete'>optionalOperation type that was performed
totalnumber✅Total number of records in the batch
succeedednumber✅Number of records that succeeded
failednumber✅Number of records that failed
results{ id?: string; success: boolean; errors?: object[]; data?: Record<string, any>; … }[]✅Detailed results for each record

Nested Shape: BatchUpdateResponse.error

PropertyTypeRequiredDescription
codeEnum<'VALIDATION_ERROR' | 'INVALID_FIELD' | 'MISSING_REQUIRED_FIELD' | …>✅Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages)
declaredCodestringoptionalThe 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)
messagestring✅Readable error message
userMessagestringoptionalProducer-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.
refusaltrueoptionalProducer-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.
categorystringoptionalError category (e.g. validation, authorization)
httpStatusintegeroptionalHTTP status of the response carrying this error
detailsanyoptionalAdditional error context (e.g. field validation errors)
requestIdstringoptionalRequest ID for tracking

Nested Shape: BatchUpdateResponse.meta

PropertyTypeRequiredDescription
timestampstring✅
durationintegeroptionalServer-side processing duration in milliseconds
requestIdstringoptional
traceIdstringoptional

Nested Shape: BatchUpdateResponse.results[number]

PropertyTypeRequiredDescription
idstringoptionalRecord ID if operation succeeded
successboolean✅Whether this record was processed successfully
errors{ code: Enum<'VALIDATION_ERROR' | 'INVALID_FIELD' | 'MISSING_REQUIRED_FIELD' | …>; declaredCode?: string; message: string; userMessage?: string; … }[]optionalArray of errors if operation failed. Branch on errors[0].code — an atomic batch that rolled back marks rows that were written then undone with code ROLLED_BACK and rows never reached with NOT_ATTEMPTED, while the causal row keeps its own error. A NON-atomic batch that stopped (the continueOnError: false default) marks its un-attempted tail with the same NOT_ATTEMPTED code — rows before the failure stay written and keep reporting success, since nothing was rolled back.
dataRecord<string, any>optionalFull record data (if returnRecords=true)
indexnumberoptionalIndex of the record in the request array
droppedFields{ object: string; fields: string[]; reason: Enum<'readonly' | 'readonly_when' | 'primary_key' | 'computed'> }[]optionalWrite-observability: caller-supplied fields LEGALLY stripped from THIS row before it was written — static readonly / TRUE readonlyWhen on update, or the in-engine static readonly strip on create. Per-row because a batch can drop different fields on different rows (readonlyWhen is record-state-dependent). Present ONLY when ≥1 field was dropped for this row; the row still succeeded (success unchanged). A single response header cannot express per-row drops, so this body field is the canonical bulk channel — REST does not emit X-ObjectStack-Dropped-Fields for batches. Optional — omit-when-empty keeps the shape backward-compatible.

CrossObjectBatchDroppedFields

A cross-object batch strip event: dropped fields plus the operation index

Properties

PropertyTypeRequiredDescription
objectstring✅Object the write targeted (resolved object name)
fieldsstring[]✅Caller-supplied field names the engine removed from the write payload
reasonEnum<'readonly' | 'readonly_when' | 'primary_key' | 'computed'>✅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
indexinteger✅Index of the operation in the request operations array

CrossObjectBatchOperation

Properties

PropertyTypeRequiredDescription
objectstring✅Target object (table) name
actionEnum<'create' | 'update' | 'delete'>optional (default: "create")Operation to perform (default: create)
idstringoptionalTarget record id — required for update and delete
dataRecord<string, any>optionalRecord payload for create/update; a value may be { $ref: <opIndex> } to reference an earlier op's created id

CrossObjectBatchRequest

Properties

PropertyTypeRequiredDescription
operations{ object: string; action?: Enum<'create' | 'update' | 'delete'>; id?: string; data?: Record<string, any> }[]✅Ordered operations executed in one transaction
atomicbooleanoptional (default: true)Always true — the cross-object batch is all-or-nothing

Nested Shape: CrossObjectBatchRequest.operations[number]

PropertyTypeRequiredDescription
objectstring✅Target object (table) name
actionEnum<'create' | 'update' | 'delete'>optional (default: "create")Operation to perform (default: create)
idstringoptionalTarget record id — required for update and delete
dataRecord<string, any>optionalRecord payload for create/update; a value may be { $ref: <opIndex> } to reference an earlier op's created id

CrossObjectBatchResponse

Properties

PropertyTypeRequiredDescription
resultsany[]✅Per-operation result, index-aligned with the request operations
droppedFields{ object: string; fields: string[]; reason: Enum<'readonly' | 'readonly_when' | 'primary_key' | 'computed'>; index: integer }[]optionalWrite-observability: caller-supplied fields the engine LEGALLY stripped from an operation before it was written — static readonly or a TRUE readonlyWhen predicate. This endpoint is the console record form's save path (master-detail writes parent + children in one transaction), so without it the ONE surface where a user edits a readonlyWhen field reported plain success while the value never landed. Each event carries the index of its operation. Present ONLY when ≥1 field was dropped; the batch still committed without them (results/success semantics unchanged). Optional — omit-when-empty keeps the shape backward-compatible.

Nested Shape: CrossObjectBatchResponse.droppedFields[number]

A cross-object batch strip event: dropped fields plus the operation index

PropertyTypeRequiredDescription
objectstring✅Object the write targeted (resolved object name)
fieldsstring[]✅Caller-supplied field names the engine removed from the write payload
reasonEnum<'readonly' | 'readonly_when' | 'primary_key' | 'computed'>✅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
indexinteger✅Index of the operation in the request operations array

DeleteManyRequest

Properties

PropertyTypeRequiredDescription
idsstring[]✅Array of record IDs to delete (server caps the count — see batch.maxBatchSize)
options{ atomic?: boolean; returnRecords?: boolean; continueOnError?: boolean }optionalDelete options

Nested Shape: DeleteManyRequest.options

PropertyTypeRequiredDescription
atomicbooleanoptional (default: false)Opt-in all-or-nothing. When explicitly true the whole batch runs inside ONE engine transaction: the first failure rolls back every prior write, and the response reports zero successes — each row carries errors[0].code ROLLED_BACK (written, then undone), the causal row its own error, and rows never reached NOT_ATTEMPTED. A runtime that cannot roll back REFUSES the request (501 NOT_IMPLEMENTED) rather than silently degrading to best-effort — probe capabilities.transactionalBatch on /discovery first. Takes precedence over continueOnError. Default false: sequential best-effort.
returnRecordsbooleanoptional (default: false)If true, return full record data in response
continueOnErrorbooleanoptional (default: false)If true (and atomic=false), continue processing remaining records after errors. Default false: the first failure ENDS the run — records before it stay written (nothing is rolled back on this arm), and every record after it is reported errors[0].code NOT_ATTEMPTED rather than omitted, so results always covers all total records and succeeded + failed === total.
validateOnlyneveroptional[REMOVED] options.validateOnly was removed from BatchOptions in @objectstack/spec 17.0.0. It was never implemented: the batch surfaces persisted regardless, so a "dry-run" would have silently executed. There is no dry-run today — drop the key. If you need to preview a batch without writing, open an issue so it can be designed (no-commit cascade / constraint semantics) and reintroduced as a flag that actually holds.

UpdateManyRecord

Properties

PropertyTypeRequiredDescription
idstring✅Record ID
dataRecord<string, any>✅Fields to update

UpdateManyRequest

Properties

PropertyTypeRequiredDescription
records{ id: string; data: Record<string, any> }[]✅Array of records to update (server caps the count — see batch.maxBatchSize)
options{ atomic?: boolean; returnRecords?: boolean; continueOnError?: boolean }optionalUpdate options

Nested Shape: UpdateManyRequest.records[number]

PropertyTypeRequiredDescription
idstring✅Record ID
dataRecord<string, any>✅Fields to update

Nested Shape: UpdateManyRequest.options

PropertyTypeRequiredDescription
atomicbooleanoptional (default: false)Opt-in all-or-nothing. When explicitly true the whole batch runs inside ONE engine transaction: the first failure rolls back every prior write, and the response reports zero successes — each row carries errors[0].code ROLLED_BACK (written, then undone), the causal row its own error, and rows never reached NOT_ATTEMPTED. A runtime that cannot roll back REFUSES the request (501 NOT_IMPLEMENTED) rather than silently degrading to best-effort — probe capabilities.transactionalBatch on /discovery first. Takes precedence over continueOnError. Default false: sequential best-effort.
returnRecordsbooleanoptional (default: false)If true, return full record data in response
continueOnErrorbooleanoptional (default: false)If true (and atomic=false), continue processing remaining records after errors. Default false: the first failure ENDS the run — records before it stay written (nothing is rolled back on this arm), and every record after it is reported errors[0].code NOT_ATTEMPTED rather than omitted, so results always covers all total records and succeeded + failed === total.
validateOnlyneveroptional[REMOVED] options.validateOnly was removed from BatchOptions in @objectstack/spec 17.0.0. It was never implemented: the batch surfaces persisted regardless, so a "dry-run" would have silently executed. There is no dry-run today — drop the key. If you need to preview a batch without writing, open an issue so it can be designed (no-commit cascade / constraint semantics) and reintroduced as a flag that actually holds.

On this page