Paul Gadal
11/18/2025, 8:42 AMfrom prefect import get_client
from prefect.blocks.notifications import SlackWebhook
from prefect.automations import AutomationCore, EventTrigger, SendNotification
import asyncio
AUTOMATION_NAME = "Notify on Flow Failure"
trigger = EventTrigger(
type="event",
match={"prefect.resource.id": "prefect.flow-run.*"},
match_related={},
after=[],
expect=[
"prefect.flow-run.TimedOut",
"prefect.flow-run.Failed",
"prefect.flow-run.Crashed"
],
for_each=["prefect.resource.id"],
posture="Reactive",
threshold=1,
within=0
)
async def main():
# Initialize client
client = get_client()
slack_block_name = "slack-notify"
# Create the Slack block
block = SlackWebhook(
url="<https://hooks.slack.com/services/XXXXX>"
)
await block.save(name=slack_block_name, overwrite=True)
# Load block to get its block_document_id
loaded_block = await SlackWebhook.load(slack_block_name)
block_document_id = loaded_block._block_document_id
# Check if automation already exists
existing = await client.read_automations()
if any(a.name == AUTOMATION_NAME for a in existing):
print(f"Automation '{AUTOMATION_NAME}' already exists. Skipping creation.")
return
# Define the automation
automation = AutomationCore(
name=AUTOMATION_NAME,
description="Sends a Slack notification when any flow fails or crashes.",
trigger=trigger,
actions=[
SendNotification(
block_document_id=block_document_id,
subject="Error",
body=(
"Deployment: {{ deployment.name }} observed in state `{{ flow_run.state.name }}` at {{ flow_run.state.timestamp }}.\n"
"Flow run URL: <http://XXX:4200/runs/flow-run/{{> flow_run.id }}\n"
"State message: {{ flow_run.state.message }}"
)
)
]
)
# Register the automation
await client.create_automation(automation)
# Run async main
asyncio.run(main())Marvin
11/18/2025, 8:42 AMMarvin
11/18/2025, 8:51 AMfor_each=["prefect.resource.id"]), so every failing run triggers another message. To “latch” notifications so you only get:
- one alert when a deployment starts failing, and
- one alert when it recovers (first success after failures),
create two automations and key them per deployment with for_each=["related:deployment:id"], then use after to gate firing:
- Failure automation: fires on the first failure after a success for that deployment.
- Recovery automation: fires on the first success after a failure for that deployment.
Below is a drop‑in version of your script that creates both automations. Adjust the Slack webhook URL and Prefect UI base URL as needed.
```
from prefect import get_client
from prefect.blocks.notifications import SlackWebhook
from prefect.automations import AutomationCore, EventTrigger, SendNotification
import asyncio
FAIL_AUTOMATION_NAME = "Notify on first failure per deployment"
RECOVERY_AUTOMATION_NAME = "Notify on recovery per deployment"
# Shared Slack block name
SLACK_BLOCK_NAME = "slack-notify"
PREFECT_UI_BASE = "http://XXX:4200" # change to your Prefect UI base URL
# Triggers:
# 1) Failure: first failure AFTER a success for the same deployment
failure_trigger = EventTrigger(
type="event",
match={"prefect.resource.id": "prefect.flow-run.*"},
match_related={}, # optionally filter specific deployments/work pools via match_related
after=["prefect.flow-run.Completed"], # gate on a prior success
expect=[
"prefect.flow-run.TimedOut",
"prefect.flow-run.Failed",
"prefect.flow-run.Crashed",
],
for_each=["relateddeploymentid"], # key state per deployment
posture="Reactive",
threshold=1,
within=0,
)
# 2) Recovery: first success AFTER a failure for the same deployment
recovery_trigger = EventTrigger(
type="event",
match={"prefect.resource.id": "prefect.flow-run.*"},
match_related={},
after=[
"prefect.flow-run.TimedOut",
"prefect.flow-run.Failed",
"prefect.flow-run.Crashed",
], # gate on a prior failure
expect=["prefect.flow-run.Completed"],
for_each=["relateddeploymentid"],
posture="Reactive",
threshold=1,
within=0,
)
async def main():
client = get_client()
# Create/update Slack block
block = SlackWebhook(url="https://hooks.slack.com/services/XXXXX")
await block.save(name=SLACK_BLOCK_NAME, overwrite=True)
loaded_block = await SlackWebhook.load(SLACK_BLOCK_NAME)
block_document_id = loaded_block._block_document_id
existing = await client.read_automations()
existing_by_name = {a.name: a for a in existing}
# Failure automation
if FAIL_AUTOMATION_NAME not in existing_by_name:
fail_automation = AutomationCore(
name=FAIL_AUTOMATION_NAME,
description="Send a Slack notification on the first failure after a success per deployment.",
trigger=failure_trigger,
actions=[
SendNotification(
block_document_id=block_document_id,
subject="Flow failures detected",
body=(
"Deployment: {{ deployment.name }} started failing. "
"Observed state: {{ flow_run.state.name }} at {{ flow_run.state.timestamp }}.\n"
f"Flow run: <{PREFECT_UI_BASE}/runs/flow-run/{{{{ flow_run.id }}}}|Open in Prefect>\n"
"State message: {{ flow_run.state.message }}"
),
)
],
)
await client.create_automation(fail_automation)
print(f"Created automation: {FAIL_AUTOMATION_NAME}")
else:
print(f"Automation '{FAIL_AUTOMATION_NAME}' already exists. Skipping creation.")
# Recovery automation
if RECOVERY_AUTOMATION_NAME not in existing_by_name:
recovery_automation = AutomationCore(Marvin
11/18/2025, 8:51 AM{{ flow_run.state.name }} at {{ flow_run.state.timestamp }}.\n"
f"Flow run: <{PREFECT_UI_BASE}/runs/flow-run/{{{{ flow_run.id }}}}|Open in Prefect>\n"
"State message: {{ flow_run.state.message }}"
),
)
],
)
await client.create_automation(recovery_automation)
print(f"Created automation: {RECOVERY_AUTOMATION_NAME}")
else:
print(f"Automation '{RECOVERY_AUTOMATION_NAME}' already exists. Skipping creation.")
asyncio.run(main())
```
Notes and options:
- The key change is for_each=["related:deployment:id"] which deduplicates per deployment instead of per flow run.
- The after gates make each automation fire only once per “streak” (failure or recovery) for that deployment.
- If a deployment has never succeeded, the “first failure after success” automation won’t alert. If you want to also alert on the first-ever failure, we can discuss a variant.
- You can further reduce noise by scoping with match_related (e.g., only specific deployments or work pools).
Docs for reference:
- Event triggers and their fields: Event triggers
- Creating automations: Creating automations
- Events model (labels and related resources): Events concept
Would you like this latching behavior per deployment, or per flow (non-deployment runs) instead? If per flow, we can switch for_each back to ["prefect.resource.id"].