A single malformed row should not kill a 5,000-variant batch. This guide shows how row-level validation, partial-failure handling and idempotency keys keep valid renders moving — and how to resubmit only the rows that failed.
The 5,000-row file with two bad rows
It is the classic batch scenario: a product feed lands with thousands of rows, marketing wants variants for every SKU by tomorrow, and somewhere in row 3,187 someone typed 79,9O instead of 79.90. What should happen next is a design decision, not an accident: either the whole batch dies, or the 4,998 good rows render and the two broken rows come back with a readable reason.
Legacy export pipelines often behave like the first option — one exception aborts the loop, and a retry re-renders everything, including rows that already succeeded. This article walks through the second option: bulk rendering with row-level validation, partial-failure handling and retry-safe idempotency, using JsonCut's typed template API as the concrete example. The numbers above are an illustration; the mechanisms below are documented behavior.
Where bulk renders actually fail
Bulk failures cluster into three stages, and each stage deserves its own handling:
| Stage | Typical failure | Right response |
|---|---|---|
| Input validation | Wrong field name, bad number, missing media, wrong MIME type | Reject the row before any render work starts |
| Rendering | Output settings incompatible, media unavailable mid-run | Fail that row, keep the batch moving, expose a retryable flag |
| Delivery | Your webhook endpoint is down, artifact download times out | Retry delivery; never re-render what already completed |
The key principle: validate everything you can before expensive work begins, and never let one stage's failure invalidate another stage's finished results.
Row-level validation: reject early, render the rest
JsonCut's bulk rendering is built around a typed template contract: you publish a project as a template, every input has an explicit type (string, number, boolean, color, image, video, audio, sanitized HTML or bounded string arrays), and every row in a batch is validated against the pinned template version before it is queued.
Three documented consequences matter for error handling:
- JSON and CSV batches are capped at 500 rows. Larger feeds are split into multiple batches — which is also the natural unit for retries and progress reporting.
- Every row is validated before queueing. Invalid rows are reported with their index and a safe error message; they do not prevent valid rows from rendering.
- The batch is pinned to one immutable template version. A row that validated correctly cannot suddenly behave differently because someone republished the design mid-run.
For CSV, headers must match the template input IDs, and values are parsed against the published input types before any valid row is queued. If you are designing the input side of that contract — which fields should be typed how — see the earlier guide on template input design.
The batch contract at a glance
| Field | Purpose | Error-handling role |
|---|---|---|
items or csv | Row payload (exactly one of them per request) | Wrong shape fails fast, before rendering |
Batch-level idempotencyKey | Identifies the whole request | A resent batch with the same key cannot duplicate the run |
Row-level idempotencyKey | Identifies one output | Corrected resubmissions keep stable row identity |
Batch-level defaults | Merged first; row values take precedence | Fewer per-row mistakes to validate away |
| Template version | Batch pinned to an immutable version | Deterministic validation and rendering across retries |
Splitting a large feed into predictable groups of up to 500 rows — with their own batch keys — gives you bounded blast radius: one bad group is one corrected resubmission, not a 5,000-row rerun.
Build a correction loop, not a full rerun
When validation reports broken rows, the goal is to resubmit only what failed:
- In Studio, invalid rows remain visible with their index and safe error, and you can correct and resubmit selected rows.
- Through the API, you submit a new batch containing only the corrected rows — and keep the same explicit template version when the approved design must remain unchanged.
This is where stable row identity pays off. If row sku-1042-de failed on a malformed price and you resubmit it with the same row-level idempotency key after fixing the value, your delivery logic can key outputs by that stable identifier instead of by batch position. The result: no duplicate artifacts, no double delivery, no manual deduplication spreadsheet.
Idempotency: why retries don't double your renders
Retries are unavoidable — networks drop, deploys restart workers. Idempotency keys are what keep a retry from becoming a second render of the same work. JsonCut's API loop asks you to use an Idempotency-Key for render creation and to obey the returned retryable flag when handling errors.
The pattern is older than this API and worth understanding on its own. Payment APIs popularized it; Stripe's documentation describes the semantics precisely: a client generates a unique key per logical operation, the server saves the first response for that key, and any retry with the same key returns the same saved result — including stored errors — instead of executing the operation twice. Stripe also notes two practical guardrails: reuse a key with different parameters and the server rejects the mismatch, and keys have a lifetime (Stripe prunes them after at least 24 hours), so "same key" must always mean "same intended operation."
Applied to bulk rendering:
| Situation | Without idempotency | With idempotency keys |
|---|---|---|
| Timeout after submitting a batch | Resend? Maybe render everything twice | Resend same key; get the same answer |
| Worker crash mid-batch | Ambiguous state, full manual audit | Row keys identify completed outputs |
| Row failed, you fixed the value | New row identity? Outputs drift | Same row key, corrected payload, stable identity |
Track every row through the run
Bulk work in JsonCut is asynchronous: you submit, then either poll GET /api/v2/bulk-renders/{id} or receive signed webhooks, and you can cancel unfinished work with DELETE /api/v2/bulk-renders/{id}. Individual renders move through asynchronous states — queued, rendering, completed, failed or cancelled — the same lifecycle described in the render and webhook guide.
For operators, expose progress in the units they actually act on:
| Metric | Question it answers |
|---|---|
| Rows accepted | Did validation pass the whole file? |
| Rows rejected (with reasons) | Which cells need fixing, and by whom? |
| Renders queued / rendering | Is the system working or waiting? |
| Outputs completed | What is ready for delivery? |
| Render failures (retryable or not) | What needs a retry versus a design fix? |
A single percentage hides all of that. Five counters tell the on-call person exactly what to do next.
A production checklist
- Validate the entire input file before reserving render work; treat field-level errors as data, not exceptions.
- Batch in bounded groups (JsonCut caps JSON and CSV batches at 500 rows) with explicit template versions.
- Send a batch-level idempotency key per logical submission and stable row-level keys per output.
- Continue valid rows when a row fails; export failures with index and reason.
- Obey the
retryableflag; never blind-retry a non-retryable error. - Key delivery on row identity, not batch position, so corrected resubmissions cannot double-send.
- Keep one dashboard question per failure stage: validation, rendering, delivery.
FAQ
Does one invalid row cancel the whole batch? No. Rows are validated against the pinned template version before queueing; invalid rows are reported with their index and a safe error, and valid rows continue to render.
What happens if I retry a batch that already ran? If you reuse the same idempotency key, the API answers with the recorded result instead of executing the work again. Use a fresh key only when you genuinely want a new logical operation.
How do I fix the rows that failed? In Studio, correct and resubmit selected rows. Through the API, submit a new batch with only the corrected rows, keeping the same explicit template version if the approved design must stay unchanged.
Sources
https://jsoncut.com/docs/templates/ — JsonCut documentation, typed templates and bulk rendering — accessed 2026-10-09 https://jsoncut.com/docs/ — JsonCut documentation, the four-step API loop and idempotency — accessed 2026-10-09 https://jsoncut.com/docs/quickstart/ — JsonCut documentation, asynchronous render creation, polling and artifacts — accessed 2026-10-09 https://docs.stripe.com/api/idempotent_requests — Stripe API reference, idempotent request semantics — accessed 2026-10-09