Importing & bulk graphs
Turn one JSON document into a whole validated issue graph with clu batch — and pipe issues in from a Linear or Notion CLI.

clu batch instantiates an entire issue graph — issues, dependencies,
labels, checkpoints — from a single JSON document, in one transaction. It's
how you bulk-load work: a generated plan, a migration broken into phases, or
issues pulled from another tracker.
generate-the-graph | clu batchThe producer can be anything that emits JSON — a shell script, a Node
generator, a jq transform over another CLI's output. clu validates the
graph (acyclic, every reference resolves, every field valid), allocates real
IDs, and writes it atomically. A single bad entry aborts the whole batch, so
you never get a half-built graph.
Why this split
There are two distinct jobs, and clu batch only owns the second:
- Generation — any language, any logic: loops, conditionals, computed
fan-out, reading an API to decide the shape.
cludoesn't care what produced the graph. - Instantiation (
clu batch) — validation, ID allocation, and one atomic write. "Graphed correctly" is guaranteed here, not hoped for in the generator.
This is the programmable alternative to a static clu run
YAML template. Use a template for a fixed, repeatable shape; use batch when
the shape is computed.
The document format
Pass either a bare array of issues or an object with an optional group:
// bare array
[ { "alias": "a", "title": "…" }, { "alias": "b", "title": "…", "needs": ["a"] } ]
// or wrapped, with an umbrella parent
{ "group": "Auth rollout", "issues": [ … ] }Only alias and title are required per issue. The full field set:
| field | type | meaning |
|---|---|---|
alias | string | Local handle, unique within the batch. Wires needs; not stored. |
title | string | The issue title. |
type | string | task (default), bug, feature, epic, chore, decision, checkpoint, milestone. |
priority | int | 0 (highest) … 4 (lowest). Default 2. |
assignee | string | Pre-route to an agent lane (issue stays open). |
description / notes | string | Freeform text. |
capabilities | [string] | Stored as cap:<name> routing labels (see capability routing). |
labels | [string] | Arbitrary extra labels. |
needs | [string] | Dependencies — each is either another issue's alias (internal edge) or an existing real ID like clu-a1b2c3 (external edge onto the committed graph). |
key | string | Stable external identity, e.g. linear:ENG-123. Makes re-runs idempotent (see below). Unique within the batch. |
checkpoint | object | Makes this a manual gate: {} (anyone may clear) or {"approvers":["alice"]}. Forces type: checkpoint. |
Unknown fields are rejected — a typo like capabilites fails loudly
instead of silently dropping data. Run clu batch --docs for the same
reference from the CLI.
Validate before you commit
--dry-run validates and prints graph stats without writing anything. It
reports every problem at once (missing refs, cycles, bad fields, duplicate
aliases or keys) so a generator can be fixed in a single pass:
generate-the-graph | clu batch --dry-run
# valid: 124 issues, 130 edges (2 external), 8 roots, 11 leaves, depth 5, 3 checkpointsCommit with --json to get the alias → real-id map back:
generate-the-graph | clu batch --json | jq '.created'
# { "design": "clu-a1b2c3", "impl": "clu-d4e5f6", … }Umbrellas with --group
--group "Title" wraps the whole batch under a self-completing parent (a
milestone that auto-closes when every child closes). Every issue and the
parent get a run:<parent-id> label, so the batch is addressable as a unit:
generate-the-graph | clu batch --group "Q3 migration"
clu list -l run:clu-parent99 # everything in that batchA group field in the document does the same; the --group flag wins if both
are present.
Idempotent re-import (key + --on-existing)
Give an issue a stable key and re-running the batch won't duplicate it.
clu records the key (as an extkey:<key> row) on first create; on later
runs it matches by key and, per --on-existing:
skip(default) — the existing issue is left untouched; only genuinely-new issues are added.update— the issue's source-owned fields (title,type,priority,description) are re-synced; local workflow state (status, assignee, labels, deps, notes) is left alone.
Either way the alias still resolves to the existing ID, so a new issue that
needs an already-imported one wires up correctly. --json reports
new / existing / updated counts.
--group creates a new umbrella on every run (the parent isn't keyed).
Don't pass it on a scheduled re-import unless you want one umbrella per run.
Piping from another tracker
The contract is just JSON, so any tracker with a CLI or API can feed clu batch. The recipe is always the same: dump → map to the batch shape (give
each issue a stable key) → pipe.
Linear
A ready-made generator lives at
examples/generators/linear-todo.js.
It reads Linear issues as JSON on stdin, maps Linear's priority scale to
clu's, and tags each issue with a linear:<id> key:
linear issue query --all-teams --state unstarted --json --limit 0 --no-pager \
| node examples/generators/linear-todo.js \
| clu batch --group "Linear import"
# idempotent re-run — re-sync titles/priorities from Linear:
linear issue query --state unstarted --json \
| node examples/generators/linear-todo.js \
| clu batch --on-existing updateNotion
No generator needed — a jq transform turns a Notion database query into the
batch shape. Map the page id to a notion:<id> key so re-runs stay
idempotent:
ntn db query <database-id> --json \
| jq '[.results[] | {
key: ("notion:" + .id),
alias: ("ntn-" + (.id | gsub("[^a-zA-Z0-9]"; "-"))),
title: (.properties.Name.title[0].plain_text // "Untitled"),
priority: (if .properties.Priority.select.name == "High" then 1 else 2 end),
labels: ["notion"]
}]' \
| clu batch --group "Notion import" --on-existing updateAdjust the property paths to your Notion schema and whichever notion/ntn
CLI you use — only the output shape matters to clu.
Anything else
GitHub issues, a CSV, a JIRA export — same pattern. Produce a JSON array where
each element has at least alias + title (and ideally a key), and pipe it
in. For wholesale moves between clu databases, prefer the
export/import round-trip below.
The clu.js helper (optional)
For Node generators, examples/generators/clu.js
is a tiny zero-dependency helper. It only shapes data — clu batch still owns
all validation, so it can't drift from the real rules. The wins are
ergonomic: add() returns the alias so you wire needs by handle (typos
become JS reference errors), and duplicate aliases throw at build time.
import { Graph } from "./clu.js";
const g = new Graph();
const design = g.add("design", { title: "Design auth", type: "decision" });
const impl = g.add("impl", { title: "Implement auth", needs: [design], capabilities: ["go"] });
g.checkpoint("gate", { title: "Approve release", needs: [impl], approvers: ["alice"] });
g.add("ship", { title: "Ship", needs: ["gate"] });
g.emit(); // one JSON document on stdout → | clu batchGraph.phase(name, fn) groups issues into ordered stages: a phase can't start
until the previous one finishes. The helper drops a milestone at each
boundary that auto-closes when its phase completes, unblocking the next — no
human gate (use checkpoint() inside a phase for that). See the runnable
generators in examples/generators/:
feature-rollout.js, release-train.js, migrate.js, linear-todo.js.
Backup & restore
clu batch is for constructing graphs. To move a whole tracker verbatim
between databases, use the JSONL round-trip — it carries issues, deps,
comments, KV, and cron, preserving IDs:
clu export -o backup.jsonl # dump everything as JSONL
clu import backup.jsonl # restore into another .clu (one transaction)import is the inverse of export; --lenient skips unparseable lines. Reach
for batch when you're creating work, export/import when you're
relocating it.
For an ongoing, git-native version of the same round-trip — store the tracker on
a branch-independent refs/clu/store ref and reconcile it across clones with
last-writer-wins + tombstones — see Sync (git ref). state.jsonl
on that ref is exactly this export format; clu sync adds the git plumbing and
the merge rules on top.
Dynamic workflows (JS)
Generate issue graphs in code — loops, conditionals, computed fan-out — and pipe them into clu batch. The programmable alternative to static YAML templates.
Recipes
Common clu workflows end-to-end — solo loop, two agents, capability routing, gated releases, bulk import, and push-style delivery.