Skip to content

Canvas guide for agents ​

Read this first when you build or change the logic of a route, workflow or custom block. A canvas is a graph of blocks. Edit it with get_canvas and edit_canvas. Settings of the route itself are in the Route reference.

How a canvas runs ​

  1. A run starts at the entrypoint block.
  2. It follows the edges, one block after the other.
  3. Each block's output is the next block's input.
  4. A response block ends the run. Its input becomes the body. See Response.

The entrypoint outputs what the run started with: on a route the parsed request body (the same as the Get Request Body block), on a workflow the data it was started with. So the first block after it reads input.email, not input.body.email.

A new route starts with an entrypoint, a response block and an error handler. Call get_canvas to see them, their keys and their edges before you edit.

Blocks, handles and edges ​

An edge goes from one block's output handle to another block. A plain block has one handle, source. Blocks that branch or wrap other blocks have named handles.

Block typesHandles
Most blockssource
if, db_exists, queue_sendsuccess, failure (no source)
forloop, foreachloopsource (after the loop), executor (the body, once per round)
db_transaction, retrysuccess, failure, executor (the wrapped chain)
orchestratorsource, orchestrate
switchcase
response, db_rollback, sticky_notenone, they end the flow

Rules:

  • A handle holds one edge. Only case and orchestrate can hold several. To re-point a handle, disconnect it first, in the same edit_canvas call.
  • Nothing may point at the entrypoint or the error handler.
  • Leave handle out of an op and the block's default is used: source, or its only handle. A block with several handles and no source needs handle named.

get_block_schemas with blockTypes lists the handles of each block and what each one means.

Read the block contracts ​

Call get_block_schemas with no input for the list of built-in block types. Call it again with blockTypes (up to 10) for each block's:

  • fields and their types,
  • handles,
  • output, which is the next block's input,
  • an example of valid data.

A custom block's inputs come from get_custom_block. Any block's data can also hold blockName, blockDescription and saveAsVariable.

Block text inputs are literal unless they start with js:. See Dynamic values and js: expressions.

Read a big canvas in parts ​

get_canvas returns the full data of every block. On a big canvas that is a lot to read each time. Ask for less:

  • compact: true gives each block as key, type, note and a one-line summary. Code shows as code: 30 lines and a js: input as js: 4 lines. Long text is cut. Edges and revision are the same, so the revision works with edit_canvas.
  • blocks: ["jsrunner_1", "response_1"] gives the full data of just those blocks, and the edges that touch them. An unknown key is refused and the error lists the keys on the canvas.

Read compact first, then fetch the blocks you will change.

json
{
  "revision": 12,
  "blocks": [
    { "key": "entrypoint_1", "type": "entrypoint" },
    { "key": "jsrunner_1", "type": "jsrunner", "note": "Parks the hash: KV returns true", "summary": "blockName=\"Hash it\"; value=code: 24 lines" },
    { "key": "response_1", "type": "response", "summary": "httpCode=js: 1 line" }
  ],
  "edges": ["entrypoint_1 → jsrunner_1", "jsrunner_1 → response_1"]
}

Leave notes ​

A workaround looks like pointless plumbing to the next reader: a variable that exists only because a block returns true, or a hash format that lives inside one script. Write down why.

  • A block note. Set blockDescription in the block's data (add_block or update_block) on any block whose purpose is not obvious: a workaround, a contract, why a value is parked in a variable. One or two short sentences about why, not what the block does: "KV Set returns true, so the id is saved here for the response".
  • A sticky note. Add a sticky_note block for a rule the whole canvas follows, for example "passwords: salt:hash, pbkdf2 sha256 100k". It never runs and has no edges.
  • Read before you change. get_canvas shows each block's note as note, and a sticky note's text as the note of the sticky note block. A block with the default placeholder text shows no note. Read the notes before you remove or rewire a block that looks pointless.

