🔥 500+ people already subscribed. Why not you? Get our newsletter with handy code snippets, tips, and marketing automation insights.

background shape
background shape

25 Salesforce Marketing Cloud SSJS examples

If you have ever worked with Server-Side JavaScript (SSJS) in Salesforce Marketing Cloud, you already know two things, it’s incredibly powerful and at the same time full of limitations. I have curated 50 SSJS scripts that I have used at least once in a production environment. There is no specific order – I wrote them as they came to mind. The best way to read this article is to check the table of contents and find any example that interests you.

Read a query string parameters safely

When working with SSJS in Salesforce Marketing Cloud, you’ll often reach for:

Platform.Request.GetQueryStringParameter('id');

It works – but it’s not always safe or reliable in real-world scenarios, so let’s overengineer it a bit and wrap it in a function.

<script runat="server">
Platform.Load("Core","1");

// Function to safely get query string parameter
function getQueryParam(paramName, defaultValue) {
    try {
        var value = Platform.Request.GetQueryStringParameter(paramName);

        // Normalize
        if (value === null || value === undefined || value === '') {
            return defaultValue || '';
        }

        return String(value);
    } catch (e) {
        return 'Error: ' + String(e);
    }
}

try {
    var token = getQueryParam('token', ''),
         authorized = token === 'my-secret'
    if (!authorized) throw "Nothing to see here."
 
    //show secret content

} catch (e) {
    Platform.Response.Write('Error: ' + String(e));
}
</script>

This is the simplest form of protection for CloudPages. Without it, anyone can call your page and trigger logic (data lookups, inserts, API calls). Even a basic shared secret drastically reduces abuse.

Process form submission in cloud page

I have a confession – I’ve never used CloudPages Smart Capture. Since I wanted to be a developer, I skipped it entirely and went straight to AMPscript and SSJS for form submissions.

<!DOCTYPE html>
<html>
<head>
    <title>Signup Form</title>
</head>
<body>

<script runat="server">
Platform.Load("Core", "1.1.1");

var request = Platform.Request;
var errors = [];

// Email validation function
function isValidEmail(email) {

    var regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    if (!regex.test(email)) return false;

    email = email.toLowerCase().trim();

    var blockedDomains = [
        "test.com",
        "example.com",
        "mailinator.com",
        "tempmail.com"
    ];

    var domain = email.split("@")[1];

    if (blockedDomains.indexOf(domain) > -1) return false;

    if (email.indexOf("asdf") > -1) return false;
    if (email.indexOf("test") === 0) return false;

    return true;
}

if (request.Method === "POST") {

    try {

        var email = request.GetFormField("email");
        var firstname = request.GetFormField("firstname");
        var country = request.GetFormField("country");

        // Validation
        if (!email) {
            errors.push("Email is required");
        } else if (!isValidEmail(email)) {
            errors.push("Invalid email address");
        }

        if (!country) {
            errors.push("Country is required");
        }

        // If no errors → insert + redirect
        if (errors.length === 0) {

            var result = Platform.Function.InsertData(
                "Form_Submissions",
                ["EmailAddress", "FirstName", "Country", "CreatedDate"],
                [email, firstname, country, Now()]
            );

            // Redirect to thank you page
            Platform.Response.Redirect(CloudPagesURL(3333));

        }

    } catch (e) {
        errors.push("Unexpected error occurred");
        //you can also log errors into data extension using logger function you can find it later in this article
        // and redirect to thank you page so the user experience is intact.
    }
}

if (errors.length > 0) {
    Write("<div style='color:red;'>");
    for (var i = 0; i < errors.length; i++) {
        Write("• " + errors[i] + "<br>");
    }
    Write("</div><br>");
}
</script>
<form method="POST" action="%%=RequestParameter('PAGEURL')=%%">
    <input type="hidden" name="submitted" value="true">

    <label>Email:</label><br>
    <input type="email" name="email"><br><br>

    <label>First Name:</label><br>
    <input type="text" name="firstname"><br><br>

    <label>Country:</label><br>
    <input type="text" name="country"><br><br>

    <button type="submit">Submit</button>
</form>

</body>
</html>

Retrieve Data Extension Rows

We have more than a handful of different ways to retrieve data from Data Extensions using SSJS. We will start with some very simple examples without filters and gradually move to more advanced ones that use complex filters and loops. The following lookup methods have a limitation of retrieving only the first 2,500 rows after filtering.

