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": 20000 }
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": 20000, "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

When requesting status of an in-progress job, per-row outcomes are provided only as counts: successes, failures, and skips - row-level details are not included.

When requesting status of a completed job (including failed jobs), minimal row details are provided (for example, the key of a created issue) in a structured format documented in the API. Reports will be paginated.

Reporting details will be governed by the individual use-case, so an API use-case that doesn’t require detailed results may not include them.

GET /rest/api/3/jobs/{jobId}/report?status=FAILED&page=1&pageSize=100
{
  "summary": { "total": 20000, "succeeded": 19978, "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. All limits are per-tenant.

Limit Proposed value
Rows per job 20k
Payload size per job 100MB
Inline (non-upload) body 5 MB
Concurrent running jobs per tenant 10
Maximum queued jobs 200
Accepted-but-unfinished jobs per tenant 50
Maximum job duration 24 hours
Job and report retention 30 days after terminal state

Standard API rate limits still govern the submit call. When the maximum number of in-flight concurrent jobs is reached, subsequent requests will be enqueued up to the queue limit, and then rejected once that limit is reached.

When a Jira instance is under load, bulk jobs may be 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?

Updates

  • 15 September 2026:
    • Clarified details of job status and report endpoints.
  • 16 September 2026:
    • Fixed row count discrepancy in code samples.
    • Clarified limits as per-tenant

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

@PaoloCampanelli I second your idea about a callback event on the bulk job completion, especially if these jobs could be deferred for a time, scoped for thousands of issues and can run for 24 hours.

Having to poll a long running job “are you done yet” thousands of times is not a good use of expensive lambda cpu.

Thanks for the feedback @PaoloCampanelli, really appreciate you taking the time to consider this.

In response to your suggestions;

Firstly, there is provision for queued jobs (maximum of 200 queued jobs per tenant) - I see that is possibly not clear from the language in the document, so I’m making a couple of changes to clear that up. If the concurrent job limit is reached, jobs will be enqueued up to the queue limit, and once the queue limit is reached, additional requests will be rejected. Additionally, we will ensure a /jobs/list endpoint is available to query the detailed of enqueued jobs that the caller has permission to access.

And with regards to job completion events, that’s a good suggestion and we will take that back to the engineering team to explore. It definitely makes sense to include in our implementation.

Thanks again,
Ryan

Thanks for the proposal!

This would be unusable for “create issue”, since we’d need the newly created issue keys in the response, like the current synchronous API is providing, for successful rows. Or did I misunderstand?

Another requirement we have is to stop bulk processing at the first error, rather than continuing with all rows. We sometimes need to retry create issue with a different body if it fails. If the whole batch would continue being processed, the issue numbering would get mixed up.

Hi @BenRomberg - thanks for the feedback and for pointing out that issue in the RFC. You are correct to note that some APIs will need to provide more details than counts. I have clarified the language and intent in the Report section of the RFC - let me know if this clears things up?

Thanks for the suggestion @BenRomberg - I’ll take this back to the team and see how we might approach this ask - perhaps as an optional parameter to the job submission API to allow fast-fail mode?

Sounds good!

That could work - thanks!

Thank you for proposing this change! We have a couple of features that would benefit from it.

A few questions on the limits:

  • The example under “Polling” shows “total”: 50000, but the Limits table caps rows per job at 20k. Probably just a stale example — but which is correct, 20k or 50k?

  • The Limits table says 10 concurrent jobs per tenant, while the closing question asks about 25 running jobs per tenant. Which one is right?

  • The QUEUED state description doesn’t say how long a job can realistically sit there. Since “up to 24 hours” is already the maximum processing duration, it would help to know whether queue time counts against that or is separate.

  • Is the 200 queued-job maximum per tenant, or shared across all tenants?

Could you give more detail on the “Notifications and events” section, please? Specifically this part:

▎ 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

What does that mean in practice? We rely on the product events we receive to tell us when issues change. Does it mean some events will be suppressed, or delayed, or batched?

On the proposed questions:

  • The 25-per-tenant limit looks suitable, but the guarantees for queued jobs need to be clarified.

  • Polling job status is sufficient for us, not a blocker. A completion webhook would be a nice-to-have.

  • NDJSON works well for us.

  • Which bulk operations beyond the three above: issue link creation.

Hi Katia,

Thanks for the message on this RFC. Apologies for the inconsistencies - the team was debating various limits and I missed some of the edits when I posted the RFC.

In response to your questions;

  • The example under “Polling” shows “total”: 50000, but the Limits table caps rows per job at 20k. Probably just a stale example — but which is correct, 20k or 50k?

    • The correct limit should be 20,000 (20k) to start. We may be able to raise this over time but want to start somewhat conservatively.
  • The Limits table says 10 concurrent jobs per tenant, while the closing question asks about 25 running jobs per tenant. Which one is right?

    • The correct limit will be 25 concurrent jobs per tenant - again this is likely to be raised over time. We believe this is a conservative place to start. We will also likely consider separating limits in the future according to the specific job and the system resources involved in serving that job.
  • The QUEUED state description doesn’t say how long a job can realistically sit there. Since “up to 24 hours” is already the maximum processing duration, it would help to know whether queue time counts against that or is separate.

    • Good question, I will get back to you on this one.
  • Is the 200 queued-job maximum per tenant, or shared across all tenants?

    • All limits are per-tenant in the first instance.

I have made changes to the RFC content to reflect the above clarifications.

Thanks for the proposal — bulk entity property writes are the core of our Data Center to Cloud migration, so this lands right on our hot path. That write is where most of a migration’s time goes, across many work items, so anything that lets it run wider matters to us.

Concurrency is the part I’d most like to understand, because today there effectively isn’t any: on the current endpoint (POST /rest/api/2/issue/properties/multi) a bulk property write is refused while another one is still running, even when the two calls cover different work items and different property keys. Jobs are meant to run in parallel, so the question is what that looks like for entity properties. Can two jobs in flight write the same work item, and the same property key on it — is that allowed, and is the outcome defined? It isn’t a corner case for us: one issue’s data reaches us across several deliveries whose boundaries we don’t control, so the same issue naturally lands in more than one batch. And is there any exclusivity per operation type on top of the quota — does it also apply across apps, or between an app and the Migration Assistant running at the same time?

Two more questions about the proposal:

  • For entity properties the payload limit will probably bind before the row limit — a single issue property can approach 32 KB, so 20 000 rows can be well past 100 MB. Is the 100 MB measured before or after the uploaded parts are assembled, and is a row one issue with its whole property set, or one property?
  • Property writes replace the whole value, so where an issue’s data arrives in pieces we read-modify-write before sending. A job batches that cycle but doesn’t remove it — would a merge/patch mode for entity properties be in scope? Related and cheap to state: if two rows within a single job target the same issue and key, is that last-write-wins or undefined?

On your questions:

  • Limits are workable for us, with the caveat above. We’re the one-off migration rather than steady-state traffic, so 25 concurrent jobs isn’t the binding constraint — the per-operation behaviour might be.
  • Polling vs completion events: for app migrations the problem isn’t the polling cost, it’s the clock. The Migration Assistant settles a transfer on its own timeout once the app stops acknowledging it, and a job outliving that has nowhere left to report to. A completion event would solve it, and so would simply being able to read a job’s status and report later with only the jobId — the 30-day retention suggests that may already be the intent, and confirming it would be enough for us.
  • NDJSON works. Worth confirming there’s no line-length limit below the row limit, since one of our rows can be a single 32 KB value.
  • Beyond the three proposed, the one we’d want next is a bulk read of entity properties across many issues, so a write can be verified without going through search.

Just noticed the addition of read/write:bulk-job:jira scopes to the official Jira REST API documentation Swagger export.
My expectation would be that these new APIs only require the already existing non-bulk scopes for the related operation. Otherwise, those new APIs will become hard to use for existing apps, given that additions to Forge OAuth scopes require admin consent.

Thanks you @PaulinaKumicka for the feedback. Apologies for the delay in responding, I missed your original post.

In response to your questions;

(1) Both row count and payload limit will be checked at each upload step and any attempt to add a chunk that exceeds either limit would be rejected at that time. The job being assembled can then be completed and submitted for processing, and additional rows created in a second job.

(2) Regarding concurrent attempts to write to the same entity/row, we will not implement any additional protections at this stage, and so the last entry processed in a series would be the final value applied. This maintains the behaviour of how existing APIs behave. Does that work for your usecases?

Hi @BenRomberg thanks for the input. I understand your thinking here, though our concern is that the bulk jobs endpoints themselves should have a protection for things like submission/upload, reading job responses and cancellation. We will explore what it might look like if those endpoints were all authorized dynamically using the underlying endpoint scopes.

Sorry but I don’t understand the reasoning. If the app has access to read data or to mutate data, it doesn’t really matter if we have access using 1 bulk request or 1000 single requests. I’m assuming that an app only has access to its own bulk jobs though?

From the “Project summary”:

IMHO adding additional scopes goes against developer experience. Also, system reliability will not improve if we cannot use those new APIs at all. It might even get worse if the old bulk APIs will get deprecated in the future, forcing us to move away from bulk APIs altogether.