Documentation
Everything you need to write a step, describe its fields and publish it.
A plugin is one step somebody can drop on a canvas. It is a Node module exporting an execute function, and a manifest.json beside it saying which fields the canvas should render and what the step hands on.
In the developer portal. The editor there knows the whole runtime: hover any part of context for its documentation, and it completes context.storage., context.state. and the rest from the same declarations this page is generated from.
A command-line tool — login, init, test, push, with a local runner that gives your plugin a working storage and state to test against — is built and not published yet.
import type { PluginModule } from "./visualautomate"
const plugin: PluginModule = {
async execute(input, config, context) {
// input what the steps before this one produced
// config the fields somebody filled in on the canvas
// context storage, state, media, connections, and the run's ids
const bytes = await context.storage.get(config.source as string)
if (!bytes) {
return { output: "error", data: { error: "No such file." } }
}
// `output` names which port the run leaves by, so it decides which
// line on the canvas is followed. `data` is what the next step reads.
return { output: "success", data: { size: bytes.length } }
},
}
export default pluginpropertiesobjectrequiredConfigurable node properties shown in the properties panel
inputsstring[]Input port names. Default: ["input"]
outputsstring[]Output port names. Default: ["success", "error"]
outputFieldsobjectWhat the step returns, for the variable picker: a description per field, or { type, description, fields } so the picker can show a list and its fields before the step has run. Types: text, number, true/false, list, object
iconstringIcon name for the node (e.g., 'globe', 'database')
colorstringAccent color for the node (hex, e.g., '#3b82f6')
timeoutMsnumberMax execution time in ms (default: 10000, max: 60000)
memoryLimitMbnumberMax memory in MB (default: 32, max: 128)
descriptionstringDescription shown in the plugin browser
stringText input. Supports pattern validation and default values.
numberNumeric input with min/max validation.
booleanToggle switch for true/false values.
selectDropdown with predefined options.
multiselectMulti-value selector with predefined options.
jsonJSON editor for structured data (objects, arrays).
colorColor picker input.
dateDate picker input.
The execute function receives three arguments:
What every earlier step on the route produced, keyed by its step name: input.get_customer.email. One predecessor is still read by name.
User-configured properties from the @schema. Key-value pairs matching your property names.
Workflow metadata: workflowId, nodeId, userId, secrets, executionId.
Your function must return an object with output (the port name) and data (the result payload):
// Success output
return {
output: "success",
data: { message: "Hello!", count: 42 }
};
// Error output
return {
output: "error",
data: { errorMessage: "Something went wrong" }
};In a Code step you write only the body of that function, and the return is forgiven: return anything and it becomes the step's data on the success port. Return an object and each key is a field the next step can read on its own, as {{step_name.data.field}}. Throwing fails the step with the message on the run, and console.log writes to the terminal beside the canvas.
Inside a loop, the turn's element is input.item, with input.index and input.count; where a loop ends, what every turn produced is input.collected.
The ordinary language is there — fetch, JSON, crypto, URL, TextEncoder, timers, and the rest. What is worth reading is what the platform adds:
context.workflowIdThe workflow this run belongs to.
context.nodeIdThis step's id on the canvas.
context.userIdWhose run it is.
context.executionIdThis run, distinct from every other run of the workflow.
context.startedAtWhen the run started, as an ISO string.
context.secretsValues the plugin was published with. Never the user's credentials.
context.storage.put(path, data, contentType?) => Promise<StorageResult>Write a file to the account's storage. Data may be a string or bytes.
context.storage.get(path) => Promise<Uint8Array | null>Read a file back. null when it is not there.
context.storage.delete(path) => Promise<void>Remove a file.
context.storage.list(prefix?) => Promise<{ files, totalSize }>Everything under a prefix, or everything.
context.storage.head(path) => Promise<{ size, contentType } | null>Size and content type without moving the bytes.
context.storage.signedUrl(path, expiresInSeconds?) => Promise<string | null>A link somebody else can fetch the file from, for a few minutes. For APIs that pull rather than take bytes.
context.storage.download(url, path, contentType?) => Promise<StorageResult>Pull a URL straight into storage. The bytes never cross this step, so a large file costs the run nothing.
Only when the download Worker is configured on the server.
context.state.get(key) => Promise<unknown>A value this workflow stored on an earlier run. null when never set.
context.state.set(key, value, { ttlSeconds }?) => Promise<void>Remember something between runs — a cursor, a last-seen id, a counter.
context.state.delete(key) => Promise<void>Forget one key.
context.state.list(prefix?) => Promise<{ keys }>Which keys this workflow has stored.
context.media.run(operation, params) => Promise<Record<string, unknown>>ffmpeg, by operation name. A plugin never builds a command line — the server does, one operation at a time.
Only when the sandbox and storage are both configured.
context.connections.get(propertyName) => Promise<{ id, request }>One of the accounts this step was configured with. Takes your property name, not a connection id.
Only when the step declares a `connection` property and an account was chosen.
context.connections.get(...).request({ path, method?, query?, headers?, body? }) => Promise<{ ok, status, body }>A request to the provider, with the credential attached on the way past. Relative paths only — never a host, and there is no getToken().
Your code runs with no process, fs, child_process, os, cluster, eval, Function or __dirname, and require resolves nothing but the packages listed above. Reaching for any of them is also refused when you publish.
This is a sandbox, not a jail: every run gets a container of its own that is destroyed when the run ends, and that — rather than the list above — is what keeps one plugin out of anything that is not its own.
Once your plugin is working correctly in the test environment:
outputFields in your manifest, so whoever puts a step after yours can pick your fields from a list instead of guessing themPushing publishes to your own account and nobody else's. Review is only about the marketplace, so you can build and use a plugin in your own workflows without waiting for anyone.
After review and approval, your plugin becomes available in the VisualAutomate node browser for all users.