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.
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
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.
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.
- 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
The full lifecycle, in six honest steps.
- 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.
- 02Define
Define lifecycle endpoints in config.json
Point each endpoint to its hosted URL. Set inArguments to the contact attributes you need at runtime.
- 03Register
Register the package in Marketing Cloud
Setup → Installed Packages → New → Add Component → Journey Builder Activity. Paste your hosted endpoint base URL.
- 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.
- 05Configure
Configure inputs per step
Marketers set the Slack channel, message template, and any merge fields directly inside the activity config UI.
- 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.

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.
Stores the marketer's configuration (Slack URL, channel, message template) on the activity instance.
Last chance to reject misconfigured steps — return non-2xx and Journey Builder blocks the publish.
Fires once when the journey goes live. Use it for one-time setup like provisioning resources per journey.
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.
// 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);/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.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.
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.
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" });
}
}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.
{
"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" }
}
}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

For the deeper Slack-side walkthrough — webhook setup, block kit templating, error handling — see the dedicated guide: SFMC Journey Builder Custom Activity for Slack →
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.


Three companion articles.
This pillar covers the system end-to-end. The deep dives below zoom into each layer.
Creating SFMC Journey Builder Custom Activity
From Installed Package creation to a deployed Node.js backend — the complete creation walkthrough.
Read article ConceptsSFMC Journey Builder Custom Activity Explained
Architecture, flow, and lifecycle — how custom activities actually work under the hood.
Read article IntegrationCustom Activity for Slack Integration
Webhook setup, message templating, retry behavior — Slack-specific implementation guide.
Read article