atlassian-cli is an independent, community open-source project, not affiliated with, endorsed by, or sponsored by Atlassian, and is not Atlassian's official CLI (acli). Product names are used only to identify compatibility.
The quick answer
To create Jira issues from CSV with atlassian-cli, you loop over the rows of the file and call jira issue create once per row. There is no single csv-upload subcommand, and that is deliberate: a short shell loop keeps the column-to-field mapping explicit, works with any CSV your team already exports, and can live in version control next to the rest of your automation.
Here is the smallest version that works. Given a file with a header row and two columns, summary and issue_type, this creates one issue per line in project DEV:
# Skip the header, create one issue per row
tail -n +2 issues.csv | while IFS=, read -r summary issue_type; do
atlassian-cli jira issue create \
--project DEV \
--issue-type "$issue_type" \
--summary "$summary"
done
The rest of this guide turns that one-liner into something you can trust with real data: a field-mapping table, a dry-run pass so you see every issue before it exists, custom-field handling, and a record of every key you created so a bad import is easy to reverse. Every command below is checkable against the command reference.
How this differs from acli. atlassian-cli is an independent, community open-source project. It is not Atlassian's official CLI (acli) and is not affiliated with Atlassian. Use the official acli if you want first-party vendor support; use atlassian-cli if you want a single free Rust binary spanning Jira, Confluence, Bitbucket, and JSM with the scriptable, pipe-friendly output shown here.
Prepare your CSV file
Your CSV needs one column per Jira field you want to set, plus a header row that names those columns. Keep it flat and predictable. A typical import file for a backlog looks like this:
# issues.csv
summary,issue_type,assignee,sprint_id
"Fix login timeout on SSO redirect",Bug,alice@example.com,25446
"Add rate-limit headers to public API",Story,bob@example.com,25446
"Write onboarding runbook",Task,,
"Upgrade Postgres to 16",Task,carol@example.com,
A few habits that save trouble later:
- Quote any value that contains a comma so the column count stays stable across rows.
- Leave a cell empty rather than inventing a placeholder when a field does not apply. Empty is easy to test for in the loop.
- Use exact Jira values for issue types, sprint IDs, and account emails. A typo in
issue_typewill be rejected by the API, not silently corrected.
If you want a template whose columns already match your instance, export a handful of existing issues to CSV first and edit from there:
# Export a few issues as a starting template
atlassian-cli jira issue search \
--jql "project = DEV order by created desc" \
--limit 5 \
--format csv --output template.csv
Map CSV columns to Jira fields
Field mapping is the part people get wrong, so make it explicit. Each column becomes either a flag on jira issue create or a follow-up command run against the new issue. The table below covers the common cases:
| CSV column | Maps to | Notes |
|---|---|---|
summary |
--summary on jira issue create |
Required. The one field every issue must have. |
issue_type |
--issue-type |
Must match a type in the project, e.g. Task, Bug, Story. |
project |
--project |
Project key. Often fixed for the whole file, so you can hard-code it. |
sprint_id |
--sprint |
Numeric Agile sprint ID. No custom-field ID needed. |
| custom fields | --field 'customfield_10010={...}' |
Discover IDs with jira fields list. Value is JSON. |
assignee |
jira issue assign <key> (after create) |
Run once the new key is known. |
status |
jira issue transition <key> (after create) |
Move the issue out of the default workflow state. |
The split matters: jira issue create sets fields that exist at creation time, while assignee and status are cleanest as separate steps once you have the key back. That keeps each command small and each failure easy to attribute to a specific row.
The import loop
Now put it together into a script you can re-run. This reads the four-column CSV from earlier, creates each issue, then assigns it and drops it into a sprint when those columns are present:
#!/bin/bash
# import-issues.sh - create Jira issues from a CSV file
set -euo pipefail
CSV="issues.csv"
PROJECT="DEV"
PROFILE="prod"
DRY_RUN="${DRY_RUN:-1}" # 1 = preview only, 0 = create for real
# Skip the header row, then read each column
tail -n +2 "$CSV" | while IFS=, read -r summary issue_type assignee sprint_id; do
if [ "$DRY_RUN" = "1" ]; then
echo "[DRY-RUN] would create [$issue_type] $summary"
echo " assignee=${assignee:-none} sprint=${sprint_id:-none}"
continue
fi
# Build optional flags only when the column has a value
extra=()
[ -n "$sprint_id" ] && extra+=(--sprint "$sprint_id")
key=$(atlassian-cli jira issue create \
--profile "$PROFILE" \
--project "$PROJECT" \
--issue-type "$issue_type" \
--summary "$summary" \
"${extra[@]}" \
--format json | jq -r '.key')
echo "Created $key"
echo "$key" >> created-keys.txt
# Assign after creation when the column is filled
if [ -n "$assignee" ]; then
atlassian-cli jira issue assign "$key" \
--profile "$PROFILE" \
--assignee "$assignee"
fi
done
Two details do the heavy lifting. The extra=() array adds --sprint only when the row actually has a sprint ID, so empty cells never turn into broken flags. And --format json | jq -r '.key' pulls the new issue key straight out of the response, which is what makes the assign step and the audit log possible.
On CSV parsing. IFS=, is fine for clean data, but it splits naively on every comma. If your summaries contain commas inside quotes, convert the file to a tab-separated export first (IFS=$'\t') or parse it with a real CSV reader such as Python's csv module, then feed the fields into the same jira issue create call.
Dry-run before you write anything
Creating issues is not reversible with an undo button, so preview the whole batch first. Because jira issue create writes immediately, the dry-run lives in the script rather than in a flag. The DRY_RUN variable in the loop above is exactly that switch: when it is on, the loop prints the issue type, summary, and parsed fields for every row and skips the real API call.
# Preview every row without touching Jira
DRY_RUN=1 ./import-issues.sh
# Example output
# [DRY-RUN] would create [Bug] Fix login timeout on SSO redirect
# assignee=alice@example.com sprint=25446
# [DRY-RUN] would create [Story] Add rate-limit headers to public API
# assignee=bob@example.com sprint=25446
# Happy with the plan? Create them for real
DRY_RUN=0 ./import-issues.sh
This is the same preview-then-commit habit that the dedicated bulk operations use for transitions and assignments, where --dry-run is a built-in flag. Here you are applying the same discipline to creation. Read the dry-run output as if it were a diff: check the row count, scan for blank summaries, and confirm the issue types are spelled the way your project expects before you flip DRY_RUN to 0.
Custom fields, assignee, and status
Real backlogs carry more than a summary. Custom fields go through the --field flag, whose value is JSON. First find the field ID, since custom fields are addressed by ID, not display name:
# List fields to find the customfield_ ID you need
atlassian-cli jira fields list --format json | jq -r '.[] | "\(.id)\t\(.name)"'
Then add it to the create call. A CSV column named severity that maps to a select-list custom field becomes:
atlassian-cli jira issue create \
--project DEV \
--issue-type Bug \
--summary "$summary" \
--field 'customfield_10010={"value":"Internal"}'
Assignee and status are follow-up commands, because they read cleanly once the key exists. After the create call returns DEV-412, for example:
# Set the assignee from the CSV's assignee column
atlassian-cli jira issue assign DEV-412 --assignee carol@example.com
# Move it out of the default state if the CSV has a status column
atlassian-cli jira issue transition DEV-412 --transition "In Progress"
Wrap each in an if [ -n "$column" ] guard as the loop does, so rows that leave those cells blank simply skip the step. This keeps a single script flexible enough for both a quick summary-only import and a fully specified backlog.
Capture keys and roll back
The loop appends every new key to created-keys.txt. That file is your safety net. If an import went in against the wrong project, or a column was mismapped, you have an exact list of what to undo rather than a guess based on creation timestamps.
To reverse an import, feed those keys back into the CLI. You can transition them to a closed state or delete them outright:
# Move every imported issue to a terminal state
while read -r key; do
atlassian-cli jira issue transition "$key" --transition "Closed"
done < created-keys.txt
# Or delete them if they should never have existed
while read -r key; do
atlassian-cli jira issue delete "$key"
done < created-keys.txt
Keeping the key log also lets you verify the import succeeded. Count the lines and compare against the CSV row count, or re-query the keys with jira issue get to confirm the fields landed as intended. For a broader tidy-up of leftover or mis-created issues, the Jira project cleanup runbook pairs well with this log. And if you would rather trigger the import from a scheduled job or a webhook, the patterns in Jira automation examples show how to wire the same commands into a pipeline.
How this differs from the Jira UI importer
Jira Cloud ships a built-in CSV importer in its web interface for one-off, interactive imports. It walks you through a mapping wizard in the browser and is a fine choice when someone hands you a spreadsheet once and you never need to repeat it.
The CLI approach earns its place when the work is repeatable. A script is diffable, reviewable, and runnable from CI or a cron job. The mapping is code, not a set of dropdowns you re-click every time. You can dry-run the exact batch, capture the resulting keys, and roll back with the same tooling. If your imports are recurring, driven by another system's export, or need to slot into a larger workflow alongside search, transition, and assignment, the loop scales in ways the wizard cannot.
Neither approach is a bulk-upload endpoint: the CLI still makes one create call per row. For very large one-time loads where interactivity is fine, the UI importer may be faster to click through. For everything you want to run more than once, keep it in a script. If you also manage day-to-day work from the terminal, the wider workflow in Jira task management from the CLI builds on the same commands.
Try atlassian-cli
One free, MIT-licensed binary for Jira, Confluence, Bitbucket, and JSM. Script your CSV imports and everything around them.
Install atlassian-cliFAQ
Can atlassian-cli import a CSV of issues into Jira in one command?
There is no single csv-import subcommand. To create Jira issues from CSV you loop over the rows in a short shell script and call atlassian-cli jira issue create once per row. That keeps the mapping explicit and lets you version-control the script, but it means one API call per issue rather than a single upload.
How do I dry-run a CSV import before creating real issues?
jira issue create writes immediately, so the dry-run lives in your script, not in a flag. Gate the loop with a DRY_RUN variable: when it is on, echo the create command and the parsed fields for every row and skip the real call. Run DRY_RUN=1 first, read the output, then set DRY_RUN=0 to create the issues for real.
How do I map CSV columns to Jira fields, including custom fields?
Standard fields map to flags: summary to --summary, issue type to --issue-type, project to --project, sprint to --sprint. Custom fields go through --field 'customfield_10010={"value":"Internal"}'. Discover the exact custom field IDs with atlassian-cli jira fields list. Assignee and status are set after creation with jira issue assign and jira issue transition.
How do I capture the new issue keys after importing from CSV?
Run jira issue create with --format json and parse the key with jq -r '.key'. Append each key to a log file such as created-keys.txt. That log gives you a record of exactly what was created so you can transition, re-assign, or delete those issues later if the import needs to be undone.