> ## Documentation Index
> Fetch the complete documentation index at: https://docs.productbrain.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Mutations

> Create, update, and delete nodes.

All writes go through a single endpoint. The `action` field determines the operation.

```
POST /api/v1/mutate
```

## Add a Node

Creates a new node with an auto-generated ID.

```bash theme={null}
curl -X POST "https://productbrain.com/api/v1/mutate" \
  -H "Authorization: Bearer pb_..." \
  -H "Content-Type: application/json" \
  -d '{
    "action": "add",
    "projectId": "my-project",
    "node": {
      "type": "job",
      "parentId": "approach-1",
      "data": {
        "label": "Upload progress bar reaches 100% for a 500MB file",
        "maturity": "mvp",
        "iteration": "MVP"
      }
    }
  }'
```

**Response:**

```json theme={null}
{
  "success": true,
  "node": {
    "id": "job-199",
    "type": "job",
    "parentId": "approach-1",
    "data": {
      "label": "Upload progress bar reaches 100% for a 500MB file",
      "maturity": "mvp",
      "iteration": "MVP"
    }
  }
}
```

### Parent rules

| Type     | Parent required | Parent type |
| -------- | --------------- | ----------- |
| Goal     | No              | None        |
| Need     | Yes             | Goal        |
| Approach | Yes             | Need        |
| Job      | Yes             | Approach    |
| Task     | No              | None        |

## Update a Node

PATCH semantics. Only include fields you want to change. Existing fields are preserved.

```bash theme={null}
curl -X POST "https://productbrain.com/api/v1/mutate" \
  -H "Authorization: Bearer pb_..." \
  -H "Content-Type: application/json" \
  -d '{
    "action": "update",
    "projectId": "my-project",
    "nodeId": "job-45",
    "data": { "status": "done" }
  }'
```

To mark a job as not done, set `status` to `null`.

## Delete a Node

Hard delete. The node is removed from the table, but the full node is kept in the changelog, so a delete is **recoverable** with `restore` (below).

```bash theme={null}
curl -X POST "https://productbrain.com/api/v1/mutate" \
  -H "Authorization: Bearer pb_..." \
  -H "Content-Type: application/json" \
  -d '{
    "action": "delete",
    "projectId": "my-project",
    "nodeId": "job-99"
  }'
```

## Restore a Node

Recover a hard-deleted node from the changelog. It returns with its original id, type, parentId, and data. Identify it by `nodeId` (restores the most recent deletion of that node) or by a specific `changelogId`.

```bash theme={null}
curl -X POST "https://productbrain.com/api/v1/mutate" \
  -H "Authorization: Bearer pb_..." \
  -H "Content-Type: application/json" \
  -d '{
    "action": "restore",
    "projectId": "my-project",
    "nodeId": "job-99"
  }'
```

* `409` if a node with that id already exists (nothing to restore). `404` if no deletion is on record.
* If the node's original parent was also deleted, it comes back **orphaned** (dangling `parentId`). Restore the parent too, or reparent. The response `_meta._tip` flags this.
* Restore does not cascade. It brings back the single node, not its former children.

## Batch Mutations

Multiple operations in a single request. All-or-nothing. If any mutation fails validation, the entire batch is rejected.

```bash theme={null}
curl -X POST "https://productbrain.com/api/v1/mutate" \
  -H "Authorization: Bearer pb_..." \
  -H "Content-Type: application/json" \
  -d '{
    "action": "batch",
    "projectId": "my-project",
    "mutations": [
      {
        "action": "add",
        "node": {
          "type": "job",
          "parentId": "approach-1",
          "data": { "label": "First checkpoint", "iteration": "MVP" }
        }
      },
      {
        "action": "update",
        "nodeId": "job-100",
        "data": { "status": "done" }
      }
    ]
  }'
```

## Idempotency

Make any `mutate` call (single or batch) safe to retry by sending an `Idempotency-Key` header. An opaque per-operation string (a UUID is ideal):

```bash theme={null}
curl -X POST "https://productbrain.com/api/v1/mutate" \
  -H "Authorization: Bearer pb_..." \
  -H "Idempotency-Key: 7b1f2c84-...-your-uuid" \
  -H "Content-Type: application/json" \
  -d '{ "action": "add", "projectId": "my-project", "node": { "type": "goal", "data": { "label": "..." } } }'
```

* **Replay**. A second request with the same key *and the same body* returns the original response unchanged (with an `Idempotent-Replayed: true` header) instead of applying again. An agent that retries after a timeout never double-creates.
* **Conflict**. The same key with a *different* body returns `409` (you reused a key for a new operation. Generate a fresh one).
* **In flight**. If the first request is still running, a retry returns `409` ("in progress, retry shortly").
* **Window**. Keys are remembered for 24 hours, then expire. Scope is per user.

Without an `Idempotency-Key`, retries are not deduplicated, re-read with `GET /api/v1/nodes` or `search` and re-submit only what's missing.

## Validation

The API validates all mutations before applying them:

* **Label is required** on all nodes
* **Parent must exist** and be the correct type (need under goal, approach under need, etc.)
* **Fields must match node type**, setting `iteration` on a goal or `maturity` on a task is rejected
* **Kano values:** `must-have`, `performance`, `delighter`
* **Size values:** `skateboard`, `vespa`, `car`, `truck`, `antonov`
* **Maturity values:** `mvp`, `releasable`
* **Approach status values:** `development`, `validation`, `resolved`, `retired`

Validation errors return `{ "success": false, "errors": ["..."] }`.
