Skip to content

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.

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.

  1. Create a file called slack-action.ts next to content-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.

  1. Add slackPlugin to the plugins list in content-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']> = [];
  1. Save. pnpm dev restarts by itself.
  2. In the admin, go to Settings > Credentials and add a credential of the kind “Bearer token”. Paste the Slack bot token as the token.
  3. 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.
  4. 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.

KeyWhat it is
typeThe action’s unique technical name, stored in every workflow that uses it. Keep it stable. Use your own prefix, like slack.message
label, descriptionWhat the list of steps shows
iconAn icon name the admin knows, for example message-square, blocks, bell
toneThe colour of its tile: cyan, violet, pink, amber or plain
groupWhere it is listed: notify (Tell someone), data (Fetch and shape data), ai (AI), content (Content) or integration (Integrations)
inputsWhat the step expects to receive. Only a hint for the editor
portsExtra exits besides “Succeeded” and “Failed”, which every action has. Usually []
output, outputPathsWhat the step hands to later steps, and which parts of it the editor offers in its data picker
credentialThe credential kinds the step accepts and whether one is required, or null for none
fieldsThe 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.

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:

kindInput
text, password, number, textareaPlain inputs
template, templateAreaA line or a box where editors can use placeholders like {{ content.title }}
switchA yes or no switch
select, multiselectA choice from options, each { value, label }
stringListA list of short texts
keyValueName and value pairs, for example headers
jsonA JSON document
contentType, locale, role, userA 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.

execute receives ctx with everything the step needs:

Part of ctxWhat it is
ctx.configThe step’s settings, after validate
ctx.render(text)Fills in placeholders like {{ content.title }}. Run everything an editor typed through it
ctx.runWhat the run knows: content (the document), previous, space, actor, event, and nodes with the output of every earlier step
ctx.fetchLike fetch, but refuses private network addresses and limits redirects and response size. Use it instead of the global fetch
ctx.signalPass it to every request, so the workflow’s time limit can stop your step
ctx.credentialThe 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.loggerWrites to the CMS log
ctx.workflowThe workflow’s id, name and spaceId
ReturnMeaning
{ 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.

  • An action runs on the server in the admin’s process, where workflows run.
  • If you give your action the same type as 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 create cannot do. The generated form covers what the built-in actions need.
  • On a server, rebuild after changes, as described in Plugins.