Schedule icon
GetLastRun icon
Return icon
Switch icon
Log icon
If icon
Run icon
SlackIncomingWebhook icon

Apify Actor Run Failure Sentinel

Audit an Apify actor's last run for FAILED, TIMED-OUT, or ABORTED status, alert on breach, and optionally retry it.

Categories
DataInfrastructure

This repo's own apify-cost-quality-guardrail.yaml blueprint guards a single Apify run against runaway cost and silently-degraded output - but it only ever evaluates the run it itself just triggered, and only by item count. A scheduled actor run that Apify's own platform marks FAILED, TIMED-OUT, or ABORTED - the actor's code threw, it hit the platform's own timeout, or it was killed - is a different failure mode entirely, one that never reaches the item-count check because the run never produced a dataset at all. This blueprint audits exactly that: it reads an actor's most recent run status through GetLastRun, alerts Slack the moment it ended badly, and can launch one fresh retry run under a hard spend cap.

How it works

  1. periodic_run_check (io.kestra.plugin.core.trigger.Schedule) runs every 6 hours, always forcing auto_remediate: "false" and dry_run: "true" through its own inputs: override regardless of this flow's defaults. Shipped disabled so you can validate a manual run first.
  2. probe_last_run_status (io.kestra.plugin.apify.dataset.GetLastRun, allowFailure: true) reads actor_id's most recent run, returning status among other fields.
  3. classify_run_outcome (io.kestra.plugin.core.debug.Return) classifies the result as UNREACHABLE (status undefined), HEALTHY (SUCCEEDED), AT_RISK (FAILED, TIMED-OUT, or ABORTED), or IN_PROGRESS (any other documented status - the run is still live).
  4. route_by_outcome (io.kestra.plugin.core.flow.Switch) branches on that classification:
    • HEALTHY / IN_PROGRESS: logged, no alert.
    • AT_RISK: a nested route_remediation (Switch on auto_remediate) either considers remediation ("true", gated again by dry_run through check_dry_run) or only logs that remediation is disabled ("false"). When both gates allow it, retrigger_actor_run (io.kestra.plugin.apify.actor.Run, capped by maxTotalChargeUsd) launches a brand new run. alert_at_risk always fires to Slack regardless of which path ran.
    • UNREACHABLE: alert_unreachable pages Slack directly - a broken API call must never read as "actor is fine".
  5. log_audit_complete always runs last, printing the classification regardless of which branch fired.
  6. The errors block alerts Slack separately if the flow itself fails outside these handled branches.

What you get

  • A direct answer to "did the last scheduled run of this actor actually finish cleanly", distinct from this repo's existing item-count quality check, and catching the failure modes that never produce a dataset to check in the first place.
  • A cost cap (maxTotalChargeUsd) on every retry, so remediation cannot itself become the runaway-spend incident this blueprint exists to prevent.
  • Two independent gates (auto_remediate, dry_run) between "run failed" and "a new run actually launched".
  • A final status log every run, so the execution history doubles as a run-reliability trend for actor_id.

Who it's for

  • Teams running scheduled Apify actors (price monitors, content crawlers, lead-gen scrapers) who need to know the moment a run stops finishing cleanly, not just when its output looks thin.
  • Anyone pairing this with apify-cost-quality-guardrail.yaml - that flow catches a run that finished but returned bad data; this one catches a run that never finished at all.
  • Anyone evaluating this plugin who wants a worked example of GetLastRun used as a standing health check rather than a one-off lookup after a trigger.

Why orchestrate this with Kestra

Apify's own platform runs the schedule and records the status, but it does not independently alert on a bad outcome or decide whether a retry is warranted - that logic has to live somewhere. Kestra supplies the three-way risk classification as a first-class branch, two independent authorization gates before anything is retried, and an execution history that shows exactly when a run first failed and whether a retry was launched.

Prerequisites

  • An Apify account, API token, and at least one actor with a run history.
  • A Slack incoming webhook for alerts.

Local testing: this plugin calls Apify's own hosted API directly; there is no self-hosted Apify platform to run in Docker. Validate this flow against a real Apify account and a low-cost actor (for example apify/website-content-crawler with a small maxItems, matching this repo's own apify-cost-quality-guardrail.yaml defaults) rather than attempting a fully local run - this has not been exercised in this environment.

