Reusable blocks
Move shared logic into a block, pass typed data, and choose which version a reaction uses.
En esta página
Several reactions may repeat the same work: prepare order rows, check an application or calculate a total. Move that work into a block with declared inputs and outputs, then call it from different reactions.
Inputs describe what the block needs. Outputs describe what it returns. Temporary values stay inside the block, so two blocks using the name prepared do not mix their results.
When to use a block
| Goal | Choose |
|---|---|
| Reuse the same processing in several places | A block with inputs and outputs |
| Save or roll back several record changes together | Atomic collection transaction |
| Move to another conversation step | A transition or reaction call |
| Restore a reaction's previous settings | Reaction history |
Block versions belong to the library and are separate from reaction history. Choosing an older block version does not undo messages already sent or records already changed.
A block runs actions in sequence; it does not make them one transaction. If a later step fails, earlier successful changes are not automatically rolled back.
Create a block in the editor
- Open Reusable blocks in the project's reactions section. On a narrow screen, find it among the additional actions.
- Select Create block and enter a useful title, such as “Prepare order rows”.
- Under Inputs, add each name, type, required setting and optional default. Under Outputs, add the names, types and values the block should return. Use the regular action cards to define the steps between them.
- Select Save new version. The first save creates version
1. Each subsequent saved change creates a new number, keeping previous contents under Immutable versions.
The block title can contain spaces and non-Latin letters. Input and output names use ASCII letters, digits and _, starting with a letter. For example, orders and total_amount work. These names appear in data and templates, so choose them separately from the block's title.
If an existing block contains an action the web builder cannot yet edit, the library retains its contents and explains that limit. You can still call the block using its declared inputs and outputs. Regular forms remain available for creating a new block.
Example: prepare order rows
Suppose several reactions receive orders in this format:
[
{"product":"Brush","price":150,"quantity":2},
{"product":"Paper","price":300,"quantity":1}
]
Each row needs amount = price × quantity. Create a block called Prepare order rows.
1. Declare an input
Add a required Array input named orders. Its type is checked at the call boundary: text that cannot be parsed as an array will not be accepted as a list.
Inside the block, this input is available as {{ctx.input.orders}}. In an action that reads a list by its context key, use the exact key input.orders.
For a search result or another list action's result, choose an Object input and pass the complete result, such as {{ctx.search}}. The source inside the block remains input.orders. This retains completeness and selection-limit information alongside the rows. The Array type used in this example fits the original two-row array shown above.
2. Add processing
Add Map list fields with these settings:
- Source:
input.orders. - New field:
amount. - Operation: multiply
pricebyquantity. - Keep other fields: enabled.
- Save result:
prepared.
The action parameters are:
{
"source":"input.orders",
"fields":[{"as":"amount","op":"multiply","paths":["price","quantity"]}],
"keep_fields":true,
"save_to_ctx":"prepared"
}
See the field mapping tutorial for other operations and missing values.
3. Declare an output
Add an Object output named result with the value {{ctx.prepared}}. Return the complete processing result so that row counts and completeness travel with the list.
{"name":"result","type":"object","value":"{{ctx.prepared}}"}
Expect two rows: the brush has amount=300, and the paper has amount=300. The original product, price and quantity fields remain.
4. Call the block from a reaction
In a reaction where the original array is already saved as orders, add Call block. Select the block and its version, set the orders argument to {{ctx.orders}}, and save the result as calculation.
The technical call format is shown below. Replace block_id with your block's ID:
{
"block_id":"00000000-0000-4000-8000-000000000001",
"version":1,
"args":{"orders":"{{ctx.orders}}"},
"save_to_ctx":"calculation"
}
The declared output appears at calculation.result. The next list action should read the source calculation.result, retaining the full result instead of selecting only calculation.result.rows.
For the first row's amount in this example, use {{ctx.calculation.result.rows.0.amount}}: 0 selects the first array row. Check that the list is not empty before accessing it. This object has no automatically created calculation.result.first shortcut; field paths depend on the declared output's contents.
When a block returns outputs
Outputs are returned after all block actions finish normally. An inner outcome route or sequence stop propagates to the caller. The block publishes no outputs, and the calling reaction does not continue its remaining actions or choose generic success.
The branch_value and branch_result actions also end a block call after selecting a branch. In an ordinary reaction outside a block, the parent actions continue after the branch finishes normally. Account for this difference when moving a sequence with a shared follow-up calculation into a block.
Conditional updates inside a block have an extra guard: conflict and not_found stop the block even without routes on that inner action. updated without routes allows processing to continue. Outside blocks, the existing “update, then inspect status” sequence remains available. Configure inner action outcomes to provide a specific refusal response.
A technical error before a route is selected can be handled by the error outcome on Call block. For example, this handler runs if a branch cannot read the data it needs to evaluate. Once an inner action has selected another reaction, a failure in that reaction does not also run the calling block's error handler. This prevents one failure from producing two different responses. Handling an error does not automatically roll back actions already executed.
Types, required inputs and defaults
Supported types are text, number, boolean, object and array. Unknown arguments and missing required inputs fail explicitly. Optional inputs can have a default of the declared type.
For example, a numeric default is 3, not the string "3". Defaults are literal values and do not evaluate templates. A required input may also have a default, used when its argument is omitted.
Argument templates are rendered before type conversion. Thus "{{ctx.quantity}}" can become a number, and a string containing JSON can become an array or object. Invalid numbers do not become zero. Monetary values and large numeric identifiers retain their precision.
In Call block, an optional argument is sent only when its Pass argument checkbox is selected. Leave it unchecked to use the declared default. Displaying that default does not copy it into arguments; an explicit null is a separate supplied value and must satisfy the type.
If an older call retains an argument removed from the declaration, the editor shows it with an explicit remove action. Check new required inputs and changed types too. Refreshing block metadata does not automatically clear your unsaved values.
Choosing a version
Each saved change creates a new version. Earlier version bodies remain unchanged.
In Call block, enable Pin version and enter the number in Version. Clear the checkbox when new calls should select the current version.
- A specific version, such as
1, stays selected after new versions are published. Use this for a tested production flow. - Latest version,
version=0or an omitted number, selects the current version for each new invocation. Use it when consumers should receive updates together.
When the same persisted background job retries after a failure, it retains its selected block version. Publishing a new version of that block does not change the selection. Resolved inputs must also match: changed inputs cause an explicit error before new actions, rather than letting the remaining steps use different data.
A version fixes the block's contents. It does not copy referenced reactions or access rules, which are checked against current state. A nested call selecting another block's latest version selects it when that nested invocation starts. Pin the nested call too when its calculation must stay fixed.
Before updating, check which reactions use the block, which pin a version, and which select latest. Test the new version on sample data before moving consumers to it.
For example, version 1 accepts only orders, and version 2 adds required input discount. A latest-version call passing only orders will fail with a missing-input error. If 0 is a suitable discount, provide it as the default: existing calls can continue while you add the argument to the relevant reactions. Calls pinned to version 1 keep its previous inputs and actions. Check renamed outputs and changed output types in reactions that read the result too.
Concurrent edits and deletion
If another user saves the block while you are editing it, saving reports a version conflict. Your local changes stay in the form. Choose what to do next:
- Check current version refreshes its number while keeping your draft. It checks version metadata; it does not display the contents for comparison.
- Save my draft as a new version then creates a new version entirely from your changes. The other author's saved contents remain in history. A further concurrent update causes another conflict.
- Discarding the draft and loading the latest version replaces the unsaved form with the current saved contents.
Under Where used and deletion eligibility, check external references and active invocation pins. Counts include consumers on pages you have not loaded yet. Delete block is available when deletion is permitted and asks for explicit confirmation.
The consumer list shows reactions and block versions calling this block. Older versions of other blocks count too, preserving pinned calls. These references prevent deletion. Unfinished persisted background calls also temporarily protect the block they use.
A newer version calling an earlier version of the same block is not an external consumer: deletion removes all of that block's versions together. If usage changes after you open the page, the server checks the references again when deleting and reports a refusal.
Checks before use
- Pass a normal order and compare every calculated row.
- Omit the required input and expect an error.
- Pass the wrong type, such as plain text instead of an array.
- Call the block twice with different data and check that temporary values do not mix.
- Publish a version and compare a pinned call with a latest-version call.
Blocks run within the reaction's normal limits. Waiting for user input and delayed continuation belong in the reaction itself. Nested calls are bounded by depth, data size and action count; recursive calls cannot run indefinitely.
Payment creation, subscription creation/cancellation/update and the built-in GetCourse, Google Sheets and Google Drive actions are not yet available inside blocks. This also applies to reactions reached by a block: an unsupported step is rejected before execution. These actions remain available in ordinary reactions. Related outcome limits.
Data limits
| What is bounded | Limit |
|---|---|
| Declared inputs and outputs | Up to 256 inputs and 256 outputs |
| Stored version body | 1 MiB including metadata |
| Values and the complete input or output object | 16 MiB |
| Items in one array or fields in one object | 10,000 |
| Value nesting | 64 levels |
| JSON validation work | 1,000,000 parser tokens: values, keys and delimiters |
| Context data across a call tree | 8 MiB total, including key names and values |
| Context keys created for inputs and outputs | 65,536 per call tree |
| Work preparing these keys and values | 16 MiB of scanned data |
| Overall call depth, including reactions | Up to 16 levels |
| Actions per event, including blocks | 5000 |
| Sends per event, including blocks | 50 |
| Processing time per event | Up to 5 minutes |
The token budget is shared by defaults, outputs and action parameters in one version, and by the selected input set. Each separately parsed object or array returned by a template is also checked against this budget. Exceeding a limit fails explicitly instead of silently truncating data.
The 16 MiB value limit does not mean that every object of that size can be passed to a block. To support paths such as input.orders.0.price, the constructor prepares both the complete value and nested context keys. Deep objects can repeat the same data in several of those values. This preparation therefore has a separate shared 8 MiB budget, which nested calls do not reset. Use list field mapping to pass only the fields needed when a large object exceeds the budget.
Inputs are checked before block actions run. Outputs are fully prepared before replacing the caller's result: exceeding a limit does not publish a partial new result. Actions already executed inside the block are not rolled back.
Context key collisions are also checked. For example, the distinct fields in {"a.b":"first","a":{"b":"second"}} would both produce input.payload.a.b. The call is rejected rather than letting one value replace the other. For list rows, first select the fields using explicit paths /a.b and /a/b, give them different names and turn off keeping other fields in list field mapping. Pass the prepared list to the block with unambiguous field names.
Calling a block does not reset event limits. Static send estimation conservatively includes reachable branches, so a complex block with many alternatives can be rejected before any actual send. Use broadcasts for mass notifications.
The body size is checked before writing and after PostgreSQL normalization. For example, the short number 1e256 expands into a long decimal representation when stored. If this makes the version too large, the complete save is rolled back and the previous version remains current. For large reference lists, pass collection data at call time instead of embedding the entire list in a default.
Related material: list operations, field mapping and joins, reaction history.