Olve Heitmann
10/23/2025, 7:59 AMMarvin
10/23/2025, 7:59 AMdisplay_callable_signature
📊 Tools used:
• check_cli_command (2x)
• display_callable_signature (2x)
• explore_module_offerings (3x)
• research_prefect_topic (1x)Olve Heitmann
10/23/2025, 8:48 AMMarvin
10/23/2025, 8:48 AMMarvin
10/23/2025, 8:48 AMOlve Heitmann
10/23/2025, 8:49 AMMarvin
10/23/2025, 8:49 AMMarvin
10/23/2025, 9:02 AM# 1) Save a Slack webhook block once
from prefect.blocks.notifications import SlackWebhook
SlackWebhook(
url="<https://hooks.slack.com/services/XXX/YYY/ZZZ>"
).save("pool-alerts", overwrite=True)
# 2) Create an automation that listens for failures from a given work pool
import asyncio
from prefect.automations import AutomationCore, EventTrigger, SendNotification, Posture
POOL_NAME = "my-work-pool"
automation = AutomationCore(
name=f"Alert: failures in pool {POOL_NAME}",
description=f"Notify Slack when any flow run from work pool '{POOL_NAME}' fails",
enabled=True,
triggers=[
EventTrigger(
# catch both execution failures and crashes
expect=["prefect.flow-run.Failed", "prefect.flow-run.Crashed"],
# filter to runs related to this work pool
match_related=[{"prefect.work-pool.name": POOL_NAME}],
posture=Posture.Reactive,
threshold=1,
within=0,
)
],
actions=[
SendNotification(
# reference the Slack block you saved above
block_urn="slack-webhook/pool-alerts",
subject="Flow run failure in {{ event.related['prefect.work-pool'][0].name }}",
body=(
"Flow run {{ event.resource.name }} failed for deployment "
"{{ event.related['prefect.deployment'][0].name }} in pool "
"{{ event.related['prefect.work-pool'][0].name }}.\n"
"Link: {{ event.resource.url }}"
),
)
],
)
asyncio.run(automation.acreate())
Option B: REST API (cURL)
1) Save the Slack block once (you can do this via Python as above or REST if preferred).
2) Create the automation via POST /automations:
curl -X POST "$PREFECT_API_URL/automations" \
-H "Authorization: Bearer $PREFECT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Alert: failures in pool my-work-pool",
"description": "Notify Slack when any flow run from work pool fails",
"enabled": true,
"triggers": [{
"type": "event",
"expect": ["prefect.flow-run.Failed", "prefect.flow-run.Crashed"],
"match_related": [{"prefect.work-pool.name": "my-work-pool"}],
"posture": "Reactive",
"threshold": 1,
"within": 0
}],
"actions": [{
"type": "send_notification",
"block_urn": "slack-webhook/pool-alerts",
"subject": "Flow run failure in {{ event.related['prefect.work-pool'][0].name }}",
"body": "Flow run {{ event.resource.name }} failed. Link: {{ event.resource.url }}"
}]
}'
How to confirm the filter keys for your workspace
- The work pool filter commonly uses the related resource label key prefect.work-pool.name. To confirm, stream a real event and inspect its JSON:
prefect events stream --run-once
Then trigger a failure and check the event’s related resources to verify the exact key names.
Notes and caveats
- CLI: Creating automations via CLI isn’t exposed in the current prefect automation help; use Python or REST for creation. You can still list/inspect/pause/resume via CLI.Marvin
10/23/2025, 9:02 AMexpect.
Docs for reference
- Automations (create/configure): Creating automations
- Event triggers and filters: Event triggers
- Notifications blocks (SlackWebhook): Notifications blocks API
- REST API (Automations): Create automation (REST)
Want me to tailor this for your environment? If you share:
- the work pool name
- whether you want to include Crashed events
- the Slack channel/webhook block name
…I can give you a ready-to-run snippet.Olve Heitmann
10/23/2025, 9:33 AMMarvin
10/23/2025, 9:33 AMMarvin
10/23/2025, 9:38 AM# Reuse your saved Slack block or save one if needed
# from prefect.blocks.notifications import SlackWebhook
# SlackWebhook(url="<https://hooks.slack.com/services/XXX/YYY/ZZZ>").save("pool-alerts", overwrite=True)
import asyncio
from prefect.automations import AutomationCore, EventTrigger, SendNotification, Posture
POOL_NAME = "my-work-pool" # or set to None to alert for all pools
long_run_trigger = EventTrigger(
# Start watching when a run goes to Running
after=["prefect.flow-run.Running"],
# Expect it to finish (any terminal state) within 24h
expect=[
"prefect.flow-run.Completed",
"prefect.flow-run.Failed",
"prefect.flow-run.Crashed",
"prefect.flow-run.Cancelled",
],
posture=Posture.Proactive,
within=86400, # 24 hours in seconds
threshold=1,
for_each=["prefect.resource.id"], # evaluate per run
# Limit to a specific work pool if desired
match_related=([{"prefect.work-pool.name": POOL_NAME}] if POOL_NAME else []),
)
automation = AutomationCore(
name="Alert: runs exceeding 24h",
description="Notify Slack when a flow run is still not finished after 24 hours",
enabled=True,
triggers=[long_run_trigger],
actions=[
SendNotification(
block_urn="slack-webhook/pool-alerts",
subject="Long-running flow (>24h): {{ event.resource.name }}",
body=(
"Flow run {{ event.resource.name }} is still not finished after 24h.\n"
"{% if event.related['prefect.deployment'] %}"
"Deployment: {{ event.related['prefect.deployment'][0].name }}\n"
"{% endif %}"
"{% if event.related['prefect.work-pool'] %}"
"Work pool: {{ event.related['prefect.work-pool'][0].name }}\n"
"{% endif %}"
"Run ID: {{ event.resource.id }}\n"
"Link: {{ event.resource.url }}"
),
)
],
)
asyncio.run(automation.acreate())
Option B: REST (cURL)
curl -X POST "$PREFECT_API_URL/automations" \
-H "Authorization: Bearer $PREFECT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Alert: runs exceeding 24h",
"description": "Notify Slack when a flow run is still not finished after 24 hours",
"enabled": true,
"triggers": [{
"type": "event",
"after": ["prefect.flow-run.Running"],
"expect": [
"prefect.flow-run.Completed",
"prefect.flow-run.Failed",
"prefect.flow-run.Crashed",
"prefect.flow-run.Cancelled"
],
"posture": "Proactive",
"within": 86400,
"threshold": 1,
"for_each": ["prefect.resource.id"],
"match_related": [{"prefect.work-pool.name": "my-work-pool"}]
}],
"actions": [{
"type": "send_notification",
"block_urn": "slack-webhook/pool-alerts",
"subject": "Long-running flow (>24h): {{ event.resource.name }}",
"body": "Flow run {{ event.resource.name }} has not finished after 24h. Link: {{ event.resource.url }}"
}]
}'
Notes
- If you want this alert for all pools, omit match_related.
- If you want to exclude paused runs (i.e., only alert if truly still Running), say the word—we can refine the trigger logic. By default, this fires if the run doesn’t reach any terminal state within 24h, regardless of intermediate pauses.Marvin
10/23/2025, 9:38 AMOlve Heitmann
10/23/2025, 9:55 AMMarvin
10/23/2025, 9:55 AMMarvin
10/23/2025, 9:57 AMimport asyncio
from datetime import timedelta
from prefect.automations import AutomationCore, EventTrigger, SendNotification, Posture
# Configure how many 6h reminders after the first (24h) you want
REMINDERS = 5 # 24h + (6h * 1..5) -> 30h, 36h, ..., 54h
base_within = 24 * 3600 # seconds
interval = 6 * 3600 # seconds
triggers = []
for i in range(0, REMINDERS + 1):
triggers.append(
EventTrigger(
after=["prefect.flow-run.Running"],
expect=[
"prefect.flow-run.Completed",
"prefect.flow-run.Failed",
"prefect.flow-run.Crashed",
"prefect.flow-run.Cancelled",
],
posture=Posture.Proactive,
within=base_within + i * interval,
threshold=1,
for_each=["prefect.resource.id"], # evaluate per run
# all work pools -> no match_related filter
)
)
automation = AutomationCore(
name="Alert: long-running flows with 6h reminders",
description="Notify Slack if a flow run is still not finished after 24h, and every 6h after",
enabled=True,
triggers=triggers,
actions=[
SendNotification(
block_urn="slack-webhook/pool-alerts",
subject="Long-running flow: {{ event.resource.name }}",
body=(
"Flow run {{ event.resource.name }} has not reached a terminal state.\n"
"{% if event.related['prefect.deployment'] %}"
"Deployment: {{ event.related['prefect.deployment'][0].name }}\n"
"{% endif %}"
"{% if event.related['prefect.work-pool'] %}"
"Work pool: {{ event.related['prefect.work-pool'][0].name }}\n"
"{% endif %}"
"Run ID: {{ event.resource.id }}\n"
"Link: {{ event.resource.url }}"
),
)
],
)
asyncio.run(automation.acreate())
Option 2: Monitoring flow (indefinite reminders every 6h)
- Run a small “watchdog” flow on a 6-hour schedule.
- It finds flow runs that have been Running for > 24h and posts Slack each time it runs.
- This gives you indefinite reminders without pre-defining many triggers.
Sketch of a monitoring flow
```
from datetime import datetime, timedelta, timezone
from prefect import flow
from prefect.blocks.notifications import SlackWebhook
from prefect.client.orchestration import get_client
from prefect.client.schemas import StateType
@flow(name="long-running-watchdog")
async def long_running_watchdog(slack_block_name: str = "pool-alerts", min_age_hours: int = 24):
slack = await SlackWebhook.load(slack_block_name)
cutoff = datetime.now(timezone.utc) - timedelta(hours=min_age_hours)
async with get_client() as client:
# List recent runs and filter in Python for simplicity
# You can tighten filters using client APIs if desired
runs = await client.read_flow_runs(
# Optionally pass server-side filters to limit volume
# e.g., state={"type": ["RUNNING"]}, start_time=...
)
long_running = []
for r in runs:
# r.state.state_type == StateType.RUNNING and r.start_time older than cutoff
if getattr(r.state, "type", None) == StateType.RUNNING and r.start_time and r.start_time < cutoff:
long_running.append(r)
for r in long_running:
url = getattr(r, "dashboard_url", None) or ""
await slack.notify(
f"Long-running flow (> {min_age_hours}h): {r.name}\n"Marvin
10/23/2025, 9:57 AMprefect deploy.
- Or should I set the Automations-only version to, say, 10 reminders (up to 84h)? I can post the exact Python or REST you can run.
References
- Event triggers (Proactive): Event triggers
- Creating automations programmatically: Creating automations
- Notifications blocks (Slack): Notifications blocks APIOlve Heitmann
10/23/2025, 10:53 AMMarvin
10/23/2025, 10:53 AMMarvin
10/23/2025, 10:55 AMfrom prefect.blocks.notifications import SlackWebhook
SlackWebhook(url="<https://hooks.slack.com/services/XXX/YYY/ZZZ>").save(
"pool-alerts", overwrite=True
)
Step 2 — Create watchdog.py
```
from datetime import datetime, timedelta, timezone
from typing import List, Optional
from prefect import flow, get_run_logger
from prefect.blocks.notifications import SlackWebhook
from prefect.client.orchestration import get_client
from prefect.client.schemas import StateType
from prefect.settings import get_current_settings
# Optional: narrow by tags or flow name if needed
WATCH_TAGS: Optional[List[str]] = None # e.g., ["prod"]
SLACK_BLOCK_NAME = "pool-alerts"
THRESHOLD_HOURS = 24
def ui_link_for_run(run_id: str) -> str:
# Works for Prefect Cloud and Prefect Server if PREFECT_UI_URL is set
ui_base = get_current_settings().PREFECT_UI_URL.value
return f"{ui_base}/flow-runs/flow-run/{run_id}".rstrip("/")
@flow(name="long-running-watchdog")
async def long_running_watchdog(
threshold_hours: int = THRESHOLD_HOURS,
slack_block_name: str = SLACK_BLOCK_NAME,
watch_tags: Optional[List[str]] = WATCH_TAGS,
max_runs: int = 2000,
):
logger = get_run_logger()
cutoff = datetime.now(timezone.utc) - timedelta(hours=threshold_hours)
slack = await SlackWebhook.load(slack_block_name)
async with get_client() as client:
# Prefer server-side filtering by RUNNING state + start_time <= cutoff.
# Depending on your Prefect minor version, use one of the two calls below.
runs = []
filter_error = None
try:
# Variant A: newer Prefect 3.x (filters=FilterSet(...))
from prefect.client.schemas.filters import (
FilterSet,
FlowRunFilter,
FlowRunFilterState,
FlowRunFilterStateType,
FlowRunFilterStartTime,
FlowRunFilterTags,
Operator,
)
from prefect.client.schemas.sorting import FlowRunSort
flow_run_filter = FlowRunFilter(
state=FlowRunFilterState(
type=FlowRunFilterStateType(any_=[StateType.RUNNING])
),
start_time=FlowRunFilterStartTime(before_=cutoff),
tags=(
FlowRunFilterTags(all_=watch_tags) if watch_tags else None
),
)
filters = FilterSet(flow_runs=flow_run_filter)
runs = await client.read_flow_runs(
filters=filters,
sort=FlowRunSort.START_TIME_ASC,
limit=max_runs,
)
except Exception as e:
# Variant B: some versions use flow_run_filter=... instead of filters=...
filter_error = e
try:
from prefect.client.schemas.filters import (
FlowRunFilter,
FlowRunFilterState,
FlowRunFilterStateType,
FlowRunFilterStartTime,
FlowRunFilterTags,
)
from prefect.client.schemas.sorting import FlowRunSort
flow_run_filter = FlowRunFilter(
state=FlowRunFilterState(
type=FlowRunFilterStateType(any_=[StateType.RUNNING])
),
start_time=FlowRunFilterStartTime(before_=cutoff),
tags=(
FlowRunFilterTags(all_=watch_tags) if watch_tags else None
),
)
runs = await client.read_flow_runs(Marvin
10/23/2025, 10:55 AMStep 3 — Deploy it (every 6 hours, Python-only)
- Make sure you have a worker running on the work pool where this watchdog should run.
- In the same repo as watchdog.py, run the block below once to create the deployment.
# create_watchdog_deployment.py
from datetime import timedelta
from prefect.schedules import Interval
from watchdog import long_running_watchdog
if name == "__main__":
long_running_watchdog.from_source(
source=".", # this repo
entrypoint="watchdog.py:long_running_watchdog",
).deploy(
name="long-running-watchdog",
work_pool_name="YOUR_WATCHDOG_WORK_POOL",
schedule=Interval(interval=timedelta(hours=6)),
tags=["monitoring", "alerts"],
description="Alerts when any flow run exceeds 24h; re-alerts every 6h",
parameters={
"threshold_hours": 24,
"slack_block_name": "pool-alerts",
# "watch_tags": ["prod"], # uncomment to limit by tags
},
)
```
Then:
- Start or ensure a worker is running for YOUR_WATCHDOG_WORK_POOL.
- The flow will run every 6 hours and post a Slack message per run that’s still >24h Running.
Notes
- This scans all work pools by default (no pool filter). You can narrow by tags via the watch_tags parameter.
- The server-side filter signature can differ slightly by minor version; the code above tries both forms and falls back to Python filtering.
- If your workspace has huge run volume and you hit limits, we can add tighter filters (e.g., recent start times or paging).
Docs
- Blocks (SlackWebhook): Notifications blocks API
- Schedules: Schedules
Want me to fill in a ready-to-run repo skeleton for you? If you share:
- the Slack block name (or webhook URL),
- the work pool to run the watchdog,
- optional tag filters,
…I’ll tailor the files and params exactly.Olve Heitmann
10/23/2025, 11:25 AMMarvin
10/23/2025, 11:26 AMMarvin
10/23/2025, 11:27 AMread_flow_runs signature
- Uses that choice for all subsequent calls
- Scans all work pools
- Alerts for runs >24h and re-alerts every 6h via a scheduled deployment
Step 1 — Save Slack webhook block (one-time)
from prefect.blocks.notifications import SlackWebhook
SlackWebhook(url="<https://hooks.slack.com/services/XXX/YYY/ZZZ>").save(
"pool-alerts", overwrite=True
)
Step 2 — watchdog.py
```
from datetime import datetime, timedelta, timezone
from functools import lru_cache
import inspect
from typing import List, Optional
import prefect
from prefect import flow, get_run_logger
from prefect.blocks.notifications import SlackWebhook
from prefect.client.orchestration import get_client, PrefectClient
from prefect.client.schemas import StateType
from prefect.settings import get_current_settings
from prefect.client.schemas.filters import (
FilterSet,
FlowRunFilter,
FlowRunFilterState,
FlowRunFilterStateType,
FlowRunFilterStartTime,
FlowRunFilterTags,
)
from prefect.client.schemas.sorting import FlowRunSort
SLACK_BLOCK_NAME = "pool-alerts"
THRESHOLD_HOURS_DEFAULT = 24
def ui_link_for_run(run_id: str) -> str:
ui_base = (get_current_settings().PREFECT_UI_URL.value or "").rstrip("/")
# Works for Cloud and Server if PREFECT_UI_URL is set
return f"{ui_base}/flow-runs/flow-run/{run_id}"
@lru_cache(maxsize=1)
def _read_flow_runs_call_style() -> str:
# Decide once based on the signature on your install
try:
params = inspect.signature(PrefectClient.read_flow_runs).parameters
if "filters" in params:
return "filters"
if "flow_run_filter" in params:
return "flow_run_filter"
except Exception:
pass
# If introspection fails, we'll probe on first use
return "probe"
async def _list_long_running_runs(client: PrefectClient, cutoff, watch_tags: Optional[List[str]], limit: int):
style = _read_flow_runs_call_style()
flow_run_filter = FlowRunFilter(
state=FlowRunFilterState(type=FlowRunFilterStateType(any_=[StateType.RUNNING])),
start_time=FlowRunFilterStartTime(before_=cutoff),
tags=(FlowRunFilterTags(all_=watch_tags) if watch_tags else None),
)
if style == "filters":
return await client.read_flow_runs(
filters=FilterSet(flow_runs=flow_run_filter),
sort=FlowRunSort.START_TIME_ASC,
limit=limit,
)
if style == "flow_run_filter":
return await client.read_flow_runs(
flow_run_filter=flow_run_filter,
sort=FlowRunSort.START_TIME_ASC,
limit=limit,
)
# Probe once if we couldn’t introspect
try:
runs = await client.read_flow_runs(
filters=FilterSet(flow_runs=flow_run_filter),
sort=FlowRunSort.START_TIME_ASC,
limit=limit,
)
# Cache the working style
_read_flow_runs_call_style.cache_clear()
_read_flow_runs_call_style.cache_update({"": "filters"}) # no-op; cache_update not real; rely on cache_clear+call
return runs
except TypeError:
return await client.read_flow_runs(
flow_run_filter=flow_run_filter,
sort=FlowRunSort.START_TIME_ASC,
limit=limit,
)
@flow(name="long-running-watchdog")
async def long_running_watchdog(
threshold_hours: int = THRESHOLD_HOURS_DEFAULT,
slack_block_name: str = SLACK_BLOCK_NAME,
watch_tags: Optional[List[str]] = None, # e.g. ["prod"] to scope
max_runs: int = 2000,
):
logger = get_run_logger()
slack = await SlackWebhook.load(slack_block_name)
cutoff = datetime.now(timezone.utc) - timedelta(hours=threshold_hours)
async with get_client() as client:Marvin
10/23/2025, 11:27 AMStep 3 — Deploy to run every 6 hours
# create_watchdog_deployment.py
from datetime import timedelta
from prefect.schedules import Interval
from watchdog import long_running_watchdog
if name == "__main__":
long_running_watchdog.from_source(
source=".", # project root
entrypoint="watchdog.py:long_running_watchdog",
).deploy(
name="long-running-watchdog",
work_pool_name="YOUR_WATCHDOG_WORK_POOL",
schedule=Interval(interval=timedelta(hours=6)),
tags=["monitoring", "alerts"],
description="Alerts when any flow run exceeds 24h; re-alerts every 6h",
parameters={
"threshold_hours": 24,
"slack_block_name": "pool-alerts",
# "watch_tags": ["prod"],
},
)
```
This gives you:
- Runtime detection of the correct read_flow_runs style with a single, centralized probe
- Clean code path for your environment
- Alerts across all work pools, indefinitely every 6h until the run finishes
Want me to fill in YOUR_WATCHDOG_WORK_POOL and any tag filters and hand back a ready-to-run zip?Olve Heitmann
10/23/2025, 11:29 AMMarvin
10/23/2025, 11:29 AMMarvin
10/23/2025, 11:30 AM# test_read_flow_runs_api.py
import asyncio
import inspect
from pathlib import Path
from prefect.client.orchestration import get_client, PrefectClient
from prefect.client.schemas import StateType
from prefect.client.schemas.filters import (
FilterSet,
FlowRunFilter,
FlowRunFilterState,
FlowRunFilterStateType,
)
from prefect.client.schemas.sorting import FlowRunSort
STYLE_FILE = Path(".watchdog_style")
async def main():
suggested = "unknown"
try:
params = inspect.signature(PrefectClient.read_flow_runs).parameters
if "filters" in params:
suggested = "filters"
elif "flow_run_filter" in params:
suggested = "flow_run_filter"
except Exception:
pass
print(f"suggested by signature: {suggested}")
async with get_client() as client:
frf = FlowRunFilter(
state=FlowRunFilterState(
type=FlowRunFilterStateType(any_=[StateType.RUNNING])
)
)
try:
runs = await client.read_flow_runs(
filters=FilterSet(flow_runs=frf),
sort=FlowRunSort.START_TIME_ASC,
limit=1,
)
confirmed = "filters"
except TypeError:
runs = await client.read_flow_runs(
flow_run_filter=frf,
sort=FlowRunSort.START_TIME_ASC,
limit=1,
)
confirmed = "flow_run_filter"
print(f"confirmed working style: {confirmed}; returned {len(runs)} runs")
STYLE_FILE.write_text(confirmed)
print(f"wrote: {STYLE_FILE.resolve()}")
if __name__ == "__main__":
asyncio.run(main())
Script 2 — List “long-running” candidates using the detected style
- Reads .watchdog_style
- Prints up to 10 long-running RUNNING flow runs (older than 24h)
```
# test_watchdog_query.py
import asyncio
from datetime import datetime, timedelta, timezone
from pathlib import Path
from prefect.client.orchestration import get_client
from prefect.client.schemas import StateType
from prefect.client.schemas.filters import (
FilterSet,
FlowRunFilter,
FlowRunFilterState,
FlowRunFilterStateType,
FlowRunFilterStartTime,
)
from prefect.client.schemas.sorting import FlowRunSort
STYLE_FILE = Path(".watchdog_style")
async def main(threshold_hours=24, limit=2000, preview=10):
style = STYLE_FILE.read_text().strip() if STYLE_FILE.exists() else None
print(f"using style from file: {style}")
cutoff = datetime.now(timezone.utc) - timedelta(hours=threshold_hours)
frf = FlowRunFilter(
state=FlowRunFilterState(
type=FlowRunFilterStateType(any_=[StateType.RUNNING])
),
start_time=FlowRunFilterStartTime(before_=cutoff),
)
async with get_client() as client:
if style == "filters":
runs = await client.read_flow_runs(
filters=FilterSet(flow_runs=frf),
sort=FlowRunSort.START_TIME_ASC,
limit=limit,
)
elif style == "flow_run_filter":
runs = await client.read_flow_runs(
flow_run_filter=frf,
sort=FlowRunSort.START_TIME_ASC,
limit=limit,
)
else:
# last-resort probe
try:
runs = await client.read_flow_runs(
filters=FilterSet(flow_runs=frf),
sort=FlowRunSort.START_TIME_ASC,
limit=limit,
)
except TypeError:
runs = await client.read_flow_runs(Marvin
10/23/2025, 11:30 AMScript 3 — Send a Slack test via your block
# test_slack_block.py
import asyncio
from prefect.blocks.notifications import SlackWebhook
async def main(block_name="pool-alerts"):
blk = await SlackWebhook.load(block_name)
await blk.notify("✅ Prefect watchdog Slack test message")
if name == "__main__":
asyncio.run(main())
How to run the tests
- Make sure your Prefect profile is pointed at your workspace (env vars or logged in).
- Run:
python test_read_flow_runs_api.py
python test_watchdog_query.py
python test_slack_block.py
If these pass and look good, you’re ready to bake the results into a single, clean watchdog deployment.
Final step — Implement the single watchdog flow using the detected style
- Read the style from .watchdog_style and use it directly (no branching except that one read).
- Schedule every 6 hours to get repeated reminders.
watchdog.py (final)
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Optional, List
from prefect import flow, get_run_logger
from prefect.blocks.notifications import SlackWebhook
from prefect.client.orchestration import get_client
from prefect.client.schemas import StateType
from prefect.client.schemas.filters import (
FilterSet,
FlowRunFilter,
FlowRunFilterState,
FlowRunFilterStateType,
FlowRunFilterStartTime,
FlowRunFilterTags,
)
from prefect.client.schemas.sorting import FlowRunSort
from prefect.settings import get_current_settings
STYLE_FILE = Path(".watchdog_style")
def ui_link_for_run(run_id: str) -> str:
ui = (get_current_settings().PREFECT_UI_URL.value or "").rstrip("/")
return f"{ui}/flow-runs/flow-run/{run_id}"
@flow(name="long-running-watchdog")
async def long_running_watchdog(
threshold_hours: int = 24,
slack_block_name: str = "pool-alerts",
watch_tags: Optional[List[str]] = None,
max_runs: int = 2000,
):
logger = get_run_logger()
style = STYLE_FILE.read_text().strip() if STYLE_FILE.exists() else "filters"
cutoff = datetime.now(timezone.utc) - timedelta(hours=threshold_hours)
frf = FlowRunFilter(
state=FlowRunFilterState(
type=FlowRunFilterStateType(any_=[StateType.RUNNING])
),
start_time=FlowRunFilterStartTime(before_=cutoff),
tags=FlowRunFilterTags(all_=watch_tags) if watch_tags else None,
)
async with get_client() as client:
if style == "filters":
runs = await client.read_flow_runs(
filters=FilterSet(flow_runs=frf),
sort=FlowRunSort.START_TIME_ASC,
limit=max_runs,
)
else:
runs = await client.read_flow_runs(
flow_run_filter=frf,
sort=FlowRunSort.START_TIME_ASC,
limit=max_runs,
)
if not runs:
logger.info("No long-running flow runs found.")
return
slack = await SlackWebhook.load(slack_block_name)
for r in runs:
if not r.start_time:
continue
duration = datetime.now(timezone.utc) - r.start_time
hours = int(duration.total_seconds() // 3600)
link = ui_link_for_run(r.id)
flow_name = getattr(r, "flow_name", "") or ""
run_name = getattr(r, "name", "") or str(r.id)
dep_name = getattr(r, "deployment_name", "") or ""
msg = (
f"⚠️ Long-running flow run detected\n"
f"- Flow: {flow_name}\n"Marvin
10/23/2025, 11:30 AMDeploy it (every 6 hours)
# create_watchdog_deployment.py
from datetime import timedelta
from prefect.schedules import Interval
from watchdog import long_running_watchdog
if name == "__main__":
long_running_watchdog.from_source(
source=".",
entrypoint="watchdog.py:long_running_watchdog",
).deploy(
name="long-running-watchdog",
work_pool_name="YOUR_WATCHDOG_WORK_POOL",
schedule=Interval(interval=timedelta(hours=6)),
tags=["monitoring", "alerts"],
description="Alerts when any flow run exceeds 24h; re-alerts every 6h",
parameters={
"threshold_hours": 24,
"slack_block_name": "pool-alerts",
# "watch_tags": ["prod"],
},
)
```
Run order suggestion
1) Create/load your Slack block and test it
2) Run test_read_flow_runs_api.py to generate .watchdog_style
3) Run test_watchdog_query.py to preview results
4) Deploy the final watchdog using the same repo
References
- Blocks (SlackWebhook): Notifications blocks API
- Schedules: Schedules
If you paste your detected style (filters vs flow_run_filter) and your work pool name, I’ll fill those into a ready-to-run zip.Olve Heitmann
10/23/2025, 12:00 PMMarvin
10/23/2025, 12:00 PMMarvin
10/23/2025, 12:01 PMfrom datetime import datetime, timedelta, timezone
from typing import Optional, List
from prefect import flow, get_run_logger
from prefect.blocks.notifications import SlackWebhook
from prefect.client.orchestration import get_client
from prefect.client.schemas import StateType
from prefect.client.schemas.filters import (
FlowRunFilter,
FlowRunFilterState,
FlowRunFilterStateType,
FlowRunFilterStartTime,
FlowRunFilterTags,
)
from prefect.client.schemas.sorting import FlowRunSort
from prefect.settings import get_current_settings
# Import the saved block name from your create_block.py
from create_block import SLACK_BLOCK_NAME # e.g., "pool-alerts"
def ui_link_for_run(run_id: str) -> str:
ui = (get_current_settings().PREFECT_UI_URL.value or "").rstrip("/")
return f"{ui}/flow-runs/flow-run/{run_id}"
@flow(name="long-running-watchdog")
async def long_running_watchdog(
threshold_hours: int = 24,
slack_block_name: str = SLACK_BLOCK_NAME,
watch_tags: Optional[List[str]] = None, # leave None to scan all
max_runs: int = 2000,
):
logger = get_run_logger()
cutoff = datetime.now(timezone.utc) - timedelta(hours=threshold_hours)
frf = FlowRunFilter(
state=FlowRunFilterState(
type=FlowRunFilterStateType(any_=[StateType.RUNNING])
),
start_time=FlowRunFilterStartTime(before_=cutoff),
tags=FlowRunFilterTags(all_=watch_tags) if watch_tags else None,
)
async with get_client() as client:
runs = await client.read_flow_runs(
flow_run_filter=frf,
sort=FlowRunSort.START_TIME_ASC,
limit=max_runs,
)
if not runs:
<http://logger.info|logger.info>("No long-running flow runs found.")
return
slack = await SlackWebhook.load(slack_block_name)
for r in runs:
if not r.start_time:
continue
duration = datetime.now(timezone.utc) - r.start_time
hours = int(duration.total_seconds() // 3600)
link = ui_link_for_run(r.id)
flow_name = getattr(r, "flow_name", "") or ""
run_name = getattr(r, "name", "") or str(r.id)
dep_name = getattr(r, "deployment_name", "") or ""
msg = (
f":warning: Long-running flow run detected\n"
f"- Flow: {flow_name}\n"
f"- Deployment: {dep_name}\n"
f"- Run: {run_name}\n"
f"- Duration: ~{hours}h (threshold: {threshold_hours}h)\n"
f"- Link: <{link}|Open in Prefect>"
)
await slack.notify(msg)
create_watchdog_deployment.py
from datetime import timedelta
from prefect.schedules import Interval
from watchdog import long_running_watchdog
if __name__ == "__main__":
long_running_watchdog.from_source(
source=".", # project root
entrypoint="watchdog.py:long_running_watchdog",
).deploy(
name="long-running-watchdog",
work_pool_name="YOUR_WATCHDOG_WORK_POOL", # replace with the pool where the watchdog itself should run
schedule=Interval(interval=timedelta(hours=6)), # re-alert cadence
tags=["monitoring", "alerts"],
description="Alerts when any flow run exceeds 24h; re-alerts every 6h",
parameters={
"threshold_hours": 24,
"slack_block_name": None, # default pulls from create_block.SLACK_BLOCK_NAME
# "watch_tags": ["prod"], # optional scoping; omit to scan all runs across all pools
"max_runs": 2000,
},
)
How to run
- You’ve already created the Slack block—great.Marvin
10/23/2025, 12:01 PMpython create_watchdog_deployment.py
This will schedule the watchdog every 6 hours and post Slack alerts for any flow run that remains in Running for 24+ hours, across all work pools.
References
- Blocks (SlackWebhook): Notifications blocks API
- Schedules: Schedules
If you share the watchdog work pool name, I can return these two files with that filled in.Olve Heitmann
10/23/2025, 12:44 PMMarvin
10/23/2025, 12:44 PMMarvin
10/23/2025, 12:45 PMimport asyncio
from brrr.orchestration.prefect_constants import production_pool
from .create_block import SLACK_BLOCK_NAME
from prefect.automations import (
AutomationCore,
EventTrigger,
SendNotification,
Posture,
)
# Adjust this if you only want execution failures (remove "Crashed")
EVENTS_TO_ALERT = ["prefect.flow-run.Failed", "prefect.flow-run.Crashed"]
async def main():
automation = AutomationCore(
name=f"Alert: Failed runs in pool {production_pool}",
description=(
f"Send a Slack message when any flow run from work pool "
f"'{production_pool}' ends in a failure or crash."
),
enabled=True,
triggers=[
EventTrigger(
expect=EVENTS_TO_ALERT,
# Filter by related resource: work pool name
match_related=[{"prefect.work-pool.name": production_pool}],
posture=Posture.Reactive,
threshold=1,
within=0,
)
],
actions=[
SendNotification(
# Reference your saved Slack webhook block by URN
block_urn=f"slack-webhook/{SLACK_BLOCK_NAME}",
subject="Flow run {{ event.resource.name }} failed (pool: {{ event.related['prefect.work-pool'][0].name }})",
body=(
":x: Flow run failure detected\n"
"- Flow run: {{ event.resource.name }}\n"
"{% if event.related['prefect.deployment'] %}"
"- Deployment: {{ event.related['prefect.deployment'][0].name }}\n"
"{% endif %}"
"- Work pool: {{ event.related['prefect.work-pool'][0].name }}\n"
"- State: {{ event.payload.state.name if event.payload and event.payload.state else 'FAILED' }}\n"
"- Run ID: {{ event.resource.id }}\n"
"- Link: {{ event.resource.url }}"
),
)
],
)
created = await automation.acreate()
print(f"Created automation: {created.name} (id={created.id})")
if __name__ == "__main__":
asyncio.run(main())
Notes
- Filtering by pool: The key prefect.work-pool.name in match_related filters events to runs associated with that work pool. If you want to double-check labels in your workspace, you can run prefect events stream --run-once when a failure occurs to inspect the event JSON.
- Only failures vs crashes too: If you only want execution failures, set EVENTS_TO_ALERT = ["prefect.flow-run.Failed"].
- Auth: Make sure your environment is logged into Prefect Cloud or pointed at your Prefect Server profile before running the script.
Docs for reference
- Creating automations: Creating automations
- Event triggers: Event triggers
- Notifications blocks (SlackWebhook): Notifications blocks API
Want me to add a companion script to pause/resume or delete this automation by name, or tailor the Slack message content further?Olve Heitmann
10/23/2025, 12:49 PMMarvin
10/23/2025, 12:49 PMMarvin
10/23/2025, 12:54 PMimport asyncio
from brrr.orchestration.prefect_constants import production_pool
from .create_block import SLACK_BLOCK_NAME
from prefect.automations import AutomationCore, EventTrigger, Posture, SendNotification
from prefect.blocks.notifications import SlackWebhook
EVENTS_TO_ALERT = ["prefect.flow-run.Failed", "prefect.flow-run.Crashed"] # remove Crashed if you only want failures
async def main():
# Load your saved Slack block to get its block_document_id
slack_block = await SlackWebhook.load(SLACK_BLOCK_NAME)
automation = AutomationCore(
name=f"Alert: Failed runs in pool {production_pool}",
description=f"Send Slack when any flow run from work pool '{production_pool}' fails or crashes.",
enabled=True,
triggers=[
EventTrigger(
expect=EVENTS_TO_ALERT,
match_related=[{"prefect.work-pool.name": production_pool}],
posture=Posture.Reactive,
threshold=1,
within=0,
)
],
actions=[
SendNotification(
block_document_id=slack_block._block_document_id, # <-- required on your version
subject="Flow run {{ event.resource.name }} failed (pool: {{ event.related['prefect.work-pool'][0].name }})",
body=(
":x: Flow run failure detected\n"
"- Flow run: {{ event.resource.name }}\n"
"{% if event.related['prefect.deployment'] %}"
"- Deployment: {{ event.related['prefect.deployment'][0].name }}\n"
"{% endif %}"
"- Work pool: {{ event.related['prefect.work-pool'][0].name }}\n"
"- State: {{ event.payload.state.name if event.payload and event.payload.state else 'FAILED' }}\n"
"- Run ID: {{ event.resource.id }}\n"
"- Link: {{ event.resource.url }}"
),
)
],
)
created = await automation.acreate()
print(f"Created automation: {created.name} (id={created.id})")
if __name__ == "__main__":
asyncio.run(main())
Notes
- block_document_id is a UUID; using slack_block._block_document_id is the simplest way to obtain it at runtime.
- If you prefer to avoid loading the block, you can look up the document id with the client:
async with get_client() as client:
doc = await client.read_block_document_by_name(SLACK_BLOCK_NAME, block_type_slug="slack-webhook")
block_id = doc.id
…then pass block_document_id=block_id.
- Filtering by work pool: match_related [{"prefect.work-pool.name": production_pool}] is correct. If you want to double-check labels for your workspace, you can run:
prefect events stream --run-once
after a failure and inspect the event JSON.
Docs
- Automations: Creating automations
- Event triggers: Event triggers
- Slack notification blocks: Notifications blocks API
Give that a try and ping me if you want to add filters (e.g., only certain deployments or tags).Olve Heitmann
10/23/2025, 12:56 PMMarvin
10/23/2025, 12:56 PMMarvin
10/23/2025, 1:00 PMimport asyncio
from brrr.orchestration.prefect_constants import production_pool
from .create_block import SLACK_BLOCK_NAME
from prefect.automations import AutomationCore, EventTrigger, Posture
from prefect.events.actions import SendNotification
from prefect.blocks.notifications import SlackWebhook
# Include "Crashed" if you also want infra/runtime crashes
EVENTS_TO_ALERT = ["prefect.flow-run.Failed", "prefect.flow-run.Crashed"]
async def main():
# Load your Slack block to get its document id
slack_block = await SlackWebhook.load(SLACK_BLOCK_NAME)
automation = AutomationCore(
name=f"Alert: Failed runs in pool {production_pool}",
description=(
f"Send a Slack message when any flow run from work pool "
f"'{production_pool}' ends in a failure (or crash)."
),
enabled=True,
trigger=EventTrigger( # <-- singular
expect=EVENTS_TO_ALERT,
match_related=[{"prefect.work-pool.name": production_pool}],
posture=Posture.Reactive,
threshold=1,
within=0,
),
actions=[
SendNotification(
block_document_id=slack_block._block_document_id, # required by your version
subject="Flow run {{ event.resource.name }} failed (pool: {{ event.related['prefect.work-pool'][0].name }})",
body=(
":x: Flow run failure detected\n"
"- Flow run: {{ event.resource.name }}\n"
"{% if event.related['prefect.deployment'] %}"
"- Deployment: {{ event.related['prefect.deployment'][0].name }}\n"
"{% endif %}"
"- Work pool: {{ event.related['prefect.work-pool'][0].name }}\n"
"- State: {{ event.payload.state.name if event.payload and event.payload.state else 'FAILED' }}\n"
"- Run ID: {{ event.resource.id }}\n"
"- Link: {{ event.resource.url }}"
),
)
],
)
created = await automation.acreate()
print(f"Created automation: {created.name} (id={created.id})")
if __name__ == "__main__":
asyncio.run(main())
Notes
- trigger (singular) is required for AutomationCore. actions remains a list.
- If you want to scope to failures only, change EVENTS_TO_ALERT to ["prefect.flow-run.Failed"].
- To verify the work-pool label in your workspace, you can run:
prefect events stream --run-once
on a failure and check the event’s related labels.
Docs
- Automations: Creating automations
- Event triggers: Event triggers
- Slack blocks: Notifications blocks API
Try running it again with python -m prefect_setup.slack_automation.notify_failed_runs and let me know if you want to add filters (e.g., tags or deployment names).Olve Heitmann
10/23/2025, 1:02 PMMarvin
10/23/2025, 1:02 PMMarvin
10/23/2025, 1:04 PMimport asyncio
from brrr.orchestration.prefect_constants import production_pool
from .create_block import SLACK_BLOCK_NAME
from prefect.blocks.notifications import SlackWebhook
from prefect.client.orchestration import get_client
from prefect.events.schemas.automations import AutomationCore, EventTrigger, Posture
from prefect.events.actions import SendNotification
# Include "Crashed" if you also want infra/runtime crashes
EVENTS_TO_ALERT = ["prefect.flow-run.Failed", "prefect.flow-run.Crashed"]
async def main():
# Load your Slack block to get its document id
slack_block = await SlackWebhook.load(SLACK_BLOCK_NAME)
automation = AutomationCore(
name=f"Alert: Failed runs in pool {production_pool}",
description=(
f"Send a Slack message when any flow run from work pool "
f"'{production_pool}' ends in a failure (or crash)."
),
enabled=True,
trigger=EventTrigger(
expect=EVENTS_TO_ALERT,
match_related=[{"prefect.work-pool.name": production_pool}],
posture=Posture.Reactive,
threshold=1,
within=0,
),
actions=[
SendNotification(
block_document_id=slack_block._block_document_id,
subject="Flow run {{ event.resource.name }} failed (pool: {{ event.related['prefect.work-pool'][0].name }})",
body=(
":x: Flow run failure detected\n"
"- Flow run: {{ event.resource.name }}\n"
"{% if event.related['prefect.deployment'] %}"
"- Deployment: {{ event.related['prefect.deployment'][0].name }}\n"
"{% endif %}"
"- Work pool: {{ event.related['prefect.work-pool'][0].name }}\n"
"- State: {{ event.payload.state.name if event.payload and event.payload.state else 'FAILED' }}\n"
"- Run ID: {{ event.resource.id }}\n"
"- Link: {{ event.resource.url }}"
),
)
],
)
async with get_client() as client:
created = await client.create_automation(automation)
print(f"Created automation: {created.name} (id={created.id})")
if __name__ == "__main__":
asyncio.run(main())
Tips
- Verify it was created: prefect automation ls
- Only want failures (exclude crashes)? Set EVENTS_TO_ALERT = ["prefect.flow-run.Failed"].
- If you later want to change it, you can look it up by name and call client.update_automation(...) or delete/recreate.
Docs
- Creating automations: Creating automations
- Event triggers: Event triggers
- Notifications block: Notifications blocks API
If you hit a name conflict or want an upsert version, I can add a small helper that deletes any existing automation with the same name before creating.Olve Heitmann
10/23/2025, 1:05 PMMarvin
10/23/2025, 1:05 PMMarvin
10/23/2025, 1:05 PMimport asyncio
from brrr.orchestration.prefect_constants import production_pool
from .create_block import SLACK_BLOCK_NAME
from prefect.blocks.notifications import SlackWebhook
from prefect.client.orchestration import get_client
from prefect.events.schemas.automations import AutomationCore, EventTrigger, Posture
from prefect.events.actions import SendNotification
EVENTS_TO_ALERT = ["prefect.flow-run.Failed", "prefect.flow-run.Crashed"]
async def main():
slack_block = await SlackWebhook.load(SLACK_BLOCK_NAME)
automation = AutomationCore(
name=f"Alert: Failed runs in pool {production_pool}",
description=f"Slack alert when any run from '{production_pool}' fails/crashes.",
enabled=True,
trigger=EventTrigger(
expect=EVENTS_TO_ALERT,
match_related=[{"prefect.work-pool.name": production_pool}],
posture=Posture.Reactive,
threshold=1,
within=0,
),
actions=[
SendNotification(
block_document_id=slack_block._block_document_id,
subject="Flow run {{ event.resource.name }} failed (pool: {{ event.related['prefect.work-pool'][0].name }})",
body=(
":x: Flow run failure detected\n"
"- Flow run: {{ event.resource.name }}\n"
"{% if event.related['prefect.deployment'] %}"
"- Deployment: {{ event.related['prefect.deployment'][0].name }}\n"
"{% endif %}"
"- Work pool: {{ event.related['prefect.work-pool'][0].name }}\n"
"- State: {{ event.payload.state.name if event.payload and event.payload.state else 'FAILED' }}\n"
"- Run ID: {{ event.resource.id }}\n"
"- Link: {{ event.resource.url }}"
),
)
],
)
async with get_client() as client:
created = await client.create_automation(automation)
print(f"Created automation: {created.name} (id={created.id})")
if __name__ == "__main__":
asyncio.run(main())
Verify it’s there
prefect automation ls
prefect automation inspect --id <the-id-printed>
Docs
- Creating automations: Creating automations
- Event triggers: Event triggers
If you prefer a helper that “upserts” (delete-if-exists-then-create) by name, I can add that in a few lines.Olve Heitmann
10/23/2025, 1:18 PMMarvin
10/23/2025, 1:18 PMMarvin
10/23/2025, 1:19 PMprefect automation inspect "Alert: Failed runs in pool production-pool" --yaml
Confirm:
- trigger.posture is Reactive
- trigger.expect contains prefect.flow-run.Failed (and Crashed if you kept it)
- trigger.match_related has prefect.work-pool.name: production-pool
- actions[0].type is send-notification and points to your Slack block (block_document_id present)
- Generate a test failure
- Manually run a deployment in that pool and raise an exception to produce a Failed event.
- Or simulate: run something that crashes to test the Crashed path.
- If no message arrives
- Verify the Slack block works: run your create_block.py or a direct test:
from prefect.blocks.notifications import SlackWebhook
blk = await SlackWebhook.load(SLACK_BLOCK_NAME)
await blk.notify("Test from automation check")
- Stream the event to confirm pool label keys:
prefect events stream --run-once
after a failure and inspect related labels include prefect.work-pool.name: production-pool. If the key differs in your build, we’ll adjust match_related.
Bottom line: your automation listing looks correct. If you want, paste the YAML from inspect and I’ll double-check the fields.Olve Heitmann
10/23/2025, 7:14 PMMarvin
10/23/2025, 7:14 PMMarvin
10/23/2025, 7:15 PMprefect events stream --format json --run-once
- Look at the printed event:
- event name (e.g., "prefect.flow-run.Crashed" or "prefect.flow-run.Cancelled")
- related section keys for the work pool (e.g., you should see either a related resource like prefect.work-pool with a name, or the work pool name attached to the related deployment). This confirms the correct match_related key to use.
2) Broaden the trigger to catch common cases and remove the pool filter temporarily to test
- Update your automation definition to:
- expect: ["prefect.flow-run.Failed", "prefect.flow-run.Crashed", "prefect.flow-run.Cancelled"]
- temporarily remove match_related to see if notifications fire at all
- If that works, the issue is the pool filter key; then add back match_related with the correct label key you observed from the event stream. If prefect.work-pool.name does not appear, try filtering on the deployment’s work pool label, e.g. prefect.deployment.work_pool_name.
Example: recreate automation with broader events and no pool filter (just to test)
import asyncio
from brrr.orchestration.prefect_constants import production_pool
from .create_block import SLACK_BLOCK_NAME
from prefect.blocks.notifications import SlackWebhook
from prefect.client.orchestration import get_client
from prefect.events.schemas.automations import AutomationCore, EventTrigger, Posture
from prefect.events.actions import SendNotification
EVENTS_TO_ALERT = [
"prefect.flow-run.Failed",
"prefect.flow-run.Crashed",
"prefect.flow-run.Cancelled",
]
async def main():
slack_block = await SlackWebhook.load(SLACK_BLOCK_NAME)
automation = AutomationCore(
name="Debug: Any flow-run failure",
description="Temporary: notify on any Failed/Crashed/Cancelled flow run",
enabled=True,
trigger=EventTrigger(
expect=EVENTS_TO_ALERT,
posture=Posture.Reactive,
threshold=1,
within=0,
# match_related=[] # no pool filter for debugging
),
actions=[
SendNotification(
block_document_id=slack_block._block_document_id,
subject="Flow-run {{ event.event }}: {{ event.resource.name }}",
body=(
":rotating_light: {{ event.event }}\n"
"- Flow run: {{ event.resource.name }}\n"
"{% if event.related['prefect.deployment'] %}"
"- Deployment: {{ event.related['prefect.deployment'][0].name }}\n"
"{% endif %}"
"{% if event.related['prefect.work-pool'] %}"
"- Work pool: {{ event.related['prefect.work-pool'][0].name }}\n"
"{% endif %}"
"- Run ID: {{ event.resource.id }}\n"
"- Link: {{ event.resource.url }}"
),
)
],
)
async with get_client() as client:
created = await client.create_automation(automation)
print(f"Created automation: {created.name} (id={created.id})")
if __name__ == "__main__":
asyncio.run(main())
If this debug automation fires, we’ve confirmed actions are fine and the mismatch is in the filter:
- Add back the pool filter using the exact key from your event JSON. Examples to try after you confirm your labels:Marvin
10/23/2025, 7:15 PMprefect automation inspect "Alert: Failed runs in pool production-pool" --yaml
- If you want, paste the YAML (esp. trigger) here and I’ll validate the keys.
Once you share the event JSON snippet for a failed run (just the event name and the related keys), I’ll give you the exact match_related to use.Olve Heitmann
10/23/2025, 7:26 PMMarvin
10/23/2025, 7:26 PMMarvin
10/23/2025, 7:27 PMprefect events stream --format json --run-once
Then trigger a failure. Note:
- event (e.g., "prefect.flow-run.Cancelled" vs "Failed"/"Crashed")
- which keys exist under event.related (e.g., "prefect.work-pool" or only "prefect.deployment" with "work_pool_name")
Two changes I recommend now
A) Broaden event types to include Cancelled while we confirm your environment.
B) Make the Slack template resilient to missing labels so a missing key doesn’t break the action.
Here’s an update script that replaces the automation by name. It:
- expects Failed, Crashed, and Cancelled
- keeps your pool filter but you can comment it out to test
- uses safer Jinja
```
import asyncio
from brrr.orchestration.prefect_constants import production_pool
from .create_block import SLACK_BLOCK_NAME
from prefect.blocks.notifications import SlackWebhook
from prefect.client.orchestration import get_client
from prefect.events.schemas.automations import AutomationCore, EventTrigger, Posture
from prefect.events.actions import SendNotification
EVENTS_TO_ALERT = [
"prefect.flow-run.Failed",
"prefect.flow-run.Crashed",
"prefect.flow-run.Cancelled",
]
AUTOMATION_NAME = f"Alert: Failed runs in pool {production_pool}"
async def main():
slack_block = await SlackWebhook.load(SLACK_BLOCK_NAME)
# Optional: delete existing automation to "upsert" by name
async with get_client() as client:
# Try to find existing by name
existing = None
autos = await client.read_automations()
for a in autos:
if a.name == AUTOMATION_NAME:
existing = a
break
if existing:
await client.delete_automation(existing.id)
automation = AutomationCore(
name=AUTOMATION_NAME,
description=f"Notify Slack when runs from work pool '{production_pool}' fail, crash, or are cancelled.",
enabled=True,
trigger=EventTrigger(
expect=EVENTS_TO_ALERT,
posture=Posture.Reactive,
threshold=1,
within=0,
match_related=[
{"prefect.work-pool.name": production_pool},
# If your events don’t include a 'prefect.work-pool' related resource,
# comment the above and try this alternative after checking the event JSON:
# {"prefect.deployment.work_pool_name": production_pool},
],
),
actions=[
SendNotification(
block_document_id=slack_block._block_document_id,
subject=(
"Flow run {{ event.resource.name }} "
"{{ event.payload.state.name if event.payload and event.payload.state else '' }}"
"{% if event.related['prefect.work-pool'] %}"
" (pool: {{ event.related['prefect.work-pool'][0].name }})"
"{% endif %}"
),
body=(
"❌ Flow run issue detected ({{ event.event }})\n"
"- Flow run: {{ event.resource.name }}\n"
"{% if event.related['prefect.deployment'] %}"
"- Deployment: {{ event.related['prefect.deployment'][0].name }}\n"Marvin
10/23/2025, 7:27 PMOlve Heitmann
10/23/2025, 7:33 PMMarvin
10/23/2025, 7:33 PMMarvin
10/23/2025, 7:34 PMMarvin
10/23/2025, 7:34 PMOptional quick test
- Temporarily remove the pool filter to make sure notifications fire at all.
- Create a tiny flow that raises and deploy it to the production pool; run once to produce a controlled Failed event. This is the most reliable way to validate end-to-end.
Helpful debugging
- Stream one event while reproducing a failure to confirm the exact related labels in your workspace:
prefect events stream --format json --run-once
```
Look for:
- event: should be "prefect.flow-run.Failed" for your case
- related: verify whether you see "prefect.work-pool" or only a "prefect.deployment" with a work_pool_name label. We’ve accounted for both above.
If you’d like, paste a redacted event JSON from events stream and I’ll tighten the match_related and templates exactly to your workspace’s labels.Olve Heitmann
10/23/2025, 8:46 PMMarvin
10/23/2025, 8:46 PMMarvin
10/23/2025, 8:47 PMfrom prefect import flow
@flow(name="automation-failure-test")
def intentional_failure():
# This will raise every time to produce a Failed flow run
raise RuntimeError("Intentional failure to test failed-run automation")
2) create_fail_deployment.py
from brrr.orchestration.prefect_constants import production_pool
from fail_demo import intentional_failure
if __name__ == "__main__":
intentional_failure.from_source(
source=".", # project root with fail_demo.py
entrypoint="fail_demo.py:intentional_failure",
).deploy(
name="intentional-failure", # deployment name
work_pool_name=production_pool, # your production pool
tags=["alerts-test", "automation"],
description="Deliberately raises to test failed-run automation",
# no schedule; we'll run on-demand
)
3) Run it
- Ensure a worker is running for your production pool
- Trigger a run and watch it fail
prefect deployment run "automation-failure-test/intentional-failure" --watch
If your automation and pool filter are correct, this should produce a Slack alert. If it doesn’t:
- Temporarily remove the pool filter or broaden expect to include Cancelled as we discussed
- Stream the event to confirm the pool-related labels
prefect events stream --format json --run-once
then re-run the failing deployment and check event.related for the exact work-pool/deployment labels.
Reference
- Deploying from Python in Prefect 3.x: DeploymentsOlve Heitmann
10/23/2025, 9:19 PMMarvin
10/23/2025, 9:19 PMMarvin
10/23/2025, 9:19 PMOlve Heitmann
10/23/2025, 9:20 PMMarvin
10/23/2025, 9:20 PMMarvin
10/23/2025, 9:21 PMimport asyncio
# For production later, swap pool_name to production_pool
from brrr.orchestration.prefect_constants import production_pool
from .create_block import SLACK_BLOCK_NAME
from prefect.blocks.notifications import SlackWebhook
from prefect.client.orchestration import get_client
from prefect.events.schemas.automations import AutomationCore, EventTrigger, Posture
from prefect.events.actions import SendNotification
EVENTS_TO_ALERT = [
"prefect.flow-run.Failed",
"prefect.flow-run.Crashed",
# Add "prefect.flow-run.Cancelled" if you also want cancellations
]
# Test with dev pool first
pool_name = "dev-pool" # set to production_pool when ready
AUTOMATION_NAME = f"Alert: Failed runs in pool {pool_name}"
async def main():
slack_block = await SlackWebhook.load(SLACK_BLOCK_NAME)
async with get_client() as client:
# Delete existing automation with the same name (simple upsert)
existing = None
for a in await client.read_automations():
if a.name == AUTOMATION_NAME:
existing = a
break
if existing:
await client.delete_automation(existing.id)
automation = AutomationCore(
name=AUTOMATION_NAME,
description=f"Notify Slack when runs from work pool '{pool_name}' fail/crash.",
enabled=True,
trigger=EventTrigger(
expect=EVENTS_TO_ALERT,
posture=Posture.Reactive,
threshold=1,
within=0,
match_related=[
{
"prefect.resource.role": "work-pool",
"prefect.resource.name": pool_name,
}
],
),
actions=[
SendNotification(
block_document_id=slack_block._block_document_id,
subject="Flow run {{ event.resource.name }} {{ event.payload.state.name if event.payload and event.payload.state else '' }}",
body=(
":x: Flow run issue ({{ event.event }})\n"
"- Flow run: {{ event.resource.name }}\n"
"{% if event.related %}"
"{% for r in event.related %}"
"{% if r.role == 'deployment' %}- Deployment: {{ r.name }}\n{% endif %}"
"{% if r.role == 'work-pool' %}- Work pool: {{ r.name }} (type: {{ r.labels['prefect.work-pool.type'] if r.labels and 'prefect.work-pool.type' in r.labels else '' }})\n{% endif %}"
"{% endfor %}"
"{% endif %}"
"- Run ID: {{ event.resource.id }}\n"
"- Link: {{ event.resource.url }}"
),
)
],
)
created = await client.create_automation(automation)
print(f"Upserted automation: {created.name} (id={created.id})")
if __name__ == "__main__":
asyncio.run(main())
Why this should work now
- The event you shared clearly shows the work pool as a related resource with generic labels:
- role: work-pool
- name: dev-pool
- Filtering with those two labels exactly matches the related work-pool resource.Marvin
10/23/2025, 9:21 PMprefect deployment run "automation-failure-test/intentional-failure" --watch
4) You should get a Slack message
When you’re ready for prod
- Change pool_name = production_pool and re-run the script to upsert the prod automation.
If it still doesn’t fire, paste the YAML from:
prefect automation inspect "Alert: Failed runs in pool dev-pool" --yaml
and I’ll double-check the trigger wiring.Olve Heitmann
10/23/2025, 9:23 PMMarvin
10/23/2025, 9:24 PMMarvin
10/23/2025, 9:24 PMMarvin
10/23/2025, 9:24 PMTest
- Keep your intentionally failing deployment on dev-pool and run it:
prefect deployment run "automation-failure-test/intentional-failure" --watch
```
- You should get a Slack message for the root run. Subflows (which lack a deployment in related) will not match and won’t send alerts.
If you need OR logic later (e.g., match either of two pools), create two dicts in separate automations or use a composite trigger configured for “any.”Olve Heitmann
10/23/2025, 10:04 PMMarvin
10/23/2025, 10:04 PMMarvin
10/23/2025, 10:05 PMmatch_related = [
{"prefect.resource.role": "work-pool", "prefect.resource.name": "<POOL>"},
{"prefect.resource.role": "deployment"},
]
means:
- There must be a related resource with role work-pool and name <POOL> AND
- There must be a related resource with role deployment
This has the effect you want: it filters to runs from that work pool and excludes subflows (which typically lack a deployment related resource).
If you ever need OR semantics (e.g., pool A OR pool B), use either:
- Two separate automations, or
- A composite/compound trigger with separate EventTriggers each targeting a different pool.
Docs for reference: Event triggersOlve Heitmann
10/23/2025, 10:11 PMOlve Heitmann
10/23/2025, 10:11 PMOlve Heitmann
10/23/2025, 10:11 PMMarvin
10/23/2025, 10:11 PMMarvin
10/23/2025, 10:12 PMbody=(
":x: Flow run issue ({{ event.event }})\n"
"- Flow run: {{ event.resource.name }}\n"
"{% if event.related %}"
"{% for r in event.related %}"
"{% if r.role == 'deployment' %}- Deployment: {{ r.name }}\n{% endif %}"
"{% if r.role == 'work-pool' %}- Work pool: {{ r.name }}\n{% endif %}"
"{% endfor %}"
"{% endif %}"
"{% set has_tags = false %}"
"{% for r in event.related %}{% if r.role == 'tag' %}{% set has_tags = true %}{% endif %}{% endfor %}"
"{% if has_tags %}- Tags:\n{% for r in event.related %}{% if r.role == 'tag' %} - {{ r.id | replace('prefect.tag.', '') }}\n{% endif %}{% endfor %}{% endif %}"
"- Run ID: {{ event.resource.id }}\n"
"{% if event.resource.url %}- Link: {{ event.resource.url }}{% endif %}"
)
If you prefer a full upsert snippet (keeping your AND filter for pool + deployment and excluding subflows), here it is—just swap POOL_NAME as needed:
```
import asyncio
POOL_NAME = "dev-pool"
from .create_block import SLACK_BLOCK_NAME
from prefect.blocks.notifications import SlackWebhook
from prefect.client.orchestration import get_client
from prefect.events.schemas.automations import AutomationCore, EventTrigger, Posture
from prefect.events.actions import SendNotification
EVENTS_TO_ALERT = ["prefect.flow-run.Failed", "prefect.flow-run.Crashed"]
AUTOMATION_NAME = f"Alert: Failed root runs in pool {POOL_NAME}"
async def main():
slack_block = await SlackWebhook.load(SLACK_BLOCK_NAME)
async with get_client() as client:
# Upsert by name
existing = None
for a in await client.read_automations():
if a.name == AUTOMATION_NAME:
existing = a
break
if existing:
await client.delete_automation(existing.id)
automation = AutomationCore(
name=AUTOMATION_NAME,
description=f"Notify Slack when root (deployment) runs in '{POOL_NAME}' fail/crash, including tags.",
enabled=True,
trigger=EventTrigger(
expect=EVENTS_TO_ALERT,
posture=Posture.Reactive,
threshold=1,
within=0,
match_related=[
{"prefect.resource.role": "work-pool", "prefect.resource.name": POOL_NAME},
{"prefect.resource.role": "deployment"}, # exclude subflows
],
),
actions=[
SendNotification(
block_document_id=slack_block._block_document_id,
subject="Flow run {{ event.resource.name }} {{ event.payload.state.name if event.payload and event.payload.state else '' }}",
body=(
"❌ Flow run issue ({{ event.event }})\n"
"- Flow run: {{ event.resource.name }}\n"
"{% if event.related %}"
"{% for r in event.related %}"
"{% if r.role == 'deployment' %}- Deployment: {{ r.name }}\n{% endif %}"
"{% if r.role == 'work-pool' %}- Work pool: {{ r.name }}\n{% endif %}"
"{% endfor %}"
"{% endif %}"
"{% set has_tags = false %}"
"{% for r in event.related %}{% if r.role == 'tag' %}{% set has_tags = true %}{% endif %}{% endfor %}"
"{% if has_tags %}- Tags:\n{% for r in event.related %}{% if r.role == 'tag' %} - {{ r.id | replace('prefect.tag.', '') }}\n{% endif %}{% endfor %}{% endif %}"Marvin
10/23/2025, 10:12 PM