# 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](https://docs.qa.clankership.net/api/authentication.md).
- 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](https://docs.qa.clankership.net/api/tags/list.md) 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](https://docs.qa.clankership.net/api/tickets/create.md) 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](https://docs.qa.clankership.net/get-started/concepts/planning/agent-instructions.md)
  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](https://docs.qa.clankership.net/api/idempotent-requests.md).
- 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](https://docs.qa.clankership.net/api/agents/list.md).
- **Send the new tickets to an agent.** [Send a message](https://docs.qa.clankership.net/api/messages/send.md) 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."
  ```
