> ## 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.

# Nodes

> List and filter nodes in a project.

## List nodes

```
GET /api/v1/nodes?projectId={projectId}
```

Returns all matching nodes in a project, ordered by creation date. Use it for bulk reads ("every job in phase X", "export the tree"). To find a specific node, use [Search](/api-reference/search) instead.

### Parameters

| Parameter | Required | Description |
| - | - | - |
| `projectId` | Yes | The project to query |
| `type` | No | Filter by node type: `goal`, `need`, `approach`, `job`, `task` |
| `iteration` | No | Filter by phase name (jobs and tasks only) |
| `externalSystem` | No | Only nodes with an `externalRefs` entry for this system, for example `jira` |
| `externalKey` | No | Only nodes with an `externalRefs` entry with this key, for example `PROJ-1234`. Combine with `externalSystem` for an exact lookup. |
| `serves` | No | A need id. Only bets placed under other needs whose `serves` lists it. The need's own bets are not included. |

Passing `q` or `search` returns `400` with a pointer to `/api/v1/search`. This endpoint does not search.

### Example

```bash theme={null}
# All nodes
curl -s "https://productbrain.com/api/v1/nodes?projectId=my-project" \
  -H "Authorization: Bearer pb_..."

# Goals only
curl -s "https://productbrain.com/api/v1/nodes?projectId=my-project&type=goal" \
  -H "Authorization: Bearer pb_..."

# Jobs in a specific phase
curl -s "https://productbrain.com/api/v1/nodes?projectId=my-project&type=job&iteration=MVP" \
  -H "Authorization: Bearer pb_..."

# The node for Jira issue PROJ-1234
curl -s "https://productbrain.com/api/v1/nodes?projectId=my-project&externalSystem=jira&externalKey=PROJ-1234" \
  -H "Authorization: Bearer pb_..."

# Bets placed elsewhere that also serve need-14
curl -s "https://productbrain.com/api/v1/nodes?projectId=my-project&serves=need-14" \
  -H "Authorization: Bearer pb_..."
```

`externalSystem` and `externalKey` match exactly and are case-sensitive. More than one node can carry the same key, so the result is a list.

### Response

```json theme={null}
{
  "nodes": [
    {
      "id": "goal-1",
      "type": "goal",
      "parentId": null,
      "data": {
        "label": "Help shoppers save money",
        "description": "Core value prop."
      },
      "createdAt": "2026-09-17T23:19:53.979099+00:00"
    },
    {
      "id": "job-1",
      "type": "job",
      "parentId": "approach-1",
      "data": {
        "label": "Scan returns a price",
        "maturity": "mvp",
        "iteration": "MVP"
      },
      "createdAt": "2026-09-17T23:20:20.817022+00:00"
    }
  ],
  "_meta": {
    "currentIteration": "MVP",
    "iterations": {
      "active": ["MVP", "Later"],
      "total": 3
    },
    "nodeCounts": {
      "goal": 2,
      "need": 5,
      "approach": 8,
      "job": 23,
      "task": 4
    }
  }
}
```

`createdAt` is an ISO-8601 timestamp of when the node entered the plan. It is also returned on [Search](/api-reference/search) results and [Tree](/api-reference/tree) nodes.

`_meta.currentIteration` is the first active phase by sort order, including the system `Later` phase, so it can be `"Later"` when no other phase is active. For the current phase as ProductBrain defines it (the first active non-system phase), use [`GET /iterations?current=true`](/api-reference/iterations).

When an unfiltered read returns more than 50 nodes, `_meta._tip` reminds you to use search for lookups.

<Note>
  If you are looking for a specific node by concept, use [Search](/api-reference/search) instead of pulling all nodes and filtering. Search uses semantic matching and returns ranked results with ancestor paths.
</Note>

## Node structure

Every node has:

| Field | Type | Description |
| - | - | - |
| `id` | string | Generated by the API: `{type}-{number}` (for example `job-42`) |
| `type` | string | `goal`, `need`, `approach` (a bet), `job`, or `task` |
| `parentId` | string \| null | Parent node ID. Null for goals and tasks. |
| `data` | object | Label, description, and the type-specific fields below |
| `createdAt` | string | ISO-8601 creation timestamp |

### Data fields by type

| Field | Type | Valid on | Description |
| - | - | - | - |
| `label` | string | all | Node title (required) |
| `description` | string | all | Human-readable summary |
| `notes` | string | all | Extended context, rationale, implementation detail. Used mainly on bets and jobs. |
| `iteration` | string | job, task | Phase the work is assigned to. Defaults to `Later`. |
| `status` | string | job, task | `"done"` when live in production, otherwise absent. See below. |
| `maturity` | string | job | `"mvp"` or `"releasable"` |
| `measure` | string | goal, approach | On a bet: how you will know it paid off (expected). On a goal: an optional target, such as an OKR key result. |
| `kano` | string | approach | `"must-have"`, `"incremental"`, or `"delighter"`. `"performance"` is accepted as an alias for `"incremental"` and stored as sent. |
| `size` | string | approach | `"skateboard"`, `"vespa"`, `"car"`, `"truck"`, `"antonov"` |
| `approachStatus` | string | approach | `"development"`, `"validation"`, `"resolved"`, `"retired"` |
| `blockedBy` | string\[] | approach | IDs of blocking bets |
| `goalStatus` | string | goal | `"focus"`, `"later"`, `"done"` |
| `kind` | string | task | `"memo"` (untriaged inbox item) or `"do-it"` (small, resolve in place). Set automatically after creation. |
| `urgent` | boolean | task | Time-critical flag, only on a `do-it` task |
| `externalRefs` | object\[] | all | Pointers to the same thing in another system. See below. |
| `serves` | string\[] | approach | Other needs this bet also helps. See below. |
| `demand` | object\[] | need | Evidence that the need is real. See below. |

