Mass operations
Mass operations let you execute large batches of Crystallize mutations by uploading a JSON file that lists every action you want to run. Each entry in the file is called an operation. Operations are consumed by the mass-operations runner as part of a BulkTask and executed with the same validation and side effects as if you had called the corresponding GraphQL mutation yourself.
We ship first-class support in the Crystallize CLI. The CLI validates your file, uploads it through a presigned request, creates the bulk task, and can wait for completion while streaming logs.When to Reach for Mass Operations
- Seeding or migrating a tenant (shapes, pieces, customers, orders, subscription contracts, etc.).
- Replaying or fixing data outside the API rate limits.
- Coordinating multi-step changes (e.g., create a piece, then a shape that references it, then publish the shape).
- Updating product content at scale (components, stock levels, publish requests).
High-Level Flow
- Prepare the JSON file that describes every operation you need.
- Validate and upload the file through the generatePresignedUploadRequest mutation using the MASS_OPERATIONS upload type.
- Register the file by calling createMassOperationBulkTask with the returned storage key.
- Start the bulk task immediately by passing autoStart: true, or later through startMassOperationBulkTask.
- Monitor the task with the bulkTask query and inspect detailed per-operation logs with the operationLogs query.
Each task runs asynchronously in the standalone mass-operations-runner. Operations inside a task execute sequentially; success or failure is recorded per operation in the log stream.Mass Operation File Format
Mass operation files are strict JSON documents validated against @crystallize/schema/mass-operation (currently version 0.0.1).
{
"version": "0.0.1",
"operations": [
{
"intent": "piece/upsert",
"identifier": "rating-system",
"name": "Rating System",
"components": [
{
"id": "name",
"type": "singleLine",
"singleLine": { "required": true }
}
]
}
]
}
{
"version": "0.0.1",
"operations": [
{
"intent": "piece/upsert",
"identifier": "rating-system",
"name": "Rating System",
"components": [
{
"id": "name",
"type": "singleLine",
"singleLine": { "required": true }
}
]
}
]
}
Required Top-Level Fields
- version: Schema version of the file. Only 0.0.1 is accepted today.
- operations: Ordered array of operation objects. Operations are processed in order.
Operation Structure
Every operation:
- Must contain an intent of the form domain/action (optionally with extra path segments).
- Accepts exactly the same payload shape as the matching GraphQL input. You can copy an input you already use in the API and paste it into the JSON file.
- May include helper identifiers (e.g., identifier, id, language) exactly as in GraphQL.
- Can reference entities created earlier in the file using shared identifiers (e.g., upsert a customer, then reference it from an order).
If an operation fails validation or execution, the runner records the failure in the log and continues with the next operation. No implicit rollback is performed.Supported Intents
Intent | Description |
| Manage pieces. |
| Manage shapes. |
| Update a component on an item by item ID. |
| Update a component on a product variant (SKU). |
| Send publish requests for items. |
| Adjust variant stock levels. |
| Manage customers. |
| Manage customer groups. |
| Manage orders. |
| Manage subscription contracts. |
New intents are added over time. The @crystallize/schema/mass-operation package is the source of truth.Richer Example
Below is a shortened example that upserts a customer, registers a new order that references the customer, and updates an item component:
{
"version": "0.0.1",
"operations": [
{
"intent": "customer/upsert",
"identifier": "customer-for-order-123",
"firstName": "John",
"lastName": "Doe",
"type": "individual"
},
{
"intent": "order/register",
"customer": {
"identifier": "customer-for-order-123",
"type": "individual"
},
"additionalInformation": "Please deliver between 9am-5pm",
"cart": [
{
"sku": "SP-RED-001",
"name": "Sample Product",
"productId": "67e5d2d12d31ee752710a74b",
"quantity": 2,
"price": {
"currency": "USD",
"gross": 1000,
"net": 800,
"tax": { "name": "VAT", "percent": 20 }
}
}
]
},
{
"intent": "item/updateComponent/item",
"itemId": "632958a35dfc2c90cbbad20d",
"language": "en",
"component": {
"componentId": "title",
"singleLine": { "text": "Mass operation updated title" }
}
}
]
}
{
"version": "0.0.1",
"operations": [
{
"intent": "customer/upsert",
"identifier": "customer-for-order-123",
"firstName": "John",
"lastName": "Doe",
"type": "individual"
},
{
"intent": "order/register",
"customer": {
"identifier": "customer-for-order-123",
"type": "individual"
},
"additionalInformation": "Please deliver between 9am-5pm",
"cart": [
{
"sku": "SP-RED-001",
"name": "Sample Product",
"productId": "67e5d2d12d31ee752710a74b",
"quantity": 2,
"price": {
"currency": "USD",
"gross": 1000,
"net": 800,
"tax": { "name": "VAT", "percent": 20 }
}
}
]
},
{
"intent": "item/updateComponent/item",
"itemId": "632958a35dfc2c90cbbad20d",
"language": "en",
"component": {
"componentId": "title",
"singleLine": { "text": "Mass operation updated title" }
}
}
]
}
Uploading the File
- Request a presigned upload:
mutation PresignMassOp($filename: String!, $contentType: String!) {
generatePresignedUploadRequest(
input: {
type: MASS_OPERATIONS
filename: $filename
contentType: $contentType
}
) {
url
fields
key
}
}
mutation PresignMassOp($filename: String!, $contentType: String!) {
generatePresignedUploadRequest(
input: {
type: MASS_OPERATIONS
filename: $filename
contentType: $contentType
}
) {
url
fields
key
}
}
- Upload the JSON file to the returned url using the provided form fields. The response includes a key that identifies the stored file in the mass-operations bucket.
- Register the bulk task:
mutation CreateMassOp($key: String!, $autoStart: Boolean) {
createMassOperationBulkTask(input: { key: $key, autoStart: $autoStart }) {
id
key
status
createdAt
}
}
mutation CreateMassOp($key: String!, $autoStart: Boolean) {
createMassOperationBulkTask(input: { key: $key, autoStart: $autoStart }) {
id
key
status
createdAt
}
}
- If autoStart is true, the API immediately dispatches the mass-operations-runner task.
- If autoStart is omitted or false, call startMassOperationBulkTask(id: ID!) later when you are ready.
Monitoring Progress
- Bulk task status: Query
bulkTask(id: ID!)
orbulkTasks(filter: { type: massOperation })
to retrieve task status. The lifecycle ispending → started → complete
orerror
. - Execution logs: Use
operationLogs(filter: { operationId: "<bulkTaskId>" })
to stream detailed logs. Each log entry stores the original input payload, the command executed, the result, and astatus
(success
,partial
,failure
) with astatusCode
(e.g.,200
,206
,400
,500
). - Outputs: Successful operations include the command output (IDs, payloads, etc.), making it easy to chain follow-up actions.
If the runner detects invalid JSON or schema violations, it records the issues and marks the task as error without attempting the remaining operations.
Using the Crystallize CLI
The Crystallize CLI provides turnkey helpers around the workflow:
~/crystallize mass-operation dump-content-model <tenant> <file>
: Generate a starter mass operation file containing the current shapes and pieces.~/crystallize mass-operation run <tenant> <file>
: Validate against the schema, request the presigned upload, push the file, create the bulk task (with autoStart), and wait for completion while tailing logs.~/crystallize mass-operation execute-mutations <tenant> <file>
: Run client-side GraphQL mutations that complement mass operations (e.g., to script additional steps).
All commands support --no-interactive
for CI usage and reuse stored credentials. The CLI performs JSON validation locally before uploading, so you catch schema issues early.Best Practices & Tips
- Validate early: Keep files under version control and lint them locally; the CLI validation mirrors the server schema.
- Chunk large jobs: Split very large migrations into multiple tasks to simplify retries.
- Reuse identifiers: Prefer identifier-based references so subsequent operations can find the entities created earlier in the file.
- Idempotency with upsert: Use
upsert
intents when you want to rerun the same file safely. - Monitor logs: Always review
operationLogs
for failures or partial successes; follow-up actions can be scripted based on the emitted output. - Security: Presigned URLs are short-lived. Upload immediately after requesting them and keep the file size within your tenant’s upload limits.
Mass operations are powerful but operate with elevated privileges (the runner acts as a tenant admin). Treat the files as infrastructure artefacts, review them like code, and automate verification through the CLI wherever possible.