Theme
Sending a Slack summary after a batch transfer
Per file notifications are noisy. A job that uploads fifty files and sends fifty Slack messages trains people to ignore all of them, which defeats the purpose. A better pattern is to collect the results of the whole batch, build a single formatted summary, send it once, and exit. This recipe downloads all the files waiting in a remote directory, counts successes and failures, tracks the bytes transferred, and delivers one Slack message at the end of the run.
How it works
The script lists the files waiting in a remote directory, downloads each one to a local inbox folder, keeps counters through the loop, and then sends a formatted Slack notification that summarizes the outcome. A guard at the top skips the notification entirely when the remote directory is empty, so that your Slack channel does not fill up with "nothing to do" messages overnight.
javascript
// Batch transfer with a Slack summary notification
// Downloads all the files in a remote directory into a local inbox, tracks the
// successes, the failures and the total byte count, and sends a single Slack
// message summarizing the batch.
var remoteVfsName = Param("src_vfs") || "partner-sftp";
var remoteDir = Param("src_dir") || "/outbound/";
var localInbox = Param("dst_dir") || "/data/inbox/";
var webhookUrl = GetSecret("slack-webhook-url");
var vfs = new VirtualFSByName(remoteVfsName);
var rdRes = vfs.ReadDir(remoteDir, SortByName, SortAsc);
if (!rdRes.Ok()) {
Log.Error("cannot list remote directory: " + rdRes.ErrorMsg());
Exit(1);
}
var files = rdRes.Infos().filter(function(f) { return f.Type === "FILE"; });
if (files.length === 0) {
Log.Info("no files to process, exiting quietly");
Exit(0);
}
var filesOk = 0;
var filesFailed = 0;
var totalBytes = 0;
for (var i = 0; i < files.length; i++) {
var f = files[i];
var srcPath = remoteDir + f.Name;
var dstPath = localInbox + f.Name;
var res = vfs.ExportFile(srcPath, dstPath);
if (res.Ok()) {
filesOk += 1;
totalBytes += f.Size;
Log.Info("downloaded: " + f.Name);
} else {
filesFailed += 1;
Log.Error("failed: " + f.Name + " (" + res.ErrorMsg() + ")");
}
}
// Format the byte count into a human readable label.
function bytesLabel(n) {
if (n >= 1073741824) return (n / 1073741824).toFixed(2) + " GB";
if (n >= 1048576) return (n / 1048576).toFixed(2) + " MB";
if (n >= 1024) return (n / 1024).toFixed(2) + " KB";
return n + " B";
}
var stamp = FormatDateTime("YYYY-MM-DD HH:mm");
var icon = filesFailed > 0 ? ":warning:" : ":white_check_mark:";
var summary = "[" + stamp + "] Batch from " + remoteVfsName + " complete. " +
filesOk + " succeeded, " + filesFailed + " failed. " +
"Total: " + bytesLabel(totalBytes) + ".";
SendToSlackWebHook(webhookUrl, summary, "AFT Transfer Bot", icon);
if (filesFailed > 0) {
Exit(1);
}Param("src_vfs"): the source and destination come from job parameters when the job is triggered with them, and fall back to the defaults otherwise, so the same script serves several partners.
files.filter(...): ReadDir returns both files and subdirectories. Filtering on f.Type === "FILE" makes the loop process actual files only, not directory entries, which cannot be downloaded as files.
Exit(0) for an empty batch: Exit(0) reports a clean, successful status to the job engine without any Slack notification. This is the right behavior for a scheduled job that runs more often than files arrive: the AFT! job log records the run, and your team's Slack channel stays quiet.
SendToSlackWebHook(webhookUrl, summary, "AFT Transfer Bot", icon): the third argument is the sender name shown in Slack, and the fourth is a Slack emoji shortcode for the icon. Choosing a different icon when there are failures (:warning:) versus all successes (:white_check_mark:) lets people scan the channel at a glance without reading every message.
Exit(1) after the Slack message when failures occurred: the exit code feeds back to the AFT! job engine and is recorded in the job status. If this job runs on a cron schedule, a non zero exit marks the run as failed, which lets you configure alerting at the job engine level as a secondary safety net.
Setting up the Slack webhook
In your Slack workspace, go to the App Directory, find the Incoming Webhooks app, and create a new webhook pointed at the channel you want to post to. Copy the webhook URL and store it in AFT! as a secret named slack-webhook-url. The URL is sensitive; treat it like a credential, because anyone who has it can post to your channel.
Richer formatting with Slack Block Kit
SendToSlackWebHook sends a plain text message. For richer formatting, with sections, dividers and fields laid out in columns, build a Block Kit payload yourself and post it to the webhook URL with HttpCli. The body of the request is set with ReqBody before the verb is called:
javascript
var payload = JSON.stringify({
blocks: [
{ type: "section", text: { type: "mrkdwn", text: "*Batch complete*: " + filesOk + " files, " + bytesLabel(totalBytes) } }
]
});
var hc = new HttpCli();
var resp = hc.Url(webhookUrl)
.Header("Content-Type", "application/json")
.ReqBody(payload)
.Post();This gives you the full Slack formatting palette without any additional dependency.