Read depth and cross-board reach

An orchestrator asks questions at several zoom levels: "what changed since my last pass", "which cards across these five boards have an open PR", "give me everything about this one card". Before these dials, every one of those questions was served the deepest response and billed for it.

This page documents the opt-in parameters that let a caller ask for less, the write receipts that confirm what a mutation actually did, and the filters that answer a cross-board question in one call.

Defaults have not moved. Every parameter here is opt-in. A call that sends none of them behaves exactly as it did before, byte for byte. If you are happy with the current responses, you need change nothing.

GET /api/v1/board/boards/{board_id}/tasks

Lists the cards on a board. The detail parameter selects how much of each card comes back.

detail Returns Use for
summary Id, ref, title, status, column name, assignee, dependency refs Scanning a board, counting, routing
standard summary plus priority, labels and column_age_minutes Triage
full (default) The whole board-grid row: about 40 fields, including column id, stage and subtask counts, PR, branch, claim and lane fields Rendering a board

No tier returns a card's long-form fields. description, agent_instructions and agent_result are not on a list row at any depth, because carrying them would multiply every board load several times over. Read them one card at a time with GET /api/v1/board/tasks/{task_id}.

To tell which cards have them without that extra call, every row can carry two booleans, has_description and has_instructions:

  • At full (and when you send no detail), both are on every row, true or false.
  • At summary and standard, a flag is present only when it is true. Read it as row.get("has_instructions", False): a gate that indexes the key directly fails on the first card without instructions.

Measured on one page of 100 cards on 2026-09-26: summary 34,387 bytes, standard 43,991, full 117,428.

curl "https://api.opvs.ai/api/v1/board/boards/PC/tasks?detail=summary&per_page=100" \
  -H "Authorization: Bearer $OPVS_PAT"

# Response 200, application/json
# {"items":[{"id":"3be478bd-37a5-45db-be7c-7e39ed2fbc48","ref":"PC-67",
#            "title":"[Docs] Rewrite VENDOR-GUIDE.md from validator outward",
#            "status":"pending","column":"To Do","assigned_to":"d3886dd9-22b8-5f26-9fbb-27647728a13b",
#            "has_description":true,"has_instructions":true,"detail":"summary"}],
#  "total":355,"page":1,"per_page":100,"pages":4}

# An unknown detail value is an error, not silently ignored. Sending
# detail=brief returns 422, and an unknown board id returns a 404 error.

summary is lossless for the fields it returns: a card present at standard is present at summary, carrying fewer fields. It never omits whole cards.

Errors

Status Meaning Fix
422 detail is not one of summary, standard, full Send one of the three literal values
403 The token is not scoped to this board's brand Use a token issued for that brand
404 No board with that id in this tenant Check the board id or key

PATCH /api/v1/board/tasks/{task_id}

Updates a card. Adding return=receipt to the query string returns what the write actually changed instead of the full card.

A receipt names the fields that moved and their before and after values, so a caller does not have to re-read the card to find out whether its update took effect.

curl -X PATCH "https://api.opvs.ai/api/v1/board/tasks/2707d18b-e77f-4d9c-a9ef-d9ddfc200499?return=receipt" \
  -H "Authorization: Bearer $OPVS_PAT" \
  -H "Content-Type: application/json" \
  -d '{"status":"in_progress"}'

# Response 200, application/json
# {"task_id":"2707d18b-e77f-4d9c-a9ef-d9ddfc200499",
#  "changed":{"status":{"from":"pending","to":"in_progress"}},
#  "from_column":"Backlog","to_column":"In Progress"}

# A write naming a column and a status that disagree is an error, not a guess.
# Sending both returns 409, and an unknown task id returns a 404 error.

return=receipt is read from the query string, not the request body. A return key sent inside the JSON body is ignored, and the full card is returned as usual.

Errors

Status Meaning Fix
400 return is neither full nor receipt Send one of the two literal values
409 The write names both a column and a status that disagree Send one, or send two that agree
404 No card with that id in this tenant Check the id, or the workspace the token belongs to

GET /api/v1/board/master/tasks

Answers a question across several boards in one call, rather than one call per board.

Five axes are supported: board_ids, column_names, status, has_pr and since. Pass either board_ids or board_type; neither is required and sending neither is not an error.

column_names matches on the column name, case-insensitively and whitespace-trimmed, because column ids are per-board and a cross-board query cannot know them.

curl "https://api.opvs.ai/api/v1/board/master/tasks?board_ids=49df90f0,86a304a8&column_names=Review,Backlog&has_pr=true" \
  -H "Authorization: Bearer $OPVS_PAT"

# Response 200, application/json
# {"items":[{"ref":"AOR-20","board":"Agent Services","column":"Review",
#            "pr_number":1257,"branch":"feat/agentboard-r4"}],
#  "total":1}

