> This page is for version v2026-02-09 (default).
> For other versions, use one of these documentation indexes:
> - v2026-02-09 (default): https://docs.extend.ai/2026-02-09/llms.txt
> - v2025-04-21: https://docs.extend.ai/2025-04-21/llms.txt
> - v2024-12-23: https://docs.extend.ai/2024-12-23/llms.txt

> ## Documentation Index
> Fetch the complete documentation index at: https://docs.extend.ai/llms.txt
> Use this file to discover all available pages before exploring further.
>
> ## API version
> The current API version is `2026-02-09`, served at the site root (no version prefix in URLs).
> If this page URL contains `/2025-04-21/` or `/2024-12-23/`, you are reading an older API version.
> Prefer the current docs at https://docs.extend.ai/llms.txt unless the user explicitly needs that older version.
> Do not treat older-version pages as the source of truth for new integrations.

> Complete reference for configuring Extend workflows via the API — the steps array, every step type, routing rules, and the create, deploy, and run lifecycle.

## Configuring Workflows

A workflow's behavior is defined by its `steps` array. You set it when you [create a workflow](/api-reference/endpoints/workflow/create-workflow), [update its draft](/api-reference/endpoints/workflow/update-workflow), or [create a version](/api-reference/endpoints/workflow/create-workflow-version). This page is the complete reference for every step type and routing rule. For the end-to-end create → deploy → run lifecycle, start with [Create a Workflow](/workflows/overview).

Each step has a `name`, a `type`, an optional `config`, and a `next` array that defines where documents flow after the step completes. The same shape is used in request bodies and in the workflow version responses returned by the API.

## Quick Start

The simplest useful workflow extracts structured data from a document:

```
trigger → parse → extract → review
```

```json
{
  "name": "Invoice Processing",
  "steps": [
    {
      "name": "trigger",
      "type": "TRIGGER",
      "next": [{ "step": "parse" }]
    },
    {
      "name": "parse",
      "type": "PARSE",
      "next": [{ "step": "extract" }]
    },
    {
      "name": "extract",
      "type": "EXTRACT",
      "config": {
        "extractor": { "id": "ex_abc123", "version": "latest" }
      },
      "next": [{ "step": "review" }]
    },
    {
      "name": "review",
      "type": "HUMAN_REVIEW"
    }
  ]
}
```

Every workflow starts with a `TRIGGER` step followed by a `PARSE` step. After parsing, you can chain any combination of processing, branching, and validation steps.

## Key Concepts

### Routing

Each step's `next` array defines where documents flow. For most step types, you only need to specify the target `step`:

```json
"next": [{ "step": "extract" }]
```

For branching step types, each `next` entry includes a routing field specific to the step type:

```json
// CLASSIFY or SPLIT — use classificationId
"next": [
  { "step": "extract_invoice", "classificationId": "cls_invoice" },
  { "step": "extract_receipt", "classificationId": "cls_receipt" }
]

// CONDITIONAL — use conditionId
"next": [
  { "step": "review", "conditionId": "high_value" },
  { "step": "webhook", "conditionId": "default_path" }
]

// VALIDATION or RULE_VALIDATION — use result
"next": [
  { "step": "webhook", "result": "pass" },
  { "step": "review", "result": "fail" }
]
```

For `ROUTER`, use `branchId` to select a configured branch or the implicit `"otherwise"` route:

```json
"next": [
  { "step": "review", "branchId": "high_value" },
  { "step": "webhook", "branchId": "otherwise" }
]
```

### Saved processors vs. inline configs

`EXTRACT`, `CLASSIFY`, and `SPLIT` steps specify their processor in one of two ways — provide exactly one per step:

* **Saved reference** — `extractor` / `classifier` / `splitter`: points at a saved processor by ID and version.

  ```json
  "config": { "extractor": { "id": "ex_abc123", "version": "latest" } }
  ```

* **Inline config** — `extractorConfig` / `classifierConfig` / `splitterConfig`: embeds the full processor configuration directly in the step, using the same config shape as the standalone run endpoints (e.g. [Create Extract Run](/api-reference/endpoints/extract/create-extract-run)). No saved processor is needed.

  ```json
  "config": {
    "extractorConfig": {
      "schema": {
        "type": "object",
        "properties": {
          "invoice_number": { "type": "string" },
          "total": { "type": "number" }
        }
      },
      "extractionRules": "Prefer the remit-to address."
    }
  }
  ```

