---
name: planiq-api
description: Read and modify Planiq roadmap boards via the REST API. Use whenever the user asks to list, create, update, move, or delete boards, nodes, or edges on Planiq.
allowed-tools: Bash(curl *), Bash(jq *), WebFetch
---

# Planiq API skill

This skill teaches Claude (or any LLM agent) how to drive a Planiq roadmap
board through its REST API. Treat the user's natural-language request as the
spec and translate it into the right sequence of API calls.

## Setup

Before any call, two environment variables MUST be set:

- `PLANIQ_API_URL` — base URL of the Planiq deployment (e.g. `https://planiq.cloud`)
- `PLANIQ_API_TOKEN` — long-lived API key. Generate one at
  `$PLANIQ_API_URL/profile` under "Developer / API keys". The full token is
  shown exactly once, prefixed `pq_live_…`.

If either is unset, ask the user before making any call. Do not invent values.

Smoke test:

```bash
curl -s -H "Authorization: Bearer $PLANIQ_API_TOKEN" \
  "$PLANIQ_API_URL/api/v1/boards" | jq
```

A 200 with a JSON array means you're configured correctly.

## Auth & error model

Every request carries `Authorization: Bearer $PLANIQ_API_TOKEN`. Errors come
back as JSON:

```json
{ "error": "human-readable message", "code": "stable.dotted.code", "details": { } }
```

Status codes worth distinguishing:

| Status      | Meaning                                                            | What to do                                                |
| ----------- | ------------------------------------------------------------------ | --------------------------------------------------------- |
| 200/201/204 | Success                                                            | Continue                                                  |
| 400         | Validation error                                                   | Fix the request body and retry                            |
| 401         | Missing/invalid token                                              | Stop. Ask the user to re-check `PLANIQ_API_TOKEN`         |
| 402         | Plan upgrade required (feature gated, e.g. sub-boards, deadlines)  | Surface the message verbatim. **Do not retry.**           |
| 403         | No role on this board                                              | Stop. Tell the user they lack permission                  |
| 404         | Board/node/edge not found                                          | Re-list (`GET /boards`) and double-check the id           |
| 409         | Conflict (e.g. duplicate edge)                                     | Re-fetch state and reconcile                              |
| 422         | Unprocessable (e.g. an edge that would create a cycle)             | Stop and explain the constraint to the user               |

## Data model

### Node

A node is a task or step on the board.

| Field         | Type              | Notes                                                              |
| ------------- | ----------------- | ------------------------------------------------------------------ |
| `id`          | string            | Short id, e.g. `lqcg5ur0`. Two sentinels: `__start__`, `__end__`   |
| `label`       | string ≤ 255      | Title                                                              |
| `content`     | string (HTML)     | Body; server sanitizes                                             |
| `url`         | string ≤ 2048     | External link                                                      |
| `resolved`    | boolean           | Done state                                                         |
| `col`         | integer ≥ 0       | Grid column. **Cell-indexed, not pixels.**                         |
| `row`         | integer ≥ 0       | Grid row                                                           |
| `assigneeId`  | UUID or null      | Assigned user                                                      |
| `deadline`    | ISO-8601 or null  | Due date (Pro+ plans only)                                         |
| `subtasks`    | array             | Checklist items (see Sub-tasks); edit via the subtask endpoints below |
| `isMilestone` | boolean           | Milestone marker                                                   |

`col` and `row` are **integer cell coordinates** on a snap grid. Never compute
pixel positions.

### Edge

Edges connect nodes:

```json
{ "id": "edg_abc", "fromId": "lqcg5ur0", "toId": "__end__" }
```

The graph is a DAG. Cycles, self-loops, edges *out of* `__end__`, and edges
*into* `__start__` are rejected with 422.

### Special nodes

Every board has one `__start__` and one `__end__`. They cannot be deleted. To
anchor "the first task", set `parentId: "__start__"`.

## Placement algorithm (important)

When creating a node, **do NOT supply `col` or `row`.** Instead, anchor with
exactly one of:

- `parentId: "<id>"` — appends a child below the parent, shifting siblings
  if needed.
- `onEdgeId: "<edgeId>"` — splits the edge `A → B` into `A → new → B`.

The server runs the same collision-detection + subtree-shifting algorithm the
UI uses, so the result is identical to a user clicking "add child" in the
canvas. To **move** an existing node afterwards, `PATCH` with explicit
integer `col`/`row`.

