Creating Stories with the Jira CLI

Creating stories manually in Jira’s web UI can be painful. If you have a large set of notes that can be turned into stories, this page has a helper script that will read a text file of stories and create them in bulk using the Jira CLI.

This script intentionally does not set a Fix Version, Sprint, Size, and Time Estimate. Those are best set after the stories are created while planning for upcoming sprints.

What You Need

  • The Jira CLI installed and initialized (jira init completed).

  • Your JIRA_API_TOKEN exported in your shell and sourced.

  • The script and a stories file (links below).

If you have not set up the Jira CLI yet, follow the Jira CLI guide first. The script is just a thin wrapper around jira issue create, so anything that breaks your jira command will also break the script.

Download

Save both to the same directory and make the script executable:

chmod +x create-jira-stories.sh

The Stories File Format

Stories live in a text file that follows a simple format.

  • Lines starting with # are comments.

  • Stories are separated by a line that is just ---.

  • Sticky single-line fields — set them once, and they apply to every following story until you change them:

    Field Meaning

    PROJECT:

    Jira project key (e.g. CKM)

    COMPONENT:

    Component name (e.g. privacy-policy-administration-service)

    TYPE:

    Issue type (e.g. Story, Technical Debt, Task)

    PRIORITY:

    Priority (e.g. Standard)

    EPIC:

    Key of an existing epic to attach each story to (e.g. CKM-40000). See Attaching stories to an epic.

  • Per-story fields — reset for each story:

    Field Meaning

    TITLE:

    One-line summary (required)

    VALUE:

    The value statement (multi-line)

    NOTES:

    Notes / fix details that provide context (multi-line)

    ACCEPTANCE:

    Acceptance criteria (multi-line)

  • Optional per-story fields — also reset for each story; omit them to leave the field unset in Jira:

    Field Meaning

    FIX_VERSION:

    Release / fix version name (e.g. privacy-policy-administration-service: Release A)

    STORY_POINTS:

    Story points estimate (a number, e.g. 3)

    ORIGINAL_ESTIMATE:

    Original time estimate (e.g. 2d, 4h, 30m)

    ID:

    A local alias for this story that other stories can depend on (e.g. date-roundtrip). See Ordering stories with dependencies.

    AFTER:

    Alias(es) and/or issue key(s) this story must be done after (e.g. date-roundtrip, CKM-39999). See Ordering stories with dependencies.

The VALUE, NOTES, and ACCEPTANCE sections continue across multiple lines until the next field or the --- separator. The description sent to Jira is assembled from them like this:

<VALUE>

Notes / Fix:
<NOTES>

Acceptance Criteria:
<ACCEPTANCE>

Any empty section is omitted. Likewise, any optional field you leave out is simply not sent to Jira.

Example stories file

PROJECT:   CKM
COMPONENT: privacy-policy-administration-service
TYPE:      Technical Debt
PRIORITY:  Standard

---

TITLE: Remove redundant/incorrect date round-trip in UserAgreementMapper

FIX_VERSION:       privacy-policy-administration-service: Release A
STORY_POINTS:      2
ORIGINAL_ESTIMATE: 4h

VALUE:
As a maintainer of the privacy-policy-administration-service, I want the service to be
as efficient as possible so that I am able to use it better.

NOTES:
getUserAgreementMapper(...) parses an OffsetDateTime that is already an OffsetDateTime.
The parse/format round-trip is wasteful and can throw on locale/format edge cases.
Use the value directly.

ACCEPTANCE:
- mapper uses getCreatedDate() directly; tests unchanged/green.

---

Usage

Run the script with the path to your stories file:

./create-jira-stories.sh stories.txt

If you omit the argument it defaults to stories.txt in the current directory.

Do a dry run first

Always preview before creating real tickets. Set DRY_RUN=1 to print exactly what would be created without touching Jira:

DRY_RUN=1 ./create-jira-stories.sh stories.txt

The output shows the project, type, priority, component, summary, and the fully assembled description for each story, followed by a count such as Done. 3 ticket(s) previewed.

A dry run also validates your file. If any story is missing a required field (PROJECT, COMPONENT, TYPE, PRIORITY, or TITLE), the script stops with an error naming the story and the missing field — so you can catch mistakes before creating anything.

Create the tickets

When the dry run looks right, run it:

./create-jira-stories.sh stories.txt

Each created ticket prints its new key (e.g. CKM-31299).

Re-running safely (duplicate protection)

After a ticket is created, the script writes its key back into your stories file as a TICKET NUMBER: line directly above that story’s TITLE:

TICKET NUMBER: CKM-31299
TITLE: Add metrics via Micrometer
NOTES:
...

On any later run, every story that already has a TICKET NUMBER is skipped, so you can safely re-run the same file — for example after appending a few new stories — without creating duplicates:

-- skipping (already created as CKM-31299): Add metrics via Micrometer
>> creating: A newly added story
Done. 1 ticket(s) created, 1 skipped (already created).

