> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.runtype.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.runtype.com/_mcp/server.

# Manage flows as code

Keep Flow definitions in your repository and review them with your code. In your deploy pipeline, run `flows.ensure()` to create or update each saved Flow so that it matches the definition.

## Choose between `ensure` and `upsert`

Use these two code-first Flow methods:

* `flows.upsert()` saves and runs a Flow in one dispatch. It creates or updates the Flow by name, then executes it against a record in the same request. Use it in your application when the Flow definition lives beside the code that runs it.
* `flows.ensure()` converges the saved Flow definition without executing it. It creates, updates, or leaves the Flow unchanged. Use it in continuous integration and continuous delivery (CI/CD) jobs.

`flows.upsert()` is not deprecated or an older version of `flows.ensure()`. Run a Flow with `flows.upsert()` or by dispatching it by ID. Converge the saved definition with `flows.ensure()`.

## What `ensure` does and does not manage

`ensure` converges the Flow's `name` and `steps`, and validates them the same way as creating a Flow. When the definition declares inline `evals`, `ensure` also converges each eval suite after the Flow converges. The result contains the suite outcomes in `result.evals`.

`ensure` does not manage the Flow's description, schedules, Surfaces, Records, or executions. It does not delete or rename Flows.

## Before you begin

Before you converge a Flow, set up the following:

* Install the TypeScript SDK with `npm install @runtypelabs/sdk`.
* Create an API key for the environment that you deploy to. The API key's organization or personal scope determines which account `ensure` writes to, and the API endpoint selects the environment.
* Give the API key the **Flows (Write)** permission (`FLOWS:WRITE`) for `ensure`. Add **Flows (Read)** (`FLOWS:READ`) to use `pull`, or select **Flows (Full)** (`FLOWS:*`) for both. For more information, see [Managing API keys](/user-guide/settings/managing-api-keys).
* Create any saved Tools, Agents, and Flows that the definition references by name in the target environment.

## Define a Flow

`defineFlow` builds a definition locally without network requests. It checks the definition shape and returns the definition object that `ensure` sends.

Keep the definition portable across environments. `defineFlow` rejects account-scoped resource references that use `tool_`, `agent_`, or `flow_` prefixes. Use portable `builtin:`, `platform:`, or `mcp:` tool references, or reference saved resources by name with `tool:TOOL_NAME`, `agent:AGENT_NAME`, or `flow:FLOW_NAME`. Replace each placeholder with the exact saved resource name.

Define a Flow with a name and ordered steps:

**`TypeScript SDK`**

```typescript title="TypeScript SDK"
import { defineFlow } from '@runtypelabs/sdk'

export const onboardingDigest = defineFlow({
  name: 'Onboarding Digest',
  steps: [
    {
      type: 'prompt',
      name: 'Summarize signups',
      config: {
        model: 'claude-sonnet-5',
        userPrompt: "Summarize this week's signups: {{signups}}",
        outputVariable: 'summary',
      },
    },
    {
      type: 'send-email',
      name: 'Send digest',
      config: {
        to: 'dana@example.com',
        subject: 'Weekly digest',
        text: '{{summary}}',
        outputVariable: 'digest_email',
      },
    },
  ],
})
```