## Endpoint reference

All paths are relative to `$PLANIQ_API_URL`. All carry `Authorization: Bearer
$PLANIQ_API_TOKEN`. JSON request bodies need `Content-Type: application/json`.

### Boards

#### `GET /api/v1/boards` — list boards you can see

```bash
curl -s -H "Authorization: Bearer $PLANIQ_API_TOKEN" \
  "$PLANIQ_API_URL/api/v1/boards"
```

Returns `[{ "id": "...", "name": "...", "role": "owner|admin|editor|viewer", ... }]`.

#### `GET /api/v1/boards/{boardId}` — full graph

```bash
curl -s -H "Authorization: Bearer $PLANIQ_API_TOKEN" \
  "$PLANIQ_API_URL/api/v1/boards/$BOARD_ID"
```

Returns the board metadata plus `nodes[]` and `edges[]`. Use this to look up
node ids by label before mutating.

### Nodes

#### `POST /api/v1/boards/{boardId}/nodes` — create a node

Body MUST include `label` and exactly one of `parentId` or `onEdgeId`.

Append as child of an existing node:

```bash
curl -s -X POST "$PLANIQ_API_URL/api/v1/boards/$BOARD_ID/nodes" \
  -H "Authorization: Bearer $PLANIQ_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"label":"Pick a domain name","parentId":"__start__"}'
```

Insert on an existing edge (`A → new → B`):

```bash
curl -s -X POST "$PLANIQ_API_URL/api/v1/boards/$BOARD_ID/nodes" \
  -H "Authorization: Bearer $PLANIQ_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"label":"Review draft","onEdgeId":"edg_abc"}'
```

Returns 201 with the created node (server-computed `col`/`row`).

#### `PATCH /api/v1/boards/{boardId}/nodes/{nodeId}` — partial update

Accepts any subset of: `label`, `content`, `url`, `resolved`, `assigneeId`
(nullable), `deadline` (nullable, ISO-8601), `isMilestone`, `col`, `row`.

```bash
curl -s -X PATCH "$PLANIQ_API_URL/api/v1/boards/$BOARD_ID/nodes/$NODE_ID" \
  -H "Authorization: Bearer $PLANIQ_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"resolved":true,"label":"Pick a domain name (done)"}'
```

To move a node, pass integer `col` and `row`:

```bash
-d '{"col":3,"row":2}'
```

#### `DELETE /api/v1/boards/{boardId}/nodes/{nodeId}`

```bash
curl -s -X DELETE "$PLANIQ_API_URL/api/v1/boards/$BOARD_ID/nodes/$NODE_ID" \
  -H "Authorization: Bearer $PLANIQ_API_TOKEN"
```

Returns 204. Cascade-deletes any edges touching the node.

### Edges

#### `POST /api/v1/boards/{boardId}/edges` — connect two nodes

```bash
curl -s -X POST "$PLANIQ_API_URL/api/v1/boards/$BOARD_ID/edges" \
  -H "Authorization: Bearer $PLANIQ_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"fromId":"lqcg5ur0","toId":"__end__"}'
```

Rejected with 422 if it would create a cycle, self-loop, or violate the
`__start__` / `__end__` rules.

#### `DELETE /api/v1/boards/{boardId}/edges/{edgeId}`

```bash
curl -s -X DELETE "$PLANIQ_API_URL/api/v1/boards/$BOARD_ID/edges/$EDGE_ID" \
  -H "Authorization: Bearer $PLANIQ_API_TOKEN"
```

### Time tracking

Each node has a timer. State is exposed as `timeSpent` (total accrued
seconds), `timerStartedAt` (ISO-8601 or null), and `timerUserId` (UUID of the
user whose timer is running, or null). Time tracking requires a **Pro/Ultra**
plan — a `402` means upgrade required; do not retry.

#### `POST /api/v1/boards/{boardId}/nodes/{nodeId}/timer/start`

Only the node's assignee may start. Returns `{ "timer": {...} }`.

```bash
curl -s -X POST \
  "$PLANIQ_API_URL/api/v1/boards/$BOARD_ID/nodes/$NODE_ID/timer/start" \
  -H "Authorization: Bearer $PLANIQ_API_TOKEN"
```

`402` Pro plan required · `403` only the assignee can start · `409` timer
already running.

#### `POST /api/v1/boards/{boardId}/nodes/{nodeId}/timer/stop`

