# Sending a Slack summary after a batch file 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 an entire batch, build a single formatted summary, send it once, and exit. This article shows how to write a script that downloads all new files from a remote directory, counts successes and failures, tracks total bytes transferred, and delivers one Slack message at the end of the run.

#### How It Works

The script retrieves the list of files waiting in a remote directory, downloads each one to a local inbox folder, tracks counters through the loop, and then sends a formatted Slack notification summarizing the outcome. A guard at the top skips the Slack notification entirely if the remote directory is empty so your Slack channel does not fill up with "nothing to do" messages overnight.

```javascript
// Batch transfer with Slack summary notification
// Downloads all files from a remote directory into a local inbox, tracks
// success/failure and total byte count, and sends a single Slack message
// summarising the batch.

var remoteVfsName = Params("src_vfs")  || "partner-sftp";
var remoteDir     = Params("src_dir")  || "/outbound/";
var localInbox    = Params("dst_dir")  || "/data/inbox/";
var webhookUrl    = GetSecret("slack-webhook-url");

var vfs   = 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 total bytes 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);
}
```

`files.filter(...)`: `ReadDir` returns both files and subdirectories. Filtering on `f.Type === "FILE"` ensures the loop only processes actual files, not directory entries, which cannot be downloaded as files.

`Exit(0)` for an empty batch: calling `Exit(0)` sends a clean successful status to the job engine without any Slack notification. This is the right approach for scheduled jobs that run more frequently than files arrive; the AFT! job log records the run, but your team's Slack channel stays quiet.

`SendToSlackWebHook(webhookUrl, summary, "AFT Transfer Bot", icon)`: the third argument is the sender display name that appears in Slack, and the fourth is a Slack emoji shortcode for the icon. Choosing a different emoji 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 is triggered by a cron schedule, a non-zero exit marks the run as crashed, 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; 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 manually and use `HttpCli` to post it directly to the webhook URL:

```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")
             .Post(payload);
```

This gives you the full Slack formatting palette without any additional dependencies.