Inline configs make a workflow definition **self-contained and portable**: it carries no workspace-specific processor IDs, so the same file validates and deploys against any workspace. This is especially useful for [GitHub-managed workflow files](/workflows/github-app-integration) and for provisioning workflows programmatically across environments.

A few things to know about inline configs:

* Inline extractor configs **require `schema`** — schema-less extraction (schema inferred from the file at run time) is a run-endpoint feature and is not supported in workflows.
* Inline configs have no `version`; the config embedded in the deployed workflow version is exactly what runs. Responses return the inline config verbatim.
* `CONDITIONAL_EXTRACT` rules only support saved references.

You can mix the two styles freely within one workflow — for example, an inline classifier routing to extract steps that reference saved extractors.

## Workflow Patterns

### Linear Extraction

The simplest pattern — every step has exactly one downstream step.

```
trigger → parse → extract → webhook
```

```json
[
  { "name": "trigger", "type": "TRIGGER", "next": [{ "step": "parse" }] },
  { "name": "parse", "type": "PARSE", "next": [{ "step": "extract" }] },
  {
    "name": "extract",
    "type": "EXTRACT",
    "config": { "extractor": { "id": "ex_abc123", "version": "latest" } },
    "next": [{ "step": "webhook" }]
  },
  { "name": "webhook", "type": "WEBHOOK_RESPONSE" }
]
```

### Classify and Route

Use a `CLASSIFY` step to route documents to different extractors based on document type. Each `next` entry's `classificationId` must match a classification **ID** from the classifier's config.

```
                         ┌─ cls_invoice ─→ extract_invoice ─┐
trigger → parse → classify─ cls_receipt ─→ extract_receipt ─┼→ review → webhook
                         └─ cls_other ──→───────────────────┘
```

First, your classifier defines classifications with stable IDs:

```typescript
const classifierConfig = {
  classifications: [
    { id: "cls_invoice", type: "invoice", description: "Invoice documents" },
    { id: "cls_receipt", type: "receipt", description: "Receipt documents" },
    { id: "cls_other", type: "other", description: "Other documents" },
  ],
};
```

Then the workflow step uses those IDs as `classificationId` values:

```json
[
  { "name": "trigger", "type": "TRIGGER", "next": [{ "step": "parse" }] },
  { "name": "parse", "type": "PARSE", "next": [{ "step": "classify" }] },
  {
    "name": "classify",
    "type": "CLASSIFY",
    "config": {
      "classifier": { "id": "cl_abc123", "version": "0.1" }
    },
    "next": [
      { "step": "extract_invoice", "classificationId": "cls_invoice" },
      { "step": "extract_receipt", "classificationId": "cls_receipt" },
      { "step": "review", "classificationId": "cls_other" }
    ]
  },
  {
    "name": "extract_invoice",
    "type": "EXTRACT",
    "config": { "extractor": { "id": "ex_invoice456", "version": "1.0" } },
    "next": [{ "step": "review" }]
  },
  {
    "name": "extract_receipt",
    "type": "EXTRACT",
    "config": { "extractor": { "id": "ex_receipt789", "version": "1.0" } },
    "next": [{ "step": "review" }]
  },
  { "name": "review", "type": "HUMAN_REVIEW", "next": [{ "step": "webhook" }] },
  { "name": "webhook", "type": "WEBHOOK_RESPONSE" }
]
```

> **Note**
>
> Conditions use classification **IDs** (e.g. `"cls_invoice"`), not type strings (e.g. `"invoice"`). IDs are stable across renames — if you rename a classification type from `"invoice"` to `"billing_invoice"`, the ID stays the same and routing continues to work.

### Split and Route

Use a `SPLIT` step to break a multi-document file into individual sub-documents and route each one by type. The same ID-based routing rules apply as for `CLASSIFY`.

```
                      ┌─ cls_invoice ─→ extract_invoice ─┐
trigger → parse → split─ cls_receipt ─→ extract_receipt ─┼→ collect → webhook
                      └─ cls_other ──→ review ───────────┘
```

