CSV importer and exporter for pm-cli.
Import pm items from a CSV file, export them back out, or wire up a programmatic csv-import importer — all with zero external runtime dependencies.
The extension requires pm CLI 2026.8.20 or newer. Its development and release gates are pinned to pm CLI/SDK 2026.8.21 so host behavior stays reproducible.
pm install npm:pm-csv --globalThe
npm:prefix is required. A barepm install pm-csvresolves only a local directory or a bundled alias, never the registry, and agithub.com/unbraind/pm-csvsource cannot work either — pm copies a GitHub source as-is without building it, and this repository does not commitdist/.
Or install per-project:
pm install npm:pm-csv --projectRead a CSV file and create or update pm items.
pm csv import tasks.csv
pm csv import backlog.csv --delimiter ';'
pm csv import data.tsv --delimiter tab
pm csv import jira.csv --auto-map # infer common aliases (summary->title, owner->assignee)
pm csv import jira.csv --map 'Summary=title,Owner=assignee'
pm csv import items.csv --key title # idempotent re-import (no duplicates)
pm csv import legacy.csv --encoding latin1 # non-UTF-8 source
pm csv import sprint12.csv --source 'jira-export-2026-06'
pm csv import vendor.csv --strict # fail before writing on bad row data
pm csv import items.csv --dry-run
pm csv import tasks.csv --atomic # writer-locked, crash-recoverable import
Flags
| Flag | Type | Default | Description |
|---|---|---|---|
--delimiter <char> |
string | , |
Field delimiter, or alias tab / comma / semicolon / pipe |
--map <col=field> |
string | — | Remap a CSV header to a pm field (repeatable, comma-joined), e.g. --map 'Summary=title' |
--auto-map |
boolean | false | Auto-map common third-party headers when unambiguous (e.g. summary -> title, owner -> assignee) |
--key <field> |
string | — | Dedup key column: a re-import updates the matching item instead of creating a duplicate |
--encoding <enc> |
string | utf-8 |
Source file encoding: utf-8 | utf16le | latin1 |
--source <label> |
string | — | Record an import-provenance label in the csv_source field of created/updated items |
--status <filter> |
string | — | Import only rows whose (normalized) status matches; non-matching rows are skipped |
--type <type> |
string | — | Import only rows whose type matches (case-insensitive) |
--priority <n> |
integer | — | Import only rows whose integer priority equals this value |
--strict |
boolean | false | Abort before writing if validation finds missing titles, unknown statuses, invalid/out-of-range priorities, or duplicate mapped columns |
--dry-run |
boolean | false | Preview what would be imported without writing any data |
--atomic |
boolean | false | Use one writer-locked, crash-recoverable transaction. On failure, attempt best-effort create compensation; pre-existing item updates are intentionally not reverted. Inspect and reconcile any marked orphan before retrying. Incompatible with --stream |
Default imports remain lenient for messy spreadsheets: empty titles are skipped,
unknown statuses fall back to open, and invalid priorities are ignored. Add
--strict when the CSV is expected to be a production source of truth. In
strict mode the importer runs the same parser as pm csv validate before any
write, then aborts on row-level data issues or duplicate mapped columns so a bad
file cannot partially mutate the pm store.
By default, csv import creates items one row at a time. If a row fails
mid-import, the rows already written stay in the tracker — a partial,
non-atomic state — and concurrent agents can interleave writes. --atomic
(requires pm-cli >= 2026.7.19) wraps the whole row set in a single
commitWorkspaceTransaction primitive: mutations are serialized under one
workspace writer lock and recorded in a crash-recoverable journal.
- Failure compensation: if any row fails, the coordinator attempts
reverse-order, best-effort compensation for creates by closing them with
reason
atomic csv import rolled back, rolls back the transaction journal, and exits non-zero with a reconciliation warning. A failed close deliberately retains both transaction markers so an operator can inspect and reconcile the open orphan by transaction id before retrying. Pre-existing item updates are intentionally not reverted because arbitrary prior state is not captured. - Crash-recoverable / resumable: the transaction id is stable and derivable
from the resolved file path and parsed content fingerprint, so re-running the
same unchanged
--atomicimport resumes from the durable journal while edited content receives a fresh transaction id. Resume/compensation matching is per-row-precise: every item this transaction writes is stamped with a per-row ownership markercsv-txrow:<transactionId>#<rowIndex>(plus a batch-levelcsv-tx:<transactionId>marker for scanning), and a resumed run detects already-applied rows by parsing the rowIndex out of that marker. This means a CSV with duplicate titles or duplicate keys is handled correctly — a row is only skipped when its exact rowIndex was already applied, never because a same-titled/same-keyed sibling happens to match. - In-batch duplicate
--keyguard: when--keyis set, two rows in the same file that share a key which does NOT already exist in the tracker would both plan as a create (the key index is not updated during planning). The atomic planner tracks keys claimed by earlier planned creates in the same run and skips a later duplicate with a clear per-row warning (counted inskipped), so only one item is created per new key. Rows whose key already exists in the tracker still update normally. - Parity:
--atomicshells out to the samepm createthe non-atomic path uses; without--atomic, import output and exit codes are unchanged. - Incompatible with
--stream: an unbounded stream cannot be committed as one bounded transaction, so--atomic --streamfails fast with a usage error. - Compensation uses
close, notdelete, to avoid the known history- resurrection issue, so compensated items remain in the tracker as closed items rather than being erased. For--keyupsert rows that update pre-existing items, the update is applied within the transaction but is not reverted on failure (an arbitrary update cannot be safely undone without capturing prior state). Compensation is best-effort and reports remaining work for operator reconciliation.
pm csv import tasks.csv --atomic # resumes an unchanged interrupted import
pm csv import tasks.csv --atomic --source q2 # provenance still recorded--status, --type and --priority filter the CSV rows before any items
are created — non-matching rows are skipped, never written. They mirror the
csv export filter semantics exactly (status is normalized through the same
alias table, so --status open matches a row whose status cell is todo).
Multiple criteria are ANDed. The result reports how many rows were filtered out:
pm csv import tasks.csv --status open # import open rows only
pm csv import tasks.csv --type Feature --priority 1 # AND: Feature *and* priority 1
# → Imported 1, updated 0, skipped 5 (5 filtered out).
Required column: title
Optional columns: type, status, priority, tags, deadline, body, parent, assignee, sprint, release, blocked_by
Parse a CSV and report data-quality issues without importing. Exits non-zero
(usage error) when there is a structural problem (no title column after --map
or an empty file); otherwise exits 0 even when it reports row-level warnings.
pm csv validate tasks.csv
pm csv validate jira.csv --map 'Summary=title'
pm csv validate jira.csv --auto-map
pm csv validate data.tsv --delimiter tab --json
The report includes: row count, detected columns, mapped columns (after --map),
whether the required title column is present, duplicate mapped columns, and
counts of rows missing a title, rows with an unrecognized status, rows with a
non-integer priority, and rows with a priority outside pm's 0..4 range.
--auto-map applies the same alias vocabulary as import, then validates against
the effective mapped headers.
Flags
| Flag | Type | Default | Description |
|---|---|---|---|
--delimiter <char> |
string | , |
Field delimiter, or alias tab / comma / semicolon / pipe |
--map <col=field> |
string | — | Remap a CSV header to a pm field before validating |
--auto-map |
boolean | false | Auto-map common third-party headers when unambiguous before validating |
--encoding <enc> |
string | utf-8 |
Source file encoding: utf-8 | utf16le | latin1 |
--json |
boolean | false | Emit the report as JSON |
Export all pm items (or a filtered subset) to CSV. Without --output the CSV is returned in the command result object.
Every export, upsert index, and atomic-resume scan reads the whole tracker with
canonical pm list --all, strict source reads, full metadata, and explicitly
unbounded row and token controls. The public pm SDK certifies source status,
filter scope, pagination, projection, counts, and item identity before pm-csv
uses a row. pm-csv additionally rejects missing or contradictory omission,
read-output, and unreadable-item receipts tracked in pm-cli issue 1078. A
partial or unverifiable workspace therefore fails closed instead of exporting
missing rows, creating duplicate upserts, or reapplying an atomic import.
pm csv export
pm csv export --output items.csv
pm csv export --output backlog.csv --delimiter ';'
pm csv export --status open --output todos.csv
pm csv export --type Feature --output features.csv
pm csv export --excel --output for-excel.csv
Flags
| Flag | Type | Default | Description |
|---|---|---|---|
--output <file> |
string | — | Write CSV to this file path instead of returning it |
--delimiter <char> |
string | , |
Field delimiter, or alias tab / comma / semicolon / pipe |
--status <status> |
string | — | Filter: open | in_progress | blocked | closed | canceled | draft |
--type <type> |
string | — | Filter by item type |
--columns <list> |
string | all | Comma-separated columns to export, in order (custom fields selectable when --all-fields is set) |
--all-fields |
boolean | false | Discover custom item fields registered in the workspace schema and append them as columns |
--discover-fields |
boolean | false | Alias for --all-fields |
--no-header |
boolean | false | Omit the CSV header row |
--crlf |
boolean | false | Use CRLF line endings (RFC-4180 / Excel) |
--excel |
boolean | false | Excel-friendly output: CRLF line endings and a UTF-8 BOM prefix |
Export columns (fixed order):
id, title, type, status, priority, tags, deadline, body, parent, assignee, sprint, release, blocked_by, csv_source, created_at, updated_at
By default only the built-in columns above are exported. --all-fields (alias
--discover-fields) discovers any custom item fields registered in the
workspace runtime schema and appends them as extra columns — the default column
set is otherwise unchanged, so the flag is strictly additive.
Discovery reads the same inputs the SDK's resolveRuntimeFieldRegistry consumes
(the workspace settings.json schema.fields plus the file it points at via
schema.files.fields, default schema/fields.json). A standalone-installed
extension only loads its own dist/, so the SDK function isn't importable at
runtime; reading its inputs directly is the runtime-safe equivalent.
pm csv export --all-fields --output full.csv
# header gains your custom columns, e.g. …,updated_at,story_points,teamRegister the importer in your pm-cli config to automatically pull from a CSV file on every sync:
Config options
| Key | Type | Required | Description |
|---|---|---|---|
file |
string | yes | Path to the CSV file to import |
delimiter |
string | no (default ,) |
CSV field delimiter (or alias tab/comma/semicolon/pipe) |
map |
string | no | Header remap, e.g. Summary=title |
auto-map / autoMap |
boolean | no (default false) |
Auto-map common third-party headers when unambiguous |
key |
string | no | Dedup key column for idempotent re-import |
encoding |
string | no (default utf-8) |
utf-8 | utf16le | latin1 |
source |
string | no | Provenance label recorded in csv_source |
The first row must be a header row. Column names are case-insensitive. Column order does not matter. Only title is required.
title,type,status,priority,tags,deadline,body,parent,assignee,sprint,release,blocked_by
"Add login page",Feature,open,2,"auth,ui",2026-05-30,"Landing page with email+password login",,alice,Sprint-12,v1.0,
"Fix navbar bug",Issue,in_progress,1,"bug,ui",,"Navbar collapses on mobile Safari",,bob,Sprint-12,v1.0,
"Write API docs",Task,open,3,docs,,,,,Sprint-13,v1.1,Fix navbar bug| Column | Description | Notes |
|---|---|---|
title |
Item title | Required |
type |
Item type | Any string, e.g. Feature, Issue, Task |
status |
Item status | See status mapping below |
priority |
Numeric priority | Integer |
tags |
Comma-separated tags | Quote the field if tags contain commas |
deadline |
Deadline | ISO 8601, e.g. 2026-05-30 (legacy due_date header accepted) |
body |
Description / body text | Supports multiline when quoted |
parent |
Parent item id | Maps to pm create --parent |
assignee |
Assignee | Maps to pm create --assignee |
sprint |
Sprint identifier | Maps to pm create --sprint |
release |
Release identifier | Maps to pm create --release |
blocked_by |
Blocked-by item id or reason | Maps to pm create --blocked-by (legacy blocked-by header accepted) |
All columns except title are optional. Round-tripping (export then import --key) is lossless for these fields.
The importer accepts common status strings and maps them to pm-cli statuses:
| Input values | pm-cli status |
|---|---|
open, todo, new |
open |
in_progress, wip, in progress, doing |
in_progress |
blocked, on_hold, on hold |
blocked |
closed, done, complete, completed |
closed |
canceled, cancelled |
canceled |
draft |
draft |
Unrecognized values default to open.
Exports use the same CSV format and can be re-imported. An extra read-only
csv_source column surfaces the import-provenance label (see --source); it is
ignored on import.
id,title,type,status,priority,tags,deadline,body,parent,assignee,sprint,release,blocked_by,csv_source,created_at,updated_at
pm-abc123,Add login page,Feature,open,2,"auth,ui",2026-05-30,Landing page,,alice,Sprint-12,v1.0,,jira-export,2026-05-01T10:00:00Z,2026-05-09T12:00:00Z--key <field> makes re-imports update existing items instead of creating
duplicates. The first import tags each created item with an internal
csv-key:<value> provenance tag; a later import keyed on the same column finds
and updates the matching item. Key matching is case-insensitive (pm folds tag
case on storage). The internal csv-key: and csv-source: tags are stripped
from exports so round-trips stay clean.
The extension registers an optional csv_source schema item field (via the
schema capability). When you import with --source <label>, the label is
recorded on each created/updated item and is surfaced back in the csv_source
export column. Note: as of pm 2026.5.31 the CLI does not expose a scalar setter
for extension-registered fields, so the value is persisted as an internal
csv-source:<label> tag (stripped from the normal tags export column). On
hosts whose SDK lacks registerItemFields, schema registration degrades to a
no-op without breaking any other behavior.
- Fields containing the delimiter, double-quotes, or newlines must be wrapped in double-quotes.
- Embedded double-quotes inside a quoted field are escaped by doubling them:
"". - Multiline field values (newlines inside quotes) are fully supported.
- Both
\n(LF) and\r\n(CRLF) line endings are accepted on import.
npm run accept:packed packs the real artifact and exercises strict CSV import
plus complete CSV export in fresh npm/npx and Bun/bunx projects on the pinned
development host, and in npm/npx on the declared minimum host. It is mandatory
inside npm run release:check.
npm install
npm run build # runs tsc → dist/TypeScript 5, ES2022 target, NodeNext module resolution, strict mode. Zero runtime dependencies.
pm csv export and the native pm csv-export export both accept --columns to pick and order columns:
pm csv export --columns id,title,status --output todos.csv
pm csv-export export --columns title,priority # native export pipelineValid columns: id, title, type, status, priority, tags, deadline, body, parent, assignee, sprint, release, blocked_by, csv_source, created_at, updated_at.
MIT
This package is release-ready for GitHub, npm, and Bun-compatible installs. CI runs type checking, build, production dependency audit, package packing, Bun install verification, and pm-changelog validation. The daily release workflow publishes only when commits exist after the latest release tag and uses pm-changelog to generate CHANGELOG.md and GitHub release notes.
This repo tracks its project management in .agents/pm/ and ships a committed .gitattributes
that maps those tracker artifacts to pm-cli's field-aware Git merge drivers, so concurrent-branch
tracker edits merge cleanly instead of hard-conflicting. The driver definitions live in
per-clone Git config; npm install / npm ci wires them automatically via the prepare script (a portable Node guard, scripts/prepare-merge-driver.mjs: it runs
pm merge install only when the pm CLI is on PATH, and no-ops cleanly otherwise so
production / --omit=dev installs are not broken; being Node-based it behaves identically
on POSIX shells and Windows cmd.exe). To (re)run manually: npm run merge:install.
After merging a branch that touched .agents/pm/, reconcile any residual history-hash drift with
pm merge reconcile (pm-cli ≥ 2026.7.22): preview with pm merge reconcile --dry-run, apply with
pm merge reconcile --message "post-merge reconcile", then confirm with pm validate, which scans the
whole tracker and flags remaining history drift across every affected item (pm merge reconcile
itself lists each affected stream in its output; pm history --verify <id> spot-checks one item). The field-aware driver already unions every author's
content, so reconcile only re-greens the hash chain (no data loss) — see the authoritative
pm-cli merge-safety guide. The
older blunt pm history-repair --all remains available as a lower-level primitive.
{ "importers": [ { "name": "csv-import", "config": { "file": "./data/items.csv", "delimiter": "," } } ] }