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
enabledbooleanEnable batch operations
maxRecordsPerBatchintegerMaximum records per batch
defaultOptions{ atomic: boolean; returnRecords: boolean; continueOnError: boolean }optionalDefault batch options

BatchOperationResult

Properties

PropertyTypeRequiredDescription
idstringoptionalRecord ID if operation succeeded
successbooleanWhether this record was processed successfully
errors{ code: Enum<'VALIDATION_ERROR' | 'INVALID_FIELD' | 'MISSING_REQUIRED_FIELD' | … +266 more>; message: string; category?: string; httpStatus?: integer; … }[]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 (#4793). 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 (#7539).
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 (#3407/#3431/#3455): caller-supplied fields LEGALLY stripped from THIS row before it was written — static readonly (#2948) / TRUE readonlyWhen (#3042) on update, or the #3043 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.

BatchOperationType

Allowed Values

  • create
  • update
  • upsert
  • delete

BatchOptions

Properties

PropertyTypeRequiredDescription
atomicbooleanOpt-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.
returnRecordsbooleanIf true, return full record data in response
continueOnErrorbooleanIf 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 (#7539).
validateOnlyneveroptional[REMOVED] options.validateOnly was removed from BatchOptions in @objectstack/spec (#4052). 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

BatchUpdateResponse

Properties

PropertyTypeRequiredDescription
successbooleanOperation success status
error{ code: Enum<'VALIDATION_ERROR' | 'INVALID_FIELD' | 'MISSING_REQUIRED_FIELD' | … +266 more>; message: string; category?: string; httpStatus?: integer; … }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

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 (#2948), a TRUE readonlyWhen predicate (#3042), or the primary-key strip of a payload id the engine ruled is not an identifier (#6437)
indexintegerIndex of the operation in the request operations array

CrossObjectBatchOperation

Properties

PropertyTypeRequiredDescription
objectstringTarget object (table) name
actionEnum<'create' | 'update' | 'delete'>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
atomicbooleanAlways true — the cross-object batch is all-or-nothing

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 (#3407/#3431/#3455/#3794): caller-supplied fields the engine LEGALLY stripped from an operation before it was written — static readonly (#2948) or a TRUE readonlyWhen predicate (#3042). 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.

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

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

On this page