Keep the canvas honest ​

  • Do not invent rules. One response block per happy path is fine. Do not force several end paths onto one block just to make the canvas look tidy. Never write a note that states a rule the canvas does not already follow.
  • Notes must stay true. After an edit, re-read every blockDescription and sticky note against the new edges, yours and earlier ones. Fix any note the edit made false in the same edit_canvas call.
  • Give blocks unique names that say where they sit. Set blockName so no two blocks on a canvas share one: 200 OK: cached users and 200 OK: users from DB, not two blocks called 200 OK: users. Rename a block when you clone it. Saving warns when two blocks share a name; the default name is ignored.
  • Claim "no behaviour change" only after you test it. Call the rewired branch with input that takes it, with call_route or a test suite. Any 200 from the other branch proves nothing.

Block keys ​

Every block has a key: its type and a number, such as response_1, db_insert_2 or custom_send_mail_1. You name blocks by key in get_canvas, edit_canvas and test suite hooks. You never need a block id.

  • The server gives a block its key when the block is created. You cannot choose or change it.
  • A key is never reused on the same canvas, even after its block is deleted. If response_2 is removed, the next new response block is response_3.
  • A key tells you the block type. If you write a key that does not belong to the block it names, the op is refused.

Edit with edit_canvas ​

Call get_canvas first. It gives revision, blocks (key, type, data) and edges, written as text: entrypoint_1 → response_1, or if_1.success → db_insert_1 for a block with several handles (from.handle → to). Pass that revision to edit_canvas. If the canvas changed since, you get Canvas changed since you read it: read it again and redo the edit.

json
{
  "target": { "kind": "route", "id": "<route id>" },
  "revision": 4,
  "ops": [
    { "op": "add_block", "ref": "find", "type": "db_exists",
      "data": { "connection": "<integration id>", "tableName": "users", "conditions": [] },
      "connect_from": { "from": "entrypoint_1" } },
    { "op": "add_block", "ref": "gone", "type": "response", "data": { "httpCode": "404" },
      "connect_from": { "from": "find", "handle": "failure" } },
    { "op": "connect", "from": "find", "to": "response_1", "handle": "success" }
  ]
}
OpFields
add_blockref (your name for it), type, data, optional position, optional connect_from: { from, handle? }
update_blockid, data. Only the fields that change. They are merged into the block's data.
edit_codeid, optional field, old, new. Replaces exact text inside one field. See Change part of a script.
remove_blockid. Its edges go with it. The entrypoint and error handler cannot be removed.
connect, disconnectfrom, to, optional handle (the handle on from)
  • from can also carry the handle: "from": "if_1.success".
  • Ops run in order. A later op can use a ref added earlier in the same call, and a disconnect followed by a connect on the same handle works.
  • All or nothing: one bad op (unknown block type, missing id, a handle the block does not have, a handle already used) refuses the whole call and saves nothing. The error names the block and lists its valid handles.
  • The answer has the new revision; changes, one line per thing done (updated response_1 (httpCode), edited jsrunner_1.value (1 change, lines 12–14), connected if_1.success → db_insert_1, removed log_2 (+2 edges)), so you can see each op hit the block you meant. A block you add also says what it outputs, which is the next block's input; and refs, which maps each of your refs to the key the block got. Use those keys in later calls.
  • Leave position out and the server places new blocks. Pass auto_layout: true to lay out the whole canvas again.
  • ops: [] checks the canvas and saves nothing.

Change part of a script ​

A script is one long string. To change a line, do not send the whole script again with update_block: it costs tokens every time and a slip in the escaping can break the code without an error. Use edit_code instead:

json
{ "op": "edit_code", "id": "jsrunner_1", "old": "subtotal * 0.2", "new": "subtotal * 0.1" }
  • field is the text field to edit. Leave it out for the block's main code field: value on a JS Runner, js on a Transformer, KV raw and DB native, transformScript on a response block. Name the field to edit a js: text input, for example "field": "httpCode".
  • old must be found exactly once, spaces and line breaks included. Copy it from get_canvas. If it is found no times or several times, the call is refused and nothing is saved. The error says how many matches there are and on which lines, or shows the closest lines. Add more of the surrounding text to old to make it unique.
  • The answer says where the change landed: edited jsrunner_1.value (1 change, lines 12–14).
  • A custom block's code is the JS Runner on its own canvas, so edit_code works there too: target: { "kind": "custom_block", "id": "<custom block id>" }.