Only the user who started may stop (or a super-user). Returns
`{ "timer": {...}, "elapsedSeconds": 123 }`.

```bash
curl -s -X POST \
  "$PLANIQ_API_URL/api/v1/boards/$BOARD_ID/nodes/$NODE_ID/timer/stop" \
  -H "Authorization: Bearer $PLANIQ_API_TOKEN"
```

`409` if the timer is not running.

#### `POST /api/v1/boards/{boardId}/nodes/{nodeId}/timer/manual`

Log a retroactive entry. Body requires `start` and `end` (both ISO-8601).
Returns `{ "timer": {...}, "durationSeconds": 3600 }`.

```bash
curl -s -X POST \
  "$PLANIQ_API_URL/api/v1/boards/$BOARD_ID/nodes/$NODE_ID/timer/manual" \
  -H "Authorization: Bearer $PLANIQ_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"start":"2026-06-12T09:00:00Z","end":"2026-06-12T10:00:00Z"}'
```

### Sub-tasks

A node carries a flat, Asana-style checklist. Each subtask has `id`, `title`,
`done`, and `createdAt`. Order is significant — new items append to the end.
These are free (no plan gate) and require **editor** role to mutate.

#### `GET /api/v1/boards/{boardId}/nodes/{nodeId}/subtasks`

```bash
curl -s \
  "$PLANIQ_API_URL/api/v1/boards/$BOARD_ID/nodes/$NODE_ID/subtasks" \
  -H "Authorization: Bearer $PLANIQ_API_TOKEN"
```

Returns `{ "subtasks": [{ "id": "...", "title": "...", "done": false, "createdAt": "..." }] }`.

#### `POST /api/v1/boards/{boardId}/nodes/{nodeId}/subtasks`

Body requires `title` (≤ 500 chars); `done` is optional (defaults `false`).
Returns 201 with the created subtask.

```bash
curl -s -X POST \
  "$PLANIQ_API_URL/api/v1/boards/$BOARD_ID/nodes/$NODE_ID/subtasks" \
  -H "Authorization: Bearer $PLANIQ_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"title":"Draft the spec"}'
```

#### `PATCH /api/v1/boards/{boardId}/nodes/{nodeId}/subtasks/{subtaskId}`

Partial update — rename, resolve, or both. Provide at least one of `title` or
`done`.

```bash
# mark done
curl -s -X PATCH \
  "$PLANIQ_API_URL/api/v1/boards/$BOARD_ID/nodes/$NODE_ID/subtasks/$SUBTASK_ID" \
  -H "Authorization: Bearer $PLANIQ_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"done":true}'
```

#### `DELETE /api/v1/boards/{boardId}/nodes/{nodeId}/subtasks/{subtaskId}`

```bash
curl -s -X DELETE \
  "$PLANIQ_API_URL/api/v1/boards/$BOARD_ID/nodes/$NODE_ID/subtasks/$SUBTASK_ID" \
  -H "Authorization: Bearer $PLANIQ_API_TOKEN"
```

Returns 204.

### Anything not covered above

Members, attachments, chat messages, sub-boards, activities,
API-key management — the full OpenAPI 3.1 spec lives at:

```
$PLANIQ_API_URL/api/v1/openapi.json
```

Fetch it with `WebFetch` when needed.

## Worked example

> "Add a task 'Pick a domain name' as the first step on my Launch Plan board."

Step 1 — find the board id:

```bash
curl -s -H "Authorization: Bearer $PLANIQ_API_TOKEN" \
  "$PLANIQ_API_URL/api/v1/boards" \
  | jq -r '.[] | select(.name == "Launch Plan") | .id'
```

Step 2 — create the node anchored to `__start__`:

```bash
curl -s -X POST "$PLANIQ_API_URL/api/v1/boards/$BOARD_ID/nodes" \
  -H "Authorization: Bearer $PLANIQ_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"label":"Pick a domain name","parentId":"__start__"}'
```

Step 3 — confirm to the user with the new node's `id` and grid position.

## Tips

- Always `GET /api/v1/boards/$BOARD_ID` first to discover existing node ids by
  label. Don't ask the user to type ids — look them up.
- For multi-step plans, do all reads first, then all writes, so a single 4xx
  halts the whole operation cleanly.
- A `402` is never a retry condition. Show the upgrade message and stop.
- Be careful with `DELETE` — there is no soft-delete. Confirm with the user
  before deleting a node that has children.
