AutomationFlowsMarketing & Ads › Multi-channel Campaign Orchestrator

Multi-channel Campaign Orchestrator

Multi-Channel Campaign Orchestrator. Event-driven trigger; 11 nodes.

Event trigger★★★★☆ complexity11 nodes
Marketing & Ads Trigger: Event Nodes: 11 Complexity: ★★★★☆ Added:

The workflow JSON

Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →

Download .json
{
  "name": "Multi-Channel Campaign Orchestrator",
  "nodes": [
    {
      "parameters": {},
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        -800,
        1152
      ],
      "id": "2372a22f-83a7-4f44-8cd8-b2c8372804ab",
      "name": "Manual Trigger (dev)"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "c1b5fd3e-aaaa-4a3c-9d01-111111111111",
              "name": "name",
              "value": "Q2 Product Launch",
              "type": "string"
            },
            {
              "id": "c1b5fd3e-a001-4a3c-9d01-111111111111",
              "name": "campaign_id",
              "value": "q2-product-launch-2025",
              "type": "string"
            },
            {
              "id": "c1b5fd3e-bbbb-4a3c-9d01-111111111111",
              "name": "audience_segment",
              "value": "existing-customers",
              "type": "string"
            },
            {
              "id": "c1b5fd3e-cccc-4a3c-9d01-111111111111",
              "name": "start_date",
              "value": "2025-04-01",
              "type": "string"
            },
            {
              "id": "c1b5fd3e-dddd-4a3c-9d01-111111111111",
              "name": "end_date",
              "value": "2025-04-30",
              "type": "string"
            },
            {
              "id": "c1b5fd3e-eeee-4a3c-9d01-111111111111",
              "name": "budget_usd",
              "value": "25000",
              "type": "string"
            },
            {
              "id": "c1b5fd3e-ffff-4a3c-9d01-111111111111",
              "name": "channels",
              "value": "{\"email\": true, \"ads\": true, \"crm\": true, \"internal_comms\": true}",
              "type": "string"
            },
            {
              "id": "c1b5fd3e-9999-4a3c-9d01-111111111111",
              "name": "goals",
              "value": "{\"pipeline_created_usd\": 150000, \"signups\": 500}",
              "type": "string"
            },
            {
              "id": "c1b5fd3e-a002-4a3c-9d01-111111111111",
              "name": "simulate_failures",
              "value": "{}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        -592,
        1152
      ],
      "id": "9f784548-91f2-455d-86c1-e01101deb4ac",
      "name": "Set Campaign"
    },
    {
      "parameters": {
        "jsCode": "const item = items[0].json;\n\nconst required = ['campaign_id', 'name', 'audience_segment', 'start_date', 'end_date', 'budget_usd'];\nfor (const field of required) {\n  if (item[field] === undefined || item[field] === null || item[field] === '') {\n    throw new Error(`Missing required field: ${field}`);\n  }\n}\n\nconst budget = Number(item.budget_usd);\nif (Number.isNaN(budget) || budget <= 0) {\n  throw new Error('budget_usd must be a positive number');\n}\n\nlet channels = {};\nlet goals = {};\nlet simulate_failures = {};\ntry {\n  channels = item.channels ? JSON.parse(item.channels) : {};\n} catch (e) {\n  throw new Error('channels must be valid JSON');\n}\n\ntry {\n  goals = item.goals ? JSON.parse(item.goals) : {};\n} catch (e) {\n  throw new Error('goals must be valid JSON');\n}\n\ntry {\n  simulate_failures = item.simulate_failures ? JSON.parse(item.simulate_failures) : {};\n} catch (e) {\n  throw new Error('simulate_failures must be valid JSON');\n}\n\nconst enabledChannels = Object.entries(channels).filter(([, enabled]) => Boolean(enabled));\nif (enabledChannels.length === 0) {\n  throw new Error('At least one campaign channel must be enabled');\n}\n\nreturn [{\n  json: {\n    ...item,\n    budget_usd: budget,\n    channels,\n    goals,\n    simulate_failures,\n  },\n}];\n"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -384,
        1152
      ],
      "id": "a1bb8645-c23a-43c3-b8c0-812d3f6de37f",
      "name": "Validate & Normalise"
    },
    {
      "parameters": {
        "jsCode": "const campaign = { ...items[0].json };\nconst channel = 'email';\nconst startedAt = new Date().toISOString();\n\ntry {\n  if (!campaign.channels?.email) {\n    return [{ json: { ...campaign, channel_results: { ...campaign.channel_results, email: { channel, status: 'skipped', enabled: false, reason: 'Channel disabled' } } } }];\n  }\n\n  if (campaign.simulate_failures?.email) {\n    throw new Error('Simulated email platform failure');\n  }\n\n  const result = {\n    channel,\n    enabled: true,\n    status: 'success',\n    idempotency_key: campaign.control.idempotency_keys.email,\n    retry_policy: campaign.control.retry_policy,\n    started_at: startedAt,\n    completed_at: new Date().toISOString(),\n    entity: {\n      segmentId: 'seg_placeholder',\n      campaignId: 'cmp_email_placeholder',\n      status: 'draft'\n    }\n  };\n\n  return [{ json: { ...campaign, email: result.entity, channel_results: { ...campaign.channel_results, email: result } } }];\n} catch (error) {\n  const result = {\n    channel,\n    enabled: true,\n    status: 'failed',\n    idempotency_key: campaign.control?.idempotency_keys?.email,\n    retry_policy: campaign.control?.retry_policy,\n    started_at: startedAt,\n    completed_at: new Date().toISOString(),\n    error: error.message\n  };\n  return [{ json: { ...campaign, email: { enabled: false, error: error.message }, channel_results: { ...campaign.channel_results, email: result } } }];\n}\n"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        160,
        960
      ],
      "id": "fa1cb0f6-52c1-48cd-a6d6-24d1efc90321",
      "name": "Setup Email Campaign",
      "notes": "Demo-safe email branch. Catches channel failures and returns a structured partial-success/failed status instead of hiding other branch results. In production, replace with connector/API nodes using the supplied idempotency key and retry policy."
    },
    {
      "parameters": {
        "jsCode": "const campaign = { ...items[0].json };\nconst channel = 'ads';\nconst startedAt = new Date().toISOString();\n\ntry {\n  if (!campaign.channels?.ads) {\n    return [{ json: { ...campaign, channel_results: { ...campaign.channel_results, ads: { channel, status: 'skipped', enabled: false, reason: 'Channel disabled' } } } }];\n  }\n\n  if (campaign.simulate_failures?.ads) {\n    throw new Error('Simulated ads API failure or rate limit');\n  }\n\n  const result = {\n    channel,\n    enabled: true,\n    status: 'success',\n    idempotency_key: campaign.control.idempotency_keys.ads,\n    retry_policy: campaign.control.retry_policy,\n    started_at: startedAt,\n    completed_at: new Date().toISOString(),\n    entity: {\n      metaAdSetId: 'adset_meta_placeholder',\n      googleCampaignId: 'gads_placeholder',\n      budget_usd: campaign.budget_usd\n    }\n  };\n\n  return [{ json: { ...campaign, ads: result.entity, channel_results: { ...campaign.channel_results, ads: result } } }];\n} catch (error) {\n  const result = {\n    channel,\n    enabled: true,\n    status: 'failed',\n    idempotency_key: campaign.control?.idempotency_keys?.ads,\n    retry_policy: campaign.control?.retry_policy,\n    started_at: startedAt,\n    completed_at: new Date().toISOString(),\n    error: error.message\n  };\n  return [{ json: { ...campaign, ads: { enabled: false, error: error.message }, channel_results: { ...campaign.channel_results, ads: result } } }];\n}\n"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        160,
        1184
      ],
      "id": "9157d998-b758-46aa-85bc-272a147fa265",
      "name": "Setup Ads",
      "notes": "Demo-safe ads branch. Catches channel failures and returns a structured partial-success/failed status instead of hiding other branch results. In production, replace with connector/API nodes using the supplied idempotency key and retry policy."
    },
    {
      "parameters": {
        "jsCode": "const campaign = { ...items[0].json };\nconst channel = 'crm';\nconst startedAt = new Date().toISOString();\n\ntry {\n  if (!campaign.channels?.crm) {\n    return [{ json: { ...campaign, channel_results: { ...campaign.channel_results, crm: { channel, status: 'skipped', enabled: false, reason: 'Channel disabled' } } } }];\n  }\n\n  if (campaign.simulate_failures?.crm) {\n    throw new Error('Simulated CRM API failure');\n  }\n\n  const result = {\n    channel,\n    enabled: true,\n    status: 'success',\n    idempotency_key: campaign.control.idempotency_keys.crm,\n    retry_policy: campaign.control.retry_policy,\n    started_at: startedAt,\n    completed_at: new Date().toISOString(),\n    entity: {\n      campaignId: 'crm_campaign_placeholder',\n      taggedAccounts: 120\n    }\n  };\n\n  return [{ json: { ...campaign, crm: result.entity, channel_results: { ...campaign.channel_results, crm: result } } }];\n} catch (error) {\n  const result = {\n    channel,\n    enabled: true,\n    status: 'failed',\n    idempotency_key: campaign.control?.idempotency_keys?.crm,\n    retry_policy: campaign.control?.retry_policy,\n    started_at: startedAt,\n    completed_at: new Date().toISOString(),\n    error: error.message\n  };\n  return [{ json: { ...campaign, crm: { enabled: false, error: error.message }, channel_results: { ...campaign.channel_results, crm: result } } }];\n}\n"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        160,
        1408
      ],
      "id": "57c482d6-0d7c-4ff0-9b7d-c41e29cf8ed0",
      "name": "Setup CRM Campaign",
      "notes": "Demo-safe crm branch. Catches channel failures and returns a structured partial-success/failed status instead of hiding other branch results. In production, replace with connector/API nodes using the supplied idempotency key and retry policy."
    },
    {
      "parameters": {
        "jsCode": "const campaign = { ...items[0].json };\nconst channel = 'internal_comms';\nconst startedAt = new Date().toISOString();\n\ntry {\n  if (!campaign.channels?.internal_comms) {\n    return [{ json: { ...campaign, channel_results: { ...campaign.channel_results, internal_comms: { channel, status: 'skipped', enabled: false, reason: 'Channel disabled' } } } }];\n  }\n\n  if (campaign.simulate_failures?.internal_comms) {\n    throw new Error('Simulated internal comms posting failure');\n  }\n\n  const result = {\n    channel,\n    enabled: true,\n    status: 'success',\n    idempotency_key: campaign.control.idempotency_keys.internal_comms,\n    retry_policy: campaign.control.retry_policy,\n    started_at: startedAt,\n    completed_at: new Date().toISOString(),\n    entity: {\n      slackChannel: '#campaigns',\n      messageLink: 'https://your-workspace.slack.com/archives/campaign_message_placeholder'\n    }\n  };\n\n  return [{ json: { ...campaign, internalComms: result.entity, channel_results: { ...campaign.channel_results, internal_comms: result } } }];\n} catch (error) {\n  const result = {\n    channel,\n    enabled: true,\n    status: 'failed',\n    idempotency_key: campaign.control?.idempotency_keys?.internal_comms,\n    retry_policy: campaign.control?.retry_policy,\n    started_at: startedAt,\n    completed_at: new Date().toISOString(),\n    error: error.message\n  };\n  return [{ json: { ...campaign, internalComms: { enabled: false, error: error.message }, channel_results: { ...campaign.channel_results, internal_comms: result } } }];\n}\n"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        160,
        1632
      ],
      "id": "98ce6bb9-b554-465e-a804-94e81d11b737",
      "name": "Notify Internal Comms",
      "notes": "Demo-safe internal_comms branch. Catches channel failures and returns a structured partial-success/failed status instead of hiding other branch results. In production, replace with connector/API nodes using the supplied idempotency key and retry policy."
    },
    {
      "parameters": {
        "mode": "combine",
        "combinationMode": "mergeByPosition",
        "options": {}
      },
      "type": "n8n-nodes-base.merge",
      "typeVersion": 2,
      "position": [
        448,
        1024
      ],
      "id": "5f6ad41b-e441-4960-a545-979f002d4440",
      "name": "Merge Email + Ads"
    },
    {
      "parameters": {
        "mode": "combine",
        "combinationMode": "mergeByPosition",
        "options": {}
      },
      "type": "n8n-nodes-base.merge",
      "typeVersion": 2,
      "position": [
        704,
        1184
      ],
      "id": "bf1fcc03-5a39-40c0-992e-469bf3c81608",
      "name": "Merge Campaign + CRM"
    },
    {
      "parameters": {
        "mode": "combine",
        "combinationMode": "mergeByPosition",
        "options": {}
      },
      "type": "n8n-nodes-base.merge",
      "typeVersion": 2,
      "position": [
        960,
        1392
      ],
      "id": "0fbb29b4-b5e9-4e27-8238-12c045a19823",
      "name": "Merge Full Campaign"
    },
    {
      "parameters": {
        "jsCode": "const c = items[0].json;\nconst results = c.channel_results ?? {};\nconst channelOrder = [\n  ['email', 'Email'],\n  ['ads', 'Ads'],\n  ['crm', 'CRM'],\n  ['internal_comms', 'Internal comms'],\n];\n\nconst counts = { success: 0, failed: 0, skipped: 0 };\nfor (const [key] of channelOrder) {\n  const status = results[key]?.status ?? 'missing';\n  if (counts[status] !== undefined) counts[status] += 1;\n}\n\nconst overall_status = counts.failed > 0\n  ? (counts.success > 0 ? 'partial_success' : 'failed')\n  : 'success';\n\nconst lines = [];\nlines.push(`# Campaign: ${c.name}`);\nlines.push('');\nlines.push(`Campaign ID: ${c.control?.campaign_id ?? c.campaign_id}`);\nlines.push(`Audience: ${c.audience_segment}`);\nlines.push(`Dates: ${c.start_date} \u2192 ${c.end_date}`);\nlines.push(`Budget: $${c.budget_usd}`);\nlines.push(`Overall status: ${overall_status}`);\nlines.push('');\nlines.push('## Channel status');\n\nfor (const [key, label] of channelOrder) {\n  const result = results[key];\n  if (!result) {\n    lines.push(`- ${label}: missing result`);\n    continue;\n  }\n\n  if (result.status === 'success') {\n    lines.push(`- ${label}: success (idempotency key: ${result.idempotency_key})`);\n  } else if (result.status === 'failed') {\n    lines.push(`- ${label}: failed (${result.error})`);\n  } else if (result.status === 'skipped') {\n    lines.push(`- ${label}: skipped (${result.reason})`);\n  } else {\n    lines.push(`- ${label}: ${result.status}`);\n  }\n}\n\nlines.push('');\nlines.push('## Control notes');\nlines.push(`- Retry policy: ${c.control?.retry_policy?.max_attempts ?? 3} attempts with ${c.control?.retry_policy?.backoff ?? 'exponential'} backoff for transient API failures.`);\nlines.push('- Idempotency keys are generated per campaign/channel to avoid duplicate creation after retries.');\nlines.push('- Failed branches are captured in the final summary instead of masking successful branches.');\n\nconst summary_markdown = lines.join('\\n');\n\nreturn [{\n  json: {\n    ...c,\n    overall_status,\n    summary_markdown,\n  },\n}];\n"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1200,
        1392
      ],
      "id": "fd6db943-3001-4146-9990-d565adececc1",
      "name": "Build Summary"
    },
    {
      "parameters": {
        "jsCode": "const campaign = { ...items[0].json };\nconst generatedAt = new Date().toISOString();\nconst campaignId = String(campaign.campaign_id).trim();\n\nconst branchNames = ['email', 'ads', 'crm', 'internal_comms'];\nconst idempotencyKeys = Object.fromEntries(\n  branchNames.map(channel => [channel, `${campaignId}:${channel}:${campaign.start_date}`])\n);\n\nconst retryPolicy = {\n  max_attempts: 3,\n  backoff: 'exponential',\n  retry_on: ['429', '408', '5xx', 'network_timeout'],\n  notes: 'Use this policy on real HTTP/native connector nodes for transient external API failures.'\n};\n\nreturn [{\n  json: {\n    ...campaign,\n    control: {\n      campaign_id: campaignId,\n      generated_at: generatedAt,\n      idempotency_keys: idempotencyKeys,\n      retry_policy: retryPolicy,\n    },\n    channel_results: {},\n  },\n}];\n"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -192,
        1152
      ],
      "id": "d4ba586e-744c-4292-b371-acaa503d6798",
      "name": "Prepare Campaign Control",
      "notes": "Adds campaign-level control metadata: campaign ID, per-channel idempotency keys, and a documented retry policy for production connector/API nodes."
    },
    {
      "parameters": {
        "jsCode": "const source = items[0]?.json ?? {};\nconst auditRecord = {\n  audit_type: 'campaign_orchestration_result',\n  logged_at: new Date().toISOString(),\n  campaign_id: source.control?.campaign_id ?? source.campaign_id,\n  campaign_name: source.name,\n  overall_status: source.overall_status,\n  channel_results: source.channel_results,\n  idempotency_keys: source.control?.idempotency_keys,\n  retry_policy: source.control?.retry_policy,\n  summary_markdown: source.summary_markdown,\n  source_payload: {\n    audience_segment: source.audience_segment,\n    start_date: source.start_date,\n    end_date: source.end_date,\n    budget_usd: source.budget_usd,\n    channels: source.channels,\n    goals: source.goals,\n  },\n};\n\nreturn [{\n  json: {\n    ...source,\n    audit_record: auditRecord,\n    audit_status: 'ready_for_durable_store',\n  },\n}];\n"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1440,
        1392
      ],
      "id": "1acf933f-768d-4dc8-8463-929e9b63837d",
      "name": "Log Final Output (demo)",
      "notes": "Demo audit checkpoint. In production replace or extend with a database, object storage, Google Drive, Notion, Sheets, or ticketing node."
    }
  ],
  "connections": {
    "Manual Trigger (dev)": {
      "main": [
        [
          {
            "node": "Set Campaign",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set Campaign": {
      "main": [
        [
          {
            "node": "Validate & Normalise",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate & Normalise": {
      "main": [
        [
          {
            "node": "Prepare Campaign Control",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Setup Email Campaign": {
      "main": [
        [
          {
            "node": "Merge Email + Ads",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Setup Ads": {
      "main": [
        [
          {
            "node": "Merge Email + Ads",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Merge Email + Ads": {
      "main": [
        [
          {
            "node": "Merge Campaign + CRM",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Setup CRM Campaign": {
      "main": [
        [
          {
            "node": "Merge Campaign + CRM",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Merge Campaign + CRM": {
      "main": [
        [
          {
            "node": "Merge Full Campaign",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Notify Internal Comms": {
      "main": [
        [
          {
            "node": "Merge Full Campaign",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Merge Full Campaign": {
      "main": [
        [
          {
            "node": "Build Summary",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Campaign Control": {
      "main": [
        [
          {
            "node": "Setup Email Campaign",
            "type": "main",
            "index": 0
          },
          {
            "node": "Setup Ads",
            "type": "main",
            "index": 0
          },
          {
            "node": "Setup CRM Campaign",
            "type": "main",
            "index": 0
          },
          {
            "node": "Notify Internal Comms",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Summary": {
      "main": [
        [
          {
            "node": "Log Final Output (demo)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1",
    "binaryMode": "separate",
    "availableInMCP": false
  },
  "versionId": "967b4024-3ef0-4b39-a5b0-0387c14576e5",
  "id": "4f5lLTpBvQrig4x7",
  "tags": []
}
Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

Multi-Channel Campaign Orchestrator. Event-driven trigger; 11 nodes.

Source: https://github.com/tfest-dev/n8n-marketing/blob/main/multi-channel_campaign_orchestrator.json — original creator credit. Request a take-down →

More Marketing & Ads workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

Marketing & Ads

⚡AI-Powered YouTube Playlist & Video Summarization and Analysis v2. Uses httpRequest, n8n-nodes-youtube-transcription-dmr, redis. Event-driven trigger; 73 nodes.

HTTP Request, N8N Nodes Youtube Transcription Dmr, Redis
Marketing & Ads

Lead Cleanup - SMS Errors & DNC. Uses httpRequest, slack. Event-driven trigger; 49 nodes.

HTTP Request, Slack
Marketing & Ads

Maximize your conversion rates with this end-to-end automated outreach and lead nurturing system. This workflow manages the entire sales lifecycle—from instant contact enrollment via WhatsApp to AI-pe

N8N Nodes Wati, Airtable, HTTP Request
Marketing & Ads

This workflow is designed to take user inputs in order to generate an image using the Riverflow 2.0 model through the Replicate API. It can handle both image generation as well as image editing. Addit

Form Trigger, Data Table, HTTP Request +1
Marketing & Ads

Lead Gen -> Outreach -> Reply Digest. Uses googleSheets, httpRequest, emailSend, emailReadImap. Event-driven trigger; 36 nodes.

Google Sheets, HTTP Request, Email Send +1