Validation ​

Validation runs by default and returns issues: { severity, message, block }, where block is the key of the block it is about. Pass validate: false only to skip them.

  • An error on a block you wrote in this call refuses the save. Fix it and call again.
  • A warning still saves. Read it: it catches things like a template or a missing js:.
  • Reachability warnings name a block that is not connected to the flow, a route with no path from entrypoint to a response, or an open if branch (if_1.failure is not connected). Fix them before testing: an unconnected route answers the default response (NO RESULT). Retry and transaction failure, loop bodies and workflows or custom blocks may end open, so they stay quiet.
  • Issues on blocks you did not touch are reported but never block the save.

The response block ​

The body of a response is the output of the block before it. The status is its httpCode: a fixed code like "404", or js: code that returns one when it depends on the flow. To reshape the body in place, set transformEnabled: true and transformScript (plain code, it gets the body as input and returns what to send). Recipes: Composed flows.

A workflow has no caller, so its end block only finishes the run and its httpCode is not read.

Errors ​

If a block throws, the run jumps to the error handler block. Wire it with an edge from the handler's source handle to the first block of your error flow:

json
{ "op": "connect", "from": "error_handler_1", "to": "<first error block key>" }
  • The first block of that chain gets the error as text for its input, like Error: <message>.
  • End the chain with a response block, such as httpCode "500", to answer with JSON.
  • With nothing on its source handle, the error fails the route.
  • The handler's next field is not what the runtime follows. Use the edge.
  • retry and db_transaction can catch a failure themselves on their failure handle. See their contracts in get_block_schemas.
  • call_route returns the real cause as error even when callers only see a generic message. See Debug and fix.

Save output to variable ​

Any block, built in or custom, can keep its output for later blocks. Set saveAsVariable in the block's data:

json
{ "saveAsVariable": { "enabled": true, "name": "user" } }

Later blocks read it as outputs.user:

  • in a text input: js: return outputs.user.email
  • in a code field (JS Runner, Transformer, a response transformScript): outputs.user.email

It is stored for that request only. The next request starts empty.

RuleDetail
NameA valid identifier: starts with a letter, _ or $, then letters, digits, _ or $. An enabled setting with a bad name is refused when you save.
Saved whenThe block finishes and moves on through its source handle. For retry and db_transaction that is success. db_exists saves the matching row on success and null on failure. queue_send saves on either branch.
Read before it ranoutputs is undefined until some block has saved, so outputs.user throws on a path where the block did not run.
Same name twiceThe later block overwrites the earlier one.

Use it when a later block needs data that input no longer holds:

  • the body is needed after a database block replaced input with a row,
  • one loaded row feeds an update and the response,
  • a value is used in several branches.

If the next block needs only the previous output, just read input. Do not save everything.

The build loop ​

  1. get_block_schemas with blockTypes for the blocks you will use.
  2. get_canvas for the keys and revision.
  3. edit_canvas with small ops. Fix every error in issues.
  4. get_system_logs for compile errors. call_route also gives the real error and a trace of the blocks that ran.
  5. call_route with real input; it runs in development. When the route is ready, commit it with commit_version, switch production on with save_route and active: true, and ask the user to approve promote_version: production serves only a promoted version.
  6. Stop when the route works and its tests pass. Do not rewrite a working canvas.

Common problems ​

What you seeWhat to do
Canvas changed since you read itget_canvas again, redo the edit with the new revision.
... has no "x" handle. Use ...The error lists the handles the block has. Pick one.
... handle already goes to ...Add a disconnect before the connect, in the same call.
no block "x"Use a key from get_canvas (the error lists similar ones), or a ref added earlier in the same call.
x is a ... block, not a ... blockThe key does not belong to that block. Read the canvas again and use the key it shows.
a canvas already has its one error_handler blockA canvas has exactly one entrypoint and one error handler. Edit the existing one.
A block shows its text in the responseThe value is a literal. Start it with js:.

Released under the Apache License 2.0. Enterprise features are under the Fluxify Enterprise Edition License.