Use flow_patch's 11 operations and a compare-and-swap file check to restructure a flow safely, and handle a stale-file conflict correctly.
When you need this
Section titled “When you need this”A flow’s files on disk are the actual definition — the same bytes the compiler and the runtime read.
Editing a prompt’s text, or one field’s value, is just editing that file directly, the same way you’d
edit any other file in the project. flow_patch is for the other kind of change: one that touches more
than one file, renames something other files reference, or needs to succeed completely or not at all.
Reach for it whenever you’re adding, removing, renaming, or moving a node, rewiring which output feeds
which input, or renaming the flow or an agent — not by hand-editing several files and hoping they stay
consistent with each other.
- Every
flow_patchcall names aflow_id, anopslist (1 to 50 operations, applied atomically — all of them land or none of them are written to disk), aclient_op_idyou generate, andexpects— the compare-and-swap check described below. An optionalintentstring is a free-text label for the change, useful for anything else watching the project (a person with Studio open, another agent) that sees the files change and wants to know why. - The 11 real operations, by
op:add_node,remove_node,rename_node,move_node,set,unset,bind,unbind,rename_flow,rename_agent,delete_agent. There’s noreplace— changing one field’s value isset. There’s no node-leveldelete— removing a node isremove_node.add_node— insert a node (node_id,spec, optionalinferenceandprompt) at an optionalparent/index.remove_node,rename_node(to),move_node(indexand/orto_flow) — the rest of a node’s lifecycle.set/unset— write or clear one field, addressed by a path likenodes/triage/descriptionorflow/description.bind/unbind— wire one node’s input slot (target, e.g.triage.customer) to asourceexpression, or clear it.rename_flow(to),rename_agent(agent_id,to),delete_agent(agent_id) — the flow- and agent-level renames.
expectsis the compare-and-swap check. It’s a list of{path, file_hash}pairs, one per file you’re relying on being in a known state.file_hashis"sha256-"plus the sha256 of that file’s exact current bytes;nullmeans you expect the file not to exist yet. Before writing anything, AQVEN re-hashes every path inexpectsand compares it to what you sent. Any mismatch fails the whole call with one of three codes, and nothing is written:FILE_EXISTS— you expected the file to be absent (file_hash: null), but it’s there.FILE_VANISHED— you expected a hash, but the file is gone.STALE_FILE— you expected one hash, but the file’s current hash is different: someone else (a person, another agent,git) changed it since you last read it.
- On any of the three, don’t force it. Re-read the file (or take the hash straight from the error’s
conflict.current_hash), decide whether your intended change still makes sense against the new content, and retry with an updatedexpects. There’s no override flag. expectshas to name every file youropsend up touching, not just the ones you already know about — a rename or a rebind can reach into files you didn’t list. Leave one out and the call fails withREQUEST_INVALIDbefore anything is written; the error’scandidatesfield hands you exactly the missing paths with their current hashes, ready to drop straight into a retriedexpects.- The tree is also validated before anything is written: if applying your
opswould introduce a new blocking problem, the call fails withBLOCKING_PROBLEMSand nothing is written — the same all-or-nothing guarantee as a CAS conflict. On success,problemscan still carry non-blocking warnings about the result. client_op_idmakes a retry safe. Send the same ID twice — because the response was lost, or you’re not sure the first call landed — and the second call returns the exact first result again, without re-checkingexpectsor re-running the operations. Generate a newclient_op_idonly when you mean a genuinely different edit.dry_run: truereports what would happen without writing to disk — including any new validation problems the edit would introduce. It skips the compare-and-swap check and the coverage check entirely, so a cleandry_runresult only tells you the operations would produce a valid tree; it doesn’t confirm yourexpectsare still accurate. Adry_runcall also isn’t remembered under itsclient_op_id— reusing that same ID for the real call runs it for real, it doesn’t hand back the dry run’s result.
Example
Section titled “Example”aqven new my_project --template showcasecd my_project/my_projectRead flows/support_case/nodes/triage/triage.node.yaml and call flow_patch to change its
description, declaring the file’s current hash in expects:
{ "flow_id": "support_case", "expects": [ { "path": "flows/support_case/nodes/triage/triage.node.yaml", "file_hash": "sha256-d7996e9ddd1021bc9699d034f2edc31039f0658aaec21321a4682035ba03e66e" } ], "ops": [ { "op": "set", "path": "nodes/triage/description", "value": "Classifies the case and its attachments" } ], "client_op_id": "01ARZ3NDEKTSV4RRFFQ69G5FA1"}Real response — the write landed, and version.files carries the file’s new hash:
{ "ok": true, "op": "flow_patch", "dry_run": false, "version": { "files": [ { "path": "flows/support_case/nodes/triage/triage.node.yaml", "file_hash": "sha256-bbc6bc5182cefca01a1b07bad28f7353306bcbf6c17e7f83f8e5e0a02afd4396" } ], "dirty": true, "actor": { "kind": "agent", "id": "mcp" }, "client_op_id": "01ARZ3NDEKTSV4RRFFQ69G5FA1" }, "changed_paths": ["flows/support_case/nodes/triage/triage.node.yaml"], "problems": [], "candidates": []}Send the same op again with the same, now-outdated expects (a different client_op_id, so it isn’t
just a replay this time) — the real conflict this page is about:
{ "flow_id": "support_case", "expects": [ { "path": "flows/support_case/nodes/triage/triage.node.yaml", "file_hash": "sha256-d7996e9ddd1021bc9699d034f2edc31039f0658aaec21321a4682035ba03e66e" } ], "ops": [ { "op": "set", "path": "nodes/triage/description", "value": "A second, conflicting edit" } ], "client_op_id": "01ARZ3NDEKTSV4RRFFQ69G5FA2"}{ "ok": false, "op": "flow_patch", "code": "STALE_FILE", "message": "flows/support_case/nodes/triage/triage.node.yaml changed after it was read", "conflict": { "path": "flows/support_case/nodes/triage/triage.node.yaml", "your_hash": "sha256-d7996e9ddd1021bc9699d034f2edc31039f0658aaec21321a4682035ba03e66e", "current_hash": "sha256-bbc6bc5182cefca01a1b07bad28f7353306bcbf6c17e7f83f8e5e0a02afd4396" }, "candidates": [], "retry_after_ms": null}Nothing was written — current_hash is exactly the hash the first call already produced. The fix is to
re-read the file, confirm the edit is still right, and retry with current_hash as the new expects
entry, not to force the write through.
One more real case — a set on flow/description alongside another set on the triage node, but
expects only lists flow.yaml:
{ "ok": false, "op": "flow_patch", "code": "REQUEST_INVALID", "message": "edit touches paths outside expects: flows/support_case/nodes/triage/triage.node.yaml; add them with their current hashes", "candidates": [ { "path": "flows/support_case/nodes/triage/triage.node.yaml", "file_hash": "sha256-bbc6bc5182cefca01a1b07bad28f7353306bcbf6c17e7f83f8e5e0a02afd4396" } ], "conflict": null}Same fix as STALE_FILE: add the missing path to expects using the hash candidates just handed you,
and send the whole request again.
See also
Section titled “See also”- How to connect AQVEN as an MCP server — getting an agent connected in the first place.
- How to check and test a project as an agent — run
aqven_checkafter a patch to confirm the result the same way a human would before committing. - Two ways to change a project — why
flow_patch’s before-the-write check exists, and when a direct file edit is the better choice instead.