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.
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.
Persist the marketer's chosen configuration. Often a no-op since SFMC stores inArguments — but a good place to denormalize for analytics.
Last guardrail before the journey goes live. Reject configurations missing required fields, malformed templates, or unreachable webhook URLs.
Fired once. Use it to provision per-journey resources — register a webhook subscription, create a downstream record, warm a cache.
JWT-signed payload with inArguments + contact context. Do the actual work here. Idempotency is your responsibility, not Journey Builder's.
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.
{
"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"
}/execute URL could trigger your side-effect. Verifying the signature against the Installed Package's secret confirms the call originated from your tenant.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.

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 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() } ] }),
});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.
/execute. Journey Builder considers slow responses a failure signal and may skip or retry.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.