Docs
Search docs

List a team's board rows

GET/api/teams/{team_id}/board

listTeamBoard

The team board's rows: only what a row renders (no description, no author; assignees and labels as ids, resolved against the team's member and label lists; `people` names every account the page's assignee ids reference, so an agent outside the team still renders). Same filters, gate and errors as `GET /api/teams/{team_id}/issues`; an omitted `state` means `all`. ORDER, total and stable, chosen by `state` and `sort` together, every order ending `issue_id` descending. With `state=open` rows are sectioned by status first (in review, in progress, todo, backlog), then: `sort=priority` (the default) by priority (urgent, high, medium, low, none), then newest first; `sort=created` newest first; `sort=updated` most recently updated first. With `state=closed` or `all` there are no sections: `sort=created` (the default) newest first; `sort=priority` by priority, then newest first; `sort=updated` most recently updated first. Pass a response's `next_cursor` back as `cursor`, with the same `state` and `sort`, for the rows after its last one. The cursor names a POSITION in that order, not a row: rows edited, moved, closed or deleted between pages never restart or stall the walk, and every row that stays put is returned exactly once (a row that moves across the cursor's position — under `sort=updated`, any row edited mid-walk — is seen twice or not at all). A malformed or forged cursor, or one from another order (a different `state` tab or `sort`), is a 400. `counts` are exact over the whole board, first page only.

Authentication is not described for this operation in the spec — that does not mean it is public. Check tokens and scopes.

curl

curl -X GET 'https://neosource.dev/api/teams/TEAM_ID/board'

fetch

fetch("https://neosource.dev/api/teams/TEAM_ID/board", {
  method: "GET",
});

Path parameters

team_idrequired

string

Query parameters

q

Case-insensitive literal text in the title, description, `#N` issue identifier, or this team's `KEY-N` identifier. Whitespace is trimmed; an empty value is treated as omitted. Values longer than 256 characters are rejected before storage. Re-send it with `cursor` for the next page.

string

state

`open`, `closed` or `all`. **Omitted means `all`** — this endpoint returned every state before it had filters, and defaulting to `open` would silently drop closed rows from callers that send nothing.

IssueStateFilter

string

The open/closed axis a list request filters on. Deliberately NOT `Default`: an omitted `state` on the team-issue listing means `All`, and defaulting to `Open` here would make that decision invisible at the route (it would silently drop closed rows from `ns-cli issue list --team` and from the e2e client, neither of which sends the parameter). The route maps `None => All` at one visible line instead.

"open""closed""all"

status

Only issues with this exact status.

IssueStatus

string

"backlog""todo""in_progress""in_review""done""cancelled""duplicate"

blocked

Only blocked (`true`) or unblocked (`false`) issues.

boolean

priority

Only issues with this exact priority.

IssuePriority

string

Linear-style priority. Stored as the `issue_priority` Postgres enum; surfaces as a domain enum so callers never juggle magic ints.

"none""urgent""high""medium""low"

assignee_id

Only issues assigned to this account (UUID). An id that parses but names nothing returns an empty page, not an error.

string

label_id

Only issues carrying this label (UUID).

string

cycle_id

Only issues bound to this cycle (UUID).

string

cursor

Opaque pagination cursor from a previous response's `next_cursor`.

string

limit

Page size; default 50, max 500.

integerint64

sort

`priority`, `created` or `updated`. **Omitted means the tab's default**: `priority` with `state=open`, `created` with `closed` or `all`. Re-send it with `cursor`: a cursor minted under one order is a 400 under another.

TeamBoardSort

string

The sort axis a team-board request asks for (`GET /api/teams/{id}/board` `sort`). Omitted is not a variant: the board's default depends on the `state` tab (priority on Open, created on Closed / All), so the route keeps the `Option` and the store derives the total order from both.

"priority""created""updated"

Responses

200Board page

application/json

TeamBoardPage

object

One page of `GET /api/teams/{team_id}/board`.

counts
one of
  • TeamIssueCounts

    object

    The team board's exact `(status, blocked)` cross-tab, plus the scalars derived from it. The cells are the one source; the scalars are arithmetic over them (see [`TeamIssueCounts::from`]), so a scalar cannot drift from the cross-tab. Clients that need the digest's groups compute them from `cells` — "blocked wins one group per row" is a UI rule, not a fact about the data.

    blocked_openrequired

    integerint64

    Open AND blocked — the digest's blocked group.

    cellsrequired
    closedrequired

    integerint64

    Terminal issues.

    openrequired

    integerint64

    Non-terminal issues (`IssueStatus::is_terminal()` is false).

    totalrequired

    integerint64

  • null

next_cursor

string | null

Pass back as `cursor` for the rows after the last one here; `null` on the last page. Opaque.

peoplerequired

array

Every account any row's `assignee_ids` names, once, ordered by id. Always emitted (possibly empty), so the schema marks it required.

items

TeamBoardPerson

object

One account a board page's rows name in `assignee_ids`: what the row's avatar renders. The same fields and wire rules as [`IssueAssigneeResponse`]. On the page because the team roster cannot name everyone: an agent is a workspace service account, not a team member. Resolved per request, so a rename is on the next read. An assignee id with no entry here names an account that no longer resolves — the client shows a former member.

account_idrequired

string

display_namerequired

string

handlerequired

string

kindrequired
AccountKind
rowsrequired

array

In the board's order — see the route's description.

items

TeamBoardRow

object

One row of `GET /api/teams/{team_id}/board`: exactly what a board row renders, and nothing it does not. No description, no author, and assignees and labels as IDS only: the client resolves names, avatars and colours from the team's member and label lists, which it already fetches and caches. An assignee id missing from the member list is a former member, rendered as a neutral avatar, never fetched per row. That is what keeps a board page small enough to refetch on every `TeamIssueList` hint. Named for the board, not for "issue" (ADR 0071's 2026-09-05 amendment keeps the old wire names only).

