Manage Flows as code
Keep your Flow definitions in your repository — reviewed, versioned, and rendered from the same data your product uses — and have the platform converge to them at deploy time. This is the Flow counterpart of managing Agents as code: the same ensure protocol, applied to Flows.
Ensure and upsert are siblings, not versions of each other
The SDK has two code-first Flow verbs, and they answer different questions:
flows.upsert()is the runtime verb: save and run in one dispatch. It creates or updates the Flow by name, then executes it against a record in the same request. Use it inside your application when the Flow definition lives next to the code that runs it.flows.ensure()is the deploy verb: converge only. It makes the platform’s saved definition match your repo’s — creating, updating, or doing nothing — and never executes anything. Use it from CI/CD, the same place you deploy the rest of your application.
upsert is not deprecated and is not an older version of ensure. If you want to run the Flow, use upsert (or dispatch by ID); if you want the deploy to make the platform match the repo, use ensure.
Define the Flow
defineFlow is a pure, local constructor. It validates the definition shape and returns the canonical definition object. Because the same definition converges every environment, it must be environment-portable: account-scoped tool IDs (tool_…) are rejected — use portable builtin:, platform:, or mcp: tool references, or reference a saved tool by name with tool:<name> (see Reference saved tools by name) — and steps carry no server-minted ids.
The definition surface is the Flow’s name and steps. The Flow’s identity is its name plus your account scope (organization, or personal); which environment you converge is decided by the credentials you run with.
Renaming a definition does not rename the Flow. ensure will create a new Flow under the new
name and leave the old one in place (it never deletes). Treat the name as the stable identity.
Reference saved tools by name
Built-in, platform, and MCP tools have portable IDs you can hardcode. Your saved tools (external HTTP tools, custom code tools, flow tools) get a per-account, per-environment ID (tool_…) that can’t appear in a portable definition. Reference them by name with the tool:<name> form — in a prompt or transform-data step’s tools.toolIds, and in a tool-call step’s toolId:
The text after tool: is the tool’s exact name. The name is canonical: it’s what gets hashed (so the same definition converges to the same content hash everywhere) and it resolves to the actual tool at run time, every dispatch. ensure validates each reference when you converge (including dryRun): a name that matches no saved tool fails so the gap surfaces in CI, and a name that matches more than one fails and names both so you can rename one. When you pull a dashboard-built Flow, saved-tool IDs are reverse-mapped to tool:<name>; references that can’t be emitted by name (deleted, or name-shadowed) are left as raw IDs with an entry in a warnings array.
Reference saved agents and flows by name
The same per-account, per-environment ID problem applies wherever a Flow points at another saved resource. Use the matching name form:
agent:<name>— anexecute-agentstep’sagentId, and a saved-subagent runtime tool’sconfig.agentId.flow:<name>— a flow-as-tool runtime tool’sconfig.flowId.
These behave exactly like tool:<name>: the name is canonical and hashed, resolution happens at run time every dispatch, and ensure (including dryRun) fails on a name that matches no agent/flow or more than one in the target account. Raw agent_… / flow_… IDs stay rejected on the ensure surface, and pull reverse-maps them to the name forms (leaving anything that can’t round-trip as a raw ID with a warnings entry).
Converge at deploy time
ensure is hash-first: in the steady state (nothing changed) it sends one tiny probe request carrying only the name and a content hash, and writes nothing. When the content differs, it writes the saved Flow and appends an immutable version snapshot — history is never mutated, and a dashboard edit you overwrite is still recoverable from version history.
The content hash covers the Flow’s steps and is the same hash the upsert protocol uses, so ensure and upsert interoperate on the same Flow. The contentHash in every response is computed by the server over its canonical, normalized form; the SDK echoes the server’s hash on later probes.
Publishing
By default ensure writes the saved Flow and a draft version. To also re-aim the published-version pointer at the version it just created:
Detect drift in CI
changedKeys names the steps that would change (steps.added.<name>, steps.removed.<name>, steps.modified.<name>). For a one-line PR gate, expectNoChanges throws when the plan is anything but none:
To make the apply step act only on the exact state the dry run saw, bind them with expectedRemoteHash:
Conflicts with dashboard edits
The platform records where each Flow write came from. If someone edits an ensure-managed Flow in the dashboard (or via the API), the next ensure fails with a 409 conflict by default, surfaced in the SDK as a FlowEnsureConflictError with code: 'external_modification'.
You have two remediation directions:
- Repo wins. Re-run with
onConflict: 'overwrite'. The dashboard edit is overwritten in the saved Flow but preserved in version history. - Dashboard wins. Absorb the edit into your repo with
pull, review it as a git diff, and commit or revert:
What ensure does and does not manage
ensure converges the Flow definition: its name and step list (validated through the same canonical validator as Flow creation). It does not manage the Flow’s description, schedules, surfaces, records, or executions, and it never deletes or renames Flows.
Wire protocol (direct API use)
If you are not using the TypeScript SDK, the protocol is one endpoint: POST /v1/flows/ensure.
- Probe with
{ "name": "...", "contentHash": "<sha-256>" }. A match returns{ "result": "unchanged" }; a miss returns a normal 200{ "result": "definitionRequired" }— retry with the fulldefinitionincluded. - The server recomputes the canonical hash over any submitted definition and returns it on every response. Echo the server’s hash in later probes.
- Conflicts are 409s (
external_modification,remote_changed); a submittedcontentHashthat disagrees with the server’s recompute over the definition is a 422 (content_hash_mismatch). OmitcontentHashon full requests to avoid it entirely.
GET /v1/flows/pull?name=... returns the canonical definition plus provenance (contentHash, lastModifiedSource, updatedAt, versionId).
Next steps
- Manage Agents as code — the same protocol for Agent definitions
- Manage tools as code — the same protocol for Tool definitions
- Flow versioning and publishing — how version snapshots and the published pointer work
- Creating and editing flows — the dashboard editing surface