Custom Activity: real data for frontend; POST endpoints for lifecycle don’t work

I want to pass/traverse contact(s) through my Custom Journey Builder Contact Filter.
For example,

  1. get contact(s) as input;
  2. (in my modal) select/remove existing fields or add new value for empty fields;
  3. output returns contact only with selected fields -> filtered contact.
    activity example

activity statuses example


I read official docs, github examples, SO questions but I didn’t find any working examples.

I use ngrok to work locally. ngrok works perfectly. I see code changes immediately. https, not http. It doesn’t matter if heroku or ngrok, I don’t have result also.

My Questions/Issues

  1. My placeholders didn’t have real data (frontend, config.json response).
  2. POST endpoints (save, validate, execute, etc) doesn’t work. For example I click Validate button, but validate endpoint is never called. I click Activate, Save but the same result.

Backend

express

app.use(bodyParser.json()); // no difference
// app.use(bodyParser.raw({ type: 'application/jwt' })); // no difference

customActivity.js

// POST
exports.validate = function(req, res) {
    console.log('validate will be never called');
    logData(req);
    return res.status(200).json({
        success: true, // false
    });
};

config.json // JS object will be converted into JSON

function configJson(req) {
    const host = req.headers.host; // It can be heroku or ngrok host

    // key and applicationExtensionKey will be added automatically. Checked
    return {
      workflowApiVersion: '1.1',
      // key:
      metaData: {
        icon: `images/icon.png`,
        category: 'customer',
      },
      // For Custom Activity this must say, "REST"
      type: 'REST', // 'RESTDECISION',
      lang: {
        'en-US': {
          name: 'Custom Contact Filter',
          description: 'Custom JB',
          step1Label: 'Select fields',
        },
      },
      arguments: {
        execute: {
          inArguments: [
            {
                FirstName1: "{{Contact.Attribute.CustomActivity.FirstName}}", // doesn't work. I suggest "John"
              },
              {
                FirstName2: "{{Contact.Default.FirstName}}", // doesn't work. I suggest "John"
              },
              {
                FirstName3: `{{Contact.Attribute.${MY_DE_AUDIENCE}.FirstName}}`, // doesn't work. I suggest "John"
              },
              {
                FirstName4: `{{Contact.Attribute.Person.FirstName}}`, // doesn't work. I suggest "John"
              },
              {
                FirstName5: "{{InteractionDefaults.FirstName}}", // doesn't work. I suggest "John"
              },
              {
                FirstName6: `{{Event.${MY_DE_AUDIENCE}.FirstName}}`, // doesn't work. I suggest "John"
              },
          ],
          outArguments: [
            // {
            //   FirstName: ,
            // },
            // {
            //   Email: ,
            // },
          ],
          // Fill in the host with the host that this is running on.
          // It must run under HTTPS
          url: `https://${host}/journeybuilder/execute`,
          "body": "",
          "format": "json",
          "useJwt": false,
          "timeout": 2000,
        },
      },
      configurationArguments: {
        // applicationExtensionKey:
        save: {
          url: `https://${host}/journeybuilder/save`,
        },
        publish: {
          url: `https://${host}/journeybuilder/publish`,
        },
        validate: {
          url: `https://${host}/journeybuilder/validate`,
          verb: 'POST',
          body: '',
          format: 'json',
          useJwt: false,
          timeout: 10000,
        },
        stop: {
          url: `https://${host}/journeybuilder/stop`,
        }
      },
      wizardSteps: [
        {
          label: 'Step 1',
          key: 'step1'
        },
      ],
      userInterfaces: {
        configModal: {
          height: 700,
          width: 700,
          fullscreen: false,
        },
      },
      // outcomes: [{
      //     arguments: {
      //       branchResult: 'no_error',
      //     },
      //     metaData: {
      //       label: 'No Error',
      //     },
      //   },
      // ],
      // schema: { // no difference if comment or uncomment schema
      //   arguments: {
      //     execute: {
      //       inArguments: [
      //         {
      //           firstName: {
      //             dataType: 'Text',
      //             direction: 'in',
      //             access: 'visible',
      //           },
      //         },
      //       ],
      //       outArguments: [],
      //     }
      //   }
      // },
    };
};

Frontend

connection.on('initActivity', initialize);

function initialize(payload) { ...parse payload... }

