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. | 
| 
 | Manage items of type:  | 
| 
 | Manage items of type:  | 
| 
 | Manage items of type:  | 
| 
 | 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" }
      }
    }
  ]
}
Dependencies across Operations
The simplest way to understand dependencies between operations is to imagine building a tree structure.
For example, you might want to create a folder inside another folder, and then import products into that newly created folder: all within the same list of operations.
To reference a previous operation in a subsequent one, you assign it an identifier using the _ref keyword.
[
    {
        "_ref": "rooms",
        "intent": "folder/create",
        "name": "My Rooms",
        "language": "en",
        "shapeIdentifier": "folder",
        "tree": {
            "parentId": "6902a7a78f7d45cf23343fab"
        }
    },
    {
        "intent": "folder/create",
        "name": "My Kitchen",
        "language": "en",
        "shapeIdentifier": "folder",
        "tree": {
            "parentId": "{{ rooms.id }}"
        }
    },
];[
    {
        "_ref": "rooms",
        "intent": "folder/create",
        "name": "My Rooms",
        "language": "en",
        "shapeIdentifier": "folder",
        "tree": {
            "parentId": "6902a7a78f7d45cf23343fab"
        }
    },
    {
        "intent": "folder/create",
        "name": "My Kitchen",
        "language": "en",
        "shapeIdentifier": "folder",
        "tree": {
            "parentId": "{{ rooms.id }}"
        }
    },
];In this example, the folder “My Kitchen” will be created inside “My Rooms.”
The list of variables available on a reference depends on the specific intent.
A complete list will be documented soon, but for now, you can inspect the OperationLog output to see which variables are available for each intent.
Defaults and Global Variables
The operation runner also provides default context variables:
- {{ defaults.rootItemId }}
- {{ defaults.languages.[0] }}
- {{ defaults.vats.[0] }}
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: QuerybulkTask(id: ID!)orbulkTasks(filter: { type: massOperation })to retrieve task status. The lifecycle ispending → started → completeorerror.
- 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 upsertintents when you want to rerun the same file safely.
- Monitor logs: Always review operationLogsfor 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.