RFC-145: Jira Cloud REST API: Async Bulk Mutation APIs

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?

This is an interesting change, and it would take some time to evaluate all the changes and edge cases, but I like the overall direction.

A few comments:

  • 25 concurrent jobs is probably ok, but ideally I should be able to create more jobs and they’d be put in a queue instead of throwing an error
  • Optionally receiving events for completion or, maybe more interesting, errors would be a good addition

This is mainly because of the ephemeral nature of Forge lambdas: I wouldn’t want to have a lambda waiting 30s until a batch job completes, and having to maintain our own queue is also annoying if it can be done directly at the API layer.

In general, being able to bulk update work items (with different change sets), entity properties and app custom fields should cover most of the use cases that I can think about right now