Tutorial · Salesforce Marketing Cloud

SFMC Journey Builder Custom Activity Tutorial — Step-by-step with Node.js

A complete, production-shaped guide to building a Salesforce Marketing Cloud (SFMC) Journey Builder custom activity from scratch — lifecycle endpoints, JWT validation, Data Extensions, and a real Slack integration. No dropped context.

SFMC · Node.js · SlackRead time · 14 minUpdated · May 2026By Lalit Chaudhari
01 — Introduction

What you'll build, and why it works in production.

This guide explains how to build a Salesforce Marketing Cloud Journey Builder custom activity from scratch using Node.js, Express, and webhooks. By the end you'll have a deployable activity that registers as a first-class drag-and-drop step inside Journey Builder and reacts to every contact that flows through it.

The reference implementation throughout this article is a Slack notifier — when a contact reaches your custom activity in a journey, the system formats a templated message and posts it to a configured Slack channel in under two seconds.

02 — Concept

What is an SFMC Journey Builder custom activity?

A Journey Builder custom activity is an externally-hosted component that registers itself with Salesforce Marketing Cloud and exposes a set of HTTPS endpoints. Once installed, it appears in the Journey Builder palette and can be dragged onto any journey canvas — exactly like the native activities Salesforce ships.

It enables you to:

  • Send real-time contact data to external APIs
  • Trigger Slack, Teams, or PagerDuty alerts at journey checkpoints
  • Call internal services without going through middleware
  • Extend Journey Builder beyond the native activity catalogue
03 — Use case

A real Slack notifier built as a custom activity.

The reference scenario: a marketing ops team needs Slack alerts when a contact crosses specific points in a journey — form submission, churn-risk score jumps, high-value trigger events. The default SFMC stack has no native Slack step, and routing through a paid middleware tool added latency the team didn't want.

Lead enters journey

Sales channel gets a Slack ping with the contact's email, source, and lead score.

Churn risk detected

Customer success channel is alerted with the at-risk contact and recent activity summary.

High-value trigger

Account team channel gets context-rich Slack message with deal size and next best action.

04 — Architecture

Architecture overview.

The system has two halves: an externally hosted Node.js service that owns the activity's config UI and lifecycle endpoints, and the SFMC side where the activity is registered as an Installed Package and surfaces inside Journey Builder.

Step 01
Journey Builder
Drag activity onto canvas
Step 02
/save · /validate · /publish
Config lifecycle
Step 03
/execute (per contact)
JWT-signed POST
Step 04
Slack webhook
Templated message
  • Node.js + ExpressREST backend hosting endpoints + UI
  • JWT validationConfirms requests originate from SFMC
  • SFMC Installed PackageRegisters the activity in your BU
  • Journey Builder triggersCalls /execute per contact
  • Data ExtensionsHold contact + activity context
  • Slack Incoming WebhooksReal-time delivery
05 — How it works

The full lifecycle, in six honest steps.

  1. 01Build

    Build a custom activity backend in Node.js

    Stand up an Express server with /save, /validate, /publish, and /execute. Add JWT verification middleware on /execute.

  2. 02Define

    Define lifecycle endpoints in config.json

    Point each endpoint to its hosted URL. Set inArguments to the contact attributes you need at runtime.

  3. 03Register

    Register the package in Marketing Cloud

    Setup → Installed Packages → New → Add Component → Journey Builder Activity. Paste your hosted endpoint base URL.

  4. 04Drag

    Drag the activity into a Journey Builder canvas

    Once published, your activity shows up alongside native steps. Drop it where you want the side-effect.

  5. 05Configure

    Configure inputs per step

    Marketers set the Slack channel, message template, and any merge fields directly inside the activity config UI.

  6. 06Run

    Activate and run contacts through the journey

    Each contact that reaches the activity hits /execute. Your handler renders the Slack payload and posts to the webhook.

Journey Builderscreenshot
Salesforce Marketing Cloud Journey Builder canvas showing a Welcome Journey with Data Extension entry source, Email activities, Wait steps, Decision Split, SMS, and Push Notification — illustrating where a custom activity slots in alongside native steps.
Journey Builder canvas — the custom activity sits in the same palette as native Email, SMS, Wait, and Decision Split steps. Marketers drag it onto the canvas like any other activity.
06 — Endpoints

