Findings API
The Findings API exposes Finding clusters, built-in Rules, on-demand analysis Runs, time-window backfills, and provider-neutral repair work items.
Base URL:
https://api.provon.dev/v1Project API keys need diagnostics:read for read routes and diagnostics:write for mutation
routes. Signed-in Workbench sessions can use the same project-qualified routes when the user has
the corresponding project permission.
A Finding is a cluster of the same supported failure mechanism observed across conversations.
One row per distinct conversation that observed the cluster is persisted in a bounded
conversationOccurrences index, and review history is kept as a separate trail. The list and
detail responses project different shapes from the same stored cluster: the list view omits the
heavy evidence, anchor, and metadata blobs, while the detail view includes them.
Findings#
| Method | Path | Purpose |
|---|---|---|
GET |
/projects/:projectId/diagnostics/findings |
List Finding clusters (lightweight list view) |
GET |
/projects/:projectId/diagnostics/findings/counts |
Filter facet counts for the current result set |
GET |
/projects/:projectId/diagnostics/findings/stats |
Time-bucketed Finding volume histogram |
GET |
/projects/:projectId/diagnostics/findings/:findingId |
Read one Finding in full, with its current handoff and work item |
PATCH |
/projects/:projectId/diagnostics/findings/:findingId |
Update review or resolution state |
POST |
/projects/:projectId/diagnostics/findings/discover |
Submit diagnostics for the latest conversation snapshot |
GET |
/projects/:projectId/diagnostics/findings/:findingId/work-items |
List work items for a Finding |
GET |
/projects/:projectId/diagnostics/findings/:findingId/work-items/:workItemId |
Read one work item |
POST |
/projects/:projectId/diagnostics/findings/:findingId/handoffs |
Start a repair handoff (creates the work item) |
POST |
/projects/:projectId/diagnostics/findings/:findingId/handoffs/:handoffId/retry |
Retry a failed handoff |
POST |
/projects/:projectId/diagnostics/findings/:findingId/handoffs/:handoffId/abandon |
Abandon a failed handoff |
POST |
/projects/:projectId/diagnostics/findings/:findingId/work-items/:workItemId/replacements |
Replace the primary work item |
POST |
/projects/:projectId/diagnostics/findings/:findingId/work-items/:workItemId/sync |
Pull the latest external work-item status |
List query parameters#
| Parameter | Type | Notes |
|---|---|---|
queue |
active, resolved, or ignored |
Convenience lifecycle filter. active is not-dismissed and not-resolved; resolved is not-dismissed and resolved; ignored is dismissed. |
reviewStatus |
unreviewed, confirmed, or dismissed |
Optional filter |
resolutionStatus |
unresolved, awaiting_verification, resolved, regressed |
Optional filter |
severity |
Comma-separated subset of critical,high,medium,low,info |
Optional filter |
stage |
Comma-separated subset of understanding,planning,execution,verification,final-answer,runtime |
Optional filter |
tracker |
Comma-separated subset of github,gitlab,linear,jira,none |
Repair owner: primary work item provider, else current handoff; none = not handed off |
search |
String up to 200 characters | Matches title, summary, cause, and fix text. |
minDiagnosticScore |
number from 0 to 1 |
Optional filter |
minConfidence |
number from 0 to 1 |
Optional filter |
lastSeenAfter |
Unix milliseconds | Inclusive lower bound on lastSeenAt |
lastSeenBefore |
Unix milliseconds | Exclusive upper bound on lastSeenAt |
limit |
integer from 1 to 500 |
Page size; defaults to 100 |
cursor |
opaque string | Cursor from prior result |
lastSeenAfter must be earlier than lastSeenBefore. Findings are ordered by regression
priority, then severity, then impact (distinct-conversation count), then recency, then id.
List responses include findings, each a lightweight FindingListItem view plus its current
handoff and primary workItem (both null when none exists), and
pagination.limit / pagination.nextCursor. The list view excludes evidence,
diagnosticAnchor, evidenceWindow, evidenceSnapshotRevision, evidenceThroughMs,
metadata, and reviewHistory; those appear only on the detail view.
{
"findings": [
{
"id": "fnd_...",
"clusterFingerprint": "...",
"fingerprintVersion": "finding-cluster-v1",
"reviewStatus": "unreviewed",
"dismissalReason": null,
"resolutionStatus": "unresolved",
"severity": "high",
"observationRole": "independent",
"causalRelations": [],
"diagnosticScore": 0.85,
"diagnosticConfidence": 0.82,
"title": "Required verification was skipped",
"summary": "The agent claimed completion without running the required verification.",
"rootCauseSummary": "The first uncorrected fault is the omitted verification step.",
"suggestedFix": "Run and report the required verification before claiming completion.",
"categoryKey": "plan-step-omitted",
"stage": "execution",
"categoryLabel": "Plan Step Omitted",
"admissionSource": "model",
"firstSeenAt": 1760000000000,
"lastSeenAt": 1760000000000,
"conversationOccurrences": [
{
"conversationId": "conv_123",
"latestTraceId": "trace_123",
"detectionTimestamps": [1760000000000],
"trimmedDetectionCount": 0
}
],
"analysisRevision": "drv_...",
"createdAt": 1760000200000,
"updatedAt": 1760000200000,
"reviewedAt": null,
"resolvedAt": null,
"regressedAt": null,
"handoff": null,
"workItem": null
}
],
"pagination": { "limit": 100, "nextCursor": null }
}conversationOccurrences has one entry per distinct conversation (bounded to the 50 most
recent). latestTraceId is null when the detection carried no trace. detectionTimestamps is
deduped and sorted; trimmedDetectionCount counts detections dropped by retention so the true
observation total is trimmedDetectionCount + detectionTimestamps.length.
Filter counts and stats#
GET .../findings/counts returns facet totals for the same filters:
{
"byQueue": { "all": 12, "active": 8, "resolved": 3, "ignored": 1 },
"bySeverity": { "critical": 1, "high": 4 },
"byStage": { "execution": 5, "final-answer": 2 },
"byTracker": {
"github": { "total": 4, "inProgress": 2, "toVerify": 1, "failing": 1 },
"none": { "total": 8, "inProgress": 0, "toVerify": 0, "failing": 0 }
}
}Each facet is counted with every other filter applied, so an option's count is what selecting it
would show. byTracker.total is the tracker facet count. Its repair buckets are disjoint and only
cover open Findings (not resolved or ignored): failing (handoff creation failed or tracker sync
error), then toVerify (awaiting verification or the external issue is done), then inProgress
(handoff pending or issue open).
GET .../findings/stats accepts the same filters plus a range and returns a
non-empty, ascending time-bucketed volume histogram:
{
"startMs": 1759900000000,
"endMs": 1760000000000,
"bucketMs": 3600000,
"buckets": [{ "bucketStartMs": 1759900000000, "findingCount": 3, "highSeverityCount": 1 }]
}Read a Finding#
GET /v1/projects/:projectId/diagnostics/findings/:findingIdThe detail view adds reviewHistory, freeform metadata, diagnosticAnchor, evidence,
evidenceWindow, evidenceSnapshotRevision, and evidenceThroughMs to the list view, and
returns the current handoff and primary workItem alongside:
{
"id": "fnd_...",
"clusterFingerprint": "...",
"fingerprintVersion": "finding-cluster-v1",
"reviewStatus": "unreviewed",
"dismissalReason": null,
"resolutionStatus": "unresolved",
"severity": "high",
"observationRole": "root",
"causalRelations": [
{
"conversationId": "conv_123",
"analysisRevision": "drv_...",
"rootFindingId": "fnd_root",
"symptomFindingId": "fnd_symptom",
"rootObservationFingerprint": "...",
"symptomObservationFingerprint": "...",
"confidence": 0.9
}
],
"reviewHistory": [],
"diagnosticScore": 0.85,
"diagnosticConfidence": 0.82,
"title": "Required verification was skipped",
"summary": "The agent claimed completion without running the required verification.",
"rootCauseSummary": "The first uncorrected fault is the omitted verification step.",
"suggestedFix": "Run and report the required verification before claiming completion.",
"metadata": {},
"categoryKey": "plan-step-omitted",
"stage": "execution",
"categoryLabel": "Plan Step Omitted",
"admissionSource": "model",
"firstSeenAt": 1760000000000,
"lastSeenAt": 1760000000000,
"conversationOccurrences": [
{
"conversationId": "conv_123",
"latestTraceId": "trace_123",
"detectionTimestamps": [1760000000000],
"trimmedDetectionCount": 0
}
],
"diagnosticAnchor": {
"conversationId": "conv_123",
"traceId": "trace_123",
"spanId": "span_123",
"stepIndex": 8,
"quotes": [{ "quote": "I have completed the task." }]
},
"evidence": [
{
"ref": "span_123",
"kind": "span",
"title": "verify-answer",
"summary": "Verification span was never executed.",
"url": null,
"location": {
"traceId": "trace_123",
"spanId": "span_123",
"stepIndex": 8,
"quotes": [{ "quote": "I have completed the task." }]
}
}
],
"evidenceWindow": {
"startMs": 1760000000000,
"endMs": 1760000100000,
"conversationIds": ["conv_123"]
},
"analysisRevision": "drv_...",
"evidenceSnapshotRevision": "cesr_...",
"evidenceThroughMs": 1760000100000,
"createdAt": 1760000200000,
"updatedAt": 1760000200000,
"reviewedAt": null,
"resolvedAt": null,
"regressedAt": null,
"handoff": null,
"workItem": null
}observationRole is derived from current causal relations (root, symptom, or independent);
causalRelations only includes edges whose analysisRevision matches the Finding's current
revision.
Update review or resolution#
Send one of review or resolution per request — they cannot be updated in the same call:
{
"reviewStatus": "confirmed"
}{
"resolutionStatus": "resolved"
}reviewStatus can be unreviewed, confirmed, or dismissed. Dismissing requires a
dismissalReason from false_positive, duplicate, accepted_risk, not_actionable, or
other, and atomically clears resolution state (resolutionStatus returns to unresolved):
{
"reviewStatus": "dismissed",
"dismissalReason": "false_positive"
}Manual resolution uses resolutionStatus: "resolved" | "unresolved". Setting resolved also
confirms the Finding and stamps resolvedAt. awaiting_verification and regressed are
transitioned by handoff sync and recurrence, not by a manual PATCH.
A Finding whose primary work item is not done or canceled (i.e. todo, doing, or
unknown) cannot be dismissed or manually resolved. The API returns 409 ACTIVE_WORK_ITEM on
dismissal or 409 WORK_ITEM_NOT_DONE on resolution.
The PATCH response is the enriched detail view with the current handoff and workItem.
Discover latest Findings#
Queue diagnostic discovery for the latest activity snapshot of one conversation:
POST /v1/projects/:projectId/diagnostics/findings/discoverResponse (202 Accepted):
{
"conversationId": "conv_123",
"runId": "diagnosticrulejob...",
"revision": "diagnosticrevision...",
"enabledRules": 1,
"runsCreated": 1
}The API selects the latest conversation and submits work; it never runs the model reviewer.
Automatic, manual, and backfill runs use the same diagnostics queue and execution service.
When no rule is enabled, runId and revision are null and runsCreated is zero.
A manual submission advances a pending automatic run with the same revision to run immediately.
Query the exact submitted revision:
GET /v1/projects/:projectId/diagnostics/analysis-runs/:conversationId?revision=<returned-revision>The response includes runId, conversationId, revision, status, outcome, findings,
diagnostics, and error. Status is pending, in_progress, completed, skipped, failed,
or superseded. outcome stays null until completed, then is findings_found or no_findings.
diagnostics contains the publication audit attributes projected from run telemetry.
The coordinator is authoritative for pending/running/retrying work; completed outcomes come from
Trace telemetry. R2 SQL read visibility can lag the write, so a cleared coordinator does not imply
that zero Findings were produced. Finding references are read from completed run telemetry;
Finding details remain the current product record, not a duplicate diagnosis history.
Workbench waits with cancellation and a bounded timeout; leaving the page cancels waiting, not
the background diagnosis. CLI provon findings discover prints the submission, and
provon findings run <conversation-id> --revision <revision> queries its status and result.
Handoffs and Work Items#
Starting a handoff accepts provider: "github" | "gitlab" | "linear" | "jira" and an optional
provider destination object. The configured connector default is used when destination is omitted.
Only one current handoff and one primary work item exist per Finding.
POST /v1/projects/:projectId/diagnostics/findings/:findingId/handoffs{
"provider": "github",
"destination": {
"owner": "provon",
"repo": "provon"
}
}A successful start returns 201 with the enriched detail view (the Finding now carries the new
handoff and workItem). Completing a handoff automatically confirms the Finding.
Lifecycle actions:
POST .../handoffs/:handoffId/retry
POST .../handoffs/:handoffId/abandon
POST .../work-items/:workItemId/replacements
POST .../work-items/:workItemId/sync- Retry only works for the current handoff when its
statusisfailed. - Abandon stops tracking a current failed handoff; no external issue exists to close. Returns
the enriched detail view with
handoff: null. - Replace starts a new handoff (same
{provider, destination}body) that creates the replacement primary work item after the old one is superseded. - Sync returns
{ outcome, finding }.outcomeisupdated,unchanged,failed,not_syncable(no external identity yet), ornot_current(no longer the primary work item).
A FindingHandoff represents creation and has status: pending | failed | completed. A
FindingWorkItem exists only after the provider returns an external identity, with
status: todo | doing | done | canceled | unknown and syncStatus: synced | error.
GET .../work-items returns { "workItems": [...] } and
GET .../work-items/:workItemId returns { "workItem": {...} }.
Rules#
Automatic diagnosis runs while at least one Rule is enabled; disabling every Rule turns automatic scheduling off. There is no separate master switch.
| Method | Path | Purpose |
|---|---|---|
GET |
/projects/:projectId/diagnostics/rules |
List built-in Rules and configuration |
GET |
/projects/:projectId/diagnostics/rules/:ruleId |
Read one Rule |
PATCH |
/projects/:projectId/diagnostics/rules/:ruleId |
Enable/disable or reconfigure one Rule |
POST |
/projects/:projectId/diagnostics/rules/:ruleId/backfill-requests |
Queue a time-window backfill |
A PATCH sends either enabled or a full configuration object, never both:
{
"enabled": false
}{
"configuration": {
"keywords": ["checkout", "payment"],
"minSignalScore": 0.25,
"minDiagnosticConfidence": 0.5,
"reviewerDiscoveryPolicy": "candidates_only",
"reviewModel": null,
"signalOverrides": {
"runtime-interruption": { "enabled": true, "severity": "high" }
}
}
}reviewerDiscoveryPolicy controls what the structured reviewer covers on a reviewed run:
candidates_only (default — adjudicate only the candidates the rules produced, so a clean
conversation makes no model call) or whole_trajectory (also scan the full trajectory for
problems no rule surfaced, which always costs one model call). Production Rules always run the
reviewed-diagnosis profile; there is no server-side "no model" option. The no-model deterministic
screening profile is only available locally through provon diagnose --mode rule-signals.
Signal override severities can be critical, high, medium, low, or info.
Backfill body:
{
"startMs": 1760000000000,
"endMs": 1760086400000,
"maxConversations": 100
}Backfill responses use 202 when Runs are queued. Include an Idempotency-Key header to avoid
duplicate backfills for the same rule and window. The default maxConversations is 10,000 and the
maximum is 100,000.
On-Demand Runs#
Queue enabled Rules for one conversation activity snapshot:
POST /v1/projects/:projectId/diagnostics/analysis-runs{
"conversationId": "conv_123",
"lastActivityAt": 1760000000000,
"traceIds": ["trace_123"]
}conversationId and lastActivityAt are required. traceIds is optional and is filtered to
string values. The response is 202:
{
"enabledRules": 5,
"runsCreated": 1
}Common Errors#
| HTTP | Code | When |
|---|---|---|
400 |
INVALID_FILTER |
Unsupported enum value, malformed cursor, malformed search, or lastSeenAfter >= lastSeenBefore. |
400 |
INVALID_REQUEST |
Review and resolution sent in the same PATCH, or no supported field to update. |
400 |
INVALID_REVIEW_STATUS |
Unsupported reviewStatus value. |
400 |
INVALID_DISMISSAL_REASON |
Dismissal without a supported dismissalReason. |
400 |
INVALID_RESOLUTION_STATUS |
Manual resolution not resolved or unresolved. |
400 |
INVALID_WORK_ITEM_PROVIDER |
Handoff provider is not github, gitlab, linear, or jira. |
400 |
INVALID_WORK_ITEM_DESTINATION |
Destination is not an object of string fields. |
400 |
INVALID_DIAGNOSTIC_RULE |
Rule PATCH is malformed or configuration is invalid. |
400 |
INVALID_DIAGNOSTIC_RUN |
On-demand run body is missing conversationId or lastActivityAt. |
400 |
INVALID_BACKFILL_WINDOW |
Backfill window is invalid or maxConversations is out of range. |
404 |
FINDING_NOT_FOUND |
Finding does not exist. |
404 |
WORK_ITEM_NOT_FOUND |
Work item does not exist or does not belong to the Finding. |
404 |
HANDOFF_NOT_FOUND |
Handoff does not exist or does not belong to the Finding. |
404 |
DIAGNOSTIC_RULE_NOT_FOUND |
Rule does not exist. |
404 |
DISCOVERY_CONVERSATION_NOT_FOUND |
No recent conversation found for discovery. |
409 |
DISCOVERY_CONVERSATION_NOT_READY |
Latest conversation is not ready for analysis. |
409 |
ACTIVE_WORK_ITEM |
Cannot dismiss a Finding whose primary work item is not done/canceled. |
409 |
WORK_ITEM_NOT_DONE |
Cannot resolve a Finding whose work item is not done. |
409 |
FINDING_DISMISSED |
Cannot start a handoff for a dismissed Finding. |
409 |
HANDOFF_ALREADY_EXISTS |
A current handoff or primary work item already exists. |
409 |
HANDOFF_NOT_CURRENT |
The handoff is no longer current. |
409 |
HANDOFF_NOT_RETRYABLE |
The handoff is not the current failed creation attempt. |
409 |
HANDOFF_IN_PROGRESS |
Another worker currently owns the handoff claim. |
409 |
WORK_ITEM_NOT_CURRENT |
The work item was replaced or is no longer primary. |
500 |
DISCOVERY_FAILED |
Discovery failed for an unmapped reason. |
500 |
HANDOFF_OPERATION_FAILED |
A handoff operation failed for an unmapped reason. |
503 |
HANDOFF_PROVIDER_UNAVAILABLE |
Connector is missing, disabled, or default destination is unset. |
503 |
DIAGNOSTIC_RULES_UNAVAILABLE |
Diagnostic rule service is not mounted in this deployment. |
503 |
FINDING_WORK_ITEMS_UNAVAILABLE |
Work item service is not mounted in this deployment. |