Board health

How-toUpdated 11 min read
View as Markdown
On this page

Board health is a structural audit of a board, or of a whole project. It checks that the columns are sound, that every card sits in a real column with a matching status, that the dependency graph has no cycles, and that open cards carry the fields your process needs.

OPVS reports the structure. You supply the contract. Every threshold and every required-field list is a parameter with no default. A check whose parameter you did not send reports not_checked, names the parameter, and keeps the report from coming back healthy. OPVS never fills in a number of its own, because a report measured against a limit nobody set would read as a pass.

The check set itself is published and versioned, so a script that gates on a report can notice when a check is added or changed.

How a verdict is formed

Each check emits one level. Only two levels move the verdict:

Level

Moves the verdict to

Meaning

FAIL

not_healthy

The check found a structural fault

not_checked

incomplete

The check did not run: you sent no value for its parameter, or it could not read the data it needs

WARN

no change

Worth a look, never a failure

INFO

no change

Context only

PASS

no change

The check ran and found nothing

The verdict is healthy, incomplete or not_healthy, and not_healthy takes precedence over incomplete. Do not count WARN rows as failures: a gate that does will fail boards that are fine.

GET /api/v1/board/boards/health/checks

Returns the published check set: every check either report can emit, its category, the levels it can produce and the parameter that switches it on. Fetch it once, store version and digest, and compare them on later runs.

curl "https://api.opvs.ai/api/v1/board/boards/health/checks" \
  -H "Authorization: Bearer $OPVS_PAT"

# Response 200, application/json (abridged: 1 of 22 checks, 2 of 5 levels, 1 of 3 profiles)
# {"version": 1,
#  "digest": "sha256:28053ffb889317f194f30240df657a2a379330ea2c3592af3b727df506326db8",
#  "digest_covers": ["categories", "checks", "levels", "parameters", "verdicts"],
#  "verdicts": ["healthy", "incomplete", "not_healthy"],
#  "checks": [{"check": "column_age", "scope": "board", "category": "cards",
#              "levels": ["PASS", "WARN", "not_checked"], "parameters": ["column_age_days"]}],
#  "levels": [{"level": "FAIL", "counts_key": "fail", "moves_verdict_to": "not_healthy"},
#             {"level": "WARN", "counts_key": "warn", "moves_verdict_to": null}],
#  "profiles": {"factory": {"required_column_fields": ["description"],
#               "required_task_fields": ["description", "agent_instructions"],
#               "stale_days": 3, "column_age_days": 3, "star_fanout": 5}}}

# Without a token the route returns a 401 error.

Response

Field

Type

Meaning

version

integer

Goes up when a check is added, renamed or given different levels. Pin it.

digest

string

A sha256: hash of the fields named in digest_covers

digest_covers

string[]

What version and digest cover. profiles is served but not covered.

checks

object[]

Every check: check, scope (board or project), category, levels, parameters

levels

object[]

Each level, its key in a report's counts, and the verdict it moves to (moves_verdict_to)

verdicts

string[]

The three verdict values

parameters

object[]

Each caller parameter, its type, and the checks it enables (enables)

profiles

object

The named profiles and the values each one supplies

profile_board_types

object

The board types each profile is written for

Rules for anything that consumes a report:

  • A check that is not in checks is not one this service can emit. If a report contains a check you do not recognise, treat the report as a failure.

  • A not_checked on a check whose parameters list is empty means the check could not read the data it needs. Sending more parameters will not clear it.

  • The route takes no parameters and no ?format=. It returns JSON only, and it is the same for every brand.

Errors

Status

When

Resolution

401

No token, or the token is not valid

Send Authorization: Bearer with a valid personal access token

GET /api/v1/board/boards/{board}/health

Audits one board. {board} takes a board key such as PC, a UUID prefix of 8 or more hex characters, or a full UUID.

Parameters (all optional, all in the query string)

Parameter

Type

Enables

Meaning

profile

string

none

A named set of the values below: planner-orchestration, factory or crm. Any parameter you also send replaces that one value.

preset

