ObjectStackObjectStack

Batch

Batch protocol schemas

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
successbooleanWhether 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'> }[]optionalWrite-observability: caller-supplied fields LEGALLY stripped from THIS row before it was written — static readonly / TRUE readonlyWhen on update, or the create-ingress strip. 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)
messagestringReadable 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.
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
objectstringObject the write targeted (resolved object name)
fieldsstring[]Caller-supplied field names the engine removed from the write payload
reasonEnum<'readonly' | 'readonly_when' | 'primary_key'>Why the fields were dropped: static readonly, a TRUE readonlyWhen predicate, or the primary-key strip of a payload id the engine ruled is not an identifier

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
successbooleanOperation 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?: number; requestId?: string; traceId?: string }optionalResponse metadata
operationEnum<'create' | 'update' | 'upsert' | 'delete'>optionalOperation type that was performed
totalnumberTotal number of records in the batch
succeedednumberNumber of records that succeeded
failednumberNumber 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)
messagestringReadable 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.
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.results[number]

PropertyTypeRequiredDescription
idstringoptionalRecord ID if operation succeeded
successbooleanWhether 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'> }[]optionalWrite-observability: caller-supplied fields LEGALLY stripped from THIS row before it was written — static readonly / TRUE readonlyWhen on update, or the create-ingress strip. 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
objectstringObject the write targeted (resolved object name)
fieldsstring[]Caller-supplied field names the engine removed from the write payload
reasonEnum<'readonly' | 'readonly_when' | 'primary_key'>Why the fields were dropped: static readonly, a TRUE readonlyWhen predicate, or the primary-key strip of a payload id the engine ruled is not an identifier
indexintegerIndex of the operation in the request operations array

CrossObjectBatchOperation

Properties

PropertyTypeRequiredDescription
objectstringTarget 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
objectstringTarget 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'>; 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
objectstringObject the write targeted (resolved object name)
fieldsstring[]Caller-supplied field names the engine removed from the write payload
reasonEnum<'readonly' | 'readonly_when' | 'primary_key'>Why the fields were dropped: static readonly, a TRUE readonlyWhen predicate, or the primary-key strip of a payload id the engine ruled is not an identifier
indexintegerIndex 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
idstringRecord 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
idstringRecord 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