Call the 8 run_* MCP tools to start, watch, fork, and cancel a run — and the two places their shape genuinely differs from the same operations over REST.
When you need this
Section titled “When you need this”You’re connected to a project over MCP and need to run a flow, or check on one that’s already running, without opening Studio. Eight tools cover the whole lifecycle: start a run, read its status, list runs, drill into one node’s execution, page through its event log, answer a wait, fork it from a point, or cancel it. Two of the eight behave differently over MCP than the REST route with the same name — worth knowing before you build assumptions on one and hit the other.
run_starttakes aflow_id, amode(livefor a real run that calls real models and tools), and exactly one ofinputordataset_item_id. It validates that input against the flow’s own input schema before anything runs, and fails withINPUT_INVALIDand aproblemslist if it doesn’t match. Otherwise it returns at once, without waiting for the run to finish:run_id,status,last_seq, aui_urlto open the same run in Studio, andwarningsfor anything the run will hit later, most commonly a missing secret. Callrun_getorrun_eventsnext to follow it. Withdataset_item_id, media the case points at byfileis read from the project and stored as blobs before the run starts, so the run’s input and trace carry ablob_id; see how to keep case media as files in the project.run_getis the full snapshot:status,mode, cost and token totals,node_counts(how many nodes are pending, running, ok, failed, skipped, suspended, cancelled), the fullexecutionslist, anderrorif the run failed. If it’s paused on a person,waitslists what’s waiting and for whom — that’s what responding to a review resolves, the same information investigating a run shows in its header.run_listpages through runs filtered byflow_id,status,mode,assignee,parent_run_id, and asince/untilwindow, withnext_cursorfor the next page. For everything waiting on a person, filterstatus: "suspended"; for your own queue, addassignee: "me"(the local user),overdue: true, and sort bydeadline_at.run_get_nodeis one node’s execution, addressed the same way the engine itself addresses every execution:node_idplusbranch_key,iteration, anditem_indexfor a node that ran inside a branch, a loop, or a map. Leave the three optional fields out to reach a top-level node. The result carries the prompt, the response, every attempt, checks, and cost — the same fields the side panel in investigating a run is built from. A node still waiting on a person also carrieshuman, with its form schema and theattemptvaluerun_resumeneeds.run_eventsbehaves differently over MCP than the REST route with the same operation. Call it with noafter_seqover MCP and you get the tail — the lastlimitevents, for catching up with a run that’s already well underway. The REST route for that same operation (GET /api/runs/{run_id}/events/log) defaults the other way: noafter_seqthere means from the start. Passafter_seqexplicitly on either side (0for the start, or aseqyou’ve already seen) and both behave the same — it’s only the default that diverges. REST also has a separate SSE push stream atGET /api/runs/{run_id}/eventsfor a live feed; there’s no MCP equivalent of that one, only the paged log both surfaces share.run_resumeanswers a wait: theaddressandattemptcome fromrun_get’swaitsor fromrun_get_node,payloadhas to match the wait’s form schema, andclient_op_idmakes a retried call safe to send twice. Studio’s review screen calls this same mechanism when a person clicks Submit and resume.run_fork’s field name is the one other real divergence. Start a new run from an existing one at a given execution address, replaying everything before that address and re-running it and everything after — over MCP the field carrying that address is calledaddress; the REST route for the same operation expects the identical value under the key"from"instead. Same meaning, different JSON key depending which surface you’re calling. Separately, that address value itself always needs all four fields present —node_id,branch_key,iteration,item_index— even when three of them arenullfor a top-level node; unlikerun_get_node’s flat arguments, none of the three has a default, so leaving one out fails validation before the call does anything.run_canceltakes arun_idand a free-textreason. Likerun_resume, itsrun_idtravels in the request body over MCP; the REST route for the same operation puts it in the URL path instead and takes justreasonin the body.- A failed
run_*call is a real MCP tool error, not a normal result with a false flag inside it.INPUT_INVALID,RUN_STATE_CONFLICT,NOT_WAITING, and the rest all come back withis_error: true, the failure itself in the same{ok, op, code, message, problems, candidates, conflict}shape every other failing call on this project uses. That’s different fromaqven_check, whoseok: falseis a normal, successful call — check which kind of failure you’re handling before you write an error branch around one of these.
Example
Section titled “Example”Create the showcase project and connect an agent to it as in How to connect AQVEN as an MCP server:
aqven new my_project --template showcasecd my_project/my_projectStart a live run of support_case with a minimal case as input — this project has no
OPENROUTER_API_KEY set, which is deliberate here: it lets the run fail predictably right after its
first model call. The call returns immediately, run_id 01a0c6ab-fdeb-70af-b98a-b35177348d19, with
warnings about every missing secret the flow could reach.
A moment later, the run has already finished — failed at triage, the first llm node, right after the
prepare code node ran fine. This is the real divergence: the same run, run_events called two ways.
With no after_seq, the tail — the run’s last event:
{ "items": [ { "seq": 7, "type": "run_finished", "status": "failed", "error": { "code": "provider_key_missing", "message": "no API key for provider openrouter (openrouter:google/gemini-2.5-flash-lite): set OPENROUTER_API_KEY in the project .env or the environment" } } ], "next_cursor": null, "total_estimate": 1}With after_seq: 0 — same run_id, limit: 1 — the start instead:
{ "items": [ { "seq": 1, "type": "run_started", "flow_id": "support_case", "mode": "live" } ], "next_cursor": "1", "total_estimate": 7}Now fork that run from prepare, the one node that succeeded — over MCP, the address is address:
{ "run_id": "01a0c6ab-fdeb-70af-b98a-b35177348d19", "address": {"node_id": "prepare", "branch_key": null, "iteration": null, "item_index": null}, "at": "original"}{"run_id": "01a0c6ab-fea5-71ac-82c5-ee5ae39cdd20", "lineage_parent": "01a0c6ab-fdeb-70af-b98a-b35177348d19"}The REST route for the same operation wants the identical value under "from" instead — sending
"address" to REST is rejected before it even looks at the run:
{ "ok": false, "op": "run_fork", "code": "REQUEST_INVALID", "message": "request failed validation", "problems": [ {"path": ["body", "from"], "code": "missing", "message": "Field required"}, {"path": ["body", "address"], "code": "extra_forbidden", "message": "Extra inputs are not permitted"} ]}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 — the other read-mostly tools,
and why
aqven_check’s own failures don’t come back as MCP tool errors. - The engineering loop — why every execution needs its own address, which
is what
run_get_nodeandrun_forkboth take as input. - How to investigate a run — the same run data as
run_getandrun_get_node, read by a person instead of an agent. - How to respond to a human-review request — the screen that calls
run_resumewhen a person clicks Submit.