---
title: "Editing Blueprints with an AI Agent · RUAL Documentation"
description: "Export, edit, dry run, stage and save: the public source API step by step, with the request and response of every step."
canonical: https://docs.rual.nl/engine/editing-with-ai
language: en
---

[Cluster](https://docs.rual.nl/cluster)

[Blocks](https://docs.rual.nl/block-types)

[Interfaces](https://docs.rual.nl/interfaces)

[Blueprints](https://docs.rual.nl/blueprints)

[Blueprint Language](https://docs.rual.nl/engine)

[Tutorials](https://docs.rual.nl/tutorials)

[Home automation](https://docs.rual.nl/home-automation)

[Examples](https://docs.rual.nl/examples)

[Reference](https://docs.rual.nl/reference)

[Architecture](https://docs.rual.nl/architecture)

[Troubleshooting](https://docs.rual.nl/troubleshooting)

Other

# Editing Blueprints with an AI Agent

Give an AI agent [Blueprint Language](https://docs.rual.nl/engine/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](https://docs.rual.nl/cluster/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](https://docs.rual.nl/engine/blueprint-language#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's [`trigger_custom_function`](https://docs.rual.nl/block-types/globals/trigger_custom_function) block 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=staged` exports what the canvas shows, unsaved edits included. A stage always applies on top of the canvas, so export what the canvas shows.

- `positions=1` adds the [`// @xy` pragmas](https://docs.rual.nl/engine/blueprint-language#positions) 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](https://docs.rual.nl/engine/blueprint-language#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 `starter` as 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 `// @xy` pragmas 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 [`branch`](https://docs.rual.nl/block-types/flow/branch) of the new guard, its [`condition_lte`](https://docs.rual.nl/block-types/condition/condition_lte) and the `10000`, and the five blocks of the new reply. A new block has a placeholder guid starting with `new-`, which stays the same across dry runs.

- **3 updates**, all rewires: the first guard's `true` output now leads to the new guard instead of the customer read, the customer read is entered from the new guard, and the body's `amount` also 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](https://docs.rual.nl/engine/blueprint-language#checks):

```
{
  "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](https://docs.rual.nl/blueprints/block-execution#debugging) 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,50400
```

## The loop in one list

- `GET /api/v1/blueprints/{guid}/source?starter={trigger guid}&view=staged&positions=1`; keep `lock`, `update_hashes` and `source_view`.

- Edit the source; add no guids and no JSON.

- `POST .../source?dry_run=1&starter=...&view=staged` with `{source, lock}`; repeat until `errors` is empty and the diff is only the change.

- `POST .../source?stage=1&starter=...` with `{source, lock, update_hashes, source_view}`; on `409 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.

- [The Studio Code View](https://docs.rual.nl/engine/studio-code-view): The same loop inside RUAL Studio: Check is the dry run and Stage to canvas is the stage.

## 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.

Was this page helpful? [Tell us what to improve](https://docs.rual.nl/support) · RUAL Docs is an integral component of the [RUAL ecosystem](https://rual.nl)