Rows.Retrieve

<script runat="server">
var de = DataExtension.Init("Master_DE");
var rows = de.Rows.Retrieve();
</script>

Rows.Lookup

<script runat="server">
var de = DataExtension.Init("Master_DE");
var rows = de.Rows.Lookup(["name","last_name"], ["John","Doe"],10,"name");
</script>

There are also Platform functions like Lookup, LookupRows, and others that you may have seen used in AMPscript.

Retrieve Data Extension Rows With Simple Filter

It’s really good practice to keep the Name and External Key the same when creating a Data Extension. This helps ensure you always use the correct identifier, since some functions use the Name while others rely on the External Key for data retrieval.

In the example below, DataExtension.Init uses the External Key.

<script runat="server">
var de = DataExtension.Init("Master_DE");
var rows = de.Rows.Retrieve({
  Property: "Email",
  SimpleOperator: "equals",
  Value: email
});
</script>

Supported operators:

  • equals
  • notEquals
  • greaterThan
  • lessThan
  • like
  • isNull
  • isNotNull

Retrieve Data Extension Rows With Advanced Filter

Usually, when we select anything from the database, we use filters. SSJS comes with various filtering methods, but it is not as advanced as having SQL at hand. The best practice is to complement SSJS with pre-made queries that run within automation.

<script runat="server">
var de = DataExtension.Init("Master_DE");
var rows = de.Rows.Retrieve({
  LeftOperand: {
    Property: "Country",
    SimpleOperator: "equals",
    Value: "DE"
  },
  LogicalOperator: "AND",
  RightOperand: {
    Property: "Status",
    SimpleOperator: "equals",
    Value: "Active"
  }
});
</script>

You can add even nested conditions but for some object not all operators are supported the best thing to do before you want to create any complex filter is to check object definition and if there are any limitations. For example automation object can only use in and equals. So any date filters wont work as expected

<script runat="server">
var de = DataExtension.Init("Master_DE");
var filter = {
  LeftOperand: {
    LeftOperand: {
      Property: "Country",
      SimpleOperator: "equals",
      Value: "DE"
    },
    LogicalOperator: "OR",
    RightOperand: {
      Property: "Country",
      SimpleOperator: "equals",
      Value: "AT"
    }
  },
  LogicalOperator: "AND",
  RightOperand: {
    Property: "Status",
    SimpleOperator: "equals",
    Value: "Active"
  }
};

var rows = de.Rows.Retrieve(filter);
</script>

Retrieve Data Extension Rows Using WSProxy

<script runat="server">
var api= new Script.Util.WSProxy();

var cols = ["Email", "FirstName"];

var res = api.retrieve(
  "DataExtensionObject[Master_DE]",
  cols
);

var rows = res.Results;
</script>

Advanced data retrieves with WSProxy

We can retrieve not only Data Extensions, but also many other SOAP service objects. In addition to retrieving data, we can also modify, add, or remove records, essentially performing full CRUD operations on these objects.

<script runat="server">
Platform.Load("core", "1");

var prox = new Script.Util.WSProxy();

// Object type for Automation Studio
var objectType = "Automation";

// Fields to retrieve
var cols = [
    "Name",
    "CustomerKey",
    "Status",
    "CreatedDate",
    "LastRunTime",
    "LastSaveDate"
];

// Optional filter (only active automations)
var filter = {
    Property: "Status",
    SimpleOperator: "equals",
    Value: 2 // 2 = Active
};

var moreData = true;
var reqID = null;
var total = 0;

while (moreData) {

    moreData = false;

    var data = (reqID == null)
        ? prox.retrieve(objectType, cols, filter)
        : prox.getNextBatch(objectType, reqID);

    if (data != null) {

        moreData = data.HasMoreRows;
        reqID = data.RequestID;

        if (data.Results && data.Results.length > 0) {

            for (var i = 0; i < data.Results.length; i++) {

                var a = data.Results[i];

                Write(
                    a.Name + " | Status: " + a.Status +
                    " | Last Run: " + a.LastRunTime + "<br>"
                );

                total++;
            }
        }
    }
}

Write("<br>Total Automations: " + total);
</script>

Insert data into data extensions

