Concepts · Architecture

How an SFMC Journey Builder Custom Activity actually works.

Past the marketing copy, an SFMC custom activity is a small REST service with very specific contracts. This article unpacks the lifecycle, the data flow, and the failure modes — so the implementation choices you make later are informed, not guessed.

SFMC · ConceptsRead time · 11 minUpdated · May 2026By Lalit Chaudhari
01 — Overview

The system in one paragraph.

A custom activity lives in two places at once: a small Express-style REST service you host, and a registration record inside Marketing Cloud that points at it. Marketing Cloud calls your service three times during configuration (save, validate, publish) and once per contact at runtime (execute). Everything flows from there.

For the step-by-step build instructions, see the full tutorial. This page is the why, not the how.

02 — Lifecycle

Lifecycle endpoints, in detail.

The four endpoints map cleanly to the lifecycle of a Journey Builder configuration: edit → validate → publish → run. Each has a single responsibility.

POST /save
On config save

Persist the marketer's chosen configuration. Often a no-op since SFMC stores inArguments — but a good place to denormalize for analytics.

POST /validate
On publish attempt

Last guardrail before the journey goes live. Reject configurations missing required fields, malformed templates, or unreachable webhook URLs.

POST /publish
On journey activation

Fired once. Use it to provision per-journey resources — register a webhook subscription, create a downstream record, warm a cache.

POST /execute
Per contact, runtime

JWT-signed payload with inArguments + contact context. Do the actual work here. Idempotency is your responsibility, not Journey Builder's.

03 — Data flow

Where the contact data comes from.

The shape of the runtime payload is determined by the inArguments you declare in config.json. SFMC interpolates merge fields against the Data Extension feeding the journey and posts the resolved object to your /execute endpoint as a JWT.

execute payload (decoded)json
{
  "inArguments": [{
    "contactKey":    "abc-123",
    "email":         "jane@example.com",
    "firstName":     "Jane",
    "webhookUrl":    "https://hooks.slack.com/services/...",
    "channel":       "#sales-alerts",
    "messageTemplate": "New lead: {{firstName}} ({{email}})"
  }],
  "activityInstanceId": "a8d1...",
  "keyValue":           "abc-123"
}
Note · Why JWT?
The JWT envelope is what makes the runtime call trustworthy. Without it, anyone who guessed your /execute URL could trigger your side-effect. Verifying the signature against the Installed Package's secret confirms the call originated from your tenant.
04 — Triggers

What triggers the activity at runtime.

A custom activity is a step, not an entry source. It only runs when an existing contact, already inside a journey, advances to the canvas position your activity occupies. The journey itself can be entered through any of Marketing Cloud's standard sources.

Step 01
Entry source fires
DE / API event / contact event
Step 02
Contact enters journey
Journey Builder runtime
Step 03
Reaches custom activity
Step on canvas
Step 04
/execute called
JWT POST per contact
Journey Builderscreenshot
Journey Builder canvas — Scott's Welcome Journey with a Data Extension entry source feeding into Welcome Email, Engagement Split, multiple Wait steps, Decision Split, SMS, and Push Notification — illustrating the canvas position a custom activity step occupies.
A real Welcome Journey on the Journey Builder canvas. The custom activity runs only when a contact already inside this flow advances to the step you've placed on the canvas.
05 — API communication

Two-way API communication.

The runtime call is the most important channel: SFMC → your service. But your service can also call back into SFMC's REST API for richer integrations — looking up Data Extension rows, updating contact attributes, or kicking off downstream sends.

upsert-row.jsnode
// Upsert a row into a Data Extension after the side-effect succeeds
const token = await getOAuthToken(); // client_credentials grant

await fetch(`${SFMC_REST_HOST}/data/v1/async/dataextensions/key:Slack_Notifications/rows`, {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${token}`,
    "Content-Type":  "application/json",
  },
  body: JSON.stringify({ items: [{ contactKey, channel, sentAt: new Date().toISOString() } ] }),
});
06 — Webhook execution

Webhook execution at runtime.

The most common pattern: /execute receives the payload, transforms it, and fans it out to a third-party webhook (Slack, Teams, PagerDuty, an internal API). The transform is where most of the actual product value lives — channel routing, message templating, suppression rules.

Latency budget
Aim for under 1.5 seconds total round-trip on /execute. Journey Builder considers slow responses a failure signal and may skip or retry.
07 — Failure modes

What goes wrong, and how to defend against it.

  • Duplicate executionsRetries can replay the same contact. Key side effects on activityInstanceId + contactKey to make them safe to re-run.
  • Slow downstream APIIf your downstream call takes 5+ seconds, JB sees the activity as stuck. Push slow work to a queue and return 200 immediately.
  • Expired or rotated JWT secretWhen you rotate the Installed Package secret, every in-flight contact starts failing JWT verification. Roll secrets during a maintenance window.
  • Misshaped inArgumentsMarketers can edit configurations and break templates. Validate early in /validate and return clear errors so they fix it before publishing.
FAQ

Frequently asked questions.

What is a Journey Builder custom activity in Salesforce Marketing Cloud?
An externally-hosted REST service that registers itself with Marketing Cloud through an Installed Package and surfaces as a drag-and-drop step in the Journey Builder canvas. It exposes four lifecycle endpoints (save, validate, publish, execute) that JB calls during configuration and runtime.
Where does Journey Builder send the contact data at runtime?
JB POSTs to your activity's /execute URL with a JWT-signed body containing the inArguments declared in your config.json — typically the contact key plus any merge fields you bind from a Data Extension.
How do retries work for an SFMC custom activity?
Journey Builder retries /execute when it returns a non-2xx response or times out. The retry policy follows the package configuration; design your handler to be idempotent on the activityInstanceId.
Can a custom activity write data back to a Data Extension?
Yes — through outArguments returned from /execute, or by calling SFMC's REST API with an OAuth client credentials token. Writing to a DE is the typical pattern for capturing scoring or status updates.

Architecture review for your team?

I review existing SFMC custom activities for production-readiness — JWT, retries, idempotency, latency. Send me what you've got.

Schedule a review