New to Kestra?
Use blueprints to kickstart your first workflows.
Audit an SFTP dropzone for files no consumer picked up, alert Slack on staleness, and quarantine them with the SFTP Move task.
A file that lands in an SFTP dropzone and is never picked up by the consuming
process, a stopped import job, a renamed filename pattern, a permissions change,
is invisible until someone notices missing data downstream or a partner asks
why their file "disappeared." This blueprint lists the dropzone on a schedule,
flags any regular file whose last modified time is older than a threshold, pages
Slack the moment one is found or the server cannot be reached, and can quarantine
the stale files into a separate directory with io.kestra.plugin.fs.sftp.Move,
then re-lists the dropzone to verify none remain.
probe_dropzone (io.kestra.plugin.fs.sftp.List) lists sftp_dropzone_path, read-only, with allowFailure: true so a connection or auth failure does not abort the flow.stale_findings (io.kestra.plugin.core.output.OutputValues) filters the listing to entries with fileType == "FILE" and computes how many minutes old each one's updatedDate is, keeping only those past stale_after_minutes.classify_status (io.kestra.plugin.core.debug.Return) returns UNREACHABLE when probe_dropzone.files is not defined, STALE when the filtered list is non-empty, otherwise CLEAN.route_by_status (io.kestra.plugin.core.flow.Switch) branches on that string:alert_stale posts to Slack naming every stale file and its age. quarantine_gate runs only when auto_quarantine is true and dry_run is false: quarantine_loop (concurrencyLimit: 1) runs one Move per stale file into sftp_quarantine_path, reread_dropzone lists the dropzone again, and verify_quarantine fails the flow if any moved file is still present.alert_unreachable posts a separate Slack message.log_audit_status records a status line on every run regardless of branch.errors block alerts Slack separately if the flow itself fails outright.schedule_every_15_minutes trigger polls every 15 minutes with explicit input overrides, shipped disabled: true and always in safe mode (auto_quarantine: false, dry_run: true).status output (CLEAN / STALE / UNREACHABLE) you can chart or gate other flows on.auto_quarantine and dry_run.plugin-fs SFTP tasks who want an example of List and Move used together for a Day 2 operations check.An ls -la over SFTP from a terminal proves the dropzone state for one moment. This flow keeps every audit in the execution history, turns the result into a typed output other flows can depend on, and gives you a branch to grow into real remediation, moving stale files aside, re-triggering the consumer, or opening a ticket, without rewriting the audit itself.
sftp_dropzone_path and sftp_quarantine_path already existing.docker run -d --name sftp-dropzone -p 2222:22 atmoz/sftp:latest kestra:pass123:1001:100:upload/inbound,upload/quarantine, then connect it to your Kestra worker's Docker network so sftp_host resolves: docker network connect YOUR_KESTRA_NETWORK sftp-dropzone (run docker network ls to find the real network name). The flow reaches the server on its container-internal port 22 over that network; host port 2222 is only for browsing the server from your own machine with a local SFTP client.SFTP_PASSWORD: password used by every List and Move task against the SFTP server.SLACK_WEBHOOK_URL: Slack incoming webhook used for the stale alert, the unreachable alert, and the flow-failure alert.echo -n "<your-password>" | base64 exported as SECRET_SFTP_PASSWORD, and likewise SECRET_SLACK_WEBHOOK_URL.| Input | Type | Default | Purpose |
|---|---|---|---|
sftp_host |
STRING | sftp-dropzone |
SFTP server hostname |
sftp_port |
STRING | 22 |
SFTP server port |
sftp_username |
STRING | kestra |
SFTP username |
sftp_dropzone_path |
STRING | /upload/inbound |
Directory listed for stale files |
sftp_quarantine_path |
STRING | /upload/quarantine |
Destination for quarantined files |
stale_after_minutes |
INT | 60 |
Age threshold for the STALE classification |
auto_quarantine |
BOOL | false |
Allow Move to run |
dry_run |
BOOL | true |
Must be false for a quarantine to apply |
SFTP_PASSWORD and SLACK_WEBHOOK_URL secrets.sftp_host, sftp_dropzone_path, and sftp_quarantine_path at your server.status.auto_quarantine: true and dry_run: false to move stale files aside and verify the dropzone is clear of them.schedule_every_15_minutes trigger, which always runs in safe mode.status (STRING): the classified outcome, {{ outputs.classify_status.value }}, also exposed as the flow-level output {{ outputs.status }}.{{ outputs.stale_findings.values.items }}: a JSON array of the stale files found, each with name, path, and minutesStale.updatedDate serializes as an ISO-8601 timestamp parseable by jq's fromdateiso8601. Verify this against your own Kestra version before enabling the schedule.CLEAN; put a file in it and wait past stale_after_minutes to exercise the STALE branch.UNREACHABLE depending on how the SFTP server reports the error; allowFailure: true does not distinguish them.rootDir: false with an absolute path assumes the SFTP user is chrooted so that / is already its home; a server configured differently may need sftp_dropzone_path and sftp_quarantine_path written as paths relative to the home directory instead, with rootDir: true.recursive: false) and does not discover other dropzones on its own.Loop task to audit several partner directories from one flow instead of one at a time.keyfile: "{{ secret('SFTP_PRIVATE_KEY') }}" instead of password.quarantined to notify the file's originating partner or open a ticket instead of only logging.