Similar to the various retrieve methods, we also have a handful of ways to insert data into Data Extensions. Here, we will mostly use AMPscript functions wrapped in SSJS. One thing to keep in mind is that AMPscript can be more forgiving, but in SSJS you need to use the exact function names as defined – otherwise, you will encounter a server runtime error.

InsertData

SSJS function to insert data from CloudPages, landing pages, microsites, and SMS messages in MobileConnect.

<script runat="server">
Platform.Load("core", "1");

try {

    var response = Platform.Function.InsertData(
        "Order_Log",
        ["OrderID", "EmailAddress", "Amount", "OrderDate", "Status"],
        ["ORD-10001", "john.doe@email.com", 129.99, Now(), "Completed"]
    );

    if (response == 1) {
        Write("Order record created");
    } else {
        Write("Insert failed");
    }

} catch (e) {
    Write("Error: " + Stringify(e));
}
</script>

InsertDE

Similar to its AMPscript sibling this function is best to use within email send.

<script runat="server">
Platform.Load("core", "1");
var response = Platform.Function.InsertDE(
        "Order_Log",
        ["OrderID", "EmailAddress", "Amount", "OrderDate", "Status"],
        ["ORD-10001", "john.doe@email.com", 129.99, Now(), "Completed"]
    );
</script>

UpsertData

To get a proper searchable list of file transfer locations first we need to perform REST API call to get json array of locations. Then we can parse JSON and save file locations into data extension.

<script runat="server">
  try{
    var fileTransferLocations = 
    [
        ...
        {
            "customerKey": "customer-key",
            "name": "File location name",
            "description": "",
            "locationType": "ExternalSftp",
            "sFtpFileTransferLocation": {
                "portNumber": 22,
                "userName": "groot",
                "url": "https://martechnotes.com",
                "authType": "Password"
            }
        }
    ],fields = [], values= [];
  
  for(var i=0;i<fileTransferLocations.length;i++){
    fields = [];
    values = [];
    fields.push('locationType'); 
    values.push(fileTransferLocations[i].locationType);
    if (fileTransferLocations[i].hasOwnProperty('sFtpFileTransferLocation')){
      fields.push('url'); 
      values.push(fileTransferLocations[i].sFtpFileTransferLocation.url);
      fields.push('user'); 
      values.push(fileTransferLocations[i].sFtpFileTransferLocation.userName);
    }
    Platform.Function.UpsertData( "file_locations_info",['name','externalKey'],[fileTransferLocations[i].name,fileTransferLocations[i].customerKey],fields, values);
  }
  }catch(e){
    Platform.Response.Write(Platform.Function.Stringify(e));
  }
  
</script>

UpsertDE

Similarly to insertDE this function is to be used in sendable context within email or sms templates

<script runat="server">
Platform.Load("core", "1");

var result = Platform.Function.UpsertDE(
    "Customer_Profile",
    ["EmailAddress"],                          // Primary key
    ["john.doe@email.com"],                    // Lookup value
    ["FirstName", "LastName", "Status", "LastLoginDate"], 
    ["John", "Doe", "Active", Now()]
);

if (result > 0) {
    Write("Customer profile updated or inserted");
} else {
    Write("No changes made");
}
</script>

UpdateData

<script runat="server">
Platform.Load("core", "1");

try {

    var result = Platform.Function.UpdateData(
        "Order_Status",
        ["OrderID"],                         // Lookup field
        ["ORD-20240301"],                    // Lookup value
        ["Status", "ShippedDate"],           // Fields to update
        ["Shipped", Now()]                   // New values
    );

    if (result > 0) {
        Write("Order status updated");
    } else {
        Write("Order not found");
    }

} catch (e) {
    Write("Error: " + Stringify(e));
}
</script>

UpdateDe

<script runat="server">
Platform.Load("core", "1");

try {

    var result = Platform.Function.UpdateDE(
        "Order_Status",
        ["OrderID"],                         // Lookup field
        ["ORD-20240301"],                    // Lookup value
        ["Status", "ShippedDate"],           // Fields to update
        ["Shipped", Now()]                   // New values
    );

    if (result > 0) {
        Write("Order status updated");
    } else {
        Write("Order not found");
    }

} catch (e) {
    Write("Error: " + Stringify(e));
}
</script>

Send API Requests

I will give here to most versatile HTTP request we have included in our SSJS within Salesforce Marketing Cloud.

