Blocks
A block is a file in src/blocks that default-exports block({...}):
import { block, param } from "@fluxify/sdk/blocks";
export default block({
name: "acme/slack/messages/post",
label: "Post message",
params: { channel: param.string({ required: true }) },
async run({ params }) {
return { output: `posted to ${params.channel}` };
},
});block() checks the definition when the file loads and throws an error that names the block, so a mistake shows up in bun test and in the build. Only name, label and run are required.
Fields
| Field | Required | Default | What it is |
|---|---|---|---|
name | Yes | The block's type: scope/name/folder/block | |
label | Yes | The name shown on the canvas and in the picker | |
description | No | One line shown in the picker | |
icon | No | A default icon | An image inside the plugin |
shape | No | square | square or wide |
docs | No | Markdown for the block's Docs tab | |
invoke | No | sync, async, queued | The ways a canvas may call the block |
permissions | No | None | What ctx may reach |
params | No | None | The settings the user fills in |
tabs | No | By first use | The order of the settings tabs |
handles | No | In target, out source | Inputs, outputs and nested chains |
run | Yes | The code | |
| Deprecation | No | A message that the block is going away |
name
The block's type. It must be the plugin name plus the path of the file under src/blocks, in lowercase. The build stops if they differ. See names.
The type is also part of the id a recorded run stores for the block, <plugin>@<version>:<project id>:<block type>, which holds 500 characters. The build budgets the longest project id (50) and stops, naming the block and how far over it is, when the id could not fit. Shorten the plugin name or a folder if that happens.
name: "acme/slack/messages/post", // src/blocks/messages/post.ts in acme/slacklabel
What the user sees on the canvas and in the picker.
label: "Post message",description
A short line under the label in the picker.
description: "Posts a message to a Slack channel.",icon
A path to an image inside the plugin: .svg, .png, .jpg, .jpeg, .gif or .webp. The build copies an icon from outside icons/ into icons/ so it ships, and stops if the image is missing or too large (about 50 KB).
icon: "./icons/post.svg",shape
square is the usual one-icon block. wide draws a wider node on the canvas, as the Agent block in the example plugin does.
shape: "wide",docs
Markdown shown in the block's Docs tab. Follow the page layout of the built-in blocks: a sentence, then When to use it, Inputs, Outputs, Example and How it behaves. A block with no docs gets a build warning.
docs: `Posts a message to a channel.
## When to use it
After a block that builds the text.
## Inputs
| Name | Required | Default |
| :--- | :--- | :--- |
| Channel | Yes | |`,The Docs tab shows a small subset of markdown. The build warns about the rest:
- Use headings, paragraphs, lists, tables, code fences and the callouts
::: info,::: tip,::: warningand::: danger. - Do not use raw HTML, images,
{#id}on headings, or[text](#anchor)links. The tab opens every link in a new tab, so an in-page link goes nowhere.
invoke
The ways a canvas may call the block. With one mode the user gets no choice; with more, the block's settings show an Execution Mode dropdown.
| Mode | The caller |
|---|---|
sync | Waits for the block, and uses its output |
async | Starts it on this worker and carries on |
queued | Puts it on a queue for a worker to pick up, and carries on |
async and queued calls pass their input and params as JSON, and the caller never learns the outcome. The handles page lists the combinations Fluxify refuses.
invoke: ["sync"], // the output holds a function, which only a sync call can pass onpermissions
What ctx may reach, beyond what every block gets. A creator sees it when approving the plugin, and the approval covers exactly what you ask for. Context lists them.
permissions: { integrations: ["database"], appconfig: true },params
The settings the user fills in, typed from the declaration so params in run has the right types. See params.
params: {
channel: param.string({ label: "Channel", required: true }),
silent: param.boolean({ default: false }),
},tabs
The settings tabs in order. Without it, tabs appear in order of first use with General first. A param's tab that tabs leaves out is added after the listed ones, and the build warns about it.
tabs: ["General", "Advanced"],handles
Inputs, outputs and nested chains. See handles.
handles: { out: [handle.out("success"), handle.out("failure")] },run
async run({ params, input, ctx, executeHandle }) {
return { output: params.channel, handle: "success" };
},| Argument | What it is |
|---|---|
params | The settings, typed from params. Defaults are filled in, required ones are checked, and js: values are already evaluated (params) |
input | The previous block's output |
ctx | The block's context (context) |
executeHandle | Runs the chains on the block's body handles (handles) |
run returns an object, or nothing:
| Return | Meaning |
|---|---|
{ output } | output goes to the next block |
{ output, handle } | The same, leaving by a declared output handle. The default is the first one |
| nothing | The output is undefined |
Returning a handle the block did not declare fails the run with an error that lists the handles it has.
Errors
A block fails by throwing. Where the failure goes depends on its outputs:
| Outputs | A throw |
|---|---|
[success, failure] | Leaves by failure with the error's message as its output, if something is wired to failure. Otherwise it fails the route |
[source] or [] | Fails the route |
Returning { handle: "failure", output } yourself takes the same path without a throw.
A missing required param, or a value of the wrong type, is an error thrown before your code runs, so it is routed like any other throw. A js: expression that throws fails the route before the block is called, and never goes to failure.
async run({ params, ctx }) {
const res = await ctx.vars.httpClient.get(params.url);
if (res.status !== 200) throw new Error(`the server answered ${res.status}`);
return { output: res.data, handle: "success" };
},Deprecation
Wrap the finished block in deprecated(...) with a message for the user. The message is shown as written, so say what to use instead and when the block goes away. Publishing covers when to use it.
import { block, deprecated } from "@fluxify/sdk/blocks";
export default deprecated(
block({ name: "acme/slack/post", label: "Post (old)", async run() {} }),
"Removed in the Jan 2027 release; use `acme/slack/messages/post`.",
);deprecated takes a param or a handle in the same way. See params and handles.
A complete block
This is the Tool block of the agent-tools example. It uses a param, a body handle and a late executeHandle call, all covered in the next pages.
// source: examples/plugins/agent-tools/src/blocks/tool.ts
export default block({
name: "fluxify-examples/agent-tools/tool",
label: "Tool",
description: "A tool an Agent block can call. Wire what it does to its Execute handle.",
shape: "square",
// the output holds a function, which only a sync call can pass on
invoke: ["sync"],
params: {
name: param.string({ label: "Name", required: true }),
description: param.string({ label: "Description", default: "" }),
},
handles: {
out: [],
body: [handle.body("execute", "bottom-right")],
},
async run({ params, executeHandle }) {
return {
output: {
name: params.name,
description: params.description,
// runs after this `run` returned, while the agent's run is still going
call: (args: unknown) => executeHandle("execute", args),
},
};
},
});