Adobe Campaign Classic JavaScript Activity: Writing Custom Logic in Workflows

Write reliable logic in the Adobe Campaign JavaScript activity: vars, instance.vars, queryDef, xtk.session.Write and an advanced wait pattern.

Executive Summary: In Campaign Classic v7, the Adobe Campaign JavaScript activity comes in two forms: JavaScript code, which runs once, and Advanced JavaScript code, a persistent task that repeats on its own schedule until task.setCompleted() is called. I explain variables, the database APIs, logging and error handling, and I build a polling step with a timeout.

At some point a workflow needs something the standard activities cannot do. Work out a date range from a business calendar, read a value from an options table, write a status row, or wait until another system has finished. That is when you reach for the Adobe Campaign JavaScript activity.

Campaign Classic v7 has two of them, and they behave very differently. I see the difference trip people up regularly, usually with an Advanced JavaScript activity that never stops running or a simple one that was expected to wait. I’ll explain both, then build a real example that uses each one correctly.

The two Adobe Campaign JavaScript activities

Both live on the Actions tab of the workflow palette. The SQL code and JavaScript code page describes their settings.

JavaScript code runs its script once each time it is activated, then passes control to its single outbound transition. If the script throws an error, the task fails and the workflow’s error handling takes over. Use it for calculations, setting variables, small reads and writes, and calling APIs.

Advanced JavaScript code is a persistent task. It has two script zones, First call and Next calls, a Transitions tab where you define as many named outbound transitions as you need, and a Schedule tab. When activated, it runs the First call script. After that it stays alive, and Campaign runs the Next calls script again on the schedule you set. It keeps doing so until your script calls task.setCompleted().

That last sentence is the whole point of the activity. If no code path ever calls task.setCompleted(), the task never finishes, and the workflow stays started indefinitely.

What an Adobe Campaign JavaScript activity can see

Scripts in a workflow run on the server, in the workflow engine, with a few extra global objects. The JavaScript scripts and templates page lists them:

  • instance, the running workflow (xtk:workflow).
  • task, the current task (xtk:workflowTask).
  • event, the event that activated the task (xtk:workflowEvent).
  • activity, the definition of the current activity.

Most properties of these objects are read-only. The exception is their vars properties, which is where you keep your own values. Variables can hold strings, numbers and dates.

There are three scopes, and choosing the right one saves a lot of confusion:

Variable Scope Typical use
instance.vars.x The whole workflow run, shared by all activities Settings used in many places, such as a campaign code
vars.x The event, passed along the transition to the next activities Values produced by one step and used by the next, such as a file name or a count
task.vars.x The current task only State kept between calls of a persistent activity

Adobe recommends event variables in preference to instance variables where they fit. In fields that accept expressions, such as query conditions or labels, you reference them as $(vars/@x) and $(instance/vars/@x).

The database APIs you will use most

Server-side scripts call Campaign’s data APIs through the schema namespaces. The two forms below are equivalent, and you will see both in existing code:

var q1 = xtk.queryDef.create(<queryDef schema="nms:recipient" operation="count"/>);
var q2 = NLWS.xtkQueryDef.create(<queryDef schema="nms:recipient" operation="count"/>);

The calls I use in nearly every project:

  • xtk.queryDef.create(...).ExecuteQuery() with operation set to select, get, getIfExists or count. Collections are filtered with a condition using setOperator="EXISTS".
  • xtk.session.Write(<record xtkschema="..." .../>) to insert, or to update with _operation="update" or "insertOrUpdate" and a _key.
  • Deleting by condition: xtk.session.DeleteCollection(schema, <where>...</where>, false).
  • xtk.workflow.Start(id) and xtk.workflow.PostEvent(...) to start or signal other workflows.
  • sqlGetInt, sqlGetString and sqlExec for direct SQL when the API would be awkward. Use these carefully and never build SQL from untrusted input.
  • getOption("optionName") to read a value from Administration > Platform > Options.
  • logInfo, logWarning and logError for the journal. Note that logError also raises an error, so it stops the script.

The Adobe page on JavaScript examples in workflows has working samples of reading, writing and triggering.

One practical note on E4X: inside an XML literal, attribute values are XML, so < must be written as &lt;. Comparisons such as >= are fine.

