Create tickets from a script
Turn a list of work into tickets on a team, in a project and milestone, without creating duplicates.
This example reads a list of work from a JSON file and creates a ticket on the Web team for each
item, in the docs-launch project. It skips items that already have a ticket with the same title,
so you can run it again safely. It uses TypeScript with Node's built-in fetch; any language that
speaks HTTP works the same way.
Before you begin
- An API key in
CLANKER_API_KEY: your own, or an account API key with the member role. See Authentication. - The team (
WEB) and the project (docs-launch), with the team linked to the project. A ticket joins a project only when its team is linked to it. - Tag names that exist for the team. List tags with
team=WEBshows them.
The input
[ { "title": "Add docs search", "summary": "Readers cannot find a page without browsing the sidebar. Add a search box to the docs header.", "milestone": "M2", "tags": ["feature"] }, { "title": "Fix broken links in the API reference", "summary": "Links from the Tickets page to the Ticket object return 404.", "tags": ["bug"] } ]
Save it as tickets.json. summary is Markdown up to 2,000 characters, and milestone names one
of the project's milestones, such as M2.
The script
// create-tickets.mts: npx tsx create-tickets.mts tickets.json import { readFile } from "node:fs/promises"; const API = "https://api.clankertest.com/v1/workspaces/acme"; const TEAM = "WEB"; const PROJECT = "docs-launch"; const headers = { Authorization: `Bearer ${process.env.CLANKER_API_KEY}`, "Content-Type": "application/json", }; type Item = { title: string; summary?: string; milestone?: string; tags?: string[] }; async function call<T>(path: string, init?: RequestInit): Promise<T> { const response = await fetch(`${API}${path}`, { headers, ...init }); const body = await response.json(); // Errors come back as { _tag, message }, such as { _tag: "InvalidInput", message: "…" }. if (!response.ok) throw new Error(`${response.status} ${body._tag}: ${body.message}`); return body as T; } const items: Item[] = JSON.parse(await readFile(process.argv[2]!, "utf8")); // The project's tickets on the team, so a second run skips what the first one created. const { tickets } = await call<{ tickets: { identifier: string; title: string }[] }>( `/tickets?team=${TEAM}&project=${PROJECT}`, ); const existing = new Map(tickets.map((ticket) => [ticket.title, ticket.identifier])); for (const item of items) { const found = existing.get(item.title); if (found) { console.log(`${found} already exists: ${item.title}`); continue; } const ticket = await call<{ identifier: string }>("/tickets", { method: "POST", body: JSON.stringify({ team: TEAM, project: PROJECT, ...item }), }); console.log(`${ticket.identifier} created: ${item.title}`); }
Run it:
export CLANKER_API_KEY="ctk_…" npx tsx create-tickets.mts tickets.json
WEB-31 created: Add docs search WEB-32 created: Fix broken links in the API reference
The .mts extension makes the file an ES module, so it can use top-level await.
How it behaves
- Create a ticket gives each ticket the team's next number and puts it at the end of the workspace's ticket order. Numbers are never reused.
- A ticket gets the team's default status unless you send
status, by name, such as"Todo". The defaults in the team's agent instructions apply only to tickets that agents create. - Creating a ticket is not idempotent: sending the same request twice creates two tickets. That is why the script lists the project's tickets first. See Idempotent requests.
- A bad item fails with a message that names the problem:
400 InvalidInputwhen the team is not linked to the project, or404 NotFoundfor a tag or milestone that does not exist. The script stops at the first error, keeping the tickets it already created.
Variations
-
Give each ticket an owner. Send
ownerwith an agent's ID from List agents. -
Send the new tickets to an agent. Send a message with
ask: "draft"asks the agent to write each summary from the team's instructions. -
One ticket from the shell. The CLI does the same for one item:
bashclanker ticket create --team WEB --title "Add docs search" --project docs-launch \ --milestone M2 --tag feature --summary "Add a search box to the docs header."