<script runat="server">
  /* Load core beacause we do not want to write Platform.Function everytime :) */
  Platform.Load('Core','1');
 /* Create an authentication string to pass as a request header */
  var token = "exapmle_token";
  var auth = "Bearer " + token;

  /* Specify the request body as a string */
  var requestBody = '{name: x,email:me@example.com}';

  try {
    /* Initialize the request handler */
    var request = new Script.Util.HttpRequest("https://www.api.example.com/put");

    /* Set request headers */
    request.setHeader("Authentication", auth);
    request.setHeader("sample-header", "HeaderValue");

    /* Configure the request properties */
    request.method = "PUT";
    request.encoding = "UTF-8";
    request.postData = requestBody;
    request.contentType = "application/json";

    /* Send the request */
    var response = request.send();

    /* Necessary lines to process the response */
    var responseString = String(response.content);
    var responseJSON = ParseJSON(responseString );

    /* Output the response body */
    Write(Stringify(responseJSON));
  } catch(e) {
    Write(Stringify(e));
  }
</script>

Remove data from autosupressions or data extensions

Very simple, yet very powerful. One of the few ways to clean suppression lists in Salesforce Marketing Cloud is to use a simple SSJS snippet that does the job. This is not only a great helper for suppression lists, but also useful anytime you need to purge a Data Extension – for example, when running daily stats or other recurring activities where you need to start with a clean slate.

<script runat="server">
      Platform.Load("Core","1");
      var api = new Script.Util.WSProxy(),
      supressionLists = [  
            // { CustomerKey: '2A7C2304-23A7-AEF0-C9BD-43DC21C860FE'},
           // { CustomerKey: '2A7C2304-23A7-AEF0-C9BD-43DC21C860FE'},
      ],data;
      for (var i=0;i<supressionLists.length;i++){
        data = api.performItem("DataExtension", supressionLists[i], "ClearData", {});  
        Write(Platform.Function.Stringify(data));
      } 
      
</script>

Create data extensions using WSProxy

Creating Data Extensions via SSJS using WSProxy is useful when you want to standardize structures or automate setup instead of manually creating them in the UI. This approach is especially helpful when deploying consistent schemas across multiple environments or projects.

<script runat="server">
Platform.Load("core", "1");

var api = new Script.Util.WSProxy();

// Data Extension configuration
var config = {
    CustomerKey: "example_de_key",
    Name: "Example_Data_Extension",
    CategoryID: 12345, // Folder ID

    Fields: [
        { Name: "SubscriberKey", FieldType: "Text", MaxLength: 100, IsPrimaryKey: true, IsRequired: true },
        { Name: "EmailAddress", FieldType: "EmailAddress", MaxLength: 254, IsRequired: true },
        { Name: "FirstName", FieldType: "Text", MaxLength: 100, IsRequired: false },
        { Name: "LastName", FieldType: "Text", MaxLength: 100, IsRequired: false },
        { Name: "Country", FieldType: "Text", MaxLength: 2, IsRequired: true },
        { Name: "Language", FieldType: "Text", MaxLength: 5, IsRequired: true },
        { Name: "Status", FieldType: "Text", MaxLength: 50, IsRequired: true },
        { Name: "Score", FieldType: "Number", IsRequired: false },
        { Name: "IsActive", FieldType: "Boolean", IsRequired: false },
        { Name: "CreatedDate", FieldType: "Date", IsRequired: true }
    ],

    // Sendable configuration
    IsSendable: true,
    SendableDataExtensionField: {
        Name: "SubscriberKey",
        FieldType: "Text"
    },
    SendableSubscriberField: {
        Name: "Subscriber Key"
    }
};

// Create Data Extension
try {
    var result = api.createItem("DataExtension", config);
    Write("Data Extension created successfully");
} catch (e) {
    Write("Error creating Data Extension: " + Stringify(e));
}
</script>

Delete data extensions using WSProxy

Just like we can create Data Extensions, we can also act as executioners and remove them with no mercy using a script. Keep in mind that these will be deleted permanently – nothing will be moved to the recycle bin. You should only execute this when you are 100% sure about the outcome.

<script runat="server">

    Platform.Load("core", "1.1.1");
   var api = new Script.Util.WSProxy(); 
   api.deleteItem("DataExtension", { 
         "CustomerKey": "your-customer-key" 
   });
