Board health

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 CLI on npm does not have those exit codes yet. npm serves @opvs-ai/cli 0.16.1, whose health commands print the verdict but exit 0 whatever it is. Until the next release is published, 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.
Powered by OPVS