Skip to main content

Validation Stages

Manage a Spec's validation stages: positional quality checkpoints that validate the pipeline phases before them. Create, reorder, and replay blocking stages.

These endpoints configure a Spec (the same entity as /v1/schemas). A Spec is a Schema plus the rail of stages that govern how documents flow through the engine — source, field registry, extraction, resolution, validation, and data product — together with the Data Policies and validation gates those stages reference. You configure a Spec with the endpoints here, then you run it by compiling the rail into a pipeline via POST /v1/pipelines. Field-level normalization and transform config is not set on fields here: it lives in Data Policies (/v1/data-policies), which the resolution stage references.

A validation stage is a positional quality checkpoint. It validates the output of the pipeline phases that ran before it at its rail position, so where a stage sits in the order determines what it checks. Several checkpoints may appear in one rail, each guarding a different segment. Stages are returned ordered by position and are appended to the end of the sequence on create.

Each stage carries a severity: info logs only, warning flags failing cells, and blocking halts the failing field and its dependents. A blocking stage also carries a blockingConfig that decides the pass threshold, the critical_fields that always block on failure, and the on_block routing. The write path enforces what the engine actually supports: on_block must be review_queue or hold, and scope must be documenton_block: auto_retry, other scopes, and retry are rejected with a 400 explaining they are not yet enforced. The fieldScope narrows a stage to a subset of fieldKeys, or leaves it validating every field.

The threshold decides when failing fields actually block. { "type": "any" } blocks on any failure; { "type": "percentage", "min_pass_rate": 95 } blocks only when the fail rate exceeds the allowance — note min_pass_rate is 0–100, not a fraction; { "type": "count", "max_failures": n } blocks once n fields fail. Fields listed in critical_fields block on failure regardless of the threshold. A percentage or count threshold with a missing parameter degrades to any (fail closed) rather than silently disabling the gate.

GET/v1/schemas/{id}/validation-stages

Path parameters

id*stringSpec (schema) UUID.
POST/v1/schemas/{id}/validation-stages

Body parameters

name*stringStage name (max 256 characters).
descriptionstringOptional human-readable description.
severitystringOne of info, warning, blocking. info logs only, warning flags cells, blocking halts the failing field and dependents. Default: info
configobjectValidator mechanisms to run: { evidence?, nshot?: { enabled, runs }, llmJudge?: { enabled, model, confidenceCeiling, onlyFlagged }, businessRules?: { enabled, rules }, groundTruth?: { enabled, referenceDataId, matchKey, mappings }, captureCrossCheck?, crossSourceAgreement? }. Enabled groundTruth requires referenceDataId plus at least one matchKey and one mappings pair (400 otherwise).
blockingConfigobjectBlocking behavior (relevant when severity is blocking): { threshold, critical_fields, verdict_sensitivity, confidence_floor, scope, on_block, auto_release_hours, notify }. Supported: on_block review_queue | hold, scope document. auto_retry, other scopes, and retry are rejected 400 as not yet enforced.
fieldScopestringOne of all or subset. subset validates only fieldKeys. Default: all
fieldKeysstring[]Fields to validate when fieldScope is subset.

Create a blocking checkpoint

curl -X POST https://api.talonic.com/v1/schemas/a1b2c3d4-e5f6-7890-abcd-ef1234567890/validation-stages \
  -H "Authorization: Bearer tlnc_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Totals must reconcile",
    "description": "Block delivery when line items do not sum to the invoice total.",
    "severity": "blocking",
    "config": { "nshot": { "enabled": true, "runs": 2 } },
    "blockingConfig": {
      "threshold": { "type": "percentage", "min_pass_rate": 95 },
      "critical_fields": ["total"],
      "scope": "document",
      "on_block": "review_queue"
    }
  }'
GET/v1/schemas/{id}/validation-stages/{stageId}
PATCH/v1/schemas/{id}/validation-stages/{stageId}

Body parameters (all optional)

namestringStage name (max 256 characters).
descriptionstringHuman-readable description.
severitystringOne of info, warning, blocking.
configobjectValidator config object.
blockingConfigobject | nullBlocking behavior config. null clears it.
fieldScopestringOne of all or subset.
fieldKeysstring[]Fields to validate when fieldScope is subset.
DELETE/v1/schemas/{id}/validation-stages/{stageId}
POST/v1/schemas/{id}/validation-stages/reorder

Body parameters