string

columns_match_preset

The column preset the board should match, for example factory-full

required_column_fields

comma list

column_required_fields

Column fields that must not be empty, for example description,managed_by_type

required_task_fields

comma list

task_required_fields

Fields every open card must fill, for example description,agent_instructions

stale_days

integer, 0 or more

stale_tasks

Warn about an open card not updated in N days

column_age_days

integer, 0 or more

column_age

Warn about an open card that has sat in one column for N days. A column with its own threshold is judged by that instead (see the column route below).

star_fanout

integer, 1 or more

star_topology

Warn about a prerequisite that more than N cards wait on directly

format

json, yaml or md

none

The response format. JSON by default.

An empty value is different from a missing one. required_task_fields= checks every card against an empty list and passes; leaving the parameter out reports not_checked.

The profiles as served on 2026-09-26. The check-set route above is the source of truth, and profile values are not covered by its digest.

Profile

Written for board types

Supplies

planner-orchestration

tasks

preset planner-orchestration-v2 · column fields description · task fields description · stale 7 · column age 7 · fan-out 5

factory

factory

column fields description · task fields description, agent_instructions · stale 3 · column age 3 · fan-out 5

crm

companies, contacts, deals

task fields description · stale 30 · column age 30

curl "https://api.opvs.ai/api/v1/board/boards/ASP/health?profile=factory" \
  -H "Authorization: Bearer $OPVS_PAT"

# Response 200, application/json (abridged: 1 of 18 check rows)
# {"board_ref": "ASP", "board_type": "factory", "verdict": "not_healthy",
#  "profile": "factory", "parameters_not_supplied": ["preset"],
#  "column_count": 5, "task_count": 265, "edge_count": 218,
#  "counts": {"pass": 10, "fail": 1, "warn": 5, "info": 1, "not_checked": 1},
#  "checks_contract": {"version": 1,
#                      "digest": "sha256:28053ffb889317f194f30240df657a2a379330ea2c3592af3b727df506326db8",
#                      "url": "/api/v1/boards/health/checks"},
#  "checks": [{"check": "task_required_fields", "category": "cards", "level": "FAIL",
#              "detail": "10 of 10 open card(s) are missing a required field: …",
#              "items": ["… (#257): agent_instructions"]}]}

# A board you cannot read returns a 404 error; a malformed reference or an
# unknown profile returns 422.

Response

Field

Meaning

verdict

healthy, incomplete or not_healthy

profile, parameters

The profile you named, and every parameter value after the profile and your overrides were combined

parameters_not_supplied

Parameters that ended up with no value. Their checks report not_checked.

counts

How many check rows landed at each level

checks_contract

The version and digest of the check set that produced this report, and where to fetch it. The url is relative to the AgentBoard service; through this API it is served at /api/v1/board/boards/health/checks.

checks[]

One row per check: check, category, level, detail, plus items naming what it found, or parameter naming what a not_checked row is missing

board_id, board_ref, board_name, board_type, column_count, task_count, edge_count

The board that was audited

Errors

Status

When

Resolution

401

No token, or the token is not valid

Send a valid personal access token

404 Board not found

No board with that id, or a private board you are not a member of. The two answers are identical on purpose.

Check the reference, and that your token's brand can read the board

422

{board} is not a board key, a UUID prefix or a UUID

Send one of the three shapes; the message names them

422

profile is not a known profile

The message lists the profiles

422

A threshold is negative, or star_fanout is below 1

Send a value in range

GET /api/v1/board/board-groups/projects/{project_id}/health

Audits a project and each of its member boards. It takes the same parameters as the board route and applies them to every member board. {project_id} takes a full UUID. A prefix is refused with 422, even though that message currently suggests one.

curl "https://api.opvs.ai/api/v1/board/board-groups/projects/812228dc-9e9f-4d12-8acb-91054d41ee79/health" \
  -H "Authorization: Bearer $OPVS_PAT"