Secrets

  • APIFY_API_TOKEN: Apify API token, matching this plugin's own documented pattern (see apify-cost-quality-guardrail.yaml).
  • SLACK_WEBHOOK_URL: Slack incoming webhook used by alert_at_risk, alert_unreachable, and the errors block.
  • In Kestra OSS (no Enterprise secrets backend), secrets are supplied as environment variables prefixed SECRET_, base64-encoded, and read back in flows with {{ secret('NAME') }} - for example SECRET_APIFY_API_TOKEN=$(echo -n '<your-apify-token>' | base64). This keeps credentials out of the flow YAML but, per Kestra's own documentation, offers no encryption at rest or access control beyond the host environment; use the Enterprise secrets backend for stronger guarantees.

Inputs

  • actor_id (STRING, default apify/website-content-crawler): actor audited.
  • retry_max_total_charge_usd (FLOAT, default 10): hard spend cap on the retry run.
  • auto_remediate (SELECT: "false", "true"; default "false"): must be "true" for a retry run to even be considered.
  • dry_run (SELECT: "true", "false"; default "true"): must be explicitly "false", together with auto_remediate: "true", for a fresh run to actually launch.

Outputs

  • outputs.probe_last_run_status.status / .id / .statusMessage / .startedAt / .finishedAt / .usageTotalUsd: the audited run's own fields for every run.
  • outputs.classify_run_outcome.value: HEALTHY, AT_RISK, IN_PROGRESS, or UNREACHABLE for every run.
  • outputs.retrigger_actor_run.id: present only when remediation actually launched a new run.

Quick start

  1. Point actor_id at an actor with at least one completed run (any Apify Store actor you have run before works).
  2. Add the APIFY_API_TOKEN and SLACK_WEBHOOK_URL secrets.
  3. Run the flow manually against an actor whose last run succeeded and confirm log_healthy fires.
  4. Manually run (or find) an actor whose last run failed or was aborted, point actor_id at it, and re-run; confirm alert_at_risk fires with the real status, and that log_remediation_disabled logs without launching a retry.
  5. Run once with auto_remediate: "true" and dry_run: "true" against that same actor and confirm log_dry_run_remediation describes the retry without launching it.
  6. Run once with auto_remediate: "true" and dry_run: "false" and confirm retrigger_actor_run launches, visible as a new run in the Apify console, capped at retry_max_total_charge_usd.
  7. Enable periodic_run_check once you trust the check; it always runs in the safe auto_remediate: "false" / dry_run: "true" mode regardless of what you leave the flow's own defaults set to.

How to extend

  • Loop over several actor_id values with io.kestra.plugin.core.flow.Loop to audit every scheduled actor in an Apify organization in one pass.
  • Chain this flow after this repo's own apify-cost-quality-guardrail.yaml (via a Subflow task) so a single schedule covers both the run-status check and the item-count quality check for the same actor.
  • Cap the number of automatic retries by tracking a counter in vars or an external store, so an actor that keeps failing escalates to a human instead of retrying forever on every scheduled audit.
  • Route the remediation branch through a Pause approval task instead of (or alongside) the static auto_remediate input, for a human-reviewed retry on a production actor.

Pitfalls

  • Apify's own ActorJobStatus enum has eight values, not just the terminal ones this blueprint names. Verified against Apify's own documentation: READY, RUNNING, SUCCEEDED, FAILED, TIMING-OUT, TIMED-OUT, ABORTING, ABORTED. This blueprint treats only FAILED/TIMED-OUT/ABORTED as AT_RISK; the transitional states (READY, RUNNING, TIMING-OUT, ABORTING) are classified IN_PROGRESS and never alert, since they are not yet a failure.
  • GetLastRun always returns the single most recent run, with no way to filter by status or page through run history. Verified from the plugin's own documented input schema (actorId, apiToken only - no status filter or pagination property). If a failed run is immediately followed by a successful manual retry before this flow's next scheduled check, this audit will only ever see the latest (successful) run and never surface the earlier failure.
  • The retry launches a brand new run; it does not resume or retry the original run in place. Apify's own run identity means retrigger_actor_run.id is always different from probe_last_run_status.id - there is no "retry this exact run" operation on this plugin.
  • allowFailure: true is what lets UNREACHABLE be classified instead of failing the execution outright. A base Task property, not specific to this plugin; without it, an invalid actorId or API token would abort the flow before classify_run_outcome ever runs.
  • auto_remediate and dry_run are independent gates, not a single boolean. Both must be "true"/"false" respectively for retrigger_actor_run to run; setting only one leaves the other still blocking.
  • Switch case keys are quoted strings matching the classifier's exact output. "HEALTHY", "IN_PROGRESS", "AT_RISK", and "UNREACHABLE" must match classify_run_outcome.value verbatim; the defaults: block exists specifically to catch any value that does not match one of those four cases.

Links

See How

New to Kestra?

Use blueprints to kickstart your first workflows.