Editing Blueprints with an AI Agent
Give an AI agent Blueprint Language source instead of JSON, and let every change go through a dry run and the canvas. This page walks one edit through the five steps of the source API: export, edit, dry run, stage, and review and save. Each step shows the request and the response.
Before you start: a token with the right scopes
The source API lives under /api/v1/blueprints/{guid}/source and authenticates like every other cluster API (see the API guide). The scopes it needs:
| Request | Needs |
|---|---|
Export (GET) and dry run (POST ?dry_run=1) |
blueprints view and blueprintactions view, the same as reading a blueprint |
Stage (POST ?stage=1) |
Also blueprintactions update, and when the source adds blocks, blueprintactions create plus the setting_edit_blueprints permission |
Nothing on this page makes a change live. A dry run writes nothing, and a stage writes unsaved edits to the canvas; only a save in RUAL Studio makes them live. A POST with neither dry_run nor stage answers 501 DRY_RUN_ONLY.
The example is the Orders API from the syntax tour: one route, POST orders, and its handler create-order. The task given to the agent: reject orders above 10,000 with a 400.
Step 1: export the flow you are changing
Export only the flow you are changing, as the canvas shows it, with positions:
starter=the guid of the handler'strigger_custom_functionblock limits the source to that flow: its function, the data it reads and the route that runs it. Staging that source only ever changes that flow, so no other flow can change by accident.view=stagedexports what the canvas shows, unsaved edits included. A stage always applies on top of the canvas, so export what the canvas shows.positions=1adds the// @xypragmas when the layout matters.
curl "https://<your-cluster>/api/v1/blueprints/<blueprint guid>/source?starter=<trigger guid>&view=staged&positions=1" \
-H "Authorization: Bearer $TOKEN"{
"source": "blueprint \"Orders API\" starter \"62e3a3b8…\"\n\nroute POST \"orders\" -> create-order // @xy 50016,50016\n…",
"lock": {…},
"warnings": [],
"stats": {"nodes": 24, "edges": 28, "source_chars": 725, "json_chars": 15949},
"update_hashes": {
"06920e2b…": "<update_hash>",
"3622500f…": "<update_hash>"
},
"source_view": "staged"
}| Field | What to do with it |
|---|---|
source |
The text to hand the agent. |
lock |
The lock that goes with the source. Send it back with the edit, or leave it out; nobody edits it. |
warnings |
Anything the export wants you to know, such as a route path that can end without a reply. |
stats |
The size of the flow: its blocks and wires, and the size of the source next to the size of the stored form. |
update_hashes, source_view |
The version of every exported block, and which view was exported. Keep both for step 4: they let the stage refuse an edit made on an out-of-date export. |
The source itself:
blueprint "Orders API" starter "62e3a3b8…"
route POST "orders" -> create-order // @xy 50016,50016
fn create-order(connection, params, query, body) { // @xy 50592,50400
let {customer_guid, amount} = body // @xy 51168,50432
guard amount > 0 else { // @xy 52096,50432
reply 400 {error: "INVALID_AMOUNT"} // @xy 52672,50688
}
customer = get@customers(customer_guid) // @xy 52992,50400
guard customer.accessible else { // @xy 53568,50432
reply 404 {error: "CUSTOMER_NOT_FOUND"} // @xy 54144,50688
}
new_order = create@orders(mutations{customer_guid: customer_guid, amount: amount}) // @xy 54752,50400
reply 200 new_order.object // @xy 55840,50400
}Step 2: let the agent edit the source like code
The agent adds one guard after the first one. New statements need no pragma: blocks without a position are placed automatically.
fn create-order(connection, params, query, body) { // @xy 50592,50400
let {customer_guid, amount} = body // @xy 51168,50432
guard amount > 0 else { // @xy 52096,50432
reply 400 {error: "INVALID_AMOUNT"} // @xy 52672,50688
}
guard amount <= 10000 else {
reply 400 {error: "AMOUNT_TOO_HIGH"}
}
customer = get@customers(customer_guid) // @xy 52992,50400
…Instructions that keep an agent's edits small and correct:
- Change only what the task needs, and keep the header line with its
starteras exported. - Keep the names the export uses: blocks are known by them, so a rename can turn an update into a removal and a new block.
- Leave
// @xypragmas alone unless the task is to move a block. - Never guess a block type or a pin name: send the source to the dry run and fix what it reports. Each error names the closest real name.
Step 3: dry run until the diff is exactly the change
Post the edited source to ?dry_run=1 with the same starter and view, and the lock if you kept it. The dry run compiles the source against the blueprint and answers what a stage would do: a diff of the blocks it would create, update and remove, and the errors and warnings. It writes nothing.
curl -X POST "https://<your-cluster>/api/v1/blueprints/<blueprint guid>/source?dry_run=1&starter=<trigger guid>&view=staged" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"source": "blueprint \"Orders API\" starter \"62e3a3b8…\"\n…", "lock": {…}}'{
"diff": {
"create": [
{"guid": "new-8e40a082…", "key": "fn:create-order|branch#2", "type": "branch"},
{"guid": "new-f45aaa2b…", "key": "fn:create-order|branch#2.condition", "type": "condition_lte"},
{"guid": "new-9965d8e6…", "key": "fn:create-order|branch#2.condition.number_2", "type": "number_default"},
{"guid": "new-bde1d891…", "key": "fn:create-order|httpconnection_set_json#2", "type": "httpconnection_set_json"},
{"guid": "new-1841f2bb…", "key": "fn:create-order|httpconnection_set_json#2.code", "type": "number_default"},
{"guid": "new-8285f8a5…", "key": "fn:create-order|httpconnection_set_json#2.connection", "type": "httpconnection_current_request"},
{"guid": "new-8286397f…", "key": "fn:create-order|httpconnection_set_json#2.object", "type": "object_new_fields"},
{"guid": "new-784ae2cf…", "key": "fn:create-order|httpconnection_set_json#2.object.error", "type": "value_default"}
],
"update": [
{
"guid": "06920e2b…", "key": "fn:create-order|branch#1", "type": "branch",
"changes": [
"…",
"flow order: false=[f7be0970….flow] true=[3622500f….flow] -> false=[f7be0970….flow] true=[new-8e40a082….flow]"
]
},
{"guid": "3622500f…", "key": "fn:create-order|customer", "type": "function_get_document", "changes": ["…"]},
{"guid": "f0da3fdf…", "key": "fn:create-order|{body}", "type": "object_field_getter_multiple", "changes": ["…"]}
],
"remove": []
},
"errors": [],
"warnings": null
}Every entry names a block by its guid, the key it is known by in the source and its type; an update also lists its changes. Read the diff before anything else. Here it says exactly what the task asked for:
- 8 creates: the
branchof the new guard, itscondition_lteand the10000, and the five blocks of the new reply. A new block has a placeholder guid starting withnew-, which stays the same across dry runs. - 3 updates, all rewires: the first guard's
trueoutput now leads to the new guard instead of the customer read, the customer read is entered from the new guard, and the body'samountalso feeds the new condition. - 0 removes, and no update to anything outside
create-order.
A diff with more in it than the task means the edit did more than intended: fix the source and dry run again. When the source has errors, nothing is compiled: errors lists each one with its line and column, and the diff is empty. This is the answer for the source with two mistakes:
{
"diff": {"create": null, "update": null, "remove": null},
"errors": [
{"pos": {"line": 7, "col": 15}, "msg": "unknown block type \"strng_lowercase\"; did you mean value_to_lowercase?"},
{"pos": {"line": 15, "col": 17}, "msg": "customer (function_get_document) has no out-pin \"acessible\"; did you mean accessible?"}
],
"warnings": null
}Warnings are strings, and those about the source start with line:col:, such as a route path that can end without a reply. Read them; they do not stop a stage.
Step 4: stage the change on the canvas
Send the same source to ?stage=1, with the update_hashes and source_view from step 1 and the lock if you kept it. The server compiles it against the canvas again and puts the diff on the canvas the way the Studio stages its own edits: new blocks appear as unsaved blocks, changed blocks as unsaved edits, and removed blocks as unsaved removals.
curl -X POST "https://<your-cluster>/api/v1/blueprints/<blueprint guid>/source?stage=1&starter=<trigger guid>" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"source": "…", "lock": {…}, "update_hashes": {"06920e2b…": "<update_hash>", …}, "source_view": "staged"}'{
"staged": 11,
"created": [
{"placeholder": "new-8e40a082…", "guid": "<new guid>", "key": "fn:create-order|branch#2", "type": "branch"},
{"placeholder": "new-f45aaa2b…", "guid": "<new guid>", "key": "fn:create-order|branch#2.condition", "type": "condition_lte"},
…
{"placeholder": "new-784ae2cf…", "guid": "<new guid>", "key": "fn:create-order|httpconnection_set_json#2.object.error", "type": "value_default"}
],
"updated": 3,
"removed": 0
}staged counts every change written: 8 new blocks and 3 updated ones. created maps each placeholder guid to the guid the new block got. A stage is never left half-written: it is refused before anything is written, or undone when a write fails.
| Answer | Meaning | What to do |
|---|---|---|
200 with staged |
The change is on the canvas as unsaved edits. | Go to step 5. |
200 with errors and staged: 0 |
The source does not compile. | Fix the errors, as in step 3. |
409 SOURCE_STALE with guids |
A block the edit changes or removes was changed on the canvas since the export, or an exported block is gone. Changes the edit does not touch are no conflict. | Export again and redo the edit on the new source. |
409 FOREIGN_OVERLAY with guids |
Another user has unsaved edits on a block this edit touches. | Wait until they save or discard. Nothing of theirs is taken over. |
409 BLOCKS_CHANGED with guids |
Blocks were removed between the compile and the write. | Dry run and stage again. |
403 CREATE_SCOPE_REQUIRED, 401 INSUFFICIENT_PERMISSIONS |
The edit adds blocks, which needs blueprintactions create and setting_edit_blueprints. |
Use a token that may create blocks. |
200 with rolled_back: true |
A write failed or collided with another change, and what had been written was undone. conflicts and failed say which blocks. |
Stage again. |
{
"error": "SOURCE_STALE",
"guids": ["3622500f…"]
}A setting the source no longer states is removed when the change is saved, so deleting a setting in source really deletes it.
Step 5: review on the canvas, simulate, save
Open the blueprint in RUAL Studio. The staged blocks show as unsaved edits, like edits made by hand.
- Look at the new blocks where they were placed, and move them if you like.
- Simulate the route: a simulation runs the unsaved edits, so you test the change before it is live. A simulation is not a full sandbox, so check what the flow calls first.
- Save in the Studio. That save is the commit: before it, production keeps running the saved blueprint. To throw the change away instead, discard the unsaved edits.
After the save, the next export of the same flow shows the new guard with the positions it was given:
guard amount > 0 else { // @xy 52096,50432
reply 400 {error: "INVALID_AMOUNT"} // @xy 52672,50688
}
guard amount <= 10000 else { // @xy 50944,51296
reply 400 {error: "AMOUNT_TOO_HIGH"} // @xy 52032,51296
}
customer = get@customers(customer_guid) // @xy 52992,50400The loop in one list
GET /api/v1/blueprints/{guid}/source?starter={trigger guid}&view=staged&positions=1; keeplock,update_hashesandsource_view.- Edit the source; add no guids and no JSON.
POST .../source?dry_run=1&starter=...&view=stagedwith{source, lock}; repeat untilerrorsis empty and the diff is only the change.POST .../source?stage=1&starter=...with{source, lock, update_hashes, source_view}; on409 SOURCE_STALE, go back to 1.- Review and simulate on the canvas; save in the Studio.
To lay out only the blocks that have no position, without compiling anything, POST /api/v1/blueprintactions/layout/{blueprint guid} with {"unpositioned": true} stages positions for just those blocks.
Frequently asked
How do I let an AI agent edit a RUAL blueprint safely?
Export the flow as Blueprint Language with GET /api/v1/blueprints/{guid}/source?starter=...&view=staged, let the agent edit the text, send it to POST .../source?dry_run=1 until there are no errors and the diff is only the intended change, then stage it with ?stage=1 and the export's update_hashes. The change appears on the canvas as unsaved edits, and nothing is live until someone saves in the Studio.
What does SOURCE_STALE mean in the RUAL source API?
A stage was refused with 409 SOURCE_STALE because a block the edit changes or removes was changed on the canvas after the source was exported, or an exported block is gone. Nothing was written. Export again and redo the edit on the new source.
Can the RUAL source API save a blueprint directly?
No. A dry run writes nothing and a stage writes unsaved edits to the canvas; a POST with neither answers 501 DRY_RUN_ONLY. Making a change live is always the Studio's save, after someone has reviewed the staged change.