My rule is that a script is the last resort, not the first. If a standard activity can do the job, I use it, because the next person can read an activity’s properties at a glance but has to read and trust every line of a script. When I do write one, I keep it short, give each activity one job and log what it decided. A 200-line script in a single activity is the hardest thing on any instance to hand over.

The worked example: wait for tonight’s transactions

Here is the brief. It is hypothetical, but it is the kind of dependency every nightly process has.

A retailer loads the day’s transactions into the cus:transaction table from my custom schema guide through an import workflow that normally finishes around 01:00, but sometimes runs late. A second workflow starts at 02:00 and builds a “bought yesterday” thank-you audience. If it runs before the import has finished, it sends to a partial audience. The team wants the second workflow to:

  1. Work out “yesterday” once and share it with every activity.
  2. Wait until yesterday’s transactions are present, checking every 15 minutes.
  3. Give up after three hours, alert the team, and not send anything.

The simple activity handles step 1, while steps 2 and 3 need the advanced one.

Step 1: a Campaign JavaScript activity to set the dates

After the Scheduler, add a JavaScript code activity labelled -- set dates --. The double hyphens are a common convention that tells other developers this activity holds a script.

// Yesterday as a date range, shared with every later activity
var now = new Date();
var start = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1);
var end = new Date(now.getFullYear(), now.getMonth(), now.getDate());

instance.vars.dayStart = formatDate(start, "%4Y/%2M/%2D");
instance.vars.dayEnd = formatDate(end, "%4Y/%2M/%2D");

logInfo("Processing transactions from " + instance.vars.dayStart + " to " + instance.vars.dayEnd);

Later, the Query condition can use @transactionDate >= #$(instance/vars/@dayStart)# and @transactionDate < #$(instance/vars/@dayEnd)#, so the dates are defined once.

Step 2: an Advanced JavaScript code activity to wait

Add an Advanced JavaScript code activity labelled -- wait for import --.

On the Transitions tab, define two transitions: ok with the label “Data ready”, and timeout with the label “Gave up”.

On the Schedule tab, set it to run every 15 minutes.

In First call, record when we started and run the first check immediately, because the import may already be done:

task.vars.startedAt = new Date().getTime();

var count = xtk.queryDef.create(
  <queryDef schema="cus:transaction" operation="count">
    <where>
      <condition expr={"@transactionDate >= #" + instance.vars.dayStart + "#"}/>
      <condition expr={"@transactionDate < #" + instance.vars.dayEnd + "#"}/>
    </where>
  </queryDef>
).ExecuteQuery().@count;

logInfo("First check: " + count + " transactions for " + instance.vars.dayStart);

if (count > 0) {
  vars.transactionCount = count;
  task.postEvent(task.transitionByName("ok"));
  task.setCompleted();
}
return 0;

In Next calls, repeat the check, and add the timeout:

var count = xtk.queryDef.create(
  <queryDef schema="cus:transaction" operation="count">
    <where>
      <condition expr={"@transactionDate >= #" + instance.vars.dayStart + "#"}/>
      <condition expr={"@transactionDate < #" + instance.vars.dayEnd + "#"}/>
    </where>
  </queryDef>
).ExecuteQuery().@count;

var waitedMinutes = (new Date().getTime() - task.vars.startedAt) / 60000;
logInfo("Check after " + Math.round(waitedMinutes) + " min: " + count + " transactions");

if (count > 0) {
  vars.transactionCount = count;
  task.postEvent(task.transitionByName("ok"));
  task.setCompleted();
} else if (waitedMinutes >= 180) {
  logWarning("No transactions for " + instance.vars.dayStart + " after 3 hours");
  task.postEvent(task.transitionByName("timeout"));
  task.setCompleted();
}
return 0;

Every path that should end the wait calls task.postEvent with the right transition and then task.setCompleted(). The path that should keep waiting does neither, so the task stays alive and Campaign calls it again in 15 minutes.

Two details in that code are worth pointing out. The condition expressions use E4X braces, expr={...}, to build the attribute from a JavaScript string. Inside the braces the value is ordinary JavaScript, so < is written as it is and E4X escapes it for you. Only a condition typed directly into a literal attribute needs &lt;. The #...# around the dates is the XTK date literal format.

