# Batch Operations

Batch operations let you create, update, upsert, or delete multiple records in a single request. They are **auto-enabled** when the corresponding write operation exists in your definition.

## Available Endpoints

| Endpoint | Method | Auto-enabled When |
|----------|--------|-------------------|
| `/{resource}/batch` | `POST` | `create` exists |
| `/{resource}/batch` | `PATCH` | `update` exists |
| `/{resource}/batch` | `DELETE` | `delete` exists |
| `/{resource}/batch` | `PUT` | `upsert` exists |

To disable a batch operation, set it to `false` in your definition:

```typescript
create: {
  access: { roles: ['hiring-manager'] },
  batch: false,  // Disable batch create
}
```

## Batch Create

```
POST /api/v1/{resource}/batch
```

### Request

```json
{
  "records": [
    { "candidateId": "cand_101", "jobId": "job_201", "stage": "applied", "notes": "Strong frontend background" },
    { "candidateId": "cand_101", "jobId": "job_202", "stage": "applied", "notes": "Also interested in backend role" },
    { "candidateId": "cand_102", "jobId": "job_201", "stage": "applied", "notes": "Referred by employee" }
  ],
  "options": {
    "failFast": false
  }
}
```

### Response (201 or 207)

```json
{
  "success": [
    { "id": "app_abc1", "candidateId": "cand_101", "jobId": "job_201", "stage": "applied", "createdAt": "2025-01-15T10:00:00Z" },
    { "id": "app_abc3", "candidateId": "cand_102", "jobId": "job_201", "stage": "applied", "createdAt": "2025-01-15T10:00:00Z" }
  ],
  "errors": [
    {
      "index": 1,
      "record": { "candidateId": "cand_101", "jobId": "job_202", "stage": "applied", "notes": "Also interested in backend role" },
      "error": {
        "error": "Database insert failed",
        "layer": "validation",
        "code": "INSERT_FAILED",
        "details": { "reason": "UNIQUE constraint failed: applications.candidateId, applications.jobId" }
      }
    }
  ],
  "meta": {
    "total": 3,
    "succeeded": 2,
    "failed": 1,
    "failFast": false,
    "transactional": true
  }
}
```

- **201** — All records created successfully
- **207** — Partial success (some errors, some successes)

`POST /batch` uses a single bulk `INSERT`, so `meta.transactional` is `true` on every provider — the database guarantees the insert is atomic regardless of the `failFast` flag.

Fields applied automatically to each record:
- ID generation (UUID, prefixed, etc.)
- Ownership fields (`organizationId`, `ownerId`, `createdBy`)
- Audit fields (`createdAt`, `modifiedAt`)
- Default values and computed fields

## Batch Update

```
PATCH /api/v1/{resource}/batch
```

Every record **must include an `id` field**.

### Request

```json
{
  "records": [
    { "id": "app_abc1", "notes": "Passed phone screen, schedule onsite" },
    { "id": "app_abc2", "notes": "Moved to technical interview round" },
    { "id": "app_xyz9", "notes": "Hiring manager approved offer" }
  ],
  "options": {
    "failFast": false
  }
}
```

### Response (200 or 207)

```json
{
  "success": [
    { "id": "app_abc1", "notes": "Passed phone screen, schedule onsite", "modifiedAt": "2025-01-15T11:00:00Z" },
    { "id": "app_abc2", "notes": "Moved to technical interview round", "modifiedAt": "2025-01-15T11:00:00Z" }
  ],
  "errors": [
    {
      "index": 2,
      "record": { "id": "app_xyz9", "notes": "Hiring manager approved offer" },
      "error": {
        "error": "Not found",
        "layer": "firewall",
        "code": "NOT_FOUND",
        "details": { "id": "app_xyz9" }
      }
    }
  ],
  "meta": {
    "total": 3,
    "succeeded": 2,
    "failed": 1,
    "failFast": false,
    "transactional": false
  }
}
```

Records that don't exist or aren't accessible through the firewall return a `NOT_FOUND` error. Guard rules (immutable, protected, not-updatable fields) are checked per record.

### Missing IDs

If any records are missing the `id` field, the entire request is rejected:

```json
{
  "error": "Records missing required ID field",
  "layer": "validation",
  "code": "BATCH_MISSING_IDS",
  "details": { "indices": [0, 2] },
  "hint": "All records must include an ID field for batch update."
}
```

## Batch Delete

```
DELETE /api/v1/{resource}/batch
```

### Request

```json
{
  "ids": ["app_abc1", "app_abc2", "app_abc3"],
  "options": {
    "failFast": false
  }
}
```

Note: Batch delete uses an `ids` array (not `records`).

### Response (200 or 207)

**Soft delete** (default):

```json
{
  "success": [
    { "id": "app_abc1", "candidateId": "cand_101", "deletedAt": "2025-01-15T12:00:00Z" },
    { "id": "app_abc2", "candidateId": "cand_103", "deletedAt": "2025-01-15T12:00:00Z" }
  ],
  "errors": [],
  "meta": {
    "total": 2,
    "succeeded": 2,
    "failed": 0,
    "failFast": false,
    "transactional": true
  }
}
```

`DELETE /batch` issues a single bulk `UPDATE … WHERE id IN (...)` (soft delete) or `DELETE … WHERE id IN (...)` (hard delete), so `meta.transactional` is `true` on every provider regardless of `failFast`.

**Hard delete**: Returns objects with only the `id` field (record data is deleted).

## Batch Upsert

