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
ensurewrites to, and the API endpoint selects the environment. - Give the API key the Flows (Write) permission (
FLOWS:WRITE) forensure. Add Flows (Read) (FLOWS:READ) to usepull, or select Flows (Full) (FLOWS:*) for both. For more information, see 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:
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.
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 totrue.when: a condition that skips the step when it is false. For more information, see 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.
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:
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 anexecute-agentstep’sagentIdor a saved-subagent runtime Tool’sconfig.agentId.flow:FLOW_NAME: Use this form for a flow-as-tool runtime Tool’sconfig.flowId.
Define a Flow that references a saved Agent and Flow by name:
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:
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':
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:
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:
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.
Use expectNoChanges to fail the check when the plan reports a change:
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:
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
ensureagain withonConflict: 'overwrite'. The saved Flow changes, and the dashboard edit remains in version history. - Dashboard wins: Run
pull, review the definition as agitdiff, then commit or revert the change.
Use this call to pull the saved definition and where its last change came from:
Bring an existing Flow under code management
To manage a Flow that you built in the dashboard, follow these steps:
- Run
Runtype.flows.pull('FLOW_NAME'). ReplaceFLOW_NAMEwith the Flow’s exact name. - Commit the returned
definitionin adefineFlowcall. - Run
ensure. When the definition matches the saved Flow, the result isunchangedand 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:
defineFlowthrows before any request: the definition contains an unknown field, such asdescriptionor a stepid, 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.403: the API key is missing a permission.ensureneedsFLOWS:WRITEto create or change a Flow.pullneedsFLOWS:READ.404frompull: 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.
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 withnameandsteps.definition.namemust equalname.contentHash: a lowercase, 64-character hexadecimal SHA-256 hash of the Flow’ssteps.dryRun:trueto return a plan without writing.onConflict:errororoverwrite.release:noneorpublish.expectedRemoteHash: theremoteHashfrom a dry run.version: an object with optionallabelandnotes.
Each request needs definition, contentHash, or both. Send the full definition to converge a Flow:
Replace YOUR_API_KEY with your Runtype API key.
To skip sending an unchanged definition, follow this sequence:
- Send a probe with
{ "name": "Onboarding Digest", "contentHash": "YOUR_CONTENT_HASH" }. ReplaceYOUR_CONTENT_HASHwith thecontentHashfrom the last response. - If the response is
{ "result": "definitionRequired" }, resend the request with the fulldefinitionand omitcontentHash. A matching hash returns{ "result": "unchanged" }with200. - Store the
contentHashfrom each response for later probes. The server computes the hash over each submitted definition. - Handle
409conflicts forexternal_modificationandremote_changed. A full request with acontentHashthat differs from the server’s hash returns422withcontent_hash_mismatch. OmitcontentHashfrom 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.
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: apply the same protocol to Agent definitions.
- Manage tools as code: converge Tool definitions from your repository.
- Manage evals as code: converge eval suites and run them as a CI gate.
- Flow versioning and publishing: manage draft and published versions.
- Creating and editing flows: edit Flows in the dashboard.
- Run flows and agents with dispatch: run a Flow or Agent through the dispatch API, with streaming and async execution.