<@ULVA73B9P> I want to add an automation for prefe...
# ask-marvin
o
@Marvin I want to add an automation for prefect to send a message on slack if any scheduled deployment fail from a given pool. Is that possible without going through the UI?
m
🔄 Researching your answer... this may take a while 🔧 Using tool:
display_callable_signature
📊 Tools used: •
check_cli_command
(2x) •
display_callable_signature
(2x) •
explore_module_offerings
(3x) •
research_prefect_topic
(1x)
o
@Marvin you okay bro
m
thought for 3.6 seconds
All good—thanks for checking in! How can I help with your Prefect setup today?
o
@Marvin I want to add an automation for prefect to send a message on slack if any scheduled deployment fail from a given pool. Is that possible without going through the UI?
m
thought for 781.6 seconds
Yes—this is possible in Prefect 3.x without using the UI. You can create an Automation programmatically (Python or REST) that sends a Slack message when any flow run from a specific work pool fails. High-level steps - Save a Slack webhook notification block - Create an Automation with an EventTrigger that: - listens for flow-run failure events - filters by your work pool via related resources - Add a SendNotification action that uses your Slack block Note on “scheduled deployment fail” - If you mean “a scheduled flow run ends in a failure,” listen for: - prefect.flow-run.Failed (execution failed) - prefect.flow-run.Crashed (infra/runtime crash) - If you only want execution failures, you can listen for just prefect.flow-run.Failed. Option A: Pure Python (no UI)
Copy code
# 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:
Copy code
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:
Copy code
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.
- Slack setup: Incoming Webhooks work great. Create one in Slack, then save the webhook URL in a Prefect SlackWebhook block as shown above. - If you want only execution failures (not infrastructure crashes), remove "prefect.flow-run.Crashed" from
expect
. 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.
o
@Marvin i'd also like to receive (separate) alerts whenever a flow has been running for more than a day
m
thought for 243.7 seconds
Absolutely—you can set up a separate automation to alert when any flow run has been running for more than a day, without the UI. Approach: Proactive EventTrigger - Use a Proactive EventTrigger that starts when a flow run enters Running and fires if it does not reach a terminal state within 24 hours. - Terminal states to expect: Completed, Failed, Crashed, Cancelled. - Use for_each so the rule evaluates per run, not globally. - Optionally filter by the same work pool with match_related. Option A: Python
Copy code
# 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)
Copy code
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.
- This fires once at the 24-hour mark per run. If you want repeated reminders (e.g., every 6 hours thereafter), we can add a chained or repeating automation. Docs - Event triggers and posture: Event triggers - Creating automations programmatically: Creating automations - Slack notifications block: Notifications blocks API Quick clarifications so I can tailor this: - Limit to the same work pool as before, or all pools? - Include crashes/cancellations in the “terminal” expectation? - Exclude paused runs from triggering this alert?
o
@Marvin good idea on the repeated reminders (every 6 hour after is fine). Also i guess this "long running" can be for all workpools, not just one
m
thought for 161.2 seconds
Great—here are two good ways to do “long-running” alerts with 6-hour reminders, across all work pools. Pick the one that fits best: Option 1: Automations-only (fixed number of reminders) - Create one Proactive EventTrigger for 24h, then add more triggers for 30h, 36h, etc. - This is simple, but you must choose how many reminders you want (e.g., 5 reminders = up to 54h). Python example (sends at 24h, then every 6h for 5 reminders)
Copy code
import 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"
f"Deployment: {getattr(r, 'deployment_name', '')}\n" f"Run ID: {r.id}\n" f"Link: {url}" ) # Then schedule this flow every 6 hours (via deployment) ``` Which should you choose? - If “every 6h indefinitely” is the goal, the monitoring flow is the cleanest. - If a capped number of reminders is okay, the Automations-only approach is quick. Let me know: - Do you want indefinite reminders? If yes, I’ll give you a complete watchdog flow + deployment you can
prefect 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 API
o
@Marvin think a watchdog flow with deployment is the way to go for me. Prefer python path
m
thought for 154.8 seconds
Awesome—here’s a complete “watchdog” flow you can deploy from Python that alerts to Slack whenever any flow run has been Running for >24h, and it will re-alert every 6 hours until the run finishes. This covers all work pools (we’ll filter across the workspace). Step 1 — Save a Slack webhook block (one-time)
Copy code
from 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(
flow_run_filter=flow_run_filter, sort=FlowRunSort.START_TIME_ASC, limit=max_runs, ) except Exception as e2: logger.warning( "Could not apply server-side filters; " "falling back to Python filtering. " f"Errors were: {filter_error} and {e2}" ) # Minimal fallback: fetch some recent runs and filter in Python # Note: If your workspace has many runs, consider tightening this. runs = await client.read_flow_runs(limit=max_runs) # Filter in Python just to be safe long_running = [] for r in runs: try: if getattr(r.state, "type", None) != StateType.RUNNING: continue if not r.start_time: continue if r.start_time >= cutoff: continue if watch_tags: rt = set(getattr(r, "tags", []) or []) if not set(watch_tags).issubset(rt): continue long_running.append(r) except Exception: continue if not long_running: logger.info("No long-running flow runs found.") return # Notify once per run for r in long_running: duration = datetime.now(timezone.utc) - r.start_time hours = int(duration.total_seconds() // 3600) link = ui_link_for_run(r.id) dep_name = getattr(r, "deployment_name", None) or "" flow_name = getattr(r, "flow_name", None) or "" run_name = getattr(r, "name", None) or r.id message = ( f"⚠️ 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(message)
Copy code
Step 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.
o
@Marvin rather than having all this excess exception handling for different setups, can't we aim to find out which would work on my prefect install and use that?
m
thought for 69.8 seconds
Absolutely—let’s make the watchdog pick the right API path for your Prefect install at runtime, without a bunch of scattered try/excepts. Below is a tightened version that: - Introspects the Prefect client once to choose the correct
read_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)
Copy code
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:
runs = await _list_long_running_runs(client, cutoff, watch_tags, max_runs) # Defensive: filter again in Python long_running = [] for r in runs: if getattr(r.state, "type", None) == StateType.RUNNING and r.start_time and r.start_time < cutoff: if watch_tags: rt = set(getattr(r, "tags", []) or []) if not set(watch_tags).issubset(rt): continue long_running.append(r) if not long_running: logger.info("No long-running flow runs found.") return for r in long_running: 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) msg = ( f"⚠️ Long-running flow run detected\n" f"- Flow: {flow_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)
Copy code
Step 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?
o
@Marvin i was more thinking a long the lines of trying different things out and then handle that in one, implemented and deployed flow. So writing a few ad hoc scripts for testing
m
thought for 47.7 seconds
Love that. Here are a few small, ad‑hoc scripts you can run locally to “discover” what works in your environment, then we’ll wire the result into a clean, single watchdog deployment. Script 1 — Detect and confirm the working read_flow_runs API - Tries introspection first, then confirms by actually calling the API - Writes the detected style to a local file .watchdog_style
Copy code
# 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(
flow_run_filter=frf, sort=FlowRunSort.START_TIME_ASC, limit=limit, ) print(f"found {len(runs)} long-running candidates (>={threshold_hours}h)") for r in runs[preview] hours = int((datetime.now(timezone.utc) - r.start_time).total_seconds() // 3600) print(f"- {r.id} {getattr(r,'flow_name','')} {getattr(r,'name','')} ~{hours}h") if name == "__main__": asyncio.run(main())
Copy code
Script 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())
Copy code
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
Copy code
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"
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)
Copy code
Deploy 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.
o
@Marvin block is created. test 1 gave: suggested by signature: flow_run_filter confirmed working style: flow_run_filter; returned 0 runs (nothing is currently running, so that is expected). Remember - i want to check for long running tasks across all work pools. I have also named the block as the variable SLACK_BLOCK_NAME in create_block.py (same dir as the other scripts will be added to)
m
thought for 96.7 seconds
Perfect—that’s exactly the info we needed. Since your environment confirmed flow_run_filter is the working style and your Slack block is defined as SLACK_BLOCK_NAME in create_block.py, here’s a clean, single watchdog flow that: - Scans ALL work pools - Alerts for any flow run still Running after 24h - Re-alerts every 6h via its schedule - Uses the flow_run_filter API path only (no branching) watchdog.py
Copy code
from 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
Copy code
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.
- Replace YOUR_WATCHDOG_WORK_POOL with the pool where you want the watchdog to execute. - Ensure a worker is running for that pool. - Run:
Copy code
python 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.
o
@Marvin Great. Lets return to the automation (in python) of failed runs. I want to get the production pool as from brrr.orchestration.prefect_constants import production_pool and slack block as from .create_block import SLACK_BLOCK_NAME
m
thought for 47.2 seconds
Perfect—here’s a clean Python script to create a Slack alert automation for failed runs from your production pool, using your constants: - production pool name: from brrr.orchestration.prefect_constants import production_pool - Slack block name: from .create_block import SLACK_BLOCK_NAME It listens for flow-run failures (and crashes, optional) and posts to your Slack webhook block. create_failed_run_alert_automation.py
Copy code
import 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?
o
@Marvin (brrr) olveh@olve-machine:/mnt/c/qfin/qfin$ python -m prefect_setup.slack_automation.notify_failed_runs Traceback (most recent call last): File "<frozen runpy>", line 198, in _run_module_as_main File "<frozen runpy>", line 88, in _run_code File "/mnt/c/qfin/qfin/prefect_setup/slack_automation/notify_failed_runs.py", line 58, in <module> asyncio.run(main()) File "/home/olveh/.local/share/uv/python/cpython-3.12.11-linux-x86_64-gnu/lib/python3.12/asyncio/runners.py", line 195, in run return runner.run(main) ^^^^^^^^^^^^^^^^ File "/home/olveh/.local/share/uv/python/cpython-3.12.11-linux-x86_64-gnu/lib/python3.12/asyncio/runners.py", line 118, in run return self._loop.run_until_complete(task) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/home/olveh/.local/share/uv/python/cpython-3.12.11-linux-x86_64-gnu/lib/python3.12/asyncio/base_events.py", line 691, in run_until_complete return future.result() ^^^^^^^^^^^^^^^ File "/mnt/c/qfin/qfin/prefect_setup/slack_automation/notify_failed_runs.py", line 35, in main SendNotification( File "/home/olveh/envs/myproject/lib/python3.12/site-packages/pydantic/main.py", line 253, in init validated_self = self.__pydantic_validator__.validate_python(data, self_instance=self) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ pydantic_core._pydantic_core.ValidationError: 1 validation error for SendNotification block_document_id Field required [type=missing, input_value={'block_urn': 'slack-webh... event.resource.url }}"}, input_type=dict]
m
thought for 277.0 seconds
Thanks for the traceback—that pinpoints it. Your Prefect version expects SendNotification to reference the Slack block by block_document_id, not block_urn. That’s why you see “block_document_id Field required”. Here’s a fixed version that loads your Slack block and passes its ID:
Copy code
import 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:
Copy code
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:
Copy code
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).
o
@Marvin pydantic_core._pydantic_core.ValidationError: 1 validation error for AutomationCore trigger Field required [type=missing, input_value={'name': 'Alert: Failed r...vent.resource.url }}")]}, input_type=dict] For further information visit https://errors.pydantic.dev/2.11/v/missing 145553.877 | WARNING | EventsWorker - Still processing items: 1 items remaining...
m
thought for 206.9 seconds
Thanks for the details — that error is because AutomationCore expects trigger (singular) not triggers. Here’s a corrected, minimal script that uses your imports and the block_document_id fix. notify_failed_runs.py
Copy code
import 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:
Copy code
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).
o
@Marvin AttributeError: 'AutomationCore' object has no attribute 'acreate' 150148.039 | WARNING | EventsWorker - Still processing items: 1 items remaining...
m
thought for 112.1 seconds
Got it — your Prefect install doesn’t expose acreate on AutomationCore. Create via the client instead. Here’s an updated, minimal script that: - Uses trigger (singular) - Loads your Slack block to get block_document_id - Creates the automation via PrefectClient.create_automation - Filters to your production work pool - Scans for Failed (and Crashed) flow-run events notify_failed_runs.py
Copy code
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

# 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.
o
@Marvin could i instead use Automation instead of AutomationCore and then use .create() ? why not?
m
thought for 21.4 seconds
Short answer: No — not in your installed version. Why - AutomationCore is the “payload” model you use to create an automation. - Automation is the “persisted” server model that you get back after creation. - In many Prefect 3.x builds, neither AutomationCore nor Automation exposes .create()/.acreate() methods. Creation is done via the orchestration client. So the reliable path is: - Build an AutomationCore - Call PrefectClient.create_automation(automation_core) Example (works with your setup)
Copy code
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"]

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
Copy code
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.
o
@Marvin i did actually run it wit h the automation class and .create(), and no obvious issues. running prefect automation ls gives ┃ Automation ┃ Enabled ┃ Trigger ┃ Actions ┃ ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━┩ │ Alert: Failed runs in pool production-pool │ True │ Reactive: expecting 1 of {'prefect.flow-run.Failed', │ Send notification │ │ 2eaca407-be7a-4617-ba18-7a92924d67ba │ │ 'prefect.flow-run.Crashed'} │ │ │ Send a Slack message when any flow run from work pool 'production-pool' │ │ │ │ │ ends in a failure (or crash). │ │ │ doesnt that look correct?
m
thought for 29.8 seconds
That output looks good — your Automation was created and enabled. If Automation.create() works in your environment, that’s perfectly fine to use. A couple quick checks to be sure everything is wired as intended: - Inspect full config
Copy code
prefect 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:
Copy code
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:
Copy code
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.
o
@Marvin so had a scheduled deploy fail without getting a notification. Did a "hello world" example showing that prefect was able to post to slack, so i guess the issue is with it registering a failure?
m
thought for 84.2 seconds
Likely cause: the trigger didn’t match the actual event your “failed” run produced. A “scheduled deployment fail” can end up as different flow-run events depending on what failed: - Execution error in your flow code → prefect.flow-run.Failed - Infra/runtime failure before or during start → prefect.flow-run.Crashed - Not picked up / timed out / manually cancelled → prefect.flow-run.Cancelled - Late runs that are later cancelled → Late then Cancelled Two fast ways to verify and fix 1) Confirm the exact event and related labels - Reproduce a failure and run:
Copy code
prefect 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)
Copy code
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:
- match_related=[{"prefect.work-pool.name": production_pool}] - or match_related=[{"prefect.deployment.work_pool_name": production_pool}] Also double-check your existing automation - Inspect config to be sure it’s enabled and fields are as expected:
Copy code
prefect 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.
o
@Marvin prefect automation inspect "Alert: Failed runs in pool production-pool" --yamlml - name: 'Alert: Failed runs in pool production-pool' description: Send a Slack message when any flow run from work pool 'production-pool' ends in a failure (or crash). enabled: true tags: [] trigger: type: event match: {} match_related: - prefect.work-pool.name: production-pool after: [] expect: - prefect.flow-run.Crashed - prefect.flow-run.Failed for_each: [] posture: Reactive threshold: 1 within: 0.0 actions: - type: send-notification block_document_id: 551fc23f-fdfe-47fb-9bff-885910d334de subject: 'Flow run {{ event.resource.name }} failed (pool: {{ event.related[''prefect.work-pool''][0].name }})' body: |- Flow run failure detected - Flow run: {{ event.resource.name }} {% if event.related['prefect.deployment'] %}- Deployment: {{ event.related['prefect.deployment'][0].name }} {% endif %}- Work pool: {{ event.related['prefect.work-pool'][0].name }} - State: {{ event.payload.state.name if event.payload and event.payload.state else 'FAILED' }} - Run ID: {{ event.resource.id }} - Link: {{ event.resource.url }} actions_on_trigger: [] actions_on_resolve: [] owner_resource: null id: 2eaca407-be7a-4617-ba18-7a92924d67ba
m
thought for 56.8 seconds
Thanks for the YAML — the automation is created and enabled, but the trigger likely isn’t matching the actual event your “failed” schedule produced. What most commonly causes this - The event name wasn’t one of your expect values. Scheduled runs that never start or time out often end up Cancelled, not Failed or Crashed. - The pool label key in related doesn’t match prefect.work-pool.name for your events. - The Jinja in the notification references a missing related key and raises a template error. Quickest way to confirm 1) Stream the next failure event to see its exact event name and related labels:
Copy code
prefect 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"
"{% 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 }}" ), ) ], ) created = await client.create_automation(automation) print(f"Upserted automation: {created.name} (id={created.id})") if name == "__main__": asyncio.run(main()) ``` Next steps - Try the updated automation and intentionally cause a failure. If Slack fires, we’ve confirmed it was either the event type or the template causing issues. - If it still doesn’t fire, run prefect events stream during the failure and paste the event JSON’s top-level event and related keys. I’ll give you the exact match_related key to use for your workspace. References - Event triggers and labels: Event triggers - Creating automations programmatically: Creating automations
o
@Marvin Hard to reproduce the failure i had as it wasnt on intention and the bug have been fixed, but per logs what happened per logs is: ERROR: prefect.task_runs Task run failed with exception: ComputeError('join keys did not fulfill m:1 validation') ERROR: prefect.task_runs Finished in state Failed('Task run encountered an exception ComputeError: join keys did not fulfill m:1 validation') ERROR: prefect.flow_runs Encountered exception during execution: ComputeError('join keys did not fulfill m:1 validation') ERROR: prefect_flow_runs Finished in state Failed('Flow run encountered an exception: ComputeError: join keys did not fulfill m:1 validation') ERROR prefect.flow_runs Engine execution exited with unexpected exception ERROR prefect.flow_runs.runner Process for flow run 'conscious-ringtail' exited with status code: 1
m
thought for 42.2 seconds
Thanks for the detail — those logs clearly indicate a flow-run ended in Failed, so your expect list is fine. The two most likely reasons you didn’t get a Slack message: 1) Pool filter didn’t match - Some Prefect builds don’t attach a related resource named prefect.work-pool to flow-run events. Instead, the work pool name is present on the related deployment as prefect.deployment.work_pool_name. - Your current automation only matches prefect.work-pool.name, so it may not have matched. 2) Template error suppressed the notification - Your YAML shows the body references event.related['prefect.work-pool'][0].name without guarding for missing key. If that related key isn’t present on the event, the template can fail and the action won’t send. Fix: upsert the automation with broader filter and safe templates - Match by either work-pool related resource OR the deployment’s work_pool_name - Guard all optional fields in the template ``` 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", # Uncomment if you also want runs that end up cancelled # "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) async with get_client() as client: # Delete any existing automation by 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 '{production_pool}' fail/crash.", enabled=True, trigger=EventTrigger( expect=EVENTS_TO_ALERT, posture=Posture.Reactive, threshold=1, within=0, # OR across entries; matches if any related resource set matches match_related=[ {"prefect.work-pool.name": production_pool}, {"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 }})" "{% elif event.related['prefect.deployment'] %}" " (pool: {{ event.related['prefect.deployment'][0].labels['prefect.work_pool_name'] }})" "{% endif %}" ), body=( " Flow run issue ({{ 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" "{% elif event.related['prefect.deployment'] %}"
"- Work pool: {{ event.related['prefect.deployment'][0].labels['prefect.work_pool_name'] }}\n" "{% 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())
Copy code
Optional 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.
o
@Marvin can you create a dummy deployment to my production pool that will intentionally fail?
m
thought for 80.5 seconds
Yep—here’s a tiny “intentionally fail” flow and a deployment to your production pool that you can run on-demand to test your automation. 1) fail_demo.py
Copy code
from 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
Copy code
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
Copy code
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
Copy code
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: Deployments
o
@Marvin i switched both the automation and deployment of intentionally failing to dev workpool for testing. Didn't trigger. Here are the events from streaming in cli that i guess are most relevant: [ { "occurred": "2025-10-23 205806.828012+00:00", "event": "prefect.flow-run.Failed", "resource": { "prefect.resource.id": "prefect.flow-run.76c7cd8d-cde1-4301-9f9f-9d27e4387459", "prefect.resource.name": "archetypal-coua", "prefect.run-count": "1", "prefect.state-message": "Flow run encountered an exception: RuntimeError: Intentional failure to test failed-run automation", "prefect.state-name": "Failed", "prefect.state-timestamp": "2025-10-23T205806.828012+00:00", "prefect.state-type": "FAILED" }, "related": [ { "prefect.resource.id": "prefect.flow.676ee4bf-ba8a-494a-aea0-2e2feafa7b8a", "prefect.resource.role": "flow", "prefect.resource.name": "automation-failure-test" }, { "prefect.resource.id": "prefect.deployment.aeffc57b-e5d6-4548-9847-e16836a12c5f", "prefect.resource.role": "deployment", "prefect.resource.name": "automation-failure-test_env_development_branch_development" }, { "prefect.resource.id": "prefect.work-queue.f7dd04cd-d4e5-4ab2-a621-2163766d0cfc", "prefect.resource.role": "work-queue", "prefect.resource.name": "default" }, { "prefect.resource.id": "prefect.work-pool.93cd1536-1f09-4209-bf91-4016560423b7", "prefect.resource.role": "work-pool", "prefect.resource.name": "dev-pool", "prefect.work-pool.type": "docker" }, { "prefect.resource.id": "prefect.tag.alerts-test", "prefect.resource.role": "tag" }, { "prefect.resource.id": "prefect.tag.development", "prefect.resource.role": "tag" } ], "payload": { "intended": { "from": "RUNNING", "to": "FAILED" }, "initial_state": { "type": "RUNNING", "name": "Running" }, "validated_state": { "type": "FAILED", "name": "Failed", "message": "Flow run encountered an exception: RuntimeError: Intentional failure to test failed-run automation" } }, "id": "019a12dd-566b-7e41-a148-69d502537d51", "follows": "019a12dd-5624-713d-a60c-d2561478a782" }, { "occurred": "2025-10-23 205808.334451+00:00", "event": "prefect.docker.container.exited", "resource": { "prefect.resource.id": "prefect.docker.container.4c789019ad97d9fe9a5c4c1d7636fe4a40aa202d51d9c012f175a06df203f4ff", "prefect.resource.name": "archetypal-coua" }, "related": [ { "prefect.resource.id": "prefect.deployment.aeffc57b-e5d6-4548-9847-e16836a12c5f", "prefect.resource.role": "deployment", "prefect.resource.name": "automation-failure-test_env_development_branch_development" }, { "prefect.resource.id": "prefect.flow.676ee4bf-ba8a-494a-aea0-2e2feafa7b8a", "prefect.resource.role": "flow", "prefect.resource.name": "automation-failure-test" }, { "prefect.resource.id": "prefect.flow-run.76c7cd8d-cde1-4301-9f9f-9d27e4387459", "prefect.resource.role": "flow-run", "prefect.resource.name": "archetypal-coua" }, { "prefect.resource.id": "prefect.tag.alerts-test", "prefect.resource.role": "tag" }, { "prefect.resource.id": "prefect.tag.development", "prefect.resource.role": "tag" }, { "prefect.resource.id": "prefect.work-pool.93cd1536-1f09-4209-bf91-4016560423b7", "prefect.resource.role": "work-pool", "prefect.resource.name": "dev-pool" }, { "prefect.resource.id": "prefect.worker.docker.dockerworker-6cddfb20-382c-4312-8a3e-4665cf723246", "prefect.resource.name": "DockerWorker 6cddfb20-382c-4312-8a3e-4665cf723246", "prefect.version": "3.4.17", "prefect.worker-type": "docker", "prefect.resource.role": "worker" } ], "payload": {}, "id": "019a12dd-5c4e-76c0-aa48-c1ccb699aab0", "follows": "019a12dd-1f49-7c45-bcfb-d78aa76bf5fe" }, { "occurred": "2025-10-23 205808.349958+00:00", "event": "prefect.worker.executed-flow-run", "resource": { "prefect.resource.id": "prefect.worker.docker.dockerworker-6cddfb20-382c-4312-8a3e-4665cf723246", "prefect.resource.name": "DockerWorker 6cddfb20-382c-4312-8a3e-4665cf723246", "prefect.version": "3.4.17", "prefect.worker-type": "docker" }, "related": [ { "prefect.resource.id": "prefect.deployment.aeffc57b-e5d6-4548-9847-e16836a12c5f", "prefect.resource.role": "deployment", "prefect.resource.name": "automation-failure-test_env_development_branch_development" }, { "prefect.resource.id": "prefect.flow.676ee4bf-ba8a-494a-aea0-2e2feafa7b8a", "prefect.resource.role": "flow", "prefect.resource.name": "automation-failure-test" }, { "prefect.resource.id": "prefect.flow-run.76c7cd8d-cde1-4301-9f9f-9d27e4387459", "prefect.resource.role": "flow-run", "prefect.resource.name": "archetypal-coua", "prefect.infrastructure.identifier": "http+docker://localhost:4c789019ad97d9fe9a5c4c1d7636fe4a40aa202d51d9c012f175a06df203f4ff", "prefect.infrastructure.status-code": "0" }, { "prefect.resource.id": "prefect.tag.alerts-test", "prefect.resource.role": "tag" }, { "prefect.resource.id": "prefect.tag.development", "prefect.resource.role": "tag" }, { "prefect.resource.id": "prefect.work-pool.93cd1536-1f09-4209-bf91-4016560423b7", "prefect.resource.role": "work-pool", "prefect.resource.name": "dev-pool" } ], "payload": {}, "id": "019a12dd-5c5e-7847-85fa-5252f44cf175", "follows": "019a12dd-1c00-7356-87b5-e8c0942ecf27" } ]
m
Your message was too long, here's your message at the allowed limit: i switched both the automation and deployment of intentionally failing to dev workpool for testing Didn't trigger Here are the events from streaming in cli that i guess are most relevant [ { occurred 2025-10-23 20 58 06 828012+00 00 event prefect flow-run Failed resource { prefect resource id prefect flow-run 76c7cd8d-cde1-4301-9f9f-9d27e4387459 prefect resource name archetypal-coua prefect run-count 1 prefect state-message Flow run encountered an exception RuntimeError Intentional failure to test failed-run automation prefect state-name Failed prefect state-timestamp 2025-10-23T20 58 06 828012+00 00 prefect state-type FAILED } related [ { prefect resource id prefect flow 676ee4bf-ba8a-494a-aea0-2e2feafa7b8a prefect resource role flow prefect resource name automation-failure-test } { prefect resource id prefect deployment aeffc57b-e5d6-4548-9847-e16836a12c5f prefect resource role deployment prefect resource name automation-failure-test_env_development_branch_development } { prefect resource id prefect work-queue f7dd04cd-d4e5-4ab2-a621-2163766d0cfc prefect resource role work-queue prefect resource name default } { prefect resource id prefect work-pool 93cd1536-1f09-4209-bf91-4016560423b7 prefect resource role work-pool prefect resource name dev-pool prefect work-pool type docker } { prefect resource id prefect tag alerts-test prefect resource role tag } { prefect resource id prefect tag development prefect resource role tag } ] payload { intended { from RUNNING to FAILED } initial_state { type RUNNING name Running } validated_state { type FAILED name Failed message Flow run encountered an exception RuntimeError Intentional failure to test failed-run automation } } id 019a12dd-566b-7e41-a148-69d502537d51 follows 019a12dd-5624-713d-a60c-d2561478a782 } { occurred 2025-10-23 20 58 08 334451+00 00 event prefect docker container exited resource { prefect resource id prefect docker container 4c789019ad97d9fe9a5c4c1d7636fe4a40aa202d51d9c012f175a06df203f4ff prefect resource name archetypal-coua } related [ { prefect resource id prefect deployment aeffc57b-e5d6-4548-9847-e16836a12c5f prefect resource role deployment prefect resource name automation-failure-test_env_development_branch_development } { prefect resource id prefect flow 676ee4bf-ba8a-494a-aea0-2e2feafa7b8a prefect resource role flow prefect resource name automation-failure-test } { prefect resource id prefect flow-run 76c7cd8d-cde1-4301-9f9f-9d27e4387459 prefect resource role flow-run prefect resource name archetypal-coua } { prefect resource id prefect tag alerts-test prefect resource role tag } { prefect resource id prefect tag development prefect resource role tag } { prefect resource id prefect work-pool 93cd1536-1f09-4209-bf91-4016560423b7 prefect resource role work-pool prefect resource name dev-pool } { prefect resource id prefect worker docker dockerworker-6cddfb20-382c-4312-8a3e-4665cf723246 prefect resource name DockerWorker 6cddfb20-382c-4312-8a3e-4665cf723246 prefect version 3 4 17 prefect worker-type docker prefect resource role worker } ] payload {} id 019a12dd-5c4e-76c0-aa48-c1ccb699aab0 follows 019a12dd-1f49-7c45-bcfb-d78aa76bf5fe } { occurred 2025-10-23 20 58 08 349958+00 00 event prefect worker executed-flow-run resource { prefect resource id prefect worker docker dockerworker-6cddfb20-382c-4312-8a3e-4665cf723246 prefect resource name DockerWorker 6cddfb20-382c-4312-8a3e-4665cf723246 prefect version 3 4 17 prefect worker-type docker } related [ { prefect resource id prefect deployment aeffc57b-e5d6-4548-9847-e16836a12c5f prefect resource role deployment prefect resource name automation-failure-test_env_development_branch_development } { prefect resource id prefect flow 676ee4bf-ba8a-494a-aea0-2e2feafa7b8a prefect resource role flow prefect resource name automation-failure-test } { prefect resource id prefect flow-run 76c7cd8d-cde1-4301-9f9f-9d27e4387459 prefect resource role flow-run prefe
ct resource name archetypal-coua prefect infrastructure identifier <http+docker //localhost 4>c789019ad97d9fe9a5c4c1d7636fe4a40aa202d51d9c012f175a06df203f4ff prefect infrastructure status-code 0 } { prefect resource id prefect tag alerts-test prefect resource role tag } { prefect resource id prefect tag development prefect resource role tag } { prefect resource id prefect work-pool
o
@Marvin i switched both the automation and deployment of intentionally failing to dev workpool for testing. Didn't trigger. Here is one event from streaming in cli that i guess are relevant: { "occurred": "2025-10-23 205806.828012+00:00", "event": "prefect.flow-run.Failed", "resource": { "prefect.resource.id": "prefect.flow-run.76c7cd8d-cde1-4301-9f9f-9d27e4387459", "prefect.resource.name": "archetypal-coua", "prefect.run-count": "1", "prefect.state-message": "Flow run encountered an exception: RuntimeError: Intentional failure to test failed-run automation", "prefect.state-name": "Failed", "prefect.state-timestamp": "2025-10-23T205806.828012+00:00", "prefect.state-type": "FAILED" }, "related": [ { "prefect.resource.id": "prefect.flow.676ee4bf-ba8a-494a-aea0-2e2feafa7b8a", "prefect.resource.role": "flow", "prefect.resource.name": "automation-failure-test" }, { "prefect.resource.id": "prefect.deployment.aeffc57b-e5d6-4548-9847-e16836a12c5f", "prefect.resource.role": "deployment", "prefect.resource.name": "automation-failure-test_env_development_branch_development" }, { "prefect.resource.id": "prefect.work-queue.f7dd04cd-d4e5-4ab2-a621-2163766d0cfc", "prefect.resource.role": "work-queue", "prefect.resource.name": "default" }, { "prefect.resource.id": "prefect.work-pool.93cd1536-1f09-4209-bf91-4016560423b7", "prefect.resource.role": "work-pool", "prefect.resource.name": "dev-pool", "prefect.work-pool.type": "docker" }, { "prefect.resource.id": "prefect.tag.alerts-test", "prefect.resource.role": "tag" }, { "prefect.resource.id": "prefect.tag.development", "prefect.resource.role": "tag" } ], "payload": { "intended": { "from": "RUNNING", "to": "FAILED" }, "initial_state": { "type": "RUNNING", "name": "Running" }, "validated_state": { "type": "FAILED", "name": "Failed", "message": "Flow run encountered an exception: RuntimeError: Intentional failure to test failed-run automation" } }, "id": "019a12dd-566b-7e41-a148-69d502537d51", "follows": "019a12dd-5624-713d-a60c-d2561478a782" }
m
thought for 65.2 seconds
Boom — that event JSON gives us the missing piece. Your filter key was the issue. From your event: - The work pool appears in related as: - "prefect.resource.role": "work-pool" - "prefect.resource.name": "dev-pool" - There is NOT a key named "prefect.work-pool.name" So your trigger should filter on the generic resource labels for the work-pool related resource, e.g.: - {"prefect.resource.role": "work-pool", "prefect.resource.name": "<POOL_NAME>"} Here’s an upsert script that replaces your automation using the correct keys. I’ve parameterized pool_name so you can test with dev-pool, then switch back to production_pool.
Copy code
import 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.
- We also made the Slack template robust to missing keys by iterating related resources by role. How to test 1) Keep your failing deployment pointed at dev-pool 2) Run the script above 3) Trigger a failure again:
Copy code
prefect 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:
Copy code
prefect automation inspect "Alert: Failed runs in pool dev-pool" --yaml
and I’ll double-check the trigger wiring.
o
@Marvin but would that filter be BOTH ( {"prefect.resource.role": "work-pool", "prefect.resource.name": "<POOL_NAME>"})? Also, i'd like to exclude subflows from sending the alert. Can we do that?
m
thought for 52.8 seconds
Great questions. - Does that match_related object apply BOTH filters? Yes. In EventTrigger.match_related: - Within a single dict, all key/value pairs must match the SAME related resource (AND). - Across the list, each dict must match at least one related resource (AND across entries). - If you need OR logic (e.g., match one of two alternatives), you generally use separate automations or a Compound/Composite trigger configured for any semantics. Given your event JSON, the correct work pool filter is: - One dict that matches the related work-pool resource by role and name: {"prefect.resource.role": "work-pool", "prefect.resource.name": "<POOL_NAME>"} Exclude subflows - Subflow runs typically do not have a related deployment resource. Root flow runs created by a deployment do. - So add a second required spec that a deployment related resource is present: {"prefect.resource.role": "deployment"} - Combined, this will only notify when: - The run has a related work-pool with the given name - AND the run is associated with a deployment (i.e., not a subflow) Upsert script with both constraints ``` import asyncio # Swap this to your prod pool name when ready 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", # Add "prefect.flow-run.Cancelled" if you also want cancellations ] 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: # Simple 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.", enabled=True, trigger=EventTrigger( expect=EVENTS_TO_ALERT, posture=Posture.Reactive, threshold=1, within=0, match_related=[ # Must be a run from the target work pool { "prefect.resource.role": "work-pool", "prefect.resource.name": POOL_NAME, }, # Must be associated with a deployment (excludes subflows) { "prefect.resource.role": "deployment", }, ], ), 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 %}" "- 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())
Copy code
Test
- 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.”
o
@Marvin i thought when you do match_related [] it is only required with one element in the list? so your currenct condition is or, not and?
m
thought for 25.8 seconds
Great question—here’s how match_related works: - Within one dict: all keys must match the same related resource (AND). - Across the list: each dict must be satisfied by at least one related resource (AND across specs). They don’t have to be the same related resource; each spec can match a different related entry. - It is NOT “only one element must match.” It’s not an OR. So this list:
Copy code
match_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 triggers
o
Great. Finally got a notifciation. type: and linkk: is blank. I guess we may not have anything there, and that is fine. Could we add deployment tags however?
@Marvin
@Marvin Great. Finally got a notifciation. type: and linkk: is blank. I guess we may not have anything there, and that is fine. Could we add deployment tags however?
m
thought for 77.4 seconds
Absolutely—tags show up in the event’s related resources as role == "tag" (e.g., ids like "prefect.tag.alerts-test"). You can include them in the Slack message by iterating those related entries. Here’s a minimal update to your SendNotification action template that: - keeps your existing fields - safely prints tags if present (without breaking if missing) Only the body needs to change:
Copy code
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 }}\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 %}"
"- Run ID: {{ event.resource.id }}\n" "{% if event.resource.url %}- Link: {{ event.resource.url }}{% endif %}" ), ) ], ) created = await client.create_automation(automation) print(f"Upserted automation: {created.name} (id={created.id})") if name == "__main__": asyncio.run(main()) ``` Notes - The tag entries in your event stream have role == "tag" and the id contains the tag value (prefect.tag.<tag>). We strip the prefix for readability. - The Link field may be blank in events if your UI URL isn’t attached; we safely show it only if present. - match_related uses AND semantics across dicts, so this continues to filter to the specific pool AND require a deployment (excluding subflows). If you want tags only from certain namespaces (e.g., show only prod/*), we can filter by prefix in the template.