```json
[
  { "name": "trigger", "type": "TRIGGER", "next": [{ "step": "parse" }] },
  { "name": "parse", "type": "PARSE", "next": [{ "step": "split" }] },
  {
    "name": "split",
    "type": "SPLIT",
    "config": {
      "splitter": { "id": "spl_abc123", "version": "0.1" }
    },
    "next": [
      { "step": "extract_invoice", "classificationId": "cls_invoice" },
      { "step": "extract_receipt", "classificationId": "cls_receipt" },
      { "step": "review", "classificationId": "cls_other" }
    ]
  },
  {
    "name": "extract_invoice",
    "type": "EXTRACT",
    "config": { "extractor": { "id": "ex_invoice456", "version": "1.0" } },
    "next": [{ "step": "collect" }]
  },
  {
    "name": "extract_receipt",
    "type": "EXTRACT",
    "config": { "extractor": { "id": "ex_receipt789", "version": "1.0" } },
    "next": [{ "step": "collect" }]
  },
  { "name": "review", "type": "HUMAN_REVIEW", "next": [{ "step": "collect" }] },
  { "name": "collect", "type": "COLLECT", "next": [{ "step": "webhook" }] },
  { "name": "webhook", "type": "WEBHOOK_RESPONSE" }
]
```

### Conditional Logic

Use a `CONDITIONAL` step to route based on extracted data values. Each condition has an `id` that is referenced by `next[].conditionId`.

```
trigger → parse → extract → route_total ─┬─ high_value ──→ review → webhook
                                         └─ default_path → webhook
```

```json
[
  { "name": "trigger", "type": "TRIGGER", "next": [{ "step": "parse" }] },
  { "name": "parse", "type": "PARSE", "next": [{ "step": "extract" }] },
  {
    "name": "extract",
    "type": "EXTRACT",
    "config": { "extractor": { "id": "ex_abc123", "version": "latest" } },
    "next": [{ "step": "route_total" }]
  },
  {
    "name": "route_total",
    "type": "CONDITIONAL",
    "config": {
      "conditions": [
        {
          "id": "high_value",
          "type": "IF",
          "operation": "GTE",
          "leftOperand": "{{ extract.output.value.total }}",
          "rightOperand": "10000"
        },
        { "id": "default_path", "type": "ELSE" }
      ]
    },
    "next": [
      { "step": "review", "conditionId": "high_value" },
      { "step": "webhook", "conditionId": "default_path" }
    ]
  },
  { "name": "review", "type": "HUMAN_REVIEW", "next": [{ "step": "webhook" }] },
  { "name": "webhook", "type": "WEBHOOK_RESPONSE" }
]
```

Reference an upstream step's output in `leftOperand` (and `rightOperand`, when comparing to another value) using the `{{ stepName.output.value.field }}` template syntax — `stepName` is the producing step's `name`, `.output.value` is the extractor's value payload, and `.field` is the field to compare. `operation` accepts `EQUALS`, `GTE`, `LTE`, `IS_NULL`, `CONTAINS`, or `NO_OP`. See [Conditional Steps](/workflows/workflow-steps/conditional-steps) for the full reference syntax.

### Validation

Use `VALIDATION` for code and semantic checks, then branch on whether every block passed. The workflow below uses an inline extractor, so it does not require a saved processor.

```
trigger → parse → extract → validate ─┬─ pass → webhook
                                      └─ fail → review → webhook
```

```json
[
  { "name": "trigger", "type": "TRIGGER", "next": [{ "step": "parse" }] },
  { "name": "parse", "type": "PARSE", "next": [{ "step": "extract_invoice" }] },
  {
    "name": "extract_invoice",
    "type": "EXTRACT",
    "config": {
      "extractorConfig": {
        "schema": {
          "type": "object",
          "properties": {
            "invoice_number": { "type": "string" },
            "vendor_name": { "type": "string" }
          }
        }
      }
    },
    "next": [{ "step": "validate_invoice" }]
  },
  {
    "name": "validate_invoice",
    "type": "VALIDATION",
    "config": {
      "blocks": [
        {
          "name": "Required invoice number",
          "blockType": "code",
          "code": {
            "language": "javascript",
            "code": "const number = context.outputs.extract_invoice.invoice_number.value; return [number != null && number !== '', 'Invoice number is required'];"
          }
        },
        {
          "name": "Plausible vendor",
          "blockType": "semantic",
          "semantic": {
            "promptText": "The vendor name {{ outputs.extract_invoice.vendor_name.value }} must be a plausible company name, not a placeholder."
          }
        }
      ]
    },
    "next": [
      { "step": "webhook", "result": "pass" },
      { "step": "review", "result": "fail" }
    ]
  },
  { "name": "review", "type": "HUMAN_REVIEW", "next": [{ "step": "webhook" }] },
  { "name": "webhook", "type": "WEBHOOK_RESPONSE" }
]
```

