Skip to content

Zombie-detection automation false-positives on healthy completed flows #21932

Description

@matthewc97

Bug summary

Description

Some prefect.flow-run.* events appear in the event log but are not delivered to the automation trigger evaluation pipeline. Most notably, prefect.flow-run.Completed events are not seen by automation triggers, causing Proactive triggers that expect: prefect.flow-run.* to fire on healthy flows after they complete.

This breaks the canonical zombie-detection automation pattern documented at https://docs.prefect.io/v3/advanced/detect-zombie-flows.

Reproduction

Step 1: Create the zombie-detection automation from the docs

from datetime import timedelta
from prefect.automations import Automation
from prefect.client.schemas.objects import StateType
from prefect.events.actions import ChangeFlowRunState
from prefect.events.schemas.automations import EventTrigger, Posture
from prefect.events.schemas.events import ResourceSpecification

automation = Automation(
    name="Crash zombie flows",
    trigger=EventTrigger(
        after={"prefect.flow-run.heartbeat"},
        expect={"prefect.flow-run.*"},
        match=ResourceSpecification(
            {"[prefect.resource.id](http://prefect.resource.id/)": ["prefect.flow-run.*"]}
        ),
        for_each={"[prefect.resource.id](http://prefect.resource.id/)"},
        posture=Posture.Proactive,
        threshold=1,
        within=timedelta(seconds=600),
    ),
    actions=[
        ChangeFlowRunState(
            state=StateType.CRASHED,
            message="Flow run marked as crashed due to missing heartbeats.",
        )
    ],
)
automation.create()

Step 2: Run a normal flow that emits at least one heartbeat then completes

import time
from prefect import flow

@flow
def heartbeat_test(duration_seconds: int = 900):
    """Long enough to emit multiple heartbeats with default
    PREFECT_RUNNER_HEARTBEAT_FREQUENCY=180s, then complete normally."""
    time.sleep(duration_seconds)

Deploy and run via a Kubernetes worker. Flow runs to completion normally, emitting heartbeats every 180 seconds and then a Completed event.

Step 3: Observe automation firing 600s after the last heartbeat

The automation fires (incorrectly) and attempts to set the flow run to Crashed. The action fails because the run is already in terminal state Completed:

{
  "event": "prefect.automation.action.failed",
  "payload": {
    "reason": "Failed to set state: Run is already in terminal state COMPLETED.",
    "action_type": "change-flow-run-state"
  }
}

Event timeline (real example from our environment)

For flow run fortunate-vicugna:

Time (UTC-equivalent) Event
13:12:17 prefect.flow-run.heartbeat (1st)
13:15:17 prefect.flow-run.heartbeat (2nd)
13:18:17 prefect.flow-run.heartbeat (3rd)
13:21:17 prefect.flow-run.heartbeat (4th)
13:24:17 prefect.flow-run.heartbeat (5th)
13:27:17 prefect.flow-run.heartbeat (6th, last)
13:27:17 prefect.flow-run.Completed
13:37:22 prefect.automation.triggered (Zombie -> Crashed)
13:37:22 prefect.automation.action.failed (Run already in terminal state COMPLETED)

The prefect.flow-run.Completed event was emitted at the same instant as the last heartbeat (13:27:17) — well within the 600-second within window. The trigger should have considered the expect: prefect.flow-run.* clause satisfied and not fired. Instead, the automation fired exactly 600 seconds after the last heartbeat as if no follow-up event was ever received.

The Completed event is visible both in the UI Event Feed and via the API:

curl -X POST "$PREFECT_API_URL/events/filter" \
  -H "Authorization: Bearer $PREFECT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "filter": {
      "resource": {"id": ["prefect.flow-run.<run-id>"]},
      "event": {"name": ["prefect.flow-run.Completed"]}
    },
    "limit": 5
  }'

So the event is in the event log; it just isn't reaching the automation trigger evaluation pipeline.

Expected behavior

The trigger's expect: prefect.flow-run.* clause should be satisfied by any prefect.flow-run.* event arriving within the within window after the after event. prefect.flow-run.Completed should qualify, preventing the Proactive trigger from firing on healthy flows.

Actual behavior

prefect.flow-run.Completed (and possibly other state-change events) reach the event log but not the automation trigger evaluation pipeline. The trigger fires after the within window expires, even though qualifying events were emitted.

Impact

The zombie-detection pattern documented at https://docs.prefect.io/v3/advanced/detect-zombie-flows is unreliable. Every healthy flow run that heartbeats then completes will trigger the automation. The action gracefully fails for terminal states, but there is a race window where the automation could incorrectly mark a still-completing flow as Crashed, and the failed-action events generate significant noise in the automation feed.

Environment notes

  • Self-hosted Prefect server running on Kubernetes
  • Kubernetes worker submitting flows as Jobs
  • Default PREFECT_RUNNER_HEARTBEAT_FREQUENCY=180 (heartbeats every 3 minutes)

Version info

Version:              3.7.0
API version:          0.8.4
Python version:       3.11.15
Git commit:           85c59e0e
Built:                Wed, May 06, 2026 03:04 PM
OS/Arch:              linux/x86_64
Profile:              ephemeral
Server type:          ephemeral
Pydantic version:     2.13.4
Server:
  Database:           postgresql
  PostgreSQL version: 15.2
Integrations:
  prefect-redis:      0.2.11

Additional context

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workinggreat writeupThis is a wonderful example of our standards

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions