Workflow actions
A workflow is a chain of steps that runs by itself, for example “when an article is published, send an email to the team”. Each step that does something is an action. Manablox brings actions for email, HTTP requests, AI and content. With a plugin you can add your own, and it shows up in the workflow editor like a built-in one.
An action has two halves:
- A description: its name, icon, and the settings form editors fill in. The admin draws the form from this description, so your action works with the admin that comes with your project.
- A handler: the code that runs on the server when a workflow reaches the step.
Example: post a message to Slack
Section titled “Example: post a message to Slack”This action sends a message to a Slack channel through Slack’s chat.postMessage API. It needs a Slack bot token (created in your Slack workspace’s app settings, it starts with xoxb-), which editors store once as a credential in the admin.
- Create a file called
slack-action.tsnext tocontent-model.ts:
import { definePlugin } from '@manablox/core';import { defineWorkflowAction } from '@manablox/core/node';
interface SlackConfig extends Record<string, unknown> { channel: string; text: string;}
export const slackMessage = defineWorkflowAction<SlackConfig>({ type: 'slack.message', label: 'Post to Slack', description: 'Posts a message to a Slack channel.', icon: 'message-square', tone: 'violet', group: 'notify',
inputs: [{ name: 'in', label: 'Anything', type: 'any' }], ports: [], output: { name: 'ok', label: 'The message', type: 'json' }, outputPaths: [{ path: 'ts', type: 'text', hint: 'The id Slack gave the message' }],
// The node asks for a credential of this kind. credential: { kinds: ['bearer'], required: true },
// The settings form in the workflow editor. fields: [ { name: 'channel', label: 'Channel', kind: 'text', required: true, placeholder: '#news' }, { name: 'text', label: 'Message', kind: 'templateArea', rows: 4, required: true }, ],
defaults: () => ({ channel: '', text: 'Just published: {{ content.title }}' }),
// Tidies the settings when the workflow is saved. validate: (config) => ({ ...config, channel: config.channel.trim() }),
async execute(ctx) { const response = await ctx.fetch('https://slack.com/api/chat.postMessage', { method: 'POST', headers: { 'content-type': 'application/json; charset=utf-8', authorization: `Bearer ${ctx.credential?.data.token ?? ''}`, }, body: JSON.stringify({ channel: ctx.config.channel, text: ctx.render(ctx.config.text) }), signal: ctx.signal, }); const answer = (await response.json()) as { ok: boolean; error?: string; ts?: string }; if (!answer.ok) throw new Error(`Slack said no: ${answer.error ?? response.status}`);
return { kind: 'ok', message: `Posted to ${ctx.config.channel}`, output: { ts: answer.ts } }; },});
export const slackPlugin = definePlugin({ name: 'slack', workflowActions: [slackMessage],});defineWorkflowAction is imported from @manablox/core/node rather than from
@manablox/core: the part of the action that runs only on the server lives there.
- Add
slackPluginto thepluginslist incontent-model.ts:
import type { ManabloxConfig } from '@manablox/core';import { manabloxFields } from '@manablox/fields';import { slackPlugin } from './slack-action.ts';
export const plugins: NonNullable<ManabloxConfig['plugins']> = [manabloxFields(), slackPlugin];
export const contentTypes: NonNullable<ManabloxConfig['contentTypes']> = [];- Save.
pnpm devrestarts by itself. - In the admin, go to
Settings > Credentialsand add a credential of the kind “Bearer token”. Paste the Slack bot token as the token. - Open a workflow (or create one), and in the list of steps look under Tell someone: Post to Slack is there. You can also type “slack” into Search actions.
- Add it, pick your credential under Credential, enter a channel and adjust the message. Save the workflow.
When the workflow runs, the message appears in the channel, and the run log shows “Posted to #news”. If Slack refuses, the step fails with “Slack said no:” and Slack’s reason, for example not_in_channel when the bot has not been invited to the channel.
The description
Section titled “The description”| Key | What it is |
|---|---|
type | The action’s unique technical name, stored in every workflow that uses it. Keep it stable. Use your own prefix, like slack.message |
label, description | What the list of steps shows |
icon | An icon name the admin knows, for example message-square, blocks, bell |
tone | The colour of its tile: cyan, violet, pink, amber or plain |
group | Where it is listed: notify (Tell someone), data (Fetch and shape data), ai (AI), content (Content) or integration (Integrations) |
inputs | What the step expects to receive. Only a hint for the editor |
ports | Extra exits besides “Succeeded” and “Failed”, which every action has. Usually [] |
output, outputPaths | What the step hands to later steps, and which parts of it the editor offers in its data picker |
credential | The credential kinds the step accepts and whether one is required, or null for none |
fields | The settings form, below |
defaults() | The settings a new step starts with |
validate(config) | Optional. Cleans up the settings when the workflow is saved and returns them |
isAvailable(manablox) | Optional. Returns { ok: false, reason } when this CMS cannot run the action. The step is then still listed, marked with the reason |
execute(ctx) | The handler, below |
The credential kinds are apiKey, bearer, basic, oauth2, smtp, signing and custom. Editors create credentials under Settings > Credentials; the node only offers credentials of the kinds you list.
The settings form
Section titled “The settings form”Each entry in fields becomes one input in the step’s settings. name is the key in ctx.config, label what the editor sees, and kind the type of input:
kind | Input |
|---|---|
text, password, number, textarea | Plain inputs |
template, templateArea | A line or a box where editors can use placeholders like {{ content.title }} |
switch | A yes or no switch |
select, multiselect | A choice from options, each { value, label } |
stringList | A list of short texts |
keyValue | Name and value pairs, for example headers |
json | A JSON document |
contentType, locale, role, user | A picker filled from the space |
More options per field: hint, placeholder, required, rows (for boxes), min, max and step (for numbers), width: 'half' to put two fields on one row, and showWhen: { field, equals } to show a field only while another field has one of the given values.
What the handler gets
Section titled “What the handler gets”execute receives ctx with everything the step needs:
Part of ctx | What it is |
|---|---|
ctx.config | The step’s settings, after validate |
ctx.render(text) | Fills in placeholders like {{ content.title }}. Run everything an editor typed through it |
ctx.run | What the run knows: content (the document), previous, space, actor, event, and nodes with the output of every earlier step |
ctx.fetch | Like fetch, but refuses private network addresses and limits redirects and response size. Use it instead of the global fetch |
ctx.signal | Pass it to every request, so the workflow’s time limit can stop your step |
ctx.credential | The credential the editor picked, decrypted, or null. Its values are in ctx.credential.data (a bearer token in data.token) and are hidden in the run log automatically |
ctx.secret(value) | Hides another value from the run log, for example a token you received |
ctx.logger | Writes to the CMS log |
ctx.workflow | The workflow’s id, name and spaceId |
What the handler returns
Section titled “What the handler returns”| Return | Meaning |
|---|---|
{ kind: 'ok', message, output } | Done. message is one line for the run log, output is what later steps read as {{ nodes.<key>.<path> }} |
{ kind: 'stop', message } | End this branch of the workflow quietly |
{ kind: 'wait', minutes } | Pause the run and continue it later |
Throwing an error fails the step. The workflow then takes the step’s “Failed” exit, or stops, depending on how it is drawn.
Good to know
Section titled “Good to know”- An action runs on the server in the admin’s process, where workflows run.
- If you give your action the same
typeas a built-in action, yours replaces the built-in one. - If you remove the plugin, workflows that use the action can no longer be saved and report “No action of that kind is installed here.” Put the plugin back or remove the step.
- A step can be given a custom settings component through an admin plugin, but that needs a custom admin build, which a project made with
manablox createcannot do. The generated form covers what the built-in actions need. - On a server, rebuild after changes, as described in Plugins.