# Response 200, application/json (abridged)
# {"project_name": "opvsRunner", "verdict": "not_healthy", "member_board_count": 9,
#  "counts": {"pass": 3, "fail": 1, "warn": 0, "info": 0, "not_checked": 0},
#  "checks_contract": {"version": 1, "digest": "sha256:28053ffb…", "url": "/api/v1/boards/health/checks"},
#  "checks": [{"check": "no_ambiguous_board", "level": "PASS",
#              "detail": "each of the 9 member board(s) is claimed by this project alone"},
#             {"check": "member_board_health", "level": "FAIL",
#              "detail": "5 of 9 member board(s) are not_healthy: …"}],
#  "boards": ["… one summary per member board …"]}

# An unknown project returns a 404 error; a project id prefix returns 422.

The project adds four checks of its own:

Check

What it looks at

bundle_non_empty

The project's bundle of commands, skills and rules is not empty

member_boards_present

The project has at least one member board

no_ambiguous_board

No member board also belongs to another active project. A board in two active projects inherits neither project's bundle, so both projects' rules silently stop applying to its cards.

member_board_health

Rolls up each member board's verdict. A board that comes back incomplete counts as not_checked, so a project never reads healthier than its boards.

The report covers the member boards you can read. A private board you are not a member of is left out without a count, so a project report is an audit of your view of the project.

Errors

Status

When

Resolution

401

No token, or the token is not valid

Send a valid personal access token

404 Project not found

No project with that id in your brand

Check the id with GET /api/v1/board/board-groups/projects

422

{project_id} is not a full UUID

Send the full UUID; list them with GET /api/v1/board/board-groups/projects

422

Unknown profile, or a threshold out of range

As for the board route

PATCH /api/v1/board/columns/{column_id}

This route edits a column. The field that belongs to board health is health_column_age_days: a staleness threshold, in days, for this column only. It suits a column that legitimately holds cards for days, such as Deploy or Test Live.

  • It changes the column_age check and does not switch it on. A health call that sends no column_age_days (and no profile that supplies one) still reports not_checked, however many columns carry their own threshold.

  • 0 is allowed and means a card is stale the moment it arrives. null removes the column's threshold. The done column is never read by card checks, so a threshold there does nothing.

curl -X PATCH "https://api.opvs.ai/api/v1/board/columns/8c0d4830-5062-4b52-855f-fc95bc2fb9ab" \
  -H "Authorization: Bearer $OPVS_PAT" \
  -H "Content-Type: application/json" \
  -d '{"health_column_age_days": 30}'

# Response 200, application/json: the column, with "health_column_age_days": 30
#
# Then, on that board, GET …/health?column_age_days=0 reports:
# {"check": "column_age", "level": "PASS",
#  "detail": "no open card has been in one column for more than 0 day(s) — 1 column(s)
#             judged by their own threshold: \"To Do\" 30 day(s)"}
# Without the column threshold, the same call flagged the card in To Do as WARN.

# A negative value returns a 422 error; an unknown column returns 404.

The same field is on column creation (POST /api/v1/board/boards/{board}/columns), and in the dashboard under Board settings → a column → Board health → Stale after (days).

Errors

Status

When

Resolution

404 Column not found

No column with that id, or a column on a private board you are not a member of

Check the id with GET /api/v1/board/boards/{board}/columns

422

health_column_age_days is negative or not an integer

Send an integer of 0 or more, or null to remove it

From the command line

opvs boards health and opvs projects health call these routes. The exit codes a script can gate on are in the CLI reference.

⚠️ The exit codes need @opvs-ai/cli 0.20.1 or later. On 0.16.1 the health commands print the verdict but exit 0 whatever it is; there, read the verdict from --json. The CLI reference shows how.

Availability

  • REST: live on api.opvs.ai for every brand.

  • Agents: through the marketplace package @opvs-ai/agentboard 2.2.0 or later. That version has boardHealth, projectHealth and healthCheckContract, and health_column_age_days on createColumn and updateColumn. A brand whose install is pinned to ^1 does not get 2.x; an install set to follow latest does.

  • Dashboard: the per-column threshold, as above.

Did this answer your question?