New to Kestra?
Use blueprints to kickstart your first workflows.
Audit an Apify actor's last run for FAILED, TIMED-OUT, or ABORTED status, alert on breach, and optionally retry it.
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.
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.probe_last_run_status (io.kestra.plugin.apify.dataset.GetLastRun, allowFailure: true) reads actor_id's most recent run, returning status among other fields.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).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".log_audit_complete always runs last, printing the classification regardless of which branch fired.errors block alerts Slack separately if the flow itself fails outside these handled branches.maxTotalChargeUsd) on every retry, so remediation cannot itself become the runaway-spend incident this blueprint exists to prevent.auto_remediate, dry_run) between "run failed" and "a new run actually launched".actor_id.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.GetLastRun used as a standing health check rather than a one-off lookup after a trigger.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.
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.
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.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.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.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.actor_id at an actor with at least one completed run (any Apify Store actor you have run before works).APIFY_API_TOKEN and SLACK_WEBHOOK_URL secrets.log_healthy fires.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.auto_remediate: "true" and dry_run: "true" against that same actor and confirm log_dry_run_remediation describes the retry without launching it.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.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.actor_id values with io.kestra.plugin.core.flow.Loop to audit every scheduled actor in an Apify organization in one pass.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.vars or an external store, so an actor that keeps failing escalates to a human instead of retrying forever on every scheduled audit.Pause approval task instead of (or alongside) the static auto_remediate input, for a human-reviewed retry on a production actor.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.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.