Matt Liu
11/20/2025, 11:59 PMMarvin
11/20/2025, 11:59 PMMarvin
11/21/2025, 12:04 AM# Event processing windows (Prefect 2.x)
PRECEDING_EVENT_LOOKBACK = 15 minutes
PROCESSED_EVENT_LOOKBACK = 30 minutes
# Proactive evaluation granularity defaults to ~5 seconds
To pinpoint your case, could you share:
- Midnight in which timezone?
- The automation definition (trigger type, posture reactive vs. proactive, any “within” window, and the action used)?
- Provider used for notifications (Slack, email, Teams, webhook)?
- In the automation’s Activity, do you see the trigger firing but the action failing, or no trigger at all?
Quick checks you can run now:
- In the Automation detail page > Activity: confirm if it triggered at that time and inspect the triggered action details for any notification errors.
- Cloud Events feed: filter for your failure event (e.g., prefect.flow-run.failed) around midnight UTC to confirm the event landed when you expect.
- If using a proactive “within X” window that starts/ends at midnight, shift those boundaries away from 00:00 UTC and retest.
- Test your notification block directly (send a test message) around that time to rule out provider rate limits.
Mitigations that typically resolve midnight gaps:
- Avoid windows that start/end at 00:00 UTC; use 00:05 or another off-peak boundary.
- Prefer reactive triggers for flow-run failures instead of proactive windows when possible.
- Add a backup action (e.g., webhook) so a provider hiccup doesn’t drop the alert.
- If DST is in play, confirm you’re not depending on local midnight semantics—Automations run in UTC.
If you can share the automation’s JSON or a screenshot of the trigger/action config, I can help spot the exact issue and propose a precise fix.Matt Liu
11/21/2025, 12:12 AMMarvin
11/21/2025, 12:12 AMMarvin
11/21/2025, 12:16 AM"match_related": {
"prefect.resource.id": ["prefect.tag.prod"],
"prefect.resource.role": "tag"
}
- In Prefect 2.x, a child (subflow) flow-run event does not automatically inherit the parent’s tags. The event’s related tags are built from the child flow/deployment/work pool/queue, not the parent. So if the child flow/deployment doesn’t have the prod tag, its Failed event will be filtered out by your automation and no Slack message is sent.
- Your note that “parent flow and child flow share the same timestamp” lines up with this: overnight, it’s likely the child fails (no prod tag) and the parent either doesn’t fail or fails slightly later, so the automation misses some of those child failures.
- The “works after 10am” pattern likely correlates with different runs (e.g., top-level parent failures or runs whose child flows/deployments do carry the prod tag), not a UTC/midnight issue. Brisbane is UTC+10, so 000–900 AEST is 1400–2300 UTC — not a known boundary for Prefect’s event windows.
How to confirm
- Open one missed event in Cloud > Events and check Related resources. You’ll see the child flow-run event lacks prefect.tag.prod.
- Check the parent’s flow-run.Failed event (if it exists) around the same time — it should have the tag and would match your automation.
- Docs for how Automations match on related resources and tags: Automations (2.x), Cloud Events
Fix options
Pick one of these based on how you separate prod/non-prod:
1) Propagate prod to subflows (recommended)
- Ensure the child flow runs in prod carry the prod tag.
- Easiest approach: call the subflow with tags from the parent in prod.
from prefect import flow
@flow(name="child-flow")
def child(...):
...
@flow(name="parent-flow", tags=["prod"])
def parent(...):
# In prod, ensure the child subflow run is tagged 'prod'
child.with_options(tags=["prod"])(...)
- Or add the prod tag to the child deployment(s) used in prod so all child runs in prod inherit it.
2) Match on deployment/flow/work-pool instead of a tag
- If you have prod-specific deployments or a prod work pool, filter on those instead of tags. For example:
"match_related": {
"prefect.resource.role": "work-pool",
"prefect.resource.name": ["prod-pool"]
}
or
"match_related": {
"prefect.resource.role": "deployment",
"prefect.resource.name": ["my-prod-deployment-*"]
}
- This avoids tag propagation entirely and keys the alerting to your prod infra objects.
3) Keep current automation for parent failures and add a second automation for child failures
- A second automation without the prod tag filter (or filtered by child’s prod deployment) ensures subflow failures are caught as well.
Why this isn’t a UTC/midnight thing
- Your window (000–900 AEST) doesn’t align with any Prefect timing boundaries (all internal evaluation is UTC).
- Slack webhook rate limiting could explain sporadic misses but not a consistent 9-hour window. The tag filter mismatch is the consistent explanation for “child fails, no alert.”
If you share one missed event’s Related list (screenshot or JSON), I can confirm the absence of prefect.tag.prod and propose the exact match changes for your automation.Matt Liu
11/21/2025, 12:25 AMMarvin
11/21/2025, 12:25 AMMarvin
11/21/2025, 12:27 AMfor_each: ["prefect.resource.id"] won’t suppress one because of the other.
Why your added automations could still be silent at the same times
- Flow ID pinning: Your flow-scoped automation matches a single flow ID:
"match_related": {
"prefect.resource.role": "flow",
"prefect.resource.id": ["prefect.flow.87cb53ea-6126-4439-9e82-b424ef7fe0f9"]
}
If the flow was re-registered or runs under a different flow object (same name, new ID), events will not match. This can happen if deployments or environments recreate the flow object. The “worked in daytime” and “silent at night” could indicate different process paths creating runs with a different flow id at night.
- Work pool filter: The flow-run event must include the work-pool as a related resource for the pool automation to match. Subflows sometimes do not surface the pool relationship the way top-level runs do, and ad-hoc or locally-triggered runs may lack a pool relationship.
- Schedule filter: This one is likely a no-op. Flow-run events generally do not include a related prefect.schedule.* resource, so it won’t match those events.
- Provider side: If all automations “triggered” but you saw no Slack posts, a webhook-level issue could explain synchronized silence (429 rate limits, transient provider issues). If no automations triggered at all in Activity, it’s a matching issue rather than Slack.
What to check next (will pinpoint root cause quickly)
1) Pick one missed parent flow-run.Failed event and open it in Cloud > Events. In Related resources, confirm:
- The exact flow resource present (id and name)
- Whether a work-pool appears (and which one)
- Any tags present
2) Open the Automation’s Activity for the same time:
- If there’s no activity: it didn’t match; adjust filters as below.
- If there is activity and an action record: open the action details — look for a 429 or 5xx from Slack/webhook.
3) If possible, share the event JSON’s related resource list here; I can map it precisely to your automation filters.
Recommended fixes
- Avoid pinning to a specific flow ID. Use flow name or deployment instead:
"match_related": {
"prefect.resource.role": "flow",
"prefect.resource.name": ["<parent flow name>"]
}
or match by deployment:
"match_related": {
"prefect.resource.role": "deployment",
"prefect.resource.name": ["<prod deployment name>"]
}
This prevents “silent” periods if the flow ID changes overnight.
- For pool scoping: confirm missed events actually include a work-pool related resource with one of those IDs. If they don’t, match on deployment or flow instead.
- Remove the schedule-based automation (it won’t match flow-run events). If you want scheduled-run failures specifically, target deployments or flows, not prefect.schedule.*.
- Add a “shadow” action for diagnostics:
- Create a temporary automation that matches all flow-run failures and posts to a RequestBin/Hookbin URL. If you see those HTTP hits during the silent window, but Slack is quiet, it’s the Slack webhook pathway. If you don’t see hits, matching is the culprit.
- If you want child failures covered, either:
- Propagate prod tag to subflows (e.g., child.with_options(tags=["prod"])), or
- Create a child-specific automation matched by the child’s flow or deployment (by name, not id).
Why this explains “all silent at the same time”Marvin
11/21/2025, 12:27 AMmatch_related you should use.Matt Liu
11/21/2025, 12:43 AM{
"id": "0691f300-eeed-77a0-8000-e82cfff2c306",
"account": "1c40ba6c-24dd-4706-86ca-8b53a2e36610",
"event": "prefect-cloud.automation.action.failed",
"occurred": "2025-11-20T15:13:18.932Z",
"payload": {
"action_index": 0,
"action_type": "call-webhook",
"invocation": "0691f300-ea19-77b1-8000-75801f921950",
"reason": "Webhook call failed: Exception('Webhook call status code was 400: invalid_payload')",
"status_code": 400,
"response_body": "invalid_payload",
"response_headers": {
"date": "Thu, 20 Nov 2025 15:13:18 GMT",
"server": "Apache",
"strict-transport-security": "max-age=31536000; includeSubDomains; preload",
"referrer-policy": "no-referrer",
"x-slack-unique-id": "aR8wDsbEbqq2_xwir2L4tAAAEAA",
"x-slack-backend": "r",
"access-control-allow-origin": "*",
"x-frame-options": "SAMEORIGIN",
"content-type": "text/html",
"content-length": "15",
"x-backend": "main_normal main_canary_with_overflow main_control_with_overflow",
"x-server": "slack-www-hhvm-main-iad-89iktvxoskn2",
"x-slack-shared-secret-outcome": "no-match",
"x-edge-backend": "envoy-www",
"timing-allow-origin": "*",
"alt-svc": "h3=\":443\"; ma=2592000, h3-29=\":443\"; ma=2592000, quic=\":443\"; ma=2592000",
"x-geoname-id": "notfound",
"x-slack-edge-shared-secret-outcome": "no-match"
}
},
"received": "2025-11-20T15:13:18.933Z",
"related": [
{
"prefect.resource.id": "prefect.block-document.70ccc545-7990-4873-a132-c854ed8b87ab",
"prefect.resource.role": "block",
"prefect.resource.name": "martech-slack-webhook"
},
{
"prefect.resource.id": "prefect.block-type.webhook",
"prefect.resource.role": "block-type"
}
],
"resource": {
"prefect.resource.id": "prefect-cloud.automation.981dc554-5947-4224-b35d-8a494701d03e",
"prefect.resource.name": "Martech-Slack-Error-Alerts-ML-Test",
"prefect-cloud.trigger-type": "event",
"prefect-cloud.posture": "Reactive"
},
"workspace": "0f26a91a-5e95-4a1c-b7f8-f5fc618946f9"
}Marvin
11/21/2025, 12:43 AMMarvin
11/21/2025, 12:45 AM{{ value | default('N/A') }} and {{ long_text | truncate(2800) }}
- Only include a block if content exists:
- {% if some_value %}{ ... }{% endif %}
- Don’t send empty "blocks": [] or a section block without a valid text object.
3) Minimal working payload to test
- Temporarily set the Webhook body to this known-good shape and test around the failing window:
{
"text": "*Prefect alert:* A flow run failed.",
"mrkdwn": true
}
- If this succeeds at midnight, the issue is definitely in your templating logic for richer messages.
4) Example robust template (with conditionals)
{% set flow = (event.related | selectattr('prefect.resource.role','equalto','flow') | list | first) %}
{
"text": "*Prefect alert:* Flow run failed",
"blocks": [
{"type":"section","text":{"type":"mrkdwn","text":"*Flow:* {{ flow['prefect.resource.name'] | default('unknown') }}"}},
{"type":"section","text":{"type":"mrkdwn","text":"*Run:* {{ event.resource['prefect.resource.id'] }}"}},
{% if event.payload and event.payload.get('state', {}).get('message') %}
{"type":"section","text":{"type":"mrkdwn","text":"```{{ event.payload.state.message | truncate(2800) }}```"}}
{% endif %}
]
}
5) Consider using “Send a notification” with a Slack Webhook notification block
- It handles the Slack payload shape for you. In Automations, choose “Send a notification” and select your Slack Webhook block. This is generally safer than a custom Webhook payload.
6) Add a temporary “debug sink” action
- Add a second action that POSTs the same payload to a RequestBin/Hookbin URL so you can inspect the exact JSON produced during the failing window.
References
- Prefect Automations overview: https://docs.prefect.io/2.14/concepts/automations/
- Slack Incoming Webhooks schema: Slack incoming webhooks docs
If you can share the current payload template from martech-slack-webhook (or a screenshot), I’ll point out exactly where it can produce an invalid Slack payload and propose a corrected version.Matt Liu
11/21/2025, 12:50 AM{
"channel": "ml-test-alerts",
"username": "Prefect-Alert-Bot",
"icon_emoji": ":prefect:",
"text": "Martech-Slack-Error-Alerts-ML-Test-2:\n\n```Flow run: {{ flow.name }}/{{ flow_run.name }}\nState: `{{ flow_run.state.name }}`\nTimestamp: {{ flow_run.state.timestamp }}\nFlow run URL: {{ flow_run|ui_url }}\nState message: {{ flow_run.state.message }}```"
}Marvin
11/21/2025, 12:50 AMMarvin
11/21/2025, 12:51 AM{
"text": {{ (
"Martech-Slack-Error-Alerts-ML-Test-2:\n\n" +
"```" +
"Flow run: " + (flow.name | default("unknown")) + "/" + (flow_run.name | default("unknown")) + "\n" +
"State: `" + (flow_run.state.name | default("unknown")) + "`\n" +
"Timestamp: " + ((flow_run.state.timestamp | default(flow_run.start_time) | string) | default("unknown")) + "\n" +
"Flow run URL: " + ((flow_run | ui_url) | default("")) + "\n" +
"State message: " + ((flow_run.state.message | default("") | truncate(2800))) +
"```"
) | tojson }}
}
Notes:
- tojson ensures the text value remains valid JSON regardless of embedded quotes or newlines.
- truncate(2800) keeps the code block well within Slack limits.
- default(...) guards against None/missing values.
If you prefer to keep channel/username/icon_emoji, you can add them back; just be aware some Slack webhooks ignore or reject overrides depending on your app configuration.
Quick validation steps
- Temporarily switch to a minimal payload and test during the problem window:
{
"text": "Prefect alert: a flow run failed."
}
If this succeeds, the previous payload was invalid due to content.
- Add a second “debug” webhook action to a RequestBin/Hookbin URL to capture the exact JSON Prefect sends when Slack returns 400 — you’ll quickly see if state.message is huge or if any field is blank/unexpected.
- Consider using “Send a notification” with a Slack Webhook notification block in the Automation; Prefect will format a safe Slack payload for you.
Reference
- Slack incoming webhook payload rules: https://api.slack.com/messaging/webhooks
If you paste one failing RequestBin capture (redacted), I can point out the exact field causing Slack’s 400.Matt Liu
11/21/2025, 1:56 AMMarvin
11/21/2025, 1:56 AMMarvin
11/21/2025, 1:57 AM{
"channel": "ml-test-alerts",
"username": "Prefect-Alert-Bot",
"icon_emoji": ":prefect:",
"text": {{ (
"Martech-Slack-Error-Alerts-ML-Test-2:\n\n" +
"```" +
"Flow run: " + (flow.name | default("unknown")) + "/" + (flow_run.name | default("unknown")) + "\n" +
"State: `" + (flow_run.state.name | default("unknown")) + "`\n" +
"Timestamp: " + ((flow_run.state.timestamp | default(flow_run.start_time) | string) | default("unknown")) + "\n" +
"Flow run URL: " + ((flow_run | ui_url) | default("")) + "\n" +
"State message: " + ((flow_run.state.message | default("") | truncate(2800))) +
"```"
) | tojson }}
}
Why this works
- tojson escapes quotes, backslashes, and newlines so Slack gets valid JSON every time.
- default(...) prevents None from rendering as bare nulls.
- truncate(2800) keeps giant stack traces from bloating the message.
Optional checks
- If you still see 400s, try a minimal payload again:
{
"text": "Prefect alert: a flow run failed."
}
If that works while the rich one fails, the issue is definitely content/escaping.
- You can also POST the rendered payload to a RequestBin/Hookbin to inspect exactly what Slack is receiving.
References
- Slack incoming webhooks
- Prefect 2.x AutomationsMatt Liu
11/21/2025, 3:56 AMMarvin
11/21/2025, 3:56 AMMarvin
11/21/2025, 3:57 AM{
"channel": "ml-test-alerts",
"username": "Prefect-Alert-Bot",
"icon_emoji": ":prefect:",
"text": {{ [
"Martech-Slack-Error-Alerts-ML-Test-2:",
"Flow run: " ~ (flow.name | default("unknown")) ~ "/" ~ (flow_run.name | default("unknown")),
"State: " ~ (flow_run.state.name | default("unknown")),
"Timestamp: " ~ ((flow_run.state.timestamp | default(flow_run.start_time) | string) | default("unknown")),
"Flow run URL: " ~ ((flow_run | ui_url) | default("")),
"State message: " ~ ((flow_run.state.message | default("") | truncate(2800)))
] | join("\n") | tojson }}
}
Why this should fix it
- Single set of {{ }} around the expression after "text": …
- ~ is the Jinja string concatenation operator
- tojson escapes quotes, backslashes, and newlines so Slack gets valid JSON
- default(...) prevents None
- truncate avoids oversized payloads
If you prefer the previous “single string” style, this is also valid:
{
"channel": "ml-test-alerts",
"username": "Prefect-Alert-Bot",
"icon_emoji": ":prefect:",
"text": {{ (
"Martech-Slack-Error-Alerts-ML-Test-2:\n" ~
"Flow run: " ~ (flow.name | default("unknown")) ~ "/" ~ (flow_run.name | default("unknown")) ~ "\n" ~
"State: " ~ (flow_run.state.name | default("unknown")) ~ "\n" ~
"Timestamp: " ~ ((flow_run.state.timestamp | default(flow_run.start_time) | string) | default("unknown")) ~ "\n" ~
"Flow run URL: " ~ ((flow_run | ui_url) | default("")) ~ "\n" ~
"State message: " ~ ((flow_run.state.message | default("") | truncate(2800)))
) | tojson }}
}
Optional tips
- If you still see 400, temporarily remove the channel/username/icon_emoji fields and send only the text field; some Slack webhooks disallow channel overrides depending on the app’s configuration.
- To verify exactly what’s being sent, add a second action to POST to a RequestBin/Hookbin URL and inspect the rendered JSON.
- Alternatively, switch the action to “Send a notification” with a Slack Webhook notification block; Prefect will handle Slack-safe formatting for you.