Project Summary
Jira’s bulk mutation APIs expose inconsistent execution patterns. Some are synchronous and constrained by request timeouts; others run in the background but each report progress differently. This RFC proposes one asynchronous contract for bulk mutations: submit a request, receive 202 Accepted with a job id, and poll a single shared job resource for status, progress, and per-row results. With this asynchronous job submission pattern, large size mutation jobs can be accepted and processed by the platform, simplifying the developer experience and protecting system reliability.
Existing endpoints are unchanged. This is additive.
- Publish: 9 Sep 2026
- Discuss: 10 Sep 2026 - 30 Sep 2026
- Resolved: TBD
Problem
Most Jira write APIs are synchronous: the caller sends a request and waits. This works for small changes but does not scale effectively for large changes.
The current bulk API surface is inconsistent:
| Endpoint | Current behaviour | Existing API Reference |
|---|---|---|
POST /rest/api/3/issue/bulk |
Synchronous. Capped at 50 issues.Returns 201 on most requests with created issues and errors returned inline in same response payload. |
Bulk create issue |
POST /rest/api/3/bulk/issues/fields |
Asynchronous. Returns a task id polled at /rest/api/{2|3}/bulk/queue/{taskId}. Requires the same field set for every issue. |
Bulk edit issues |
POST /rest/api/3/bulk/issues/move |
Asynchronous. Returns a task id polled at /rest/api/{2|3}/bulk/queue/{taskId}. All issues must be moved to the same destination.Capped at 1000 issues. |
Bulk move issues |
POST /rest/api/3/bulk/issues/delete |
Asynchronous. Returns a task id polled at /rest/api/{2|3}/bulk/queue/{taskId}. Capped at 1000 issues. |
Bulk delete issues |
POST /rest/api/3/bulk/issues/transition |
Asynchronous. Returns a task id polled at /rest/api/{2|3}/bulk/queue/{taskId}. Capped at 1000 issues. Can request mixed transition IDs. |
Bulk transition issue statuses |
POST /rest/api/2/issue/properties |
Asynchronous.Returns 303 and a Location header for task polling.Requires the same field set for every issue. Capped at 10,000 issues and 10 properties. |
Bulk Set Issues properties by List |
POST /rest/api/2/issue/properties/multi |
Asynchronous.Returns 303 and a Location header for task polling.Requires the same field set for every issue. Capped at 100 issues and 10 properties. |
Bulk Set Issues properties by Issue |
| Worklog creation, issue links | No bulk endpoint. Callers loop over the single-item API. |
Some of these return 201 and point at /task/{taskId}; others return a 303 and the caller must inspect the HTTP Location header to check task completion, others return a task id polled at /bulk/queue/{taskId}, yet others return a 201 but complete synchronously. A caller integrating these various APIs implements multiple disparate contracts.
Where no bulk endpoint fits, callers loop over single-item endpoints. Developers must implement their own checkpointing and failure handling, and are susceptible to rate limits disrupting a bulk sequence of operations.
Proposal
One contract for every bulk mutation on the platform: accept fast with consistent HTTP response codes and job identifiers, execute in the background, report through a shared job resource.
Submitting a job
Each operation has its own submit endpoint, so it keeps its own request schema, OAuth scope, and rate-limit identity.
POST /rest/api/3/jobs/issues/update
Content-Type: application/json
Idempotency-Key: 8f3c1b9d6f6e0c7a4f9ba6d6123456789abc
{ ... inline JSON, or a reference to an uploaded file ... }
HTTP/1.1 202 Accepted
Location: /rest/api/3/jobs/a1b2c3d4-e5f6-7890-abcd-ef1234567890
{
"jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"status": "ACCEPTED"
}
Submit performs authorization, schema, and admission checks only. It does no mutation work, so its latency does not grow with job size.
Payload
Two ways to supply rows, one execution path;
- Inline NDJSON for small jobs, up to a bounded body size.
POST /rest/api/3/jobs/{entity}/{operation}
{
"jobId": "b3273d5f-2bcc-45af-9ba0-d9eb3e6e6794",
"status": "ACCEPTED",
"submittedAt": "2026-09-04T10:30:00Z"
}
- File reference for everything larger, via a resumable chunked upload:
POST /rest/api/3/jobs/uploads -> 201 { "uploadId": "up_7f2a" }
PUT /rest/api/3/jobs/uploads/up_7f2a/parts/1 -> 200 (each part independently retryable)
GET /rest/api/3/jobs/uploads/up_7f2a -> 200 (list received parts to resume)
POST /rest/api/3/jobs/uploads/up_7f2a/complete -> 200 { "rows": 50000 }
DELETE /rest/api/3/jobs/uploads/up_7f2a -> 204 (abort; unfinished uploads also expire)
Ordering
Note that row order is not guaranteed; do not create dependencies between rows.
Polling
Status, report, and cancellation live under one prefix, mandatory for every operation on the platform:
GET /rest/api/3/jobs/a1b2c3d4-e5f6-7890-abcd-ef1234567890
{
"jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"operation": "issues.update",
"status": "RUNNING",
"progress": { "total": 50000, "processed": 12500, "succeeded": 12497, "failed": 3 },
"reportUrl": "/rest/api/3/jobs/a1b2c3d4-e5f6-7890-abcd-ef1234567890/report",
"submittedAt": "2026-08-20T09:00:00.000Z",
"startedAt": "2026-08-20T09:00:02.000Z"
}
Responses include Retry-After to pace polling. Status is a cheap read and never computes progress on demand.
Report
Per-row outcomes are paginated and filterable. Only failures, warnings, and skips are recorded — successes are counted, not listed.
GET /rest/api/3/jobs/{jobId}/report?status=FAILED&page=1&pageSize=100
{
"summary": { "total": 50000, "succeeded": 49978, "failed": 22 },
"entries": [
{ "rowId": "PROJ-412", "code": "403", "message": "PERMISSION_DENIED" }
],
"page": 1,
"totalPages": 1
}
The default error mode runs to the end of the payload and collects every failure in one pass, so a caller fixes all bad rows once rather than one submit-retry cycle at a time.
Cancellation
POST /rest/api/3/jobs/{jobId}/cancel -> 202 Accepted
Cancellation is cooperative and takes effect at the next batch boundary. Batch sizes may be configured according to the specific operation. Rows already applied stay applied; the report reflects exactly what was done. There is no automatic rollback.
Lifecycle
State descriptions
| State | Type | Description | Expected client action |
|---|---|---|---|
REJECTED |
Submission outcome | The submission was not accepted because a service limit was reached. No job processing occurs. | Wait for the duration indicated by Retry-After, then resubmit using an appropriate retry policy. |
ACCEPTED |
Transient | The request was accepted and a durable job was created. Admission control has not yet decided whether the job should run immediately or wait. | Store the returned jobId and begin polling the job resource. |
QUEUED |
Non-terminal | The job is waiting for processing capacity. It has not started modifying Jira data. | Continue polling with exponential backoff. |
RUNNING |
Non-terminal | A worker is validating or processing the input file in batches. Progress counters are updated as work is committed. | Poll periodically. The client may request cancellation. |
CANCELLING |
Non-terminal | A cancellation request has been accepted. The worker is stopping at the next safe batch boundary. | Continue polling until the job becomes CANCELLED or another terminal state. |
COMPLETED |
Terminal | Every accepted input row was processed successfully. | Download the result report if an audit record is required. |
COMPLETED_WITH_ERRORS |
Terminal | Processing finished, but at least one input row failed. Other rows may have succeeded. | Download the result report and reconcile failed rows. |
FAILED |
Terminal | A fatal job-level error prevented processing from completing. | Inspect the job-level error and result report. Retry only after determining whether the failure is transient. |
CANCELLED |
Terminal | The job stopped after cancellation. Already committed batches may remain applied. | Download the report to determine which rows succeeded, failed, or were skipped. |
Idempotency
Two levels of request idempotency will be supported.
Job level. Idempotency-Key on submit. Resubmitting the same key returns the existing job rather than starting a second one.
Row level. Execution is at-least-once, so after an interruption a row can be attempted again. The platform tracks a committed cursor and skips work already done. For operations whose writes are unsafe to repeat, we intend to offer row-level deduplication against a caller-supplied stable row key.
Limits
Bulk work is queued and paced rather than rejected outright, but it is not unbounded capacity. The following are proposed limits for this new API model.
| Limit | Proposed value |
|---|---|
| Rows per job | 20k |
| Payload size per job | 100MB |
| Inline (non-upload) body | 5 MB |
| Concurrent running jobs per tenant | 10 |
| Accepted-but-unfinished jobs per tenant | 50 |
| Maximum job duration | 24 hours |
| Job and report retention | 30 days after terminal state |
These bound concurrent accepted work, not request rate. Standard API rate limits still govern the submit call. A caller can be within their request rate limit and still be queued or rejected because they already have the maximum number of jobs in flight.
When a Jira instance is under load, bulk jobs are slowed or paused between batches. Status stays RUNNING and no error is returned. Interactive traffic gets priority, so the time to complete a bulk operation may vary significantly; acceptance and durability are the guarantees.
Permissions
A bulk job can never do more than the caller could do one row at a time. The submitter must have permissions to both the bulk /jobs endpoints, and permission to the underling unary API to guarantee job success.
The submit endpoint runs the same authentication and permission checks as the equivalent single-item API. Permissions are then re-checked per row at execution time, not only at submit, because a large job can run for a long time and permissions can change in between. A row the caller no longer has permission for is failed in the report, not silently applied.
Job status, report, and cancellation are authorised against the job. By default only the submitter, or an admin with the bulk jobs permission, can upload parts, poll, read the report, or cancel the job.
Notifications and events
Applying the mutations for the rows involved trigger the same downstream effects as an equivalent single call. However, since these operations are run in bulk, they have the potential to overwhelm the downstream systems that handle the events/notifications.
Therefore, we will run the work in a *bulk context *that ensures we are sending these events in a more reasonable manner, at the per-batch cadence. Which effects each operation defers or suppresses is a per-operation decision, not a blanket rule. This is an open question we would specifically like partner input on.
First release
The first operations on this contract are:
- Bulk create of entity properties, accepting different property sets per issue in one job
- Bulk issue update, accepting different field sets per issue in one job
- Bulk issue create, increasing the current limit from 50 to 20,000
What is not changing
Every existing synchronous and asynchronous bulk endpoint continues to work unchanged. Nothing here deprecates an existing API. Small operations do not need to adopt the job pattern.
Asks
- Like or comment on this RFC if this contract would benefit you or your apps.
- Are the proposed limits workable for your workload? We are particularly interested in cases where 25 concurrent jobs per tenant is the binding constraint - for example continuous, steady-state updates rather than a one-off migration.
- Which downstream effects would you want suppressed under bulk load? Would this best be delivered as an event property that can be inspected by event consumers, or a parameter that can be set by the job submitter?
- Is polling sufficient, or do you need completion notifications (webhook or event)? Would one or the other be an adoption blocker?
- NDJSON is the sole proposed file format. Does this present a problem for how you generate payloads?
- Which bulk operations, beyond the three above, would you want on this contract first?