Step 3: connect the two branches

Connect the ok transition to the Query that builds the thank-you audience, then the delivery. Because vars.transactionCount was set before postEvent, the next activities can read it.

Connect the timeout transition to an Alert activity that notifies the workflow supervisors, then an End. Nothing on that branch touches a delivery, so a late import never results in a partial send.

Scheduler (02:00) --> -- set dates -- --> -- wait for import --
                                              |-- ok ------> Query (bought yesterday) --> Email delivery
                                              |-- timeout -> Alert --> End

While the activity is waiting, the workflow shows as started and the task appears as pending in the workflow’s task list. The journal shows one log line per check, which makes it easy to see how long the wait took on a given night.

Testing an Adobe Campaign JavaScript activity before it goes live

I test every script activity in a copy of the workflow before it touches the real one. The copy has a Start, the script, and an End, nothing else. That keeps a mistake from reaching a delivery.

For the wait activity, I test the three paths separately. First, run it on a day where transactions exist, and check that it leaves on ok at the first call. Second, point the dates at a future day, so the count is always zero, and shorten the timeout to 30 minutes with a schedule of every 5 minutes. It should log a few checks and then leave on timeout. Third, while it is waiting, insert a test transaction for that future day with a quick xtk.session.Write in another workflow, and check that the next call picks it up and leaves on ok.

Once all three behave, put the real timeout and schedule back, and delete the test row.

Error handling in an Adobe Campaign JavaScript activity

A script error fails the task. What happens next depends on the workflow properties: with the default Stop the process, the workflow is paused in a failed state until someone restarts it. With Ignore, the failed task is logged and a recurring workflow carries on with its next run. Both have their place. For anything feeding a delivery, I prefer to stop.

Inside the script, catch errors where you can add something useful, and rethrow so the task still fails:

try {
  var url = getOption("cusLoyaltyApiUrl");
  if (!url) throw "Option cusLoyaltyApiUrl is not set";
  var http = new HttpClientRequest(url);
  http.execute();
  if (http.response.code != 200)
    throw "Loyalty API returned HTTP " + http.response.code;
  vars.loyaltyPayload = http.response.body.toString();
} catch (e) {
  logError("Loyalty API call failed: " + e);
}

Because logError raises an error itself, there is no need to rethrow after it.

Keeping scripts maintainable

Long scripts in activities are hard to review and impossible to share. Once a function is used in more than one workflow, move it to Administration > Configuration > JavaScript codes in your own namespace, for example cus:dates.js, and load it at the top of the activity with loadLibrary("cus:dates.js");. Each workflow then calls the same tested function.

Keep secrets such as API keys out of scripts. Put them in options or external accounts, so they do not end up in workflow exports and packages.

Mistakes I keep seeing

  • No completion path. An Advanced JavaScript activity with no task.setCompleted() on some branch, so it runs forever.
  • Using the advanced activity for a one-off step. If nothing needs to wait or repeat, the simple activity is clearer.
  • Storing objects in variables. Variables take strings, numbers and dates. Serialise anything else to a string.
  • Writing in a loop. Calling xtk.session.Write once per record for thousands of records is slow. Use workflow activities such as Update data for volume, and keep scripts for control logic.
  • Building SQL from variables. It works until a value contains a quote. Prefer queryDef with bound expressions.

FAQ

Can a simple Campaign JavaScript activity have more than one outbound transition?

No. It has one outbound transition. If you need branching, set a variable and follow the activity with a Test activity, or use the Advanced JavaScript code activity and its named transitions.

What happens if the workflow is stopped while the advanced activity is waiting?

The pending task is stopped with the workflow. When you start the workflow again, it begins from the start, so the First call script runs again and the wait timer starts over.

Where do I see what my script logged?

In the workflow journal. Right-click the activity and choose Display logs to filter to that activity.

Should I use instance variables or event variables?

Use event variables for values that flow from one step to the next, and instance variables for values that many unrelated activities need. Event variables follow the data, which makes the workflow easier to read.

When a script does fail in production, the logs usually say exactly why, if you know where to look. My workflow debugging guide covers how I approach those failures.

Last updated on 26 October 2026

Adobe Campaign Classic JavaScript Workflows

Share this article

Leave a Reply

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