Search the docs

Search guides, API endpoints and MCP tools.

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=WEB shows them.

The input

json
[
  {
    "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

typescript
// 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:

bash
export CLANKER_API_KEY="ctk_…"
npx tsx create-tickets.mts tickets.json
text
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 InvalidInput when the team is not linked to the project, or 404 NotFound for 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 owner with 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:

    bash
    clanker ticket create --team WEB --title "Add docs search" --project docs-launch \
      --milestone M2 --tag feature --summary "Add a search box to the docs header."