Skip to main content
Workflow tests let you freeze the inputs to a single workflow block and assert something about its output the next time it runs. They are designed to catch regressions when you change a schema, prompt, function code, classifier categories, or split definition without having to replay the whole workflow. This page covers the API contract — target, source, assertion, the run-record shape, and the seven values of the run-record status enum. For the conceptual mental model and the dashboard workflow, see Tests.

What’s in a test

A WorkflowTest is a Pydantic model with three meaningful sections:

target — what the test runs against

A discriminated union by type: block is the only variant today. The shape is a discriminated union so workflow-level targets (e.g. { type: "workflow" } running every block end-to-end) can be added later without renaming the field at every callsite.

source — where the inputs come from

Also a discriminated union by type: When you create a test from run_step, Retab snapshots the inputs at create time — subsequent edits to the source workflow run don’t affect the test. File handles in the snapshot are materialized as durable Retab file refs so the test still runs months later even if the original upload session is gone.

assertion — required, one per test

Workflow tests intentionally normalize to one assertion per test. Multiple small tests beat one broad assertion: when an assertion fails, the failure points at exactly which output behavior changed. assertion.target always names a declared output handle (output_handle_id) and an optional dotted path inside that handle’s payload. See the operator catalog below.

Available condition.kind values

The full assertion-targeting reference, including which path syntaxes work inside each handle type, lives at Tests.

Two distinct status enums — don’t confuse them

A test surfaces TWO status fields. Most surprises with the API trace back to mixing them up.

assertion_result.status (4 values)

The outcome of evaluating ONE assertion against the block’s output.

Run-record status (7 values)

The status of a TEST RUN — aggregates the assertion result with execution-side state. This is what appears on WorkflowTestResult.status, latest_run_summary.status, and per-test result rows returned from /v1/workflows/tests/results?run_id={run_id}. After Create Workflow Test Run, poll the returned workflow-test run id. Expect transient pending / running parent-run lifecycle states before terminal counts and per-test results are available. Workflow-test execution uses the returned run id for polling, cancellation, and result inspection.

Run records

A WorkflowTestResult is the immutable snapshot of one execution. The fields most consumers care about:

outputs (renamed from handle_outputs)

Run records before the May 2026 API rewrite stored output: Any (the raw block return blob) and handle_outputs: { [handle_id]: any } (per-handle outputs). The new shape collapses these into a single field:
A backfill migration copies legacy handle_outputsoutputs on first server startup, so reads through this field always work. The legacy output and handle_outputs fields are intentionally kept on legacy docs for forensic debugging — drop them via a later cleanup migration once nothing reads them.

Fingerprints

Three deterministic hashes pinned per run record:

Schema drift and staleness

When the workflow draft changes (schema edited, block config tweaked), tests captured against the old draft get a schema_drift status other than none: Drift status is recomputed at read time — it’s not persisted on the storage doc. So the value you see in a GET response always reflects the current draft, not the draft at create time.

Async execution

Test execution is asynchronous. The flow:
  1. POST /v1/workflows/tests/runs with workflow_id in the request body returns a run object immediately.
  2. Poll GET /v1/workflows/tests/runs/{run_id} until lifecycle.status is completed, error, or cancelled, then fetch results from GET /v1/workflows/tests/results?run_id={run_id}.
  3. Workflow-test run results shape:
    • counts — one bucket per run-record status (7 fields)
    • data[] — one result per test, keyed by test_id within the parent run.
For dashboard integrations, poll the parent run status and refresh test-run records when the parent run reaches a terminal state.

Endpoints

MCP

Every endpoint above is also exposed as an MCP tool (workflows_tests_create, workflows_tests_list, workflows_tests_get, workflows_tests_update, workflows_tests_delete, workflows_tests_runs_create, workflows_tests_runs_get, workflows_tests_results_list). The tool input schemas match the request bodies documented above. The MCP layer additionally rejects the pre-rewrite top-level block_id / run_id / step_id / handle_inputs fields with a per-field migration hint pointing at the new shape (e.g. block_id → use 'target.block_id'). See the MCP server page for how to register the tools with a Claude / OpenAI agent.