Nicholas Pini
04/30/2026, 10:23 AM{
"type": "event",
"match": {
"prefect.resource.id": [
"prefect.flow-run.*"
]
},
"match_related": {},
"after": [
"prefect.flow-run.heartbeat"
],
"expect": [
"prefect.flow-run.Failed",
"prefect.flow-run.Cancelled",
"prefect.flow-run.Completed",
"prefect.flow-run.heartbeat",
"prefect.flow-run.Crashed"
],
"for_each": [
"prefect.resource.id"
],
"posture": "Proactive",
"threshold": 1,
"within": 300
}
All flows have the env variables PREFECT_FLOWS_HEARTBEAT_FREQUENCY set to 30. However, this is not working. Why?Marvin
04/30/2026, 10:23 AMMarvin
04/30/2026, 10:25 AMPREFECT_FLOWS_HEARTBEAT_FREQUENCY is only meaningful in newer Prefect 3 releases. Since I know you’ve mentioned using Prefect 3.4.9 before, that immediately stands out: that version is older than the versions where flow-run heartbeat support was introduced/fixed. If your workers/flow runtime are on 3.4.9, setting the env var alone won’t make prefect.flow-run.heartbeat events appear.
Docs: Detect zombie flows
2. Heartbeats are emitted by the flow runtime, not just the server
Your automation only works if the running flow process is actually sending prefect.flow-run.heartbeat events to the API.
So all of these must be true:
- the flow runtime version supports heartbeats
- the flow is a deployment-backed run
- the runtime has PREFECT_API_URL pointed at your self-hosted server
- events are enabled/reaching the server
3. Self-hosted server does support event triggers
So “self-hosted” by itself is not the reason. Event triggers are supported in OSS/server. The more likely issue is no heartbeat events are being emitted.
Event trigger docs: Event triggers
4. Your trigger shape is generally reasonable
This pattern is the standard idea for zombie detection:
- `after`: first heartbeat starts the window
- `expect`: another heartbeat or terminal state must arrive within within
- `posture: Proactive`: fire when expected events do not arrive
With heartbeat frequency 30s and within: 300, that should be plenty.
What I’d check first
Check whether heartbeat events exist at all
In the UI, inspect events for a running flow run and see whether you ever see prefect.flow-run.heartbeat.
If you do not see them, the automation can never fire.
Check your Prefect version on the runtime side
From the environment actually running the flow:
bash
prefect version
If that’s still 3.4.9, I would strongly suspect that’s the problem.
Check the env var is present in the flow execution environment
Not just where you build/deploy, but where the flow process actually runs.
bash
echo $PREFECT_FLOWS_HEARTBEAT_FREQUENCY
echo $PREFECT_API_URL
Check the run is a deployed flow run
Zombie detection is intended for deployed flow runs; ad hoc local execution may not behave the same way.
Bottom line
The most likely answer is:
PREFECT_FLOWS_HEARTBEAT_FREQUENCY=30 is not enough on Prefect 3.4.9, because that version likely predates the flow heartbeat/zombie-detection support you’re trying to use.
So the fix is usually to upgrade both the server and the flow runtime/worker images to a newer Prefect 3 version, then verify that prefect.flow-run.heartbeat events are actually appearing.
If you want, I can help you narrow this down quickly with a 3-step check:
1. your server version
2. your worker/runtime version
3. whether prefect.flow-run.heartbeat appears in Events for an active runNicholas Pini
04/30/2026, 10:27 AMMarvin
04/30/2026, 10:27 AMMarvin
04/30/2026, 10:32 AMbash
PREFECT_FLOWS_HEARTBEAT_FREQUENCY=30
is likely not enough in 3.6.12.
You should set:
bash
PREFECT_RUNNER_HEARTBEAT_FREQUENCY=30
on the environment where the flow run is actually executed.
Why this matters
In 3.6.12, prefect.flow-run.heartbeat events existed, and proactive triggers existed too, but heartbeats were still wired through the runner-side configuration. The later PREFECT_FLOWS_HEARTBEAT_FREQUENCY behavior/documentation landed after that.
So your automation can be perfectly valid, events/traces can generally work, and PREFECT_API_URL can be correct — but if heartbeat emission itself is disabled, you’ll never get the after event that starts the proactive window.
What to check
1. Confirm heartbeat events are present
Look for prefect.flow-run.heartbeat in Events for an actively running flow run.
If none appear, the automation won’t work.
2. Set the old setting for 3.6.12
In the runtime environment:
bash
PREFECT_RUNNER_HEARTBEAT_FREQUENCY=30
3. Optionally set both during transition
To avoid version confusion across images/components:
bash
PREFECT_RUNNER_HEARTBEAT_FREQUENCY=30
PREFECT_FLOWS_HEARTBEAT_FREQUENCY=30
4. Restart workers / runtime containers
So the setting is actually picked up.
Your trigger itself
Your trigger JSON looks conceptually fine for zombie detection:
json
{
"type": "event",
"match": {
"prefect.resource.id": [
"prefect.flow-run.*"
]
},
"after": [
"prefect.flow-run.heartbeat"
],
"expect": [
"prefect.flow-run.Failed",
"prefect.flow-run.Cancelled",
"prefect.flow-run.Completed",
"prefect.flow-run.heartbeat",
"prefect.flow-run.Crashed"
],
"for_each": [
"prefect.resource.id"
],
"posture": "Proactive",
"threshold": 1,
"within": 300
}
So I would not start by changing the automation logic. I’d first verify that heartbeat events are emitted at all.
Relevant docs
- Detect zombie flows
- Event triggers
If you want, I can also help you with a very quick test flow to confirm whether prefect.flow-run.heartbeat is being emitted in your 3.6.12 setup.Nicholas Pini
04/30/2026, 12:33 PMMarvin
04/30/2026, 12:34 PMMarvin
04/30/2026, 12:36 PMexpect matches the event name
- match matches event/resource labels like prefect.state-type and prefect.state-name
For flow-run state events, Prefect includes both:
- prefect.state-name
- prefect.state-type
So if a flow finishes with:
- state name = SomeStates
- state type = COMPLETED
then the event name is based on the name, but the event resource still has:
text
prefect.state-type = COMPLETED
So to avoid treating custom completed states like failures/crashes, configure the automation to check:
text
prefect.state-type
instead of relying on state names.
Example trigger pattern:
json
{
"match": {
"prefect.resource.id": "prefect.flow-run.*",
"prefect.state-type": "CRASHED"
},
"expect": [
"prefect.flow-run.*"
]
}
If your current automation is keying off a list of names, change it so the filter uses:
text
prefect.state-type: CRASHED
or whatever type you actually want to detect.
If you want to exclude custom state names as well, you can also add a state-name filter, but for your case the cleaner fix is to use prefect.state-type.
Useful docs:
- Event triggers
- Flow run event payloads
If you paste your current automation YAML/JSON, I can rewrite it to use state-type directly.Nicholas Pini
04/30/2026, 12:39 PMMarvin
04/30/2026, 12:39 PMMarvin
04/30/2026, 12:39 PMprefect.state-name
- replace it with type-based matching like prefect.state-type
- avoid relying on expect values that encode custom state names when you really mean canonical types
For example, this kind of name-based logic:
json
{
"match": {
"prefect.resource.id": "prefect.flow-run.*",
"prefect.state-name": "CrashedZombie"
}
}
would become:
json
{
"match": {
"prefect.resource.id": "prefect.flow-run.*",
"prefect.state-type": "CRASHED"
}
}
Send over the original automation and I’ll rewrite it exactly.Nicholas Pini
04/30/2026, 12:39 PM{
"type": "event",
"match": {
"prefect.resource.id": [
"prefect.flow-run.*"
]
},
"match_related": {},
"after": [
"prefect.flow-run.heartbeat"
],
"expect": [
"prefect.flow-run.Failed",
"prefect.flow-run.Cancelled",
"prefect.flow-run.Completed",
"prefect.flow-run.heartbeat",
"prefect.flow-run.Crashed"
],
"for_each": [
"prefect.resource.id"
],
"posture": "Proactive",
"threshold": 1,
"within": 300
}Marvin
04/30/2026, 12:39 PMMarvin
04/30/2026, 12:40 PMexpect use state types — expect matches event names, and flow-run event names are built from the state name.
So for your current automation:
json
{
"type": "event",
"match": {
"prefect.resource.id": [
"prefect.flow-run.*"
]
},
"match_related": {},
"after": [
"prefect.flow-run.heartbeat"
],
"expect": [
"prefect.flow-run.Failed",
"prefect.flow-run.Cancelled",
"prefect.flow-run.Completed",
"prefect.flow-run.heartbeat",
"prefect.flow-run.Crashed"
],
"for_each": [
"prefect.resource.id"
],
"posture": "Proactive",
"threshold": 1,
"within": 300
}
the problem is that a custom completed state like SomeStates emits an event like:
text
prefect.flow-run.SomeStates
not prefect.flow-run.Completed.
So if you want this automation to stop treating custom COMPLETED states as missing terminal states, you have two options:
Option 1: broaden expect to all flow-run state events and use `match`/logic elsewhere
Option 2: use a different automation pattern, because proactive expect is inherently name-based
A minimal rewrite that is safer is:
json
{
"type": "event",
"match": {
"prefect.resource.id": [
"prefect.flow-run.*"
]
},
"match_related": {},
"after": [
"prefect.flow-run.heartbeat"
],
"expect": [
"prefect.flow-run.*",
"prefect.flow-run.heartbeat"
],
"for_each": [
"prefect.resource.id"
],
"posture": "Proactive",
"threshold": 1,
"within": 300
}
But important caveat: this now expects any flow-run event, not specifically terminal state types.
So if your goal is specifically:
- heartbeat happens
- then within 300s, accept any state whose type is COMPLETED / FAILED / CANCELLED / CRASHED
- and only alert otherwise
then this exact proactive automation model can’t express that purely with state-type in expect, because expect is event-name-based.
So the honest answer is:
there is no direct “convert expect from state names to state types” version of this automation.
Why:
- expect uses event names like prefect.flow-run.Completed
- custom state names produce custom event names
- prefect.state-type is available in match, but not as a type-aware substitute for expect
Docs:
- Event triggers
- Flow run events
If you want, I can help you redesign this automation so it still detects zombie/crashed runs without being broken by custom completed state names.Nicholas Pini
04/30/2026, 12:43 PMprefect.state-type.COMPLETED etc inside the expect block?Marvin
04/30/2026, 12:43 PMMarvin
04/30/2026, 12:43 PMexpect.
expect only matches event names, and prefect.state-type.COMPLETED is not an event name.
So these are valid in `expect`:
json
"expect": [
"prefect.flow-run.Completed",
"prefect.flow-run.Failed",
"prefect.flow-run.Crashed"
]
but this is not valid:
json
"expect": [
"prefect.state-type.COMPLETED"
]
because prefect.state-type is a resource label on the event payload, not an event name.
For flow-run state events, Prefect emits something conceptually like:
json
{
"event": "prefect.flow-run.SomeStates",
"resource": {
"prefect.resource.id": "prefect.flow-run.<id>",
"prefect.state-name": "SomeStates",
"prefect.state-type": "COMPLETED"
}
}
So:
- event / expect -> prefect.flow-run.SomeStates
- match -> can inspect prefect.state-type = COMPLETED
That’s why custom state names break name-based expect matching.
References:
- Event triggers
- Flow run events
If you want, I can suggest the closest workable replacement for your zombie-detection automation.