</script>
<script runat="server">

    Platform.Load("core", "1.1.1");

    var deNames = [
        'test_delete'
    ],
    i = 0;

    var retrieveDataExtension = function(name){
        var api = new Script.Util.WSProxy();
        var cols = [
                "ObjectID",
                "PartnerKey",
                "CustomerKey",
                "Name",
                "CreatedDate",
                "ModifiedDate",
                "Client.ID",
                "Description",
                "IsSendable",
                "IsTestable",
                "SendableDataExtensionField.Name",
                "SendableSubscriberField.Name",
                "Template.CustomerKey",
                "CategoryID",
                "Status",
                "IsPlatformObject",
                "DataRetentionPeriodLength",
                "DataRetentionPeriodUnitOfMeasure",
                "RowBasedRetention",
                "ResetRetentionPeriodOnImport",
                "DeleteAtEndOfRetentionPeriod",
                "RetainUntil",
                "DataRetentionPeriod"
            ];

            var request = api.retrieve("DataExtension", cols, {
                Property: "Name",
                SimpleOperator: "equals",
                Value: name
            });
            if(request.Status == "OK"){
                return request.Results.shift();
            }else  return null;
    }
    try{
        var properties,
            result,
            api = new Script.Util.WSProxy();
        for (i=0;i<deNames.length;i++){
            properties = retrieveDataExtension(deNames[i])
            result  = api.deleteItem("DataExtension", { 
                "CustomerKey": properties.CustomerKey 
            });
            if (result.Status == "Ok")
              Write("Deleted- " + properties.Name + " by Customer Key " + properties.CustomerKey );
            else
              Write(Stringify(result))
        }

    }catch(e){
        Write(Stringify(e))
    }
</script>

Add columns to data extensions

This is very useful when adding fields to Data Extensions with many columns, as the UI tends to freeze and it takes ages. Not saying that having a large number of columns is the best approach, but we’ve all had projects where master tables span hundreds of columns. Also another example is to add one or more columns to multiple data extensions. Adding fields with SSJS takes effect immediately and avoids any UI freezing.

<script runat="server">
Platform.Load("Core","1.1.1");

 

var customerKeys = [
 
  "FC2C3BCF-3740-4433-A103-E6A1F2F47120",
  "A0164833-3747-4444-45DA-A01648347120"
];
// Define the new field once
var newField = {
    Name : "new_field",
    CustomerKey : GUID(), 
    FieldType : "Text",
    MaxLength : 1,
    IsRequired : false,
    DefaultValue : "Y"
};
// Loop through the DEs and add the field
for (var i = 0; i < customerKeys.length; i++) {
    try {
        var de = DataExtension.Init(customerKeys[i]);
        var result = de.Fields.Add(newField);
        Write("Added field to DE: " + customerKeys[i] + "<br>");
    } catch(e) {
        Write("Error on DE: " + customerKeys[i] + " - " + Stringify(e) + "<br>");
    }
}
</script>

Set Up Logging for CloudPages and Automations

Another really good real-life example is setting up proper logging. This is very useful when debugging your script in an automation, monitoring the outcome, or coming back to it when issues are raised and the issue lays withing your script activity.

<script runat="server">
 Platform.Load("Core", "1.1.1");

 var isCloudPage = false,
     logDataExtension = "my_log_de";
 
 function logMessage(type, message, htmlOutput) {
    function logToDataExtension(type, message) {
            Platform.Function.InsertDE(
                logDataExtension,
                ["type", "message"],
                [String(type || ""), String(message || "")]
            );
    }
    if (!isCloudPage) {
        logToDataExtension(type, message);
    } else {
        Write(htmlOutput ? htmlOutput : "<br>" + String(message || ""));
    }
}
</script>

Get query definition activities

This is used plenty when any field or data extension is removed and we want to check where the data extension or field is used so we can update our query definitions and prevent from unwanted failures