Lifecycle endpoints, explained line by line.

Journey Builder talks to your activity through four endpoints. Three are configuration endpoints called by the canvas UI; one — /execute — is the runtime endpoint called for every contact that reaches the activity.

POST /saveOn config save

Stores the marketer's configuration (Slack URL, channel, message template) on the activity instance.

POST /validateBefore publish

Last chance to reject misconfigured steps — return non-2xx and Journey Builder blocks the publish.

POST /publishOn journey activation

Fires once when the journey goes live. Use it for one-time setup like provisioning resources per journey.

POST /executePer contact

Runtime endpoint. Receives a JWT-signed body containing inArguments (config + contact data). Do the work here.

Reference implementation

A minimal Express server that wires all four endpoints. Persistence and logging are trimmed for clarity — replace the in-memory store with your database in production.

server.jsnode
// Express server hosting the four lifecycle endpoints
import express from "express";
import jwt from "jsonwebtoken";

const app = express();
app.use(express.json());
app.use(express.text({ type: "application/jwt" }));

// /save — persist activity config from the JB canvas
app.post("/save", (req, res) => res.json({ status: "saved" }));

// /validate — sanity-check config before publish
app.post("/validate", (req, res) => {
  const { webhookUrl, channel } = req.body.inArguments?.[0] ?? {};
  if (!webhookUrl || !channel) return res.status(400).json({ status: "invalid" });
  return res.json({ status: "ok" });
});

// /publish — fired once when the journey is activated
app.post("/publish", (_, res) => res.json({ status: "published" }));

// /execute — fires per contact, JWT-signed payload
app.post("/execute", verifyJwt, async (req, res) => {
  const { inArguments } = req.body;
  const { webhookUrl, channel, message, contact } = inArguments[0];

  await fetch(webhookUrl, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ channel, text: render(message, contact) }),
  });

  return res.json({ status: "executed" });
});

app.listen(3000);
Tip · Idempotency
Journey Builder may retry /execute on a non-2xx response. Make the handler idempotent by keying side effects on the contact's activityInstanceIdso retries don't double-post Slack messages.
07 — Webhook flow

What happens when a contact hits the activity.

At runtime, SFMC POSTs a JWT to your /execute endpoint containing the contact payload and the configuration the marketer chose for this step. Your handler verifies the JWT, formats the Slack payload, and posts it.

Step 01
Contact reaches step
Journey Builder runtime
Step 02
POST /execute
JWT body, contact + config
Step 03
Verify + render
JWT signature, merge fields
Step 04
POST to Slack
Incoming webhook URL
Step 05
200 OK
Contact moves to next step
08 — Security

JWT authentication — confirm the call is really from SFMC.

Marketing Cloud signs every /execute payload with the JWT signing secret from your Installed Package. Your service must verify that signature before doing anything with the body.

middleware/verify-jwt.jsnode
import jwt from "jsonwebtoken";

export function verifyJwt(req, res, next) {
  const token = typeof req.body === "string" ? req.body : "";
  try {
    req.body = jwt.verify(token, process.env.SFMC_JWT_SECRET, {
      algorithms: ["HS256"],
    });
    next();
  } catch (err) {
    res.status(401).json({ error: "invalid_jwt" });
  }
}
Required · Installed Package secret
The signing secret lives under Setup → Installed Packages → your package → JWT Signing Secret. Rotate it periodically and store it in a secret manager — never check it into the repo.
09 — Data Extensions

Data Extensions and merge fields.

Data Extensions are how Marketing Cloud carries contact context through a journey. The fields you bind in inArguments show up on the /execute body — letting your activity personalize each Slack message at runtime.