assignee_idsrequired

array

Account ids, ordered by id.

items

string

blockedrequired

boolean

closed_at

integer | nullint64

Unix milliseconds; `null` while the ticket is open.

created_atrequired

integerint64

Unix milliseconds.

cycle_id

string | null

The cycle this ticket is bound to, `null` when it is in none.

issue_idrequired

string

label_idsrequired

array

Label ids, ordered by id.

items

string

numberrequired

integerint32

The `N` of the team's `KEY-N`.

priorityrequired
IssuePriority
statusrequired
IssueStatus
titlerequired

string

updated_atrequired

integerint64

Unix milliseconds.

403Forbidden — one of: forbidden, needs_scope

application/json

one of
  • ErrorForbidden
  • NeedsScopeError

    object

    `403` body returned when a classic OAuth token is asked for something its granted scopes do not cover — an explicit private listing without `repo`, or the one-click GitHub mirror without `admin:public_key`. `scope` names the missing grant so the client can say exactly what the token lacks. There is no elevation route to widen it any more (a re-link goes through the GitHub App, whose tokens carry no scopes), so the client's answer is to say so, never to start a handshake.

    errorrequired

    string

    Always `needs_scope` — this body exists to carry the extra fields that kind needs.

    "needs_scope"

    scoperequired

    string

    The provider scope to request via elevation (e.g. `"repo"`).

Standard errors

Bodies documented once for the whole API — see standard errors.

  • 400Bad Request — one of: invalid_input
  • 404Not Found — one of: not_found
  • 429Rate limited — retry after the `Retry-After` header
  • 500Internal server error
  • 503Service temporarily unavailable / at capacity — retry after the `Retry-After` header
  • 504Gateway timeout — the request exceeded the server's handling budget

Schemas

Referenced above. Listed here rather than expanded inline, so the same definition is not repeated at every level.

TeamIssueCountCell

object

One `(status, blocked)` cell of [`TeamIssueCounts`]. Only non-empty cells are returned.

blockedrequired

boolean

countrequired

integerint64

statusrequired

IssueStatus

string

"backlog""todo""in_progress""in_review""done""cancelled""duplicate"

AccountKind

string

Rust mirror of the Pg `account_kind` enum (`accounts.kind`). `Ghost` rows are imported-author placeholders (GitHub metadata import, plans/github-metadata-import.md §6): `handle = gh-<github-id>`, `personal_workspace_id` NULL, no password, no sessions. They must never flow through `get_account*` (`AccountRow::into_record` CorruptData-errors on the NULL workspace) — author rendering goes through `PgGithubImportStore::authors_projection`, and claim-by-proof later promotes or merges them.

"human""bot""service""runner""ghost"

IssuePriority

string

Linear-style priority. Stored as the `issue_priority` Postgres enum; surfaces as a domain enum so callers never juggle magic ints.

"none""urgent""high""medium""low"

IssueStatus

string

"backlog""todo""in_progress""in_review""done""cancelled""duplicate"