Import issues into Pulse from other tools.
v1: Jira CSV → Pulse issues + Main Docs
Install
Release binary (recommended)
Download for your OS from GitHub Releases, then:
chmod +x pulse-import && sudo mv pulse-import /usr/local/bin/
pulse-import --version
Also published: deb / rpm / apk packages on each release.
Go
go install github.com/try-pulse/pulse-import/cmd/pulse-import@latest
Docker
docker pull ghcr.io/try-pulse/pulse-import:latest
docker run --rm -e PULSE_ACCESS_TOKEN -v "$PWD:/data" \
ghcr.io/try-pulse/pulse-import:latest --help
Authenticate
| Variable |
Purpose |
PULSE_ACCESS_TOKEN |
JWT Bearer token |
PULSE_WORKSPACE_ID |
Optional default workspace |
PULSE_API_URL |
Optional API base · default https://api.trypulse.tech/api/v1 |
Optional config file: ~/.config/pulse-import/config.yaml. It stores only the
API URL and workspace ID; access tokens are never saved.
[!WARNING]
Treat the token as a secret. The CLI accepts it only through
PULSE_ACCESS_TOKEN; it is never persisted or accepted as a command-line flag.
Quick start
- In Jira: Advanced issue search → Export Excel CSV (all fields)
- Run:
export PULSE_ACCESS_TOKEN="<jwt>"
pulse-import
Prompts walk a wizard: Source (CSV file picker + Jira URL) → Destination
(team / project / assignee) → User mapping → Review → import progress.
- Esc / Ctrl+C on a later step returns to the previous step; declining the final confirm cancels without writing
- Terminal width drives layout, progress bar width, and truncation
TERM=dumb, non-TTY, or NO_COLOR falls back to accessible / plain output
Safe first pass — parses, validates, and prints the full write plan and every
mapping decision. Creates nothing:
pulse-import --dry-run
Non-interactive
PULSE_ACCESS_TOKEN=… pulse-import \
--yes \
--importer jira-csv \
--file ./jira-export.csv \
--workspace <workspace-id> \
--team <team-id-or-name> \
--jira-url https://acme.atlassian.net
Undo an import
Every run writes a state journal recording exactly what it created. rollback
deletes precisely that and nothing else:
pulse-import rollback --state-file ./jira-export.csv.pulse-import.state.jsonl
Labels are never deleted — they may already be attached to work that was not
imported.
Options
| Flag |
Description |
--importer |
Importer id (jira-csv) |
--file |
Path to the export CSV |
--workspace |
Workspace ID |
--team |
Target team (id or name) |
--project |
Pin every issue to one project (id or name); disables epic→project mapping |
--jira-url |
Jira Cloud or on-prem base URL |
--assignee |
mapped (default) · self · none |
--self-assign |
Shorthand for --assignee self |
--map-user |
--map-user "Jane Doe=<pulse-user-id|email|skip>" (repeatable) |
--epics |
project (default) or label |
--sprints |
cycle (default): most recent sprint → Pulse cycle · label: every sprint stays a label |
--skip-comments |
Do not import comments |
--skip-labels |
Do not create or attach labels |
--skip-relations |
Do not import blocks / blocked-by links |
--strict-labels |
Fail instead of dropping labels past Pulse's limit of 10 per issue |
--no-migrated-label |
Do not add the Migrated label |
--skip-status |
Skip issues mapping to these Pulse statuses (comma separated) |
--only-status |
Import only issues mapping to these Pulse statuses |
--skip-stale |
Skip issues not updated within N days (or a duration like 4320h) |
--concurrency |
Parallel Pulse writes (default 4; 1 disables parallelism) |
--dry-run |
Run the complete preflight; create nothing |
--continue-on-error |
Continue after a definitive per-item failure |
--yes |
Non-interactive (no prompts) |
--api-url |
Override the API base URL |
--state-file |
Resume journal path; defaults beside the CSV |
--adopt KEY=ID |
Resolve an unknown create with an existing Pulse entity |
--retry-unknown KEY |
Explicitly retry an unknown create; may duplicate |
-v / --version |
Print version |
Field mapping
| Jira |
Pulse |
| Summary |
Issue title |
| Description |
Main Doc (Jira wiki → Markdown → editor JSON) |
| Issue type |
Pulse type when mappable, plus a Type: … label |
| Epic |
Project, with its issues filed into it (--epics label for a label instead) |
| Parent / Sub-task |
Parent / sub-issue (Pulse allows one level; deeper nesting is flattened onto the top-most ancestor) |
| Status |
Pulse workflow status, best effort |
| Resolution |
Forces done when a resolution is set but the status does not say so |
| Priority |
urgent · high · medium · low · no_priority |
| Assignee |
Matched against the target team's members by email, then name |
| Labels |
Labels |
| Component/s |
Label Component: … |
| Fix Version/s |
Label Release: … |
| Affects Version/s |
Label Affects: … |
| Sprint |
Cycle — the most recent sprint becomes the issue's cycle, created as planned when missing; earlier sprints stay Sprint: … labels (--sprints label for labels only) |
| Comments |
Comments, prefixed with the original author and date |
| Due date |
Due date |
| Story points / Original estimate |
Estimate, snapped to the team's estimate scale |
| Blocks / is blocked by |
Blocking relations, applied after every issue exists |
| Attachments |
Links to the original files, in the Main Doc |
| Issue key, Reporter, Creator, Created, Updated, Resolved, Environment, Time spent |
Recorded in the Main Doc |
| — |
Migrated label on everything the import created |
Issue description in Pulse stays empty (it is a short UI suffix, not a body).
The write-up is the Main Doc.
Anything the export carries that Pulse has no field for is listed in the plan
under Not imported, so nothing is dropped silently.
What Pulse cannot store
Reporter, Creator and the original Created date cannot be set through the
API — Pulse stamps them from the access token and the clock. They are written
into the Main Doc instead, so the information survives the migration even though
the fields cannot.
Matching Jira identifiers
Pulse allocates issue codes sequentially per team and the API accepts no explicit
code, so identifiers can only line up when the target team starts empty. The
importer creates issues in ascending source-key order to make that work, and the
plan warns when the team already holds issues.
Safe resume
[!IMPORTANT]
Pulse does not provide a server-side import id. pulse-import therefore
writes a crash-safe JSONL journal beside the CSV before every create request.
Re-running with the same CSV and target resumes completed work instead of
creating it again — including after Jira users have joined your Pulse
workspace, which is the supported way to improve user matching.
If a connection fails while creating an issue, Pulse may have accepted the
request even though the CLI received no response. The item is marked unknown
and automatic retry stops:
# After checking the issue in Pulse:
pulse-import ... --adopt ENG-123=64f... --yes
# Only when you confirmed no issue exists:
pulse-import ... --retry-unknown ENG-123 --yes
Changing the source file or the target workspace/team/project requires a
different state file.
- Export the CSV with all fields from Jira so mapping has what it needs.
- Jira issue keys must be present and unique.
- Pulse allows at most 10 labels per issue; extra labels are dropped least-first
with a warning (
--strict-labels fails instead).
- Creating labels needs the
labels:create permission — by default team
managers and workspace owners/admins; workspaces with custom roles can
differ. Use --skip-labels if you do not have it.
- Cycles need
cycles:create (same default roles) and are only possible on
leaf teams; Jira's CSV export carries no sprint dates, so created cycles are
planned with dates approximated from the issues' timestamps — complete
finished ones in Pulse afterwards. A sprint whose name matches a completed
cycle stays a label (Pulse cannot add issues to a completed cycle). Like
labels, cycles are reused by name and never deleted by rollback.
- Pulse sends notifications for every issue and comment an import creates, and
Loop automations can trigger on them; there is no server-side way to suppress
this for a bulk import. The plan warns about it on large imports.
- Blocking links that would close a dependency cycle (which Pulse rejects) are
dropped in preflight with a warning naming the dropped link.
- A Main Doc over Pulse's 10 MiB upload cap fails preflight with the offending
row, instead of a late 413 during the import.
- Main Doc, comment and link failures are reported and produce a non-zero exit.
Help