Post

Journey Builder: Custom Activity

I found myself recently diving into creating a custom activity in Journey Builder for Salesforce Marketing (SFMC). Not being a Javascript master by any means meant it was time to learn. I also found the documentation to be quite light with regards to specifics on this topic. Here’s my attempt to explain the learnings.

Background

JourneyBuilder flow For those that aren’t familiar, Journey Builder allows one to define a marketing-oriented path for a contact to follow. Think of it was a decision tree based on known attributes and then, optionally, based on attributes/behaviors collected. Below is an example of a Journey Builder flow.

In this scenario, I’m creating a custom activity to initiate an API call. Eventually, this could be posting a message to a processing queue or an async call to Twilio to send an SMS message.

Architecture

Most of the SFDC documentation refers one to host their custom activity in Heroku. I’ve chosen to use Azure App Services instead. Azure Architecture The architecture shown represents my current plan. Azure App Services will be responsible for hosting my Node.js Express app. This app is responsible for hosting the design time, which is embedded as an iFrame in Journey Builder. The app is also responsible for serving the required assets. This includes:

  • Postmonger.js

    Generically speaking, this provides the interface between Journey Builder and the iFrame. It includes event triggers that can then be listened for to trigger desired actions within the custom code.

  • config.json

    This defines the integration that SFMC will subsequently use. It defines the endpoints that are eventually called and the in/out arguments passed.

The app also provides the necessary endpoints, which are referred to in the config.json artifact.

Getting Started

Initial Creation & Routes

The following will setup your initial environment.

1
2
3
4
mkdir custom-app
cd custom-app
npm init
npm install express

The above will setup your initial express environment. With that established, I followed the example provided by Salesforce on their Github.

The steps are loosely as follows:

  • Create a sub-directory to compartmentalize the specific custom activity. This will allow the Azure App Service to host more than one custom activity in the future.
  • Add the routes to the custom activity so that they can be served

Create directory structure.

1
2
3
4
5
6
7
mkdir routes
mkdir routes\twilio-sms
mkdir routes\twilio-sms\app
mkdir routes\twilio-sms\config
mkdir routes\twilio-sms\html
mkdir routes\twilio-sms\images
mkdir routes\twilio-sms\src

The root of my app is served by app.js as defined upon creation via the npm init command. To add my custom module, I added the following within:

1
2
3
4
5
6
7
8
const customModules = [
  require('./routes/twilio-sms/app/app')
];

//...then later...
customModules.forEach((cm)=> cm(app, {
  rootDirectory: __dirname,
}));

The above will load my module that is now directed to ./routes/twilio-sms/app/app.js. This is following a similar structure to the example code provided in the Salesforce Github repository referenced earlier.

First, create some initial routes to serve the required components.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const configJSON = require('../config/config-json')

module.exports = function twilioSmsActivity(app, options) {
    const activityDir = `${options.rootDirectory}/routes/twilio-sms`;

    //static resources
    app.use('/routes/twilio-sms/dist', express.static(`${activityDir}/dist`));
    app.use('/routes/twilio-sms/images', express.static(`${activityDir}/images`));

    // index redirect
    app.get('/routes/twilio-sms', function(req, res) {
        return res.redirect(`/routes/twilio-sms/index.html`);
    });

    // index.html route
    // example: https://gist.github.com/zPugMan/501fdd73c3e50e6ecf16d35766dbeffb
    app.get('/routes/twilio-sms/index.html', function(req,res) {
        return res.sendFile(`${activityDir}/html/index.html`);
    });

    // config.json route
    app.get('/routes/twilio-sms/config.json', function(req,res){
        return res.status(200).json(configJSON(req));
    });

The above snippet provides:

  • routes for index.html which will present the design time form presented in the iFrame of Journey Builder.
  • a route for config.json, which defines the custom activity and provides all of the configuration values for it such as in/out arguments.
  • a route to images, which will be used to serve the appropriate icon for the custom activity.
  • a route to dist directory is defined. This will be used to provide css and other static assets. The directory is formed via webpack and will be discussed later.

Configuration Settings: config.json

SFMC determines how to interact with a custom activity via a configuration file config.json. More information on the attributes that can be retrieved here.

Following a similar pattern, this file is defined via a module: /routes/twilio-sms/config/config-json.js.

1
2
3
4
5
6
7
8
9
10
11
module.exports = function configJSON(req) {
    return {
        workflowApiVersion: '1.1',
        metaData: {
          // the location of our icon file
          icon: 'images/twilio-icon.png',
          category: 'message'
        },
        // For Custom Activity this must say, "REST"
        type: 'REST',
...

Below are some key configuration items to consider in this file:

KeySample ValuePurpose
metadata.icon'images/twilio-icon.png'Relative route to the icon that is ultimately displayed in Journey Builder’s available actions
configurationArguments Defines the URLs for the following actions: publish, validate, and stop.
arguments.execute.inArguments[]{ smsPhone: "" }Defines one or more input arguments that are included in the payload POSTed to the /execute endpoint
arguments.execute.outArguments[]{ status: "" }Defines one or more output arguments that are returned in the response from the /execute endpoint

Add API Endpoints

Returning to our custom activity module’s ./routes/twilio-sms/app/app.js code, the following snippet is repeated to add our necessary API endpoints.

1
2
3
4
5
app.post('/routes/twilio-sms/publish', function(req,res){
    console.log("Request to publish..");
    console.info('Publishing: ', JSON.stringify(req.body));
    return res.status(200).json({});
});

The endpoints required are:

  • /execute
  • /save
  • /publish
  • /validate
  • /stop

Validate

At this point, the express app created should have enough of the required components to run. Before we do, let’s add the package nodemon for dynamic updates while the app is running locally.

npm install nodemon --save-dev

Let’s add a purpose built dev script to our package.json file.

1
2
3
4
  "scripts": {
    "start": "node ./bin/www",
    "dev": "nodemon ./bin/www",
  }

Start up app via:

npm run dev

Validate the routes desired.

  • http://localhost:3000/routes/twilio-sms redirects to http://localhost:3000/routes/twilio-sms/index.html
  • http://localhost:3000/routes/twilio-sms/index.html serves the form used for configuration in the Journey Builder design time
  • POST requests to the following respond with the hard-coded HTTP200 status
    • http://localhost:3000/routes/twilio-sms/save
    • http://localhost:3000/routes/twilio-sms/validate
    • http://localhost:3000/routes/twilio-sms/publish
    • http://localhost:3000/routes/twilio-sms/stop
    • http://localhost:3000/routes/twilio-sms/execute
This post is licensed under CC BY 4.0 by the author.