git clone https://github.com/Intense-Visions/harness-engineering
T=$(mktemp -d) && git clone --depth=1 https://github.com/Intense-Visions/harness-engineering "$T" && mkdir -p ~/.claude/skills && cp -r "$T/agents/skills/claude-code/api-bulk-operations" ~/.claude/skills/intense-visions-harness-engineering-api-bulk-operations && rm -rf "$T"
agents/skills/claude-code/api-bulk-operations/SKILL.mdBulk Operations
BULK ENDPOINTS AMORTIZE PER-REQUEST OVERHEAD ACROSS MANY OPERATIONS IN A SINGLE HTTP CALL — BUT THE REAL DESIGN CHALLENGE IS WHAT TO RETURN WHEN SOME OPERATIONS SUCCEED AND OTHERS FAIL: TRANSACTIONAL SEMANTICS ROLL BACK EVERYTHING ON ANY ERROR WHILE BEST-EFFORT SEMANTICS COMMIT SUCCESSES AND REPORT FAILURES INDIVIDUALLY, AND CHOOSING THE WRONG MODEL PRODUCES SILENT DATA CORRUPTION OR WASTED RETRIES.
When to Use
- Designing an import endpoint that creates or updates hundreds of records from a CSV upload
- Implementing a bulk delete for a UI that allows multi-select and mass removal
- Building a notification service that sends messages to thousands of recipients in a single API call
- Reducing the number of round trips for a client that must create 50 resources sequentially
- Reviewing a PR that loops over individual create/update/delete endpoints in a batch job
- Choosing between all-or-nothing transaction semantics and best-effort per-item semantics for a batch endpoint
- Adding
support to a bulk create endpoint that may be retried on network failureIdempotency-Key - Designing partial failure responses and deciding which HTTP status code to return when some items fail
Instructions
Key Concepts
-
Bulk Create, Update, and Delete — A bulk endpoint accepts an array of operation items in the request body and executes them as a collection. The three primary patterns are:
- Bulk create:
with a bodyPOST /resources/bulk
creates multiple resources in one request.{"items": [...]} - Bulk update:
applies a set of patch operations to multiple existing resources.PATCH /resources/bulk - Bulk delete:
with a body listing resource IDs, orDELETE /resources/bulk
for clients that cannot send DELETE bodies.POST /resources/bulk-delete
- Bulk create:
-
Transactional vs. Best-Effort Semantics — This is the most consequential design decision for a bulk endpoint:
- Transactional: All items are applied in a single database transaction. If any item fails validation or encounters an error, the entire batch is rolled back. The response is either
(all succeeded) or200 OK
/422 Unprocessable Entity
(none were applied, with per-item errors). Use this when partial application would leave data in an inconsistent state.400 Bad Request - Best-effort: Each item is applied independently. Successes are committed; failures are reported per-item. The response uses
with a per-item result array. Use this when partial success is acceptable (e.g., sending notifications — some succeed, others fail) and callers can retry only the failed items.207 Multi-Status
- Transactional: All items are applied in a single database transaction. If any item fails validation or encounters an error, the entire batch is rolled back. The response is either
-
207 Multi-Status Response — When using best-effort semantics, return
with a body that maps each input item to an individual status code and optional error detail:207 Multi-Status{ "results": [ { "index": 0, "status": 201, "id": "res_001" }, { "index": 1, "status": 422, "error": { "code": "validation_failed", "message": "name is required" } }, { "index": 2, "status": 201, "id": "res_003" } ] }The outer HTTP status is
regardless of how many items failed. Never return207
for a best-effort batch where some items failed — it misleads callers into thinking all items succeeded.200 OK -
Idempotency-Key on Bulk Endpoints — A bulk create endpoint that fails mid-way through leaves the caller uncertain about which items were created. Re-submitting the full batch without an idempotency key will duplicate items. Accept an
header on bulk create and bulk update endpoints. Store the key with a short TTL and replay the previous response on a duplicate submission. For best-effort batches, the idempotency key covers the entire batch — if the batch partially succeeded and the client retries with the same key, replay the original (partial success) response rather than re-executing.Idempotency-Key -
Request Size Limits — Impose an explicit maximum on the number of items per batch and the total request body size in bytes. Document both limits. Common limits: 100–1,000 items per batch, 5–10 MB body size. Requests exceeding these limits should return
(body size) or413 Content Too Large
(item count) with the limits stated in the error body. Do not silently truncate the input.400 Bad Request -
Ordering and Dependency — Document whether the items in a bulk request are processed in order and whether later items may depend on earlier ones (e.g., bulk create where item 2 references the ID of item 1). Most bulk endpoints treat items as independent and process them in unspecified order. If ordering matters, process sequentially in input order and stop on first failure (transactional) or continue and report each failure (best-effort).
Worked Example
Stripe's Bulk operations pattern via the Batch API demonstrates the design of a transactional batch using per-operation result codes:
Bulk create price and product in a single batch call:
POST /v1/batch Content-Type: application/json Authorization: Bearer sk_example_... Idempotency-Key: batch_20240315_import_001 { "requests": [ { "method": "POST", "url": "/v1/products", "body": "name=Widget+Pro&description=Our+flagship+product" }, { "method": "POST", "url": "/v1/prices", "body": "product=prod_abc&unit_amount=2999¤cy=usd" } ] }
HTTP/1.1 200 OK Content-Type: application/json { "results": [ { "status": 200, "body": { "id": "prod_abc123", "object": "product", "name": "Widget Pro" } }, { "status": 200, "body": { "id": "price_xyz789", "object": "price", "unit_amount": 2999, "currency": "usd", "product": "prod_abc123" } } ] }
Bulk delete with best-effort semantics (207 Multi-Status):
POST /api/v1/messages/bulk-delete Content-Type: application/json Authorization: Bearer token_... { "ids": ["msg_001", "msg_002", "msg_003"] }
HTTP/1.1 207 Multi-Status Content-Type: application/json { "results": [ { "id": "msg_001", "status": 204 }, { "id": "msg_002", "status": 404, "error": { "code": "not_found", "message": "Message not found" } }, { "id": "msg_003", "status": 204 } ], "summary": { "succeeded": 2, "failed": 1 } }
Anti-Patterns
-
Returning
when some items in a best-effort batch failed. A200 OK
response signals complete success to HTTP clients, middleware, and monitoring systems. If item 3 of 10 failed and the response is200 OK
, automated retry logic and dashboards will not flag the failure. Return200 OK
for any best-effort batch response that contains a mixture of success and failure, or207 Multi-Status
/422
for a transactional batch that failed entirely.400 -
Silently truncating batches that exceed the size limit. If a client sends 500 items and the server's limit is 100, silently processing only the first 100 and returning success causes the caller to believe all 500 were processed. The remaining 400 items are silently dropped. Always validate the item count before processing and return
with the limit in the error body if exceeded.400 Bad Request -
Applying transactional semantics to operations that cannot be rolled back. If a bulk endpoint sends emails, charges cards, or triggers external webhooks as each item is processed, these side effects cannot be rolled back when a later item fails. Applying transactional semantics implies rollback is possible. Use best-effort semantics for operations with irreversible external side effects, document the semantics clearly, and design callers to handle partial success.
-
Omitting per-item indexing in the error response. A
response that lists errors without correlating them to the input items forces callers to guess which items failed. Always include the input index (0-based or 1-based, consistently) or the input item's client-provided ID in every result entry. This allows callers to retry only the failed items without re-submitting the entire batch.207
Details
Choosing Transactional vs. Best-Effort
| Factor | Transactional | Best-Effort |
|---|---|---|
| Data consistency requirement | Strong — all or nothing | Eventual — partial success OK |
| External side effects | None or fully reversible | Possible; cannot be rolled back |
| Retry behavior | Client retries full batch | Client retries failed items only |
| Response status on partial fail | / — nothing applied | — successes committed |
| Database transaction | Single transaction | Per-item transactions |
| Use cases | Financial ledger, referential imports | Notifications, bulk tag application |
Idempotency Key Scope for Bulk
When a client retries a bulk request with the same
Idempotency-Key:
- Transactional batch that fully succeeded: Replay the original
response. Do not re-execute.200 OK - Transactional batch that fully failed: Re-execute (the original transaction was rolled back; no state change occurred).
- Best-effort batch: Replay the original
response. Do not re-execute any item, including failed items. The client should re-submit only the failed items with a new idempotency key.207
Real-World Case Study: Mailchimp Batch API
Mailchimp's
/3.0/batches endpoint processes bulk subscriber list updates asynchronously. Synchronous per-subscriber API calls for a 10,000-subscriber import produced approximately 35 minutes of sequential HTTP round trips from clients with standard connection limits. After migrating to the batch API, the same 10,000-subscriber import completed in under 90 seconds — a 23x throughput improvement. The batch API uses best-effort semantics and returns an operation ID immediately; clients poll GET /3.0/batches/{batch_id} for completion and retrieve a per-item error report from a signed S3 URL included in the completed batch response.
Source
- Stripe Batch API
- RFC 4918 — HTTP Extensions for WebDAV (207 Multi-Status)
- Mailchimp Batch Operations
- Microsoft Graph Batch Requests
- Stripe Idempotency Keys
Process
- Decide on semantics: transactional (all-or-nothing, single DB transaction, return
/400
on any failure) or best-effort (per-item commit, return422
with per-item results). Document the choice in the API reference.207 Multi-Status - Define and enforce request limits: maximum item count, maximum body size in bytes. Return
or413 Content Too Large
with limits in the error body for violations.400 Bad Request - Add
header support. Store the key with the full response and TTL. On duplicate submission, replay the stored response without re-executing.Idempotency-Key - Implement per-item result reporting: every result entry must include the input index or client-provided ID, the HTTP status for that item, and for failures, a machine-readable error code and human-readable message.
- For best-effort batches with external side effects (email, webhooks, charges), document that failed items are not retried by the server and callers are responsible for resubmitting failures.
Harness Integration
- Type: knowledge -- this skill is a reference document, not a procedural workflow.
- No tools or state -- consumed as context by other skills and agents.
- related_skills: api-idempotency-keys, api-error-contracts, api-http-methods, api-status-codes
Success Criteria
- Bulk endpoints with best-effort semantics return
, never207 Multi-Status
, when any item fails.200 OK - Per-item results in
responses include the input index or client-provided ID for every item, success or failure.207 - Request size limits (item count and body bytes) are enforced and documented; violations return
/400
with the limit values in the error body.413 - Bulk create and bulk update endpoints accept and honor
headers; duplicate submissions replay the stored response without re-executing.Idempotency-Key - API documentation explicitly states whether semantics are transactional or best-effort for every bulk endpoint.