stageIds*string[]The full set of stage UUIDs in the desired order.
POST/v1/schemas/{id}/validation-stages/{stageId}/replay-blocking

Body parameters

dryRunbooleanWhen true, preview the blast radius (counts only, no writes). The stage must already be saved as blocking. Default: false

Response

A stage object reflects the persisted validation_stages row. ordinal is the stage position in the rail. blocking_config is null unless severity is blocking.

Stage fields

idstringStage UUID.
user_schema_idstringSpec (schema) UUID this stage belongs to.
namestringStage name.
descriptionstring | nullOptional description.
severitystringOne of info, warning, blocking.
configobjectValidator config object.
blocking_configobject | nullBlocking behavior config, or null when severity is not blocking.
field_scopestringOne of all or subset.
field_keysstring[]Fields validated when field_scope is subset.
ordinalintegerPosition of the stage in the rail order.
created_atstringISO 8601 creation timestamp.
updated_atstringISO 8601 update timestamp.

Response

[
  {
    "id": "f1e2d3c4-b5a6-7890-abcd-ef1234567890",
    "user_schema_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "name": "Totals must reconcile",
    "description": "Block delivery when line items do not sum to the invoice total.",
    "severity": "blocking",
    "config": {
      "nshot": { "enabled": true, "runs": 2 }
    },
    "blocking_config": {
      "threshold": { "type": "percentage", "min_pass_rate": 95 },
      "critical_fields": ["total"],
      "scope": "document",
      "on_block": "review_queue"
    },
    "field_scope": "all",
    "field_keys": [],
    "ordinal": 0,
    "created_at": "2024-09-14T10:32:00.000Z",
    "updated_at": "2024-09-14T10:32:00.000Z"
  }
]

Reorder returns { "reordered": true }. Delete returns { "deleted": true, "id": "<stageId>" }. Replay-blocking returns the blast radius of retroactively applying the stage: documents scanned and affected, fields blocked, a breakdown of skipped fields (already in review, cleared by a prior review decision, no field state, or capacity-demoted by triage), and per-pipeline counts. With dryRun: true the same counts are a preview and nothing is written.

Response (replay-blocking, dryRun)

{
  "stageId": "f1e2d3c4-b5a6-7890-abcd-ef1234567890",
  "dryRun": true,
  "documentsScanned": 42,
  "documentsAffected": 6,
  "fieldsBlocked": 9,
  "skipped": {
    "alreadyInReview": 2,
    "clearedByReview": 1,
    "noFieldState": 0,
    "capacityDemoted": 0
  },
  "byPipeline": [
    {
      "pipelineId": "1a0c681d-ea20-4bb4-8892-01a6d7f834da",
      "pipelineName": "Invoices July",
      "documentsAffected": 6,
      "fieldsBlocked": 9
    }
  ]
}
A validation node validates the phases positioned before it. Move a stage earlier in the rail to gate extraction, or later to gate resolution and assembly. Reordering changes what each checkpoint sees.

Errors

Error responses

400validation_errorInvalid body, bad severity or fieldScope value, or a non-blocking stage passed to replay-blocking.
401unauthorizedMissing or invalid API key.
404not_foundNo Spec (schema) or stage with this ID exists for your organization.
429rate_limitedToo many requests. Retry after the period indicated in the Retry-After header.

Frequently asked questions

What does a validation stage actually validate?+
A stage is a positional checkpoint: it validates the output of the pipeline phases that ran before its position in the rail. Move it earlier to gate extraction, later to gate resolution or assembly.
What is the difference between warning and blocking severity?+
`warning` flags the failing cells but lets the run continue. `blocking` halts the failing field and its dependents and routes per `on_block` (`review_queue` or `hold`). `info` only logs. Writes that set `on_block: auto_retry` are rejected with a 400 — it is declared in the type but not yet enforced by the engine.
What does replay-blocking do?+
It applies a now-blocking stage to results already stored from earlier (warning) runs, pushing the failing fields into review without re-running the pipeline. The stage must already be saved as blocking. Pass `dryRun: true` to preview the blast radius first.
When does a blocking stage actually block?+
When its threshold trips: any failure (`type: any`), the pass rate dropping below `min_pass_rate` percent (`type: percentage` — a 0–100 value, so 95 means 95%), or `max_failures` fields failing (`type: count`). Fields in `critical_fields` block on failure regardless of the threshold, and a threshold with a missing parameter degrades to `any` rather than silently letting everything through.