Tips > Building Workflows

Use Execute Sub-Workflow for Reusable Utility Workflows

Utility sub-workflows are shared building blocks that multiple parent workflows can call.

TipAdvanced2 min read

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)

Want this running in your stack?

I build production n8n and Cloudflare automation for teams — the same engineering behind HarperFlow. Fixed-price, escrow-protected, US-based.