Arrays and lists
Transforming lists
Many integrations can be summarized as a transformation of data from one format to another. For example, a JIRA ticket needs to be transformed to a Xurrent request, or the records of a system need to be exported to the data warehouse.
Many of these transformations work on arrays, or lists, of records, and AutomatorScript has excellent support for this.
Not only are all the standard array methods available,
including powerful ones like map, flatMap, filter, and reduce, but the language also has a handful of
extension methods that make common data-shaping tasks shorter and clearer.
Filtering and transforming with filter, map and flatMap
If you're not familiar with functions like map, flatMap and filter, your first instinct, when confronted with
the task to transform a list of objects, might be to write a loop with for and if.
However, as the following example shows, this approach is often not the most readable or maintainable.
❌ Don't do this
Suppose that, from a list of notes, you want one combined text and all of their
attachments in a single list, while skipping system notes. You could write a loop like this:
let text = '';
let attachments = [];
for (let note of notes ?? []) {
if (note.medium != 'system') {
if (text) {
text = text + "\n\n" + summarizeNote(note);
} else {
text = summarizeNote(note);
}
if (note.attachments && note.attachments.length) {
for (let att of note.attachments) {
attachments.push(att);
}
}
}
}
Although this works, it's hard to see what the loop is actually doing. It reads like a sequence of mechanical
operations, and the intention of the code is not clear. Much of the code has nothing to do with the actual goal:
two accumulator variables, a nested loop, and an if (text) check that exists only to avoid a leading separator.
✅ Do this instead
Filter out the system notes once, then derive both results from that list:
let nonSystemNotes = (notes ?? []).filter(note => note.medium != 'system');
let text = nonSystemNotes
.map(note => summarizeNote(note))
.join("\n\n");
let attachments = nonSystemNotes.flatMap(note => note.attachments ?? []);
Each line states a single intention:
filterkeeps only the notes we care about.mapturns each note into its text, andjoinglues the pieces together with the separator between them. There's no special case for the first element.flatMapmaps each note to its attachments and flattens the results into one list, replacing the inner loop.
No accumulators, so nothing to initialize and nothing to mutate.
Aggregating with reduce
When the result of the list transformation is a single value that aggregates every element, for example a total,
a count, or a summary object,
reduce(fn, start) can
often be used to express the transformation more clearly than a loop.
❌ Don't do this
Suppose you want the total amount of all orders that have been paid. With a loop, selecting and summing get tangled
together: the if that picks the paid orders sits inside the same loop that maintains the running total.
let total = 0;
for (let order of orders) {
if (order.status == 'paid') {
total = total + order.amount;
}
}
Like the previous example, this code feels very "mechanical". When you would read it out loud, it might not be immediately clear what the intention is.
✅ Do this instead
Rewriting this example with filter and reduce gives the following:
let total = orders
.filter(order => order.status == 'paid')
.reduce((sum, order) => sum + order.amount, 0);
Even though the code is not actually shorter, its intention is clearer. The two parts of the transformation are clearly separated: take the orders that have been paid, and sum their amounts.
In a real integration, both steps are likely to be more complex than this. By extracting them into separate functions, the overall code will stay easy to understand:
let total = orders.filter(isPaid).reduce(sumAmounts, 0);
function isPaid(order) {
return order.status == 'paid';
}
function sumAmounts(sum, order) {
return sum + order.amount;
}
Extension methods
On top of the standard JavaScript methods, AutomatorScript adds several extension methods to the Array type.
Overview
| Method | What it does |
|---|---|
first() | Returns the first element of the array, or null if it is empty. |
last() | Returns the last element of the array. |
uniq() | Returns a new array with duplicate values removed. |
groupBy(key) | Groups the elements into an object of arrays, keyed by a property. |
indexBy(key) | Indexes the elements into an object, keyed by a property (one element per key). |
inSlicesOf(n) | Splits the array into chunks of at most n elements. |
pick(keys) | Projects each object in the array down to only the given keys. |
renameKeys(mapping) | Renames the keys of each object in the array according to a mapping. |
Array.wrap(value) | Wraps a value in an array, unless it already is one. |
Examples
Create a lookup table
Turning a flat list of records into a lookup by id:
let people = [
{ id: 3, name: 'Ada' },
{ id: 6, name: 'Alan' },
];
let byId = people.indexBy('id');
log('name of 6', byId[6].name); // 'Alan'
Batching records before sending them to an API:
for (let batch of records.inSlicesOf(50)) {
http_post('/bulk', batch, 'target');
}
Transforming records with pick and renameKeys
pick and renameKeys are especially useful together when the shape of the data you read differs from the shape you
need to write. A typical case is exporting from Xurrent's REST API — which returns
snake_case field names — into the CSV import format, which expects
human-readable column headers.
For example, the configuration item REST resource returns
records like { name, label, status, serial_nr, ... }, but the CI import
expects the columns Name, Label, Status and Serial Number. Use pick to keep only the fields you need, then
renameKeys to map them onto the import column names:
let cis = fetchFilter('cis/active', 'name=foo'); // Xurrent REST returns snake_case records
let rows = cis
.pick(['name', 'label', 'status', 'serial_nr'])
.renameKeys({
name: 'Name',
label: 'Label',
status: 'Status',
serial_nr: 'Serial Number',
});
let csv = arrayToCsvString(rows); // header row taken from the renamed keys
import_csv('cis', csv, 'wdc');
Because pick runs first, the objects passed to renameKeys — and therefore the CSV header — contain only the four
columns you asked for, in that order.
Full reference
Each method, with its exact signature and more examples, is documented alongside the standard methods in the Built-in objects reference. The extension methods there are the ones that link to an anchor on that page rather than out to MDN.