Setting a type-specific field on the wrong node type is rejected with `400`.

### External references

`externalRefs` records where a node lives in another tool: a Jira key, a roadmap-tool id. Up to 50 entries. One system may appear several times (a goal behind two roadmap objectives), but the same system and key only once.

```json theme={null}
"externalRefs": [
  { "system": "jira", "key": "PROJ-1234", "url": "https://acme.atlassian.net/browse/PROJ-1234" },
  { "system": "roadmap", "key": "RM-0168" }
]
```

| Field | Rules |
| - | - |
| `system` | Required. Lowercase slug, 2 to 32 characters (`a-z`, `0-9`, `-`), starting with a letter. Free-form: name the tool. |
| `key` | Required. The id in that system, 1 to 128 characters. |
| `url` | Optional. `https://` only. |

No other fields are accepted. On update, send the whole list: it replaces the old one. `[]` or `null` clears it.

A reference is a label. ProductBrain never infers hierarchy, sync or a relationship from it. Two nodes may carry the same key. When you write one, the response's `_meta._tip` names the other nodes that already have it. Keys are not part of semantic search: look a key up with the `externalSystem` and `externalKey` filters above. Public share links never show `externalRefs`.

### A bet that serves more than one need

A bet has exactly one parent: the need it is placed under and scoped against. Its Kano, size and measure refer to that need. When one build also helps other needs (an automation engine, an export layer), list them in `serves` instead of splitting the build into one bet per need:

```json theme={null}
"serves": ["need-14", "need-22"]
```

* Bets only. Each id must be an existing need in the project, and not the bet's own parent. Needs under another goal are allowed.
* At most 5, no repeats. On update, send the whole list: it replaces the old one. `[]` or `null` clears it.
* Moving a bet under a need it already serves makes that need the parent, and the same write removes it from `serves`.
* When a write leaves a bet serving 3 or more extra needs, the response carries `_meta.advisory` with `type: "approach_serves_many"`. It usually means the bet is infrastructure whose own need is missing, or the needs overlap and should merge.
* If a served need is deleted, its id stays in the list and is ignored on read.

`serves` is not `blockedBy`. `blockedBy` is a delivery dependency between bets: one can't start until another lands. Read the other direction with the `serves` filter above, or with [Tree](/api-reference/tree) `direction=context` on the need, which returns `alsoServedBy`. Public share links never show `serves`.

### Demand: evidence on a need

`demand` records who feels a need, what's behind them, and when it was observed. Up to 25 entries, needs only.

```json theme={null}
"demand": [
  { "who": "Acme Logistics", "revenue": { "amount": 1200, "currency": "USD", "period": "month" }, "at": "2026-09-14", "source": "sales-call", "ask": "SSO with their Okta tenant" },
  { "who": "Globex", "size": "40 seats", "at": "2026-03-02" }
]
```

| Field | Rules |
| - | - |
| `who` | Required. An account or segment, not a named person, 1 to 80 characters. |
| `at` | Required. The date it was observed, `YYYY-MM-DD`. |
| `revenue` | Optional. `amount` (0 or more), `currency` (ISO 4217 in capitals, such as `USD`), `period` (`once`, `month` or `year`). Stored as entered, never converted or added up. |
| `size` | Optional. What's behind them when it isn't money, such as `40 seats`. 1 to 60 characters. |
| `source` | Optional. A lowercase slug, such as `sales-call`, `support`, `churn` or `crm`. |
| `ask` | Optional. Their words, at most 140 characters. Never a transcript. |

No other fields are accepted. On update, send the whole list: it replaces the old one. `[]` or `null` clears it.

It's evidence, never a ranking. The date belongs to the entry, not the need, and nothing ages, schedules, ranks or sorts a need by it. No endpoint filters or sorts by `demand`, and there's no total. Public share links never show it, and search doesn't index it.

### Status

`status` tracks whether a job or task is live. The rule: `done` means in production.

* Absent: not started or in progress. Work merged only to staging is not done.
* `"done"`: live in production.
* `"open"`: send this on an update to reopen a closed job or task. It is stored as `null` and `completedAt` is cleared. An explicit `null` means the same thing.
* `"delivered"`: deprecated. Still accepted for old integrations, but treat it as "not done yet" and do not set it for new work.

Bets (type `approach`) carry `approachStatus` instead, and goals carry `goalStatus`. Other node types have no status.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.