Utility sub-workflows are shared building blocks that multiple parent workflows can call.
Utility sub-workflows are shared building blocks that multiple parent workflows can call. Instead of duplicating the same Slack notification logic in 10 workflows, create one "Send Formatted Slack Alert" sub-workflow and call it from all of them. When you need to change the alert format, you update one workflow instead of ten.
Real-world example: A "Send Formatted Slack Alert" utility sub-workflow used by error handlers, monitoring workflows, and business process notifications.
Sub-workflow: "Slack - Send Formatted Alert - v2"
[Execute Sub-Workflow Trigger] → [Code: Build Block Kit] → [Slack: Send Message]
```text
The trigger receives a standardized input schema:
```json
{
"severity": "info | warning | error | critical",
"title": "Brief alert title",
"message": "Detailed description",
"source_workflow": "Name of the calling workflow",
"fields": [
{ "label": "Customer", "value": "jane@acme.com" },
{ "label": "Order ID", "value": "ORD-12345" }
],
"channel_override": null
}
```text
Code node that builds the Slack Block Kit message:
```javascript
const input = $json;
const severityConfig = {
info: { color: '#36a64f', icon: '[INFO]' },
warning: { color: '#ff9900', icon: '[WARNING]' },
error: { color: '#ff0000', icon: '[ERROR]' },
critical: { color: '#8b0000', icon: '[CRITICAL]' }
};
const config = severityConfig[input.severity] || severityConfig.info;
const fields = (input.fields || []).map(f => ({
type: 'mrkdwn',
text: `*${f.label}:*\n${f.value}`
}));
const blocks = [
{
type: 'header',
text: { type: 'plain_text', text: `${config.icon} ${input.title}` }
},
{
type: 'section',
text: { type: 'mrkdwn', text: input.message }
}
];
if (fields.length > 0) {
blocks.push({ type: 'section', fields: fields });
}
blocks.push({
type: 'context',
elements: [{
type: 'mrkdwn',
text: `Source: ${input.source_workflow} | ${new Date().toISOString()}`
}]
});
const channel = input.channel_override || process.env.SLACK_CHANNEL || '#alerts';
return [{ json: { channel, blocks, color: config.color } }];
```text
Calling the sub-workflow from any parent:
```json
// Execute Sub-Workflow node input mapping
{
"severity": "error",
"title": "Payment Processing Failed",
"message": "Stripe returned a declined card error for order ORD-12345.",
"source_workflow": "{{ $workflow.name }}",
"fields": [
{ "label": "Customer", "value": "{{ $json.customer_email }}" },
{ "label": "Error", "value": "{{ $json.stripe_error }}" },
{ "label": "Amount", "value": "${{ $json.amount }}" }
]
}
```text
> **Tip: Build a Utility Library**
>
> Common utility sub-workflows to create: Slack alert, email with template, error logger, data validation, API authentication refresh, rate limiter. Tag them all with a "utility" tag for easy discovery.
Reusable sub-workflows enforce consistency and reduce the maintenance surface area of your workflow system.
**Related:** [Always Set an Error Workflow on Every Production Workflow](../error-handling-and-reliability/01-always-set-an-error-workflow-on-every-production-workflow.md) | [Use "Pin Data" to Freeze Node Output](../testing-and-debugging/01-use-pin-data-to-freeze-node-output.md)
I build production n8n and Cloudflare automation for teams — the same engineering behind HarperFlow. Fixed-price, escrow-protected, US-based.