You never type TICKET NUMBER yourself — the script manages it. A dry run (DRY_RUN=1) never edits your file. If a jira issue create call fails, the script stops immediately and the file is not stamped for that story, so re-running picks up where it left off.

Overriding Fields Per Story

Because the single-line fields are sticky, you set common values once at the top and only specify what differs. To change a field for a specific story, just set it again inside that story’s block.

A sticky override persists for all stories that follow it — it does not automatically revert to the previous value. If you only want the change to apply to one story, set the field back to the original value in the next story (or reorder so the odd-one-out comes last).

In the example below, most stories target privacy-policy-administration-service, but the middle story is filed against callback-service, and the component is then reset:

PROJECT:   CKM
COMPONENT: privacy-policy-administration-service
TYPE:      Technical Debt
PRIORITY:  Standard

---

TITLE: First PPAS cleanup item
VALUE:
...

---

# Override component for just this one story
COMPONENT: callback-service
TITLE: A callback-service item
VALUE:
...

---

# Reset back to PPAS for the remaining stories
COMPONENT: privacy-policy-administration-service
TITLE: Another PPAS item
VALUE:
...

---

You can override TYPE, PRIORITY, and PROJECT the same way — for example, setting TYPE: Story for one ticket among a batch of Technical Debt items.

Attaching stories to an epic

Set EPIC: to the key of an already-existing epic and every story created afterwards is attached to it (the script passes it to the Jira CLI as --parent). The script does not create the epic — make it in Jira first, then reference its key.

EPIC is sticky, so a single line near the top links the whole batch:

PROJECT:   CKM
COMPONENT: privacy-policy-administration-service
TYPE:      Story
PRIORITY:  Standard
EPIC:      CKM-40000

---

TITLE: First story under the epic
VALUE:
...

---

TITLE: Second story under the same epic
VALUE:
...

Because it is sticky, you can switch epics partway through by setting EPIC: again, or stop attaching stories by setting it to an empty value (EPIC: with nothing after it).

Ordering stories with dependencies

Sometimes one story must be done after another, but when you are writing the file you do not yet have the real ticket numbers. You bridge that gap with two per-story fields:

  • ID: gives a story a local alias (any short name you choose, e.g. date-roundtrip).

  • AFTER: lists the alias(es) — and/or real issue key(s) — that a story must come after.

After all stories are created, the script resolves each AFTER reference to the real key and creates a Jira link so the relationship reads "this story is blocked by that story" (link type Blocks by default; override with the LINK_TYPE environment variable).

---
ID: date-roundtrip
TITLE: Remove redundant date round-trip in UserAgreementMapper
VALUE:
...

---
# Must be done after the story above. "date-roundtrip" is the alias, no ticket
# number needed yet.
TITLE: Add regression tests for the date handling
AFTER: date-roundtrip
VALUE:
...

---
# AFTER can list several prerequisites and can mix a local alias with a real,
# already-existing issue key.
TITLE: Document the change in the SRVDD
AFTER: date-roundtrip, CKM-39999
VALUE:
...
  • AFTER values are separated by commas and/or spaces.

  • An AFTER reference can be a local ID defined anywhere in the file (forward references are fine) or a real issue key like CKM-39999. It also resolves against stories already created on a previous run (that carry a TICKET NUMBER), so an alias keeps working after the fact.

  • Links are only created for stories created in the current run, so re-running the file does not add duplicate links.

  • A dry run (see below) previews every link and fails with an error if any AFTER reference does not match a known ID or issue key — a handy way to catch typos before creating anything.

Notes and Caveats

  • TYPE and PRIORITY values must match exactly what is configured in your Jira project (including spaces and capitalization). If a value is wrong, jira issue create will fail for that ticket.

  • EPIC must be an existing epic key; the script attaches stories to it but never creates it. A wrong key will make jira issue create fail for that ticket.

  • FIX_VERSION must match an existing release name in the project (these typically look like <component>: Release X, e.g. privacy-policy-administration-service: Release A), otherwise the create fails.

  • STORY_POINTS is sent as the story-points custom field (--custom story-points=<n>). If your project uses a differently named field, adjust the script’s --custom key.

  • ORIGINAL_ESTIMATE accepts Jira time syntax such as 2d, 4h, or 30m.

  • FIX_VERSION, STORY_POINTS, ORIGINAL_ESTIMATE, ID, and AFTER are optional and reset per story — omit them to leave the field unset. (Ticket size is otherwise not set by this script.)

  • Dependency links use the Blocks link type by default; override it with LINK_TYPE (for example LINK_TYPE="Depends") if your Jira uses a different name. The type must exist in your Jira.

  • The script stamps a TICKET NUMBER: line into your file after each successful create and skips any story that already has one (see Re-running safely (duplicate protection)). Keep the stamped file if you intend to re-run it; deleting the TICKET NUMBER lines will cause those stories to be created again.