See [Validation Step](/workflows/workflow-steps/validation-step) for the API fields, context shapes, and Studio walkthrough. Existing formula-based steps use `RULE_VALIDATION`; see [Rule Validation](#rule-validation).

## Step Reference

All saved processor references (`extractor`, `classifier`, `splitter`) require an explicit `version` field. Inline configs (`extractorConfig`, `classifierConfig`, `splitterConfig`) have no version — see [Saved processors vs. inline configs](#saved-processors-vs-inline-configs).

> **Warning**
>
> `CLASSIFY` and `SPLIT` steps do not support `"latest"` — you must pin to a specific semver version (e.g. `"0.1"`) or `"draft"`. This is because classification IDs used for routing are tied to a specific version's config. If a new version is published with different classifications, routing would break silently.

| Step type             | `"latest"` | `"draft"` | Semver (e.g. `"1.0"`) | Inline config |
| --------------------- | ---------- | --------- | --------------------- | ------------- |
| `EXTRACT`             | Yes        | Yes       | Yes                   | Yes           |
| `CONDITIONAL_EXTRACT` | Yes        | Yes       | Yes                   | **No**        |
| `CLASSIFY`            | **No**     | Yes       | Yes                   | Yes           |
| `SPLIT`               | **No**     | Yes       | Yes                   | Yes           |

### Trigger

The single entry point for every workflow. Must route to exactly one `PARSE` step.

```json
{ "name": "trigger", "type": "TRIGGER", "next": [{ "step": "parse" }] }
```

### Parse

Converts the uploaded file into structured content (OCR, text extraction). Must appear immediately after the trigger.

Optionally configure parsing behavior with `parseConfig`. See [Parse Configuration Options](/parsing/configuration).

```json
{
  "name": "parse",
  "type": "PARSE",
  "config": {
    "parseConfig": {
      "target": "markdown",
      "chunkingStrategy": { "type": "page" }
    }
  },
  "next": [{ "step": "extract" }]
}
```

### Extract

Extracts structured data from parsed content. Specify the extractor with a saved reference (`extractor` — version required: `"latest"`, `"draft"`, or semver) or an inline config (`extractorConfig`). Can be created without `config` — `next` cannot be set until `config` is provided, and `config` is required before deploy.

```json
{
  "name": "extract",
  "type": "EXTRACT",
  "config": {
    "extractor": { "id": "ex_abc123", "version": "latest" }
  },
  "next": [{ "step": "review" }]
}
```

Or with an inline config — note that `schema` is required for inline extractor configs:

```json
{
  "name": "extract",
  "type": "EXTRACT",
  "config": {
    "extractorConfig": {
      "schema": {
        "type": "object",
        "properties": {
          "invoice_number": { "type": "string" },
          "total": { "type": "number" }
        }
      }
    }
  },
  "next": [{ "step": "review" }]
}
```

### Classify

Routes documents to different downstream steps based on classification. Conditions must reference classification **IDs**, not type strings. Specify the classifier with a saved reference (`classifier` — requires a pinned version; `"latest"` is not allowed) or an inline config (`classifierConfig`). Can be created without `config` — `next` cannot be set until `config` is provided, and `config` is required before deploy.

See the [Classify and Route pattern](#classify-and-route) above for a complete example with a saved reference. With an inline config, the `next[].classificationId` values must match the `id` values in the inline `classifications` array:

```json
{
  "name": "classify",
  "type": "CLASSIFY",
  "config": {
    "classifierConfig": {
      "classifications": [
        { "id": "cls_invoice", "type": "invoice", "description": "Invoice documents" },
        { "id": "cls_other", "type": "other", "description": "Anything else" }
      ]
    }
  },
  "next": [
    { "step": "extract_invoice", "classificationId": "cls_invoice" },
    { "step": "review", "classificationId": "cls_other" }
  ]
}
```

### Split

Splits a multi-document file into sub-documents and routes each one. Same ID-based routing rules as `CLASSIFY`. Specify the splitter with a saved reference (`splitter` — requires a pinned version; `"latest"` is not allowed) or an inline config (`splitterConfig`, with routing IDs coming from its `splitClassifications` array). Can be created without `config` — `next` cannot be set until `config` is provided, and `config` is required before deploy.

See the [Split and Route pattern](#split-and-route) above for a complete example.

### Merge Extract

Combines outputs from multiple upstream extract steps. Use `mergeOrder` to control how overlapping fields are prioritized.

```json
{
  "name": "merge",
  "type": "MERGE_EXTRACT",
  "config": { "mergeOrder": "confidence" },
  "next": [{ "step": "webhook" }]
}
```

### Conditional

Routes based on extracted data values using if/else logic. See the [Conditional Logic pattern](#conditional-logic) above.

For the UI-based version of this step, see [Conditional Steps](/workflows/workflow-steps/conditional-steps).

### Conditional Extract

Chooses which extractor to run based on formula conditions. Each rule pairs a formula with an extractor reference — rules only support saved references, not inline configs. The last rule must have `formula: "TRUE"` as a default catch-all to prevent runtime failures when no other rule matches. Can be created without `config` — `next` cannot be set until `config` is provided, and `config` is required before deploy.

```json
{
  "name": "route_extractor",
  "type": "CONDITIONAL_EXTRACT",
  "config": {
    "rules": [
      {
        "name": "cigna_provider",
        "formula": "metadata.provider_name = \"cigna\"",
        "extractor": { "id": "ex_cigna", "version": "latest" }
      },
      {
        "name": "fallback",
        "formula": "TRUE",
        "extractor": { "id": "ex_generic", "version": "latest" }
      }
    ]
  },
  "next": [{ "step": "validate" }]
}
```

See [Formulas](/workflows/formulas) for the expression language and [Conditional Extraction Step](/workflows/workflow-steps/conditional-extraction-step) for the UI guide.

### Rule Validation

The legacy formula-based validation step. For new code or semantic checks, use [Validation Step](#validation-step).

`RULE_VALIDATION` checks extracted data against boolean formulas in `config.rules`. Each rule requires a `name` and non-empty `formula`, and can include a `description`. Configure from 1 to 100 rules and route with `next[].result: "pass"` or `"fail"`. It can be created without `config`; `next` cannot be set until `config` is provided, and `config` is required before deploy. See [Formulas](/workflows/formulas) for the expression language.

### Validation Step

`VALIDATION` evaluates code or semantic blocks. Every block must pass for the `pass` route; an ordinary invalid result selects `fail`, while an execution error fails the step.

| Field                | Contract                                                                                                                                                        |
| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `config.blocks`      | Required when `config` is provided. An ordered array of up to 100 blocks; an empty array passes every run.                                                      |
| `blocks[].id`        | Optional on input; generated when omitted and included in responses.                                                                                            |
| `blocks[].name`      | Required block name.                                                                                                                                            |
| `blocks[].blockType` | `"code"` or `"semantic"`.                                                                                                                                       |
| `blocks[].code`      | Required for code blocks. Contains `language` and a non-empty `code` string. Only `"javascript"` executes; `"python"` configurations return an execution error. |
| `blocks[].semantic`  | Required for semantic blocks. Contains a non-empty `promptText` string.                                                                                         |
| `next[].result`      | `"pass"` or `"fail"`; `next[].step` names the downstream step.                                                                                                  |

Code accesses `context.outputs.<step>` and `context.metadata`. Semantic text inserts upstream values with `{{ outputs.<step>.<field>.value }}`. Use public step names in both. After a Collect step, outputs may be arrays; see the [Validation context guide](/workflows/workflow-steps/validation-step#code-blocks).

Keep the step name and omit `config` when updating to preserve existing blocks. Providing `config.blocks` replaces the list. Workflow updates replace the complete `steps` array, so include the other steps and their connections. See the [Validation pattern](#validation) for a full definition and [Validation Step](/workflows/workflow-steps/validation-step#via-the-api) for the save, deploy, and result-reading flow.

### Router

`ROUTER` evaluates ordered code or semantic branch predicates and routes through the first match. Later predicates are not evaluated. No match selects the implicit `"otherwise"` route; an execution error fails the step.

| Field              | Contract                                                                                                                   |
| ------------------ | -------------------------------------------------------------------------------------------------------------------------- |
| `config.branches`  | Required when `config` is provided. From 1 to 100 branches, in evaluation order.                                           |
| `branches[].id`    | Required non-empty ID, unique within the step. `"otherwise"` is reserved and cannot be a branch ID.                        |
| `branches[].name`  | Optional on input, up to 255 characters; populated in responses.                                                           |
| `branches[].block` | Optional on input. A code or semantic block using the [Validation block fields](#validation-step); populated in responses. |
| `next[].branchId`  | A configured branch ID, or `"otherwise"` for fallback.                                                                     |
| `next[].step`      | A downstream step in the same workflow.                                                                                    |

Predicates return a boolean or `[boolean, "reason"]`; code must not return a route name. Semantic predicates express a yes-or-no criterion. If the selected route has no connected next step, processing stops at the Router.

Keep the step name and omit `config` on update to preserve existing branches. Providing `config.branches` replaces the branch list and its order. Keep IDs stable when renaming or reordering branches. Omitting a branch's `name` or `block` preserves those values for an existing branch; a new branch without `block` receives a default JavaScript predicate that returns `true`. Supply predicates for new branches explicitly.

See [Router Step](/workflows/workflow-steps/router-step#via-the-api) for a complete step definition, the deployment flow, and `matchedBranch` and per-branch results.

### External Data Validation

Sends extraction data to an external HTTP endpoint for validation. Can be created without `config` — `next` cannot be set until `config` is provided, and `config` is required before deploy.

```json
{
  "name": "external_validate",
  "type": "EXTERNAL_DATA_VALIDATION",
  "config": {
    "requestOptions": {
      "url": "https://api.example.com/validate",
      "method": "POST",
      "headers": { "x-api-key": "secret" },
      "contentType": "application/json"
    },
    "failureBehavior": "EXIT"
  },
  "next": [{ "step": "review" }]
}
```

See [External Data Validation Step](/workflows/workflow-steps/external-data-validation-step) for more context.

### Human Review

Pauses the workflow for human-in-the-loop review in the dashboard before continuing to downstream steps. The run enters a review queue where operators inspect, correct, approve, or reject the output. See [Human Review](/workflows/human-review) for routing patterns and how to consume reviewed output from code.

```json
{ "name": "review", "type": "HUMAN_REVIEW", "next": [{ "step": "webhook" }] }
```

### Collect

Joins multiple upstream branches before continuing. Use after `CLASSIFY` or `SPLIT` branches to wait for all parallel work to complete.

```json
{ "name": "collect", "type": "COLLECT", "next": [{ "step": "webhook" }] }
```

A Collect step is also what makes a package run extract across every file at once. See [Multifile Extraction in Workflows](/workflows/multifile-extraction).

### File Conversion

Converts the file format before downstream processing. Use `failureBehavior` to control whether conversion failures stop the workflow.

```json
{
  "name": "convert",
  "type": "FILE_CONVERSION",
  "config": { "failureBehavior": "CONTINUE" },
  "next": [{ "step": "parse" }]
}
```

### Webhook Response

Terminal step that delivers results to your webhook endpoint. Must not have `next`.

```json
{ "name": "webhook", "type": "WEBHOOK_RESPONSE" }
```

## Next steps

#### [Create a Workflow](/workflows/overview)

The end-to-end create → deploy → run lifecycle.

#### [Workflow Versioning](/workflows/workflow-versioning)

Deploy, pin, and promote workflow versions.

#### [Reviewing Workflow Runs](/workflows/reviewing-workflow-run)

Handle steps that pause for human review.

#### [Create Workflow Version API](/api-reference/endpoints/workflow/create-workflow-version)

Full request and response schema.