```
PUT /api/v1/{resource}/batch
```

Inserts new records or updates existing ones. Every record must include an `id` field.

**Note:** Batch upsert is only available when `generateId` is set to `false` (user-provided IDs) in your configuration.

### Request

```json
{
  "records": [
    { "id": "app_001", "candidateId": "cand_101", "jobId": "job_201", "stage": "screening", "notes": "Updated after phone screen" },
    { "id": "app_002", "candidateId": "cand_104", "jobId": "job_203", "stage": "applied", "notes": "New application from referral" }
  ],
  "options": {
    "failFast": false
  }
}
```

### Response (201 or 207)

The compiler checks which IDs already exist and splits the batch into creates and updates. Create records get ownership and default fields; update records get only `modifiedAt`. Under `failFast: true`, both the bulk insert and the per-record updates run inside the same transaction — a failed update rolls back the inserts too on tx-supporting providers.

## Fail-Fast Mode

By default, batch operations use **partial success** mode — each record is processed independently, and failures don't affect other records.

Set `"failFast": true` to stop on the first error:

```json
{
  "records": [...],
  "options": {
    "failFast": true
  }
}
```

### Fail-Fast Failure Response (400)

```json
{
  "error": "Batch failed at index 1",
  "layer": "validation",
  "code": "BATCH_FAILFAST_STOPPED",
  "details": {
    "failedAt": 1,
    "reason": "Database insert failed",
    "errorDetails": { "reason": "UNIQUE constraint failed" }
  }
}
```

## Transactional Semantics

The behavior of `failFast: true` depends on whether your database provider
supports transactions. The response's `meta.transactional` flag tells you
which guarantee you actually got — inspect it in clients that care about
rollback.

### Per-Provider Matrix

| Endpoint | Provider | `failFast` | `meta.transactional` | Behavior |
|----------|----------|-----------|----------------------|----------|
| `PATCH /batch` (update) | neon (Postgres via Hyperdrive) | `true` | `true` | `db.transaction()` wraps the per-record loop. Any failure rolls back ALL writes in the batch. |
| `PATCH /batch` (update) | (any) | `false` | `false` | Per-record loop, partial success. Each record commits independently. |
| `PATCH /batch` (update) | cloudflare-d1 | `true` | `false` | Fail-fast loop without rollback — D1 has no `db.transaction`. Records before the failure stay committed. |
| `PUT /batch` (upsert) | tx-supporting | `true` | `true` | Bulk insert + per-record update share one transaction. Any failure rolls back both. |
| `PUT /batch` (upsert) | cloudflare-d1 | `true` | `false` | Bulk insert commits; per-record update loop is fail-fast without rollback. |
| `POST /batch` (create) | (any) | (any) | `true` | Single bulk `INSERT` — atomic at the DB level. |
| `DELETE /batch` | (any) | (any) | `true` | Single bulk `UPDATE` (soft) or `DELETE` (hard) — atomic at the DB level. |

### Reading `meta.transactional` in clients

```typescript
const res = await fetch('/api/v1/jobs/batch', {
  method: 'PATCH',
  body: JSON.stringify({ records, options: { failFast: true } }),
});
const body = await res.json();

if (!body.meta.transactional && body.errors.length > 0) {
  // Some records succeeded before the failure and remain committed.
  // Compensate via the success array if needed.
}
```

### Embeddings and Realtime

When a resource has `embeddings` or `realtime` configured, queue messages
and broadcasts are emitted **after** the transaction commits. A
rolled-back batch produces no orphan embedding jobs.

## Batch Size Limit

The default maximum batch size is **100 records**. Requests exceeding the limit are rejected:

```json
{
  "error": "Batch size limit exceeded",
  "layer": "validation",
  "code": "BATCH_SIZE_EXCEEDED",
  "details": { "max": 100, "actual": 250 },
  "hint": "Maximum 100 records allowed per batch. Split into multiple requests."
}
```

## Configuration

Batch operations inherit access control from their corresponding write operation. You can override per-batch settings:

```typescript
export default defineTable(applications, {
  create: {
    access: { roles: ['hiring-manager', 'recruiter'] },
    batch: {
      access: { roles: ['hiring-manager'] },  // More restrictive than single create
      maxBatchSize: 50,                       // Default: 100
      allowFailFast: true,                    // Default: true
    },
  },
  update: {
    access: { roles: ['hiring-manager', 'recruiter'] },
    batch: {
      maxBatchSize: 100,
      allowFailFast: true,
    },
  },
  delete: {
    access: { roles: ['hiring-manager'] },
    mode: 'soft',
    batch: {
      access: { roles: ['hiring-manager'] },
      maxBatchSize: 100,
      allowFailFast: true,
    },
  },

  // Disable only batch upsert
  upsert: {
    access: { roles: ['hiring-manager'] },
    batch: false,
  },
});
```

## Security

All security pillars apply to batch operations:

1. **Authentication** — Required for all batch endpoints (401 if missing)
2. **Firewall** — Applied per-record for update/delete/upsert (not applicable to create)
3. **Access** — Role-based check using the batch operation's access config
4. **Guards** — Per-record validation (createable/updatable/immutable fields)
5. **Masking** — Applied to all records in the success array before response

## See Also

- [CRUD Endpoints](/compiler/targets/hono-api/using-the-api/crud) — Single-record operations
- [Error Responses](/compiler/targets/hono-api/using-the-api/errors) — Error format reference
- [Guards](/compiler/definitions/guards) — Field-level write protection