but payload.arguments.execute.inArguments includes only placeholders without real data. I suggest it will be real data from Entry Source contact data. For example FirstName: "John".

Also backend uses config.json endpoint only when I open modal for Custom Contact Filter.

If I’m wrong, how to do it?

inArguments: [
  {
    FirstName1: "{{Contact.Attribute.CustomActivity.FirstName}}", // doesn't work
  },
  {
    FirstName2: "{{Contact.Default.FirstName}}", // doesn't work
  },
  {
    FirstName3: `{{Contact.Attribute.${MY_DE_AUDIENCE}.FirstName}}`, // doesn't work
  },
  {
    FirstName4: `{{Contact.Attribute.Person.FirstName}}`, // doesn't work
  },
  {
    FirstName5: "{{InteractionDefaults.FirstName}}", // doesn't work
  },
  {
    FirstName6: `{{Event.${MY_DE_AUDIENCE}.FirstName}}`, // doesn't work
  },
],

In github examples sometimes I see (sometimes don’t) schema with type descriptions, but it still doesn’t work.


Answer

It work if I add arguments.execute.url and add configurationArguments.publish inside updateActivity payload (frontend).

I can comment arguments, configurationArguments, schema. It works without them. Now I use only publish and execute backend methods.

Backend

config js object/json

workflowApiVersion: "1.1",
metaData: {
  icon: `images/icon.png`,
  category: "customer", // customer/message/...
},
// For Custom Activity this must say, "REST"
type: "REST", // 'RESTDECISION',
lang: {
  // Internationalize your language here!
  "en-US": {
      name: 'Custom Contact Filter',
      description: 'Custom JB',
  },
},
wizardSteps: [
  // your steps
],
userInterfaces: {
  configModal: {
    height: 700,
    width: 700,
    fullscreen: false,
  },
},
// arguments: {}, // commented
// configurationArguments: {}, // commented
// schema: {}, // commented
// outcomes: [], // commented

main app.js

app.use(bodyParser.raw({ type: "application/jwt" }));

JWT:

module.exports = (body, secret, callback) => {
  return require("jsonwebtoken").verify(
    body.toString("utf8"),
    secret,
    {
      algorithm: "HS256",
    },
    callback
  );
};

endpoint functions:

exports.execute = async function (req, res) {
  // log data ...

  // JWT Signing Secret from Setup > Installed Packages
  const decoded = JWT(req.body, process.env.JWT_SIGNING_SECRET);

  if (decoded?.inArguments?.length > 0) { // it's better to validate all arguments
    return res.status(200).json({
      // empty object or any properties
    });
  } else {
    return res.status(500).json({ branchResult: "invalid_code" });
  }
};

Frontend

// inside DOMContentLoaded event
connection.trigger("requestTriggerEventDefinition");
connection.on(
  "requestedTriggerEventDefinition",
  function (eventDefinitionModel) {
    if (!eventDefinitionModel) {
      return;
    }
    // global variable
    eventDefinitionKey = eventDefinitionModel.eventDefinitionKey;
  }
);

// ...

function updateActivity() {
  const url = "https://" + window.location.host;
  const output = {
    // "name": "",
    // "id": null,
    // "key": "REST-1",
    arguments: {
      execute: {
        inArguments: [
          {
            ContactKey: "{{Contact.Key}}", // can be missed
          },
          {
            FirstName: `{{Event."${eventDefinitionKey}".FirstName}}`,
          },
        ],
        outArguments: [],
        url: `${url}/journeybuilder/execute`, // must be here
        useJwt: true,
      },
    },
    configurationArguments: {
      publish: {
        url: `${url}/journeybuilder/publish`, // must be here
        useJwt: true,
      },
    },
    metaData: {
      isConfigured: true,
    },
    // editable: true,
    // outcomes: [],
    // errors: [],
  };
  connection.trigger("updateActivity", output);

Also this logic will be helpful (from Get the name of the Data Extension you are working with : Custom Activity):

// inside DOMContentLoaded event
connection.trigger("requestSchema");
connection.on("requestedSchema", function (data) {
  console.log("*** Schema ***", JSON.stringify(data["schema"]));
});

Helpful and short video: https://www.youtube.com/watch?v=Naa31iOZFlI

Attribution
Source : Link , Question Author : JDoE , Answer Author : JDoE

Leave a Comment