List icon
OutputValues icon
Return icon
Switch icon
Log icon
SlackIncomingWebhook icon
If icon
Loop icon
Move icon
Fail icon
Schedule icon

SFTP Stale File Quarantine Gate

Audit an SFTP dropzone for files no consumer picked up, alert Slack on staleness, and quarantine them with the SFTP Move task.

Categories
DataInfrastructure

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.

How it works

  1. 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.
  2. 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.
  3. 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.
  4. route_by_status (io.kestra.plugin.core.flow.Switch) branches on that string:
    • CLEAN: logs a quiet confirmation.
    • STALE: 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.
    • UNREACHABLE: alert_unreachable posts a separate Slack message.
  5. log_audit_status records a status line on every run regardless of branch.
  6. The errors block alerts Slack separately if the flow itself fails outright.
  7. The 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).

What you get

  • A scheduled, plugin-native audit of a dropzone directory with zero custom client code.
  • A three-way status output (CLEAN / STALE / UNREACHABLE) you can chart or gate other flows on.
  • A Slack page naming every stale file and its age, with quarantine applied and verified behind auto_quarantine and dry_run.
  • A standing audit log line per run and a separate failure channel for the gate flow itself.

Who it's for

  • Integration and EDI teams receiving partner files over SFTP, where a silently stuck import job leaves files unpicked for days.
  • Platform teams who want an orchestrator-side check that a dropzone is being drained, instead of relying on the consuming process to report its own failure.
  • Teams evaluating the plugin-fs SFTP tasks who want an example of List and Move used together for a Day 2 operations check.

Why orchestrate this with Kestra

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.

Prerequisites

  • An SFTP server reachable from the Kestra Worker's Docker network, with sftp_dropzone_path and sftp_quarantine_path already existing.
  • For local testing: 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.
  • A Slack incoming webhook for the stale and unreachable alerts.

Secrets

  • 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.
  • OSS note: if your Kestra instance reads secrets from environment variables instead of a secrets backend, set them as base64, for example echo -n "<your-password>" | base64 exported as SECRET_SFTP_PASSWORD, and likewise SECRET_SLACK_WEBHOOK_URL.

Inputs

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

Quick start

  1. Add the SFTP_PASSWORD and SLACK_WEBHOOK_URL secrets.
  2. Point sftp_host, sftp_dropzone_path, and sftp_quarantine_path at your server.
  3. Run the flow once and read status.
  4. Run it with auto_quarantine: true and dry_run: false to move stale files aside and verify the dropzone is clear of them.
  5. Enable the schedule_every_15_minutes trigger, which always runs in safe mode.

Outputs

  • 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.

Pitfalls

  • This blueprint has not been executed against a live SFTP server; the staleness calculation assumes updatedDate serializes as an ISO-8601 timestamp parseable by jq's fromdateiso8601. Verify this against your own Kestra version before enabling the schedule.
  • An empty dropzone audits zero entries and always reads CLEAN; put a file in it and wait past stale_after_minutes to exercise the STALE branch.
  • A missing path and a down server can both surface as 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.
  • This audit lists one directory per run; it does not recurse into subdirectories (recursive: false) and does not discover other dropzones on its own.
  • A 15-minute poll can miss a file that arrives and is correctly picked up again between two runs.

How to extend

  • Loop over a list of dropzone paths with a Loop task to audit several partner directories from one flow instead of one at a time.
  • Switch to key-based auth with keyfile: "{{ secret('SFTP_PRIVATE_KEY') }}" instead of password.
  • Store the current stale-file list in a KV bucket and alert only on newly-stale entries instead of every run.
  • Chain a downstream task after quarantined to notify the file's originating partner or open a ticket instead of only logging.

Links

See How

New to Kestra?

Use blueprints to kickstart your first workflows.