The definition includes a `name`, a `steps` array, and an optional `evals` array of inline eval suites. `ensure` converges inline eval suites with the Flow. For more information, see [Colocate evals with a flow](/user-guide/evals/manage-evals-as-code#colocate-evals-with-a-flow).

Each step accepts only the following fields:

* `type`: the step type.
* `name`: the step's stable identity across converges.
* `order`: the step position. It defaults to the step's 1-based position in the array.
* `enabled`: whether the step runs. It defaults to `true`.
* `when`: a condition that skips the step when it is false. For more information, see [Skip a single step with when](/user-guide/flows/using-conditional-steps#skip-a-single-step-with-when).
* `config`: the step type's settings.

`defineFlow` throws an error for any other field, including a step `id` or a top-level `description`. Runtype matches steps by name. If you rename a step, the change appears as a removed step and an added step, and the step's previous results don't carry over to the renamed step.

> **Warning**
>
> Renaming a definition does not rename the Flow. `ensure` creates a Flow under the renamed name and
> leaves the old Flow in place. It never deletes a Flow. Treat the name as the stable identity. If
> several Flows share a name, `ensure` and `pull` act on the oldest one. Rename or delete the
> duplicates before you manage the Flow as code.

## Reference saved Tools by name

Built-in, platform, and MCP Tools have portable IDs that you can hardcode. Saved Tools receive an ID for each account and environment. These IDs start with `tool_` and cannot appear in a portable definition. Reference a saved Tool by its exact name. Use the `tool:TOOL_NAME` form in a prompt or transform-data step's `tools.toolIds`, and in a `tool-call` step's `toolId`.

Define a Flow that references a saved Tool by name:

**`TypeScript SDK`**

```typescript title="TypeScript SDK"
export const scrapeAndSummarize = defineFlow({
  name: 'Scrape and summarize',
  steps: [
    {
      type: 'tool-call',
      name: 'Scrape',
      config: {
        toolId: 'tool:My Scraper',
        parameters: { url: '{{target}}' },
        outputVariable: 'page',
      },
    },
    {
      type: 'prompt',
      name: 'Summarize',
      config: {
        model: 'claude-sonnet-5',
        userPrompt: 'Summarize: {{page}}',
      },
    },
  ],
})
```

The text after `tool:` must match the Tool's exact name. Runtype resolves the name to the saved Tool during each dispatch. If you rename the Tool, update the reference in the definition. `ensure`, including `dryRun`, validates each reference during convergence. A name that matches no saved Tool fails validation. A name that matches multiple Tools fails validation and identifies the matches.

When you use `pull` on a dashboard-built Flow, Runtype converts saved Tool IDs to `tool:TOOL_NAME` when possible. If the Tool was deleted or its name conflicts, `pull` leaves the raw ID and adds an entry to `warnings`.

## Reference saved Agents and Flows by name

Use name references when a Flow points to another saved resource. Use these forms:

* `agent:AGENT_NAME`: Use this form for an `execute-agent` step's `agentId` or a saved-subagent runtime Tool's `config.agentId`.
* `flow:FLOW_NAME`: Use this form for a flow-as-tool runtime Tool's `config.flowId`.

Define a Flow that references a saved Agent and Flow by name:

**`TypeScript SDK`**

```typescript title="TypeScript SDK"
export const researchPipeline = defineFlow({
  name: 'Research pipeline',
  steps: [
    {
      type: 'execute-agent',
      name: 'Research',
      config: {
        agentId: 'agent:Researcher',
        message: 'Research: {{topic}}',
      },
    },
    {
      type: 'prompt',
      name: 'Enrich and write',
      config: {
        model: 'claude-sonnet-5',
        userPrompt: 'Write it up: {{agent_response}}',
        tools: {
          runtimeTools: [
            {
              toolType: 'flow',
              name: 'enrich',
              description: 'Enrich a record',
              parametersSchema: {},
              config: { flowId: 'flow:Enrich Lead' },
            },
          ],
        },
      },
    },
  ],
})
```

Runtype resolves these names during each dispatch. `ensure`, including `dryRun`, rejects a name that matches no saved Agent or Flow or matches more than one. When you use `pull`, Runtype converts raw IDs to name forms when possible and adds a `warnings` entry for each reference that it cannot convert.

## Converge at deploy time

Configure the SDK and converge the Flow definition:

**`TypeScript SDK`**

```typescript title="TypeScript SDK"
import { Runtype } from '@runtypelabs/sdk'

Runtype.configure({ apiKey: process.env.RUNTYPE_API_KEY })

const result = await Runtype.flows.ensure(onboardingDigest)
```

The result contains `result`, `flowId`, `versionId`, and `contentHash`. `result` is `unchanged`, `created`, or `updated`. `versionId` can be `null`. When the definition declares inline eval suites, the result also contains `evals` with one outcome for each suite.

`ensure` first checks whether the saved Flow already matches, and sends the full definition only when needed. When the definition changes, `ensure` writes the saved Flow and adds a version to version history. Runtype never changes existing versions, so an overwritten dashboard edit remains available in version history.

### Publish a version

By default, `ensure` writes the saved Flow and a draft version. To make the version that `ensure` creates the published version, pass `release: 'publish'`:

**`TypeScript SDK`**

```typescript title="TypeScript SDK"
await Runtype.flows.ensure(onboardingDigest, { release: 'publish' })
```

If the content is unchanged but the current draft isn't published, `release: 'publish'` publishes it and the result is still `unchanged`. You can run `ensure` once to write a draft, then run it again with `release: 'publish'` to publish the same content.

### Label the version

Pass an optional `version` object to record which commit produced the version that `ensure` adds to version history:

**`TypeScript SDK`**

```typescript title="TypeScript SDK"
await Runtype.flows.ensure(onboardingDigest, {
  version: { label: process.env.GIT_SHA, notes: 'Promoted from CI run 4821.' },
})
```

Both fields are optional. `label` holds up to 100 characters and `notes` up to 2,000. A typical `label` is a git SHA or release tag. Omit `version` to keep the default `sdk-ensure` label and the generated notes.

## Detect drift in CI

Use a dry run to return a plan without writing:

**`TypeScript SDK`**

```typescript title="TypeScript SDK"
const plan = await Runtype.flows.ensure(onboardingDigest, { dryRun: true })
```

The plan contains `result: 'plan'`, a `changes` value of `none`, `create`, or `update`, and the `contentHash`. When a remote Flow exists, it also contains `remoteHash`. `changedKeys` lists step changes such as `steps.added.STEP_NAME`, `steps.removed.STEP_NAME`, and `steps.modified.STEP_NAME`. Replace `STEP_NAME` with the affected step name.

A dry run plans the Flow only. It doesn't plan or converge inline eval suites. To check eval suites for drift, see [Manage evals as code](/user-guide/evals/manage-evals-as-code).

Use `expectNoChanges` to fail the check when the plan reports a change:

**`TypeScript SDK`**

```typescript title="TypeScript SDK"
import { FlowDriftError } from '@runtypelabs/sdk'

try {
  await Runtype.flows.ensure(onboardingDigest, { expectNoChanges: true })
} catch (error) {
  if (error instanceof FlowDriftError) {
    console.error(error.plan.changedKeys)
    process.exit(1)
  }
  throw error
}
```

`expectNoChanges` runs a dry run and throws a `FlowDriftError` when the plan is not `none`. It never writes. The error's `plan` property contains the plan.

To apply the definition only when the remote state matches the dry run, pass the plan's `remoteHash` as `expectedRemoteHash`:

**`TypeScript SDK`**

```typescript title="TypeScript SDK"
await Runtype.flows.ensure(onboardingDigest, { expectedRemoteHash: plan.remoteHash })
```

## Handle dashboard edits

Runtype records whether each Flow write came from code or from somewhere else. After a dashboard edit or API write changes a Flow that code manages, the next `ensure` fails with a `409` conflict by default. The SDK raises a `FlowEnsureConflictError` with `code: 'external_modification'`.

The dashboard shows a **Managed in code** badge for a Flow that a code definition last converged. The badge appears in the Flow editor header and on the **Flows** list. When you click **Save**, click **Publish** with unsaved changes, or publish a version from **Version History**, the dashboard opens a confirmation dialog. Click **Save anyway** to continue.

Saving from the dashboard marks the Flow as edited outside code, so the next `ensure` detects the change and fails with a conflict. Publishing from the editor header with no unsaved edits only promotes the existing draft and does not open the dialog. Publishing a version from **Version History** always writes that version's definition to the Flow, so it always asks first. To make a lasting change, update the Flow definition in your code.

Choose one of these approaches:

* **Repository wins**: Run `ensure` again with `onConflict: 'overwrite'`. The saved Flow changes, and the dashboard edit remains in version history.
* **Dashboard wins**: Run `pull`, review the definition as a `git` diff, then commit or revert the change.

Use this call to pull the saved definition and where its last change came from:

**`TypeScript SDK`**

```typescript title="TypeScript SDK"
const { definition, contentHash, lastModifiedSource, updatedAt } =
  await Runtype.flows.pull('Onboarding Digest')
```

## Bring an existing Flow under code management

To manage a Flow that you built in the dashboard, follow these steps:

1. Run `Runtype.flows.pull('FLOW_NAME')`. Replace `FLOW_NAME` with the Flow's exact name.
2. Commit the returned `definition` in a `defineFlow` call.
3. Run `ensure`. When the definition matches the saved Flow, the result is `unchanged` and the dashboard shows the **Managed in code** badge.

If the Flow references saved Tools, Agents, or Flows, `pull` converts their IDs to names. The converted definition differs from the saved Flow, so the first `ensure` of a dashboard-edited Flow returns a `409` conflict. Run that first `ensure` with `onConflict: 'overwrite'`.

To start from a generated snippet instead, open the Flow in the dashboard, click **Launch flow**, and select the **Ensure Flow** example. The example contains a `defineFlow` definition of the open Flow and a `flows.ensure` call. The snippet keeps the IDs of saved Tools, Agents, and Flows, which `defineFlow` rejects. If the Flow references saved resources, use `pull` instead.

## Troubleshoot `ensure` errors

The following errors are the most common:

* `defineFlow` throws before any request: the definition contains an unknown field, such as `description` or a step `id`, or an account-scoped ID. Remove the field or use a name reference.
* `400`: a step config is invalid, such as an unknown model or a missing required field. The response lists the validation errors. For more information, see [Flow validation warnings](/user-guide/flows/flow-validation-warnings).
* `403`: the API key is missing a permission. `ensure` needs `FLOWS:WRITE` to create or change a Flow. `pull` needs `FLOWS:READ`.
* `404` from `pull`: no Flow has that name in the API key's scope.
* `409`: the Flow was edited outside code (`external_modification`), or the Flow changed after your dry run (`remote_changed`). For more information, see [Handle dashboard edits](#handle-dashboard-edits).

## Call the ensure endpoint without the SDK

Use `POST /v1/flows/ensure` when you call the Runtype API without the TypeScript SDK. The request body accepts the following fields:

* `name`: the Flow name. Required.
* `definition`: the full definition with `name` and `steps`. `definition.name` must equal `name`.
* `contentHash`: a lowercase, 64-character hexadecimal SHA-256 hash of the Flow's `steps`.
* `dryRun`: `true` to return a plan without writing.
* `onConflict`: `error` or `overwrite`.
* `release`: `none` or `publish`.
* `expectedRemoteHash`: the `remoteHash` from a dry run.
* `version`: an object with optional `label` and `notes`.

Each request needs `definition`, `contentHash`, or both. Send the full definition to converge a Flow:

**`cURL`**

```bash title="cURL"
curl -X POST https://api.runtype.com/v1/flows/ensure \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Onboarding Digest",
    "definition": {
      "name": "Onboarding Digest",
      "steps": [
        {
          "type": "prompt",
          "name": "Summarize signups",
          "config": {
            "model": "claude-sonnet-5",
            "userPrompt": "Summarize this week'\''s signups: {{signups}}",
            "outputVariable": "summary"
          }
        }
      ]
    },
    "version": { "label": "a1b2c3d" }
  }'
```

Replace `YOUR_API_KEY` with your Runtype API key.

To skip sending an unchanged definition, follow this sequence:

1. Send a probe with `{ "name": "Onboarding Digest", "contentHash": "YOUR_CONTENT_HASH" }`. Replace `YOUR_CONTENT_HASH` with the `contentHash` from the last response.
2. If the response is `{ "result": "definitionRequired" }`, resend the request with the full `definition` and omit `contentHash`. A matching hash returns `{ "result": "unchanged" }` with `200`.
3. Store the `contentHash` from each response for later probes. The server computes the hash over each submitted definition.
4. Handle `409` conflicts for `external_modification` and `remote_changed`. A full request with a `contentHash` that differs from the server's hash returns `422` with `content_hash_mismatch`. Omit `contentHash` from full requests to avoid this error.

The server trims `version.label` and `version.notes` and treats a blank value as omitted. For the length limits, see [Label the version](#label-the-version).

`GET /v1/flows/pull?name=Onboarding%20Digest` returns the saved definition, `contentHash`, `lastModifiedSource`, `updatedAt`, and `versionId`.

## Next steps

Continue with these guides:

* [Manage agents as code](/user-guide/agents/manage-agents-as-code): apply the same protocol to Agent definitions.
* [Manage tools as code](/user-guide/tools/manage-tools-as-code): converge Tool definitions from your repository.
* [Manage evals as code](/user-guide/evals/manage-evals-as-code): converge eval suites and run them as a CI gate.
* [Flow versioning and publishing](/user-guide/flows/flow-versioning-and-publishing): manage draft and published versions.
* [Creating and editing flows](/user-guide/flows/creating-and-editing-flows): edit Flows in the dashboard.
* [Run flows and agents with dispatch](/developer-guides/guides/dispatch): run a Flow or Agent through the dispatch API, with streaming and async execution.