<script runat="server">

    Platform.Load("Core","1");
    var prox = new Script.Util.WSProxy(),
        objectType = "QueryDefinition",
        cols = ["Name", "CustomerKey","CreatedDate","ObjectID","QueryText"], // Adjusted properties relevant to QueryDefinition
        moreData = true,
        reqID = null,
        numItems = 0, 
        results=[];

    while(moreData) {
        moreData = false;
        var data = reqID == null ?
            prox.retrieve(objectType, cols) :
            prox.getNextBatch(objectType, reqID);

        if(data != null) {
            moreData = data.HasMoreRows;
            reqID = data.RequestID;
            if(data && data.Results) {
                for(var i=0; i < data.Results.length; i++) {
                    // Example of logging the query definition details
                    var result = data.Results[i];
                    Platform.Function.UpsertData( 
                        "query_definition_info",
                        ['Name','CustomerKey','ObjectID'],
                        [result.Name,result.CustomerKey,result.ObjectID],
                        ['CreatedDate','QueryText',],
                        [result.CreatedDate,result.QueryText]
                    );
                    numItems++;
                }
            }
        }
    }
    Platform.Response.Write("<br />" + numItems + " total " + objectType + " items found.");
</script>

Remove query definitions

Also there are times that entire set of query activities has to be removed and there is nothing such as builk select and remove in the ui and there fore here comes the power of scripting.

<script runat="server">
    Platform.Load("Core", "1.1.1");
    var api = new Script.Util.WSProxy(),
        res, 
        batch = [],
        toRemove = 
        [""]//array of customer keys to be removed
    try{
        
        for (var i = 0; i < toRemove.length; i++){
                Write("Removing " + toRemove[i] +  " ...... " );
                res = QueryDefinition.Init(toRemove[i]).Remove();
                Write(res);
                Write("<br>");
           
        }
    }catch(e){
        Write(Stringify(e.Description));
    }
</script>

Change table occusrence in query definitions

You can change various settings of a Query Definition object, and one of them is the QueryText. You can either replace the entire query or simply search and replace specific parts as needed.

<script runat="server">
Platform.Load("Core", "1.1.1");

// Query Definition External Keys
var queryKeys = [
    "customer-key-1",
    "customer-key-2"
];

// Replace logic
var searchFor = /old_table/gi;
var replaceWith = "new_table";

for (var i = 0; i < queryKeys.length; i++) {

    var key = queryKeys[i];

    try {

        // Init Query Definition directly
        var qd = QueryDefinition.Init(key);

        // Get current SQL
        var originalQuery = String(qd.QueryText);

        if (!originalQuery) {
            Write("No query found for key: " + key + "<br>");
            continue;
        }

        // Replace text
        var updatedQuery = originalQuery.replace(searchFor, replaceWith);

        if (originalQuery === updatedQuery) {
            Write("No changes needed for: " + key + "<br>");
            continue;
        }

        // Update Query
        var result = qd.Update({
            QueryText: updatedQuery
        });

        if (result === "OK") {
            Write("Updated: " + key + "<br>");
        } else {
            Write("Failed: " + key + "<br>");
        }

    } catch (e) {
        Write("Error for " + key + ": " + Stringify(e) + "<br>");
    }
}
</script>

Advanced examples

For my fellow premium users I have also prepared set of additional scripts that are full

🔒 This content is for Premium Subsribers only.

Please log in to preview content. Log in or Register

You must log in and have a Premium Subscriber account to preview the content.

When upgrading, please use the same email address as your WordPress account so we can correctly link your Premium membership.

Please allow us a little time to process and upgrade your account after the purchase. If you need faster access or encounter any issues, feel free to contact us at info@martechnotes.com or through any other available channel.

To join the Discord community, please also provide your Discord username after subscribing, or reach out to us directly for access.

You can subscribe even before creating a WordPress account — your subscription will be linked to the email address used during checkout.

Premium Subscriber

19,99 € / Year

  • Free e-book with all revisions - 101 Adobe Campaign Classic (SFMC 101 in progress)
  • All Premium Subscriber Benefits - Exclusive blog content, weekly insights, and Discord community access
  • Lock in Your Price for a Full Year - Avoid future price increases
  • Limited Seats at This Price - Lock in early before it goes up

Oh hi there 👋
I have a SSJS skill for you.

Sign up now to get an SSJS skill that can be used with your AI companion

We don’t spam! Read our privacy policy for more info.

Share With Others

The Author
Marcel Szimonisz

Marcel Szimonisz

MarTech consultant

I specialize in solving problems, automating processes, and driving innovation through major marketing automation platforms—particularly Salesforce Marketing Cloud and Adobe Campaign.

Your email address will not be published. Required fields are marked *

Similar posts