Board health
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 |
|---|---|---|
|
| The check found a structural fault |
|
| The check did not run: you sent no value for its parameter, or it could not read the data it needs |
| no change | Worth a look, never a failure |
| no change | Context only |
| 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 |
|---|---|---|
| integer | Goes up when a check is added, renamed or given different levels. Pin it. |
| string | A |
| string[] | What |
| object[] | Every check: |
| object[] | Each level, its key in a report's |
| string[] | The three verdict values |
| object[] | Each caller parameter, its type, and the checks it enables ( |
| object | The named profiles and the values each one supplies |
| object | The board types each profile is written for |
Rules for anything that consumes a report:
A check that is not in
checksis not one this service can emit. If a report contains a check you do not recognise, treat the report as a failure.A
not_checkedon a check whoseparameterslist 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 |
|---|---|---|
| No token, or the token is not valid | Send |
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 |
|---|---|---|---|
| string | none | A named set of the values below: |
| string |
| The column preset the board should match, for example |
| comma list |
| Column fields that must not be empty, for example |
| comma list |
| Fields every open card must fill, for example |
| integer, 0 or more |
| Warn about an open card not updated in N days |
| integer, 0 or more |
| 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). |
| integer, 1 or more |
| Warn about a prerequisite that more than N cards wait on directly |
|
| 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 |
|---|---|---|
|
| preset |
|
| column fields |
|
| task fields |
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 |
|---|---|
|
|
| The profile you named, and every parameter value after the profile and your overrides were combined |
| Parameters that ended up with no value. Their checks report |
| How many check rows landed at each level |
| The |
| One row per check: |
| The board that was audited |
Errors
Status | When | Resolution |
|---|---|---|
| No token, or the token is not valid | Send a valid personal access token |
| 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 |
|
| Send one of the three shapes; the message names them |
|
| The message lists the profiles |
| A threshold is negative, or | 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 |
|---|---|
| The project's bundle of commands, skills and rules is not empty |
| The project has at least one member 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. |
| Rolls up each member board's verdict. A board that comes back |
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 |
|---|---|---|
| No token, or the token is not valid | Send a valid personal access token |
| No project with that id in your brand | Check the id with |
|
| Send the full UUID; list them with |
| Unknown | 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_agecheck and does not switch it on. A health call that sends nocolumn_age_days(and no profile that supplies one) still reportsnot_checked, however many columns carry their own threshold.0is allowed and means a card is stale the moment it arrives.nullremoves 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 |
|---|---|---|
| No column with that id, or a column on a private board you are not a member of | Check the id with |
|
| Send an integer of 0 or more, or |
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.aifor every brand.Agents: through the marketplace package
@opvs-ai/agentboard2.2.0 or later. That version hasboardHealth,projectHealthandhealthCheckContract, andhealth_column_age_daysoncreateColumnandupdateColumn. A brand whose install is pinned to^1does not get 2.x; an install set to followlatestdoes.Dashboard: the per-column threshold, as above.