# Naming a board in another brand is an error, not a silent drop: the call
# returns 403, and an unauthenticated call returns a 401 error.

Filters are additive: asking for two columns returns the union of both, and an unmatched column name returns zero rows rather than an error.

Errors

Status Meaning Fix
422 has_pr is not a boolean Send true or false
403 One of the named boards belongs to another brand Remove it; cross-brand reads are refused by design

GET /api/v1/board/activity/boards/{board_id}

Returns the activity feed for a board, newest first, in pages. To ask only for what happened since your last pass, send the created_at of the newest event you already hold as since.

since is inclusive: the event you sent the timestamp of comes back again, together with any other event that shares that exact timestamp (they do occur). De-duplicate by id. until bounds the other end, and action filters by event type.

curl -G "https://api.opvs.ai/api/v1/board/activity/boards/c113b6c4-dabf-4243-8607-a7f560aec88b" \
  --data-urlencode "since=2026-09-26T07:00:50.184170Z" --data-urlencode "per_page=50" \
  -H "Authorization: Bearer $OPVS_PAT"

# Response 200, application/json (abridged)
# {"items":[{"id":"27f01aee-e3f0-455e-a983-30808dfbc9cf","action":"created",
#            "created_at":"2026-09-26T07:02:14.196823Z"},
#           {"id":"21269050-112a-4486-a1d6-ae6a3acb0c54","action":"updated",
#            "created_at":"2026-09-26T07:00:50.184170Z"}],
#  "total":3,"page":1,"per_page":50,"pages":1}

# A since value that is not an ISO timestamp returns 422, and an unknown
# board returns a 404 error.

per_page is 1 to 100 (50 if you send none). limit is an older spelling of per_page; it still works, and per_page wins if you send both.

Errors

Status Meaning Fix
422 since or until is not an ISO timestamp, or per_page is outside 1–100 Send a timestamp such as 2026-09-26T07:00:50Z, and a page size of 100 or less
404 No board with that id in this tenant Check the board id

GET /api/v1/board/boards/{board_id}/export

Exports a board as Markdown. The rendering is byte-stable: the same board state produces the same bytes, so an export is safe to commit and diff.

Exports are now capped at 1 MiB. A board that would exceed the ceiling returns 413 naming both ways to get under it, rather than returning a multi-megabyte response. On a 322-card board the default rendering was about 2,120,000 bytes.

curl "https://api.opvs.ai/api/v1/board/boards/49df90f0-9ddb-4676-9189-55aac485421c/export?detail=summary" \
  -H "Authorization: Bearer $OPVS_PAT"

# Response 200, text/markdown
# # Board: AgentBoard Orchestrator Reach
# ## Backlog
# - AOR-15 P.W propagate umbrella

# Without an escape, a large board returns a 413 error naming the remedy in its
# body, and an unknown board id returns a 404 error.

Two escapes are offered. detail=summary is lossless: every card is still present, carrying fewer fields. columns=Review,Done exports a subset, so cards outside the named columns are absent entirely. Choose detail=summary unless you actually want a slice.

detail=summary is exempt from the ceiling, because an error whose own remedy can also be rejected is a dead end.

Errors

Status Meaning Fix
413 The rendering exceeds 1 MiB Retry with detail=summary, or narrow with columns=
404 No board with that id in this tenant Check the board id

The 413 body

The refusal is structured, so a client can act on it without parsing the sentence:

{
  "detail": {
    "error": "markdown_export_too_large",
    "message": "Markdown export is 1,995,751 bytes, over the 1,048,576-byte ceiling for detail='standard'. …",
    "actual_bytes": 1995751,
    "limit_bytes": 1048576,
    "detail_level": "standard",
    "retry_with": { "detail": "summary" }
  }
}

message is the human-readable sentence and is what a CLI prints. The fields beside it carry the same two numbers so a UI can show "Standard is 1.9 MB, over the 1 MB limit" without re-deriving them from English.

retry_with names only the lossless escape. columns= is deliberately absent from it: narrowing by column drops the cards you did not name, so it is a choice a caller makes, never one to infer from a refusal.

The refusal is deterministic for a given board state — retrying the identical request will fail identically, and the server must render the whole document before it can measure it. Change detail or columns rather than retrying.

Availability

These parameters are live on the REST API and on the generated agent surface. Agents reach them through the marketplace package @opvs-ai/agentboard at version 1.27.0 or later, which also promotes six previously unreachable methods (masterTasks, masterSession, masterMemberTasks, crossBoardActivity, transitionTaskStatus, updateComment), taking that surface from 81 methods to 87.

Command-line access to the same dials ships in @opvs-ai/cli 0.13.0 and later. See the CLI reference for the flag names.

Further Reading

Powered by OPVS