config.jsonjson
{
  "workflowApiVersion": "1.1",
  "metaData": { "icon": "images/slack.png", "category": "messaging" },
  "type": "REST",
  "lang": { "en-US": { "name": "Slack Notifier" } },
  "arguments": {
    "execute": {
      "inArguments": [{
        "contactKey": "{{Contact.Key}}",
        "email":      "{{Contact.Attribute.DE.Email}}",
        "firstName":  "{{Contact.Attribute.DE.FirstName}}"
      }],
      "url":    "https://your-host.com/execute",
      "verb":   "POST",
      "useJwt": true
    }
  },
  "configurationArguments": {
    "save":     { "url": "https://your-host.com/save" },
    "validate": { "url": "https://your-host.com/validate" },
    "publish":  { "url": "https://your-host.com/publish" }
  }
}
10 — Slack

Slack integration with Incoming Webhooks.

Slack Incoming Webhooks are the lightest possible path from your custom activity to a channel. You generate a URL once, store it on the activity config (or a secret store keyed by config), and POST a JSON payload at runtime.

  • Dynamic message templating with merge fields
  • Channel-based routing per journey step
  • Real-time delivery (sub-2s round trip)
  • Retries surfaced via non-200 from /execute
Activities · Messagesscreenshot
Journey Builder canvas with the Activities palette expanded under Messages, showing Email, In-App Message, Inbox, Push Notification, Slack Message, and SMS — the Slack Message custom activity registered alongside native messaging steps.
The Slack Message custom activity registered in the palette under Messages — once installed, it's drag-and-drop alongside Email, SMS, and Push.

For the deeper Slack-side walkthrough — webhook setup, block kit templating, error handling — see the dedicated guide: SFMC Journey Builder Custom Activity for Slack →

11 — Hosting

Hosting & deployment.

The custom activity service must be reachable from Marketing Cloud over public HTTPS with a valid certificate. Anything that gives you a stable HTTPS endpoint works.

Vercel / Netlify Functions

Serverless route per endpoint. Cheap, fast cold-start, free TLS.

AWS Lambda + API Gateway

Closer to enterprise networks. Pair with Secrets Manager for the JWT secret.

Render / Fly.io / Railway

Long-running Node containers if you prefer a single Express process.

Setup · Installed Packagesscreenshot
SFMC Setup → Installed Packages → CPM Integration. Shows Package Details, Components section, JWT Signing Secret, and API Integration block with Client Id, Client Secret, Authentication Base URI, REST and SOAP base URIs.
Installed Packages → your package → Components & JWT Signing Secret. The signing secret is what your /execute handler verifies on every runtime call.
Activity configscreenshot
Journey Builder canvas with an activity configuration drawer open on the left showing Activity Summary, Activity Name, Description, Message Definition, and Message Configuration fields — alongside an Abandoned Cart journey on the right.
Per-step configuration drawer rendered from your hosted config.html — marketers fill it inside Journey Builder without leaving the canvas.
12 — Deeper reading

Three companion articles.

This pillar covers the system end-to-end. The deep dives below zoom into each layer.

13 — FAQ

Frequently asked questions.

What languages can I build an SFMC custom activity in?
Anything that can serve HTTPS endpoints — Node.js, Python, .NET, Go, Java. Node.js is the most common because the Journey Builder UI is JavaScript and most reference samples are Express.
Do I need a Salesforce Marketing Cloud Installed Package to build a custom activity?
Yes. The Installed Package is what registers your activity inside SFMC and provides the JWT signing secret used to verify execute calls. Create it under Setup → Installed Packages.
What is the difference between a Journey Builder custom activity and an API event?
An API event is a journey entry source — it lets external systems push contacts into a journey. A custom activity is a step inside an existing journey that calls out to an external system as contacts pass through.
Why does Journey Builder retry my custom activity?
If /execute returns a non-2xx response or times out, Journey Builder will retry per the package's retry policy. Make your handler idempotent (key on activityInstanceId) to avoid double-processing.
Can the custom activity push data back to Salesforce Marketing Cloud?
Yes — through outArguments in the execute response and via SFMC's REST API. Common patterns include writing scoring fields to a Data Extension or updating contact attributes after the side-effect runs.
Get help

Building a custom activity for your stack?

I've built custom Journey Builder activities for Slack, internal scoring services, and webhook fan-outs. Happy to walk through your setup or take it on end-to-end.