<@ULVA73B9P> I'm configuring a Prefect automation ...
# ask-marvin
g
@Marvin I'm configuring a Prefect automation to send a Discord alert when a flow run stays in AwaitingConcurrencySlot for more than 2 hours. My goal is one notification per flow, not per run. I'm using a Proactive trigger with for_each. I tried: - "for_each": ["prefect.resource.name"] - "for_each": ["relatedflowprefect.resource.name"] My current trigger: { "type": "event", "match": { "prefect.resource.id": "prefect.flow-run.*" }, "match_related": { "prefect.resource.role": "deployment" }, "after": ["prefect.flow-run.AwaitingConcurrencySlot"], "expect": ["prefect.flow-run.Running", "prefect.flow-run.Cancelled", "prefect.flow-run.Crashed", "prefect.flow-run.Failed", "prefect.flow-run.Completed"], "for_each": ["relatedflowprefect.resource.name"], "posture": "Proactive", "threshold": 1, "within": 7200 } Is it possible to group proactive automation firings by flow (or deployment) so only one alert fires per flow regardless of how many runs are stuck? If so, what is the correct for_each configuration?
m
thought for 136.9 seconds
Yes — proactive triggers do support
for_each
, including labels from related resources. For your case, the best grouping key is:
Copy code
json
"for_each": ["related:flow:prefect.resource.id"]
If you want one alert per deployment instead, use:
Copy code
json
"for_each": ["related:deployment:prefect.resource.id"]
A few important details: *
prefect.resource.name
groups by the flow run resource itself, so that will still be effectively per run. *
related:flow:prefect.resource.name
is valid syntax, but grouping by name can collide if multiple flows share the same name. *
related:flow:prefect.resource.id
is the safest/recommended choice because it groups by the unique flow resource. So the answer is: yes, grouping by flow or deployment is supported, and the correct
for_each
value is usually one of these:
Copy code
json
["related:flow:prefect.resource.id"]
or
Copy code
json
["related:deployment:prefect.resource.id"]
Your trigger would look like:
Copy code
json
{
  "type": "event",
  "match": { "prefect.resource.id": "prefect.flow-run.*" },
  "match_related": { "prefect.resource.role": "deployment" },
  "after": ["prefect.flow-run.AwaitingConcurrencySlot"],
  "expect": [
    "prefect.flow-run.Running",
    "prefect.flow-run.Cancelled",
    "prefect.flow-run.Crashed",
    "prefect.flow-run.Failed",
    "prefect.flow-run.Completed"
  ],
  "for_each": ["related:flow:prefect.resource.id"],
  "posture": "Proactive",
  "threshold": 1,
  "within": 7200
}
One caveat: this gives you one firing per grouped bucket per evaluation window, not a permanent dedupe forever. So if a later run for the same flow also gets stuck in a new qualifying window, the automation can fire again. Relevant docs: * Event triggers * Flow run events If you want, I can also help sanity-check whether your use of both
after
and
expect
matches the behavior you want for
AwaitingConcurrencySlot
.
g
@Marvin I'm setting up a Prefect 3 automation to notify Discord when a flow run is stuck in AwaitingConcurrencySlot for over 2 hours. A few questions: 1. Trigger config — is this correct? trigger: type: event match: prefect.resource.id: "prefect.flow-run.*" match_related: prefect.resource.role: deployment posture: Proactive after: - prefect.flow-run.AwaitingConcurrencySlot expect: - prefect.flow-run.Running - prefect.flow-run.Cancelled - prefect.flow-run.Crashed - prefect.flow-run.Failed - prefect.flow-run.Completed for_each: - "relateddeploymentprefect.resource.id" threshold: 1 within: 7200 2. call-webhook action with a Webhook block pointing to a Discord webhook URL — is this the right approach? There's no native Discord notification block available (prefect_discord is not installed). The block has Content-Type: application/json in its headers. 3. Are {{ flow_run.name }} template variables available in the call-webhook payload? payload: '{"content": "⚠️ {{ flow_run.name }} has been awaiting a concurrency slot for over 2 hours."}' 4. Block resolution: We resolve block_document_id at deploy time by looking up the block by slug via client.read_block_document_by_name(name, block_type). Is there a better native way to reference a block by name in automation YAML?
m
thought for 165.1 seconds
Yes — overall this approach makes sense. A few notes on each point: 1. Trigger config Your trigger is basically correct for “alert if a run enters
AwaitingConcurrencySlot
and does not transition to a terminal/running state within 2 hours.” The main thing to be careful about is your
for_each
choice:
Copy code
yaml
for_each:
  - "related:deployment:prefect.resource.id"
That means the trigger is evaluated per deployment, not per flow run. So if your goal is “only one alert per deployment even if multiple runs are stuck,” this is the right grouping key. If instead you wanted one alert per flow, use:
Copy code
yaml
for_each:
  - "related:flow:prefect.resource.id"
If you wanted one alert per run, use:
Copy code
yaml
for_each:
  - "prefect.resource.id"
So your current config is correct if deployment-level grouping is what you want. I’d write it as:
Copy code
yaml
trigger:
  type: event
  match:
    prefect.resource.id: "prefect.flow-run.*"
  match_related:
    prefect.resource.role: deployment
  posture: Proactive
  after:
    - prefect.flow-run.AwaitingConcurrencySlot
  expect:
    - prefect.flow-run.Running
    - prefect.flow-run.Cancelled
    - prefect.flow-run.Crashed
    - prefect.flow-run.Failed
    - prefect.flow-run.Completed
  for_each:
    - "related:deployment:prefect.resource.id"
  threshold: 1
  within: 7200
Docs: * Event triggers * Detect zombie flows 2.
call-webhook
to Discord
Yes — this is the right native approach if you’re not using a dedicated Discord integration. Using a
Webhook
block pointing at the Discord webhook URL with:
Copy code
yaml
actions:
  - type: call-webhook
    block_document_id: ...
    payload: '{"content":"..."}'
is the expected pattern. Discord accepts JSON payloads like:
Copy code
json
{"content":"message text"}
So having
Content-Type: application/json
on the block is appropriate. Related docs: * Custom notifications 3. Are
{{ flow_run.name }}
variables available?
Yes —
{{ flow_run.name }}
is available in automation templating for event-triggered actions. So this should work:
Copy code
yaml
payload: '{"content": ":warning: {{ flow_run.name }} has been awaiting a concurrency slot for over 2 hours."}'
You can also use other objects like
flow
,
deployment
, and
event
, plus useful filters like
ui_url
. For example:
Copy code
yaml
payload: |
  {
    "content": ":warning: Flow run {{ flow_run.name }} from deployment {{ deployment.name }} has been awaiting a concurrency slot for over 2 hours. {{ flow_run | ui_url }}"
  }
4. Referencing a block by name in automation YAML As far as the current schema goes, there is not a native YAML field to reference the webhook block by slug/name instead of UUID.
call-webhook
expects:
Copy code
yaml
block_document_id: <uuid>
So resolving the block document ID at deploy time by name/slug is a reasonable workaround. I’m not aware of a more native automation-YAML option like:
Copy code
yaml
block: webhook/my-discord-block
for
call-webhook
. If you want, the most stable pattern is: * keep the Webhook block name stable * resolve its document ID during your deployment/build step * inject that UUID into the generated automation YAML Bottom line * Yes, your trigger pattern is valid. * Yes,
call-webhook
+ Webhook block is the right Discord path. * Yes,
{{ flow_run.name }}
should work. * No, there isn’t currently a better native block-by-name reference in automation YAML; UUID resolution is the normal workaround. If you want, I can help you turn this into a complete working automation YAML example for Discord, including a safer escaped payload.
g
@Marvin Here's the complete setup we ended up with. Goal: alert Discord when a flow run is stuck in AwaitingConcurrencySlot for over 2 hours. --- blocks/blocks.yaml blocks: - type: discord-webhook name: discord-concurrency-alert url_env: DISCORD_WEBHOOK_URL_DATAPLEX # https://discord.com/api/webhooks/{id}/{token} blocks/deploy_blocks.py import asyncio import os from pathlib import Path import yaml from prefect.blocks.notifications import DiscordWebhook async def deploy_block(block_spec: dict) -> None: block_type = block_spec["type"] name = block_spec["name"] if block_type == "discord-webhook": url_env = block_spec.get("url_env") url = os.environ.get(url_env) if url_env else block_spec.get("url") if not url: print(f"Skipping block {name}: env var {url_env} not set") return parts = url.rstrip("/").split("/") webhook_id = parts[-2] webhook_token = parts[-1] block = DiscordWebhook(webhook_id=webhook_id, webhook_token=webhook_token) await block.save(name=name, overwrite=True) print(f"Block {block_type}/{name} created/updated") async def main() -> None: blocks_file = Path(file).parent / "blocks.yaml" with blocks_file.open("r") as f: data = yaml.safe_load(f) for block_spec in data.get("blocks", []): await deploy_block(block_spec) if name == "__main__": asyncio.run(main()) --- automations/automations.yaml (relevant part) actions: - type: send-notification block_document_id: "$block:discord-webhook/discord-concurrency-alert" subject: "Flow travado em AwaitingConcurrencySlot" body: "⚠️ Flow travado em AwaitingConcurrencySlot - <@&1056928259700445245>\nO Flow {{ flow_run.name }} possui execuções aguardando slot de concorrência há mais de 2 horas." automations/deploy_automations.py import argparse import asyncio from pathlib import Path from typing import Any import yaml from prefect.automations import Automation def _collect_block_refs(obj: Any, refs: set) -> None: if isinstance(obj, dict): for v in obj.values(): _collect_block_refs(v, refs) elif isinstance(obj, list): for item in obj: _collect_block_refs(item, refs) elif isinstance(obj, str) and obj.startswith("$block:"): refs.add(obj) def _replace_block_refs(obj: Any, ref_to_uuid: dict) -> Any: if isinstance(obj, dict): return {k: _replace_block_refs(v, ref_to_uuid) for k, v in obj.items()} elif isinstance(obj, list): return [_replace_block_refs(item, ref_to_uuid) for item in obj] elif isinstance(obj, str) and obj in ref_to_uuid: return ref_to_uuid[obj] return obj async def resolve_block_refs(spec: dict) -> dict: from prefect.client.orchestration import get_client refs: set = set() _collect_block_refs(spec, refs) if not refs: return spec ref_to_uuid: dict = {} async with get_client() as client: for ref in refs: _, path = ref.split(":", 1) block_type, block_name = path.split("/", 1) block_doc = await client.read_block_document_by_name(block_name, block_type) ref_to_uuid[ref] = str(block_doc.id) return _replace_block_refs(spec, ref_to_uuid) async def upsert_automation(auto_spec: dict) -> None: auto_spec = await resolve_block_refs(auto_spec) automation = Automation(**auto_spec) try: existing = await automation.read(name=automation.name) except ValueError: existing = None if existing: automation.id = existing.id await automation.update() else: await automation.create() async def main(yaml_file: str) -> None: with Path.open(yaml_file, "r") as f: data = yaml.safe_load(f) for auto_spec in data.get("automations", []): await upsert_automation(auto_spec) if name == "__main__": parser = argparse.ArgumentParser() parser.add_argument("yaml_file") args = parser.parse_args() asyncio.run(main(args.yaml_file)) --- Questions: 1. You previously confirmed call-webhook + Webhook block for Discord. We found DiscordWebhook in prefect.blocks.notifications (slug discord-webhook) and switched to send-notification. Is send-notification + DiscordWebhook valid, or should we go back to call-webhook + Webhook? 2. Is client.read_block_document_by_name(name, block_type_slug) the correct/stable API to look up a block document ID by name at deploy time?
m
Your message was too long, here's your message at the allowed limit: Here's the complete setup we ended up with Goal alert Discord when a flow run is stuck in AwaitingConcurrencySlot for over 2 hours --- blocks/blocks yaml blocks - type discord-webhook name discord-concurrency-alert url_env DISCORD_WEBHOOK_URL_DATAPLEX # <https //discord com/api/webhooks/{id}/{token}> blocks/deploy_blocks py import asyncio import os from pathlib import Path import yaml from prefect blocks notifications import DiscordWebhook async def deploy_block(block_spec dict) -> None block_type = block_spec[ type ] name = block_spec[ name ] if block_type == discord-webhook url_env = block_spec get( url_env ) url = os environ get(url_env) if url_env else block_spec get( url ) if not url print(f Skipping block {name} env var {url_env} not set ) return parts = url rstrip( / ) split( / ) webhook_id = parts[-2] webhook_token = parts[-1] block = DiscordWebhook(webhook_id=webhook_id webhook_token=webhook_token) await block save(name=name overwrite=True) print(f Block {block_type}/{name} created/updated ) async def main() -> None blocks_file = Path(file) parent / blocks yaml with blocks_file open( r ) as f data = yaml safe_load(f) for block_spec in data get( blocks []) await deploy_block(block_spec) if name == main asyncio run(main()) --- automations/automations yaml (relevant part) actions - type send-notification block_document_id $block discord-webhook/discord-concurrency-alert subject Flow travado em AwaitingConcurrencySlot body warning Flow travado em AwaitingConcurrencySlot - <@&1056928259700445245>\nO Flow {{ flow_run name }} possui execuções aguardando slot de concorrência há mais de 2 horas automations/deploy_automations py import argparse import asyncio from pathlib import Path from typing import Any import yaml from prefect automations import Automation def _collect_block_refs(obj Any refs set) -> None if isinstance(obj dict) for v in obj values() _collect_block_refs(v refs) elif isinstance(obj list) for item in obj _collect_block_refs(item refs) elif isinstance(obj str) and obj startswith( $block ) refs add(obj) def _replace_block_refs(obj Any ref_to_uuid dict) -> Any if isinstance(obj dict) return {k _replace_block_refs(v ref_to_uuid) for k v in obj items()} elif isinstance(obj list) return [_replace_block_refs(item ref_to_uuid) for item in obj] elif isinstance(obj str) and obj in ref_to_uuid return ref_to_uuid[obj] return obj async def resolve_block_refs(spec dict) -> dict from prefect client orchestration import get_client refs set = set() _collect_block_refs(spec refs) if not refs return spec ref_to_uuid dict = {} async with get_client() as client for ref in refs _ path = ref split( 1) block_type block_name = path split( / 1) block_doc = await client read_block_document_by_name(block_name block_type) ref_to_uuid[ref] = str(block_doc id) return _replace_block_refs(spec ref_to_uuid) async def upsert_automation(auto_spec dict) -> None auto_spec = await resolve_block_refs(auto_spec) automation = Automation(**auto_spec) try existing = await automation read(name=automation name) except ValueError existing = None if existing automation id = existing id await automation update() else await automation create() async def main(yaml_file str) -> None with Path open(yaml_file r ) as f data = yaml safe_load(f) for auto_spec in data get( automations []) await upsert_automation(auto_spec) if name == main parser = argparse ArgumentParser() parser add_argument( yaml_file ) args = parser parse_args() asyncio run(main(args yaml_file)) --- Questions 1 You previously confirmed call-webhook + Webhook block for Discord We found DiscordWebhook in prefect blocks notifications (slug discord-webhook) and switched to send-notification Is send-notification + DiscordWebhook valid or should we go back to call-webhook + Webhook? 2 Is
g
@Marvin Here's the complete setup we ended up with. Goal: alert Discord when a flow run is stuck in AwaitingConcurrencySlot for over 2 hours. --- blocks/blocks.yaml blocks: - type: discord-webhook name: discord-concurrency-alert url_env: DISCORD_WEBHOOK_URL_DATAPLEX # https://discord.com/api/webhooks/{id}/{token} blocks/deploy_blocks.py import asyncio import os from pathlib import Path import yaml from prefect.blocks.notifications import DiscordWebhook async def deploy_block(block_spec: dict) -> None: block_type = block_spec["type"] name = block_spec["name"] if block_type == "discord-webhook": url_env = block_spec.get("url_env") url = os.environ.get(url_env) if url_env else block_spec.get("url") if not url: print(f"Skipping block {name}: env var {url_env} not set") return parts = url.rstrip("/").split("/") webhook_id = parts[-2] webhook_token = parts[-1] block = DiscordWebhook(webhook_id=webhook_id, webhook_token=webhook_token) await block.save(name=name, overwrite=True) print(f"Block {block_type}/{name} created/updated") async def main() -> None: blocks_file = Path(file).parent / "blocks.yaml" with blocks_file.open("r") as f: data = yaml.safe_load(f) for block_spec in data.get("blocks", []): await deploy_block(block_spec) if name == "__main__": asyncio.run(main()) --- automations/automations.yaml (relevant part) actions: - type: send-notification block_document_id: "$block:discord-webhook/discord-concurrency-alert" subject: "Flow travado em AwaitingConcurrencySlot" body: "⚠️ Flow travado em AwaitingConcurrencySlot - <@&1056928259700445245>\nO Flow {{ flow_run.name }} possui execuções aguardando slot de concorrência há mais de 2 horas." automations/deploy_automations.py import argparse import asyncio from pathlib import Path from typing import Any import yaml from prefect.automations import Automation def _collect_block_refs(obj: Any, refs: set) -> None: if isinstance(obj, dict): for v in obj.values(): _collect_block_refs(v, refs) elif isinstance(obj, list): for item in obj: _collect_block_refs(item, refs) elif isinstance(obj, str) and obj.startswith("$block:"): refs.add(obj) def _replace_block_refs(obj: Any, ref_to_uuid: dict) -> Any: if isinstance(obj, dict): return {k: _replace_block_refs(v, ref_to_uuid) for k, v in obj.items()} elif isinstance(obj, list): return [_replace_block_refs(item, ref_to_uuid) for item in obj] elif isinstance(obj, str) and obj in ref_to_uuid: return ref_to_uuid[obj] return obj async def resolve_block_refs(spec: dict) -> dict: from prefect.client.orchestration import get_client refs: set = set() _collect_block_refs(spec, refs) if not refs: return spec ref_to_uuid: dict = {} async with get_client() as client: for ref in refs: _, path = ref.split(":", 1) block_type, block_name = path.split("/", 1) block_doc = await client.read_block_document_by_name(block_name, block_type) ref_to_uuid[ref] = str(block_doc.id) return _replace_block_refs(spec, ref_to_uuid) async def upsert_automation(auto_spec: dict) -> None: auto_spec = await resolve_block_refs(auto_spec) automation = Automation(**auto_spec) try: existing = await automation.read(name=automation.name) except ValueError: existing = None if existing: automation.id = existing.id await automation.update() else: await automation.create() async def main(yaml_file: str) -> None: with Path.open(yaml_file, "r") as f: data = yaml.safe_load(f) for auto_spec in data.get("automations", []): await upsert_automation(auto_spec) if name == "__main__": parser = argparse.ArgumentParser() parser.add_argument("yaml_file") args = parser.parse_args() asyncio.run(main(args.yaml_file)) --- Questions: 1. Is send-notification + DiscordWebhook valid, or should we go back to call-webhook + Webhook? 2. Is client.read_block_document_by_name(name, block_type_slug) the correct/stable API to look up a block document ID by name at deploy time?
m
thought for 112.8 seconds
Yes — this looks good. 1.
send-notification
+
DiscordWebhook
Valid and supported. You do not need to go back to
call-webhook + Webhook
if you already have a
DiscordWebhook
block. In Prefect 3.x: *
send-notification
is meant for notification blocks *
DiscordWebhook
is a notification block *
subject
and
body
are valid fields on the automation action So this is a good fit:
Copy code
yaml
actions:
  - type: send-notification
    block_document_id: "$block:discord-webhook/discord-concurrency-alert"
    subject: "Flow travado em AwaitingConcurrencySlot"
    body: ":warning: ..."
A few practical notes: * Discord typically cares more about the message body than the subject. * Depending on how the Discord notification block formats messages internally, the
subject
may be ignored or folded into the message text. * If the visible output in Discord matters, put the essential content in
body
, not only in
subject
. So: your current approach is cleaner and more native than
call-webhook
.
2.
client.read_block_document_by_name(name, block_type_slug)
Yes — this is the correct and stable API to resolve a block document by name at deploy time. Your usage pattern is reasonable:
Copy code
python
block_doc = await client.read_block_document_by_name(block_name, block_type)
That method is part of the public orchestration client interface and is the right tool for: * block name known ahead of time * needing the document UUID for automation specs * deploy-time resolution A couple of small recommendations: *
block_type
should be the slug (
discord-webhook
), which you’re already using. * Consider handling the “not found” case explicitly so failures are clearer. * If you don’t need secret values from the block document, you can pass
include_secrets=False
. For example:
Copy code
python
block_doc = await client.read_block_document_by_name(
    block_name,
    block_type,
    include_secrets=False,
)
That said, your current code is fine for ID resolution. Overall verdict * `send-notification + DiscordWebhook`: yes, valid; good choice * `read_block_document_by_name(...)`: yes, correct/stable for deploy-time lookup Your
$block:...
preprocessing approach is also a sensible abstraction since automation YAML itself does not natively resolve block references by slug/name. If you want, I can suggest a couple of small hardening improvements to these scripts, like: * validating the Discord webhook URL format * better exception handling for missing blocks * avoiding repeated client lookups when many automations reference the same block
g
@Marvin One more question on deploy_automations.py. We're using three functions to resolve $block: references: def _collect_block_refs(obj: Any, refs: set) -> None: """Recursively traverses the entire spec dict to find all $block: strings.""" ... def _replace_block_refs(obj: Any, ref_to_uuid: dict) -> Any: """Recursively replaces $block: strings with resolved UUIDs.""" ... async def resolve_block_refs(spec: dict) -> dict: """Uses Prefect client to resolve each found ref to a UUID.""" ... Since block_document_id only ever appears inside actions[], we could simplify to a single targeted function: async def resolve_block_refs(spec: dict) -> dict: from prefect.client.orchestration import get_client refs = { action["block_document_id"] for action in spec.get("actions", []) if isinstance(action.get("block_document_id"), str) and action["block_document_id"].startswith("$block:") } if not refs: return spec ref_to_uuid = {} async with get_client() as client: for ref in refs: _, path = ref.split(":", 1) block_type, block_name = path.split("/", 1) block_doc = await client.read_block_document_by_name( block_name, block_type, include_secrets=False ) ref_to_uuid[ref] = str(block_doc.id) for action in spec.get("actions", []): if action.get("block_document_id") in ref_to_uuid: action["block_document_id"] = ref_to_uuid[action["block_document_id"]] return spec Is block_document_id only ever in actions[] in an automation spec, or can it appear elsewhere (e.g. nested in trigger conditions or other fields)? If only in actions, the simpler version is sufficient. If it can appear anywhere in the spec tree, the recursive traversal is justified.
m
thought for 59.5 seconds
You can simplify it, but not quite to only
actions[]
. In Prefect 3.x,
block_document_id
appears only on action objects, specifically actions like: *
send-notification
*
call-webhook
But automation specs can store actions in three top-level fields:
Copy code
yaml
actions:
actions_on_trigger:
actions_on_resolve:
So the important distinction is: * Yes:
block_document_id
is only on actions * No: those actions are not guaranteed to live only in
spec["actions"]
I did not find
block_document_id
in: * trigger definitions * trigger conditions *
match
/
match_related
* nested trigger structures * arbitrary other parts of the automation spec tree Also, there don’t appear to be nested action trees where one action contains child actions. So you do not need a fully recursive traversal of the entire spec if you handle all three action lists. A good simplified version would be something like:
Copy code
python
async def resolve_block_refs(spec: dict) -> dict:
    from prefect.client.orchestration import get_client

    action_fields = ["actions", "actions_on_trigger", "actions_on_resolve"]

    refs = {
        action["block_document_id"]
        for field in action_fields
        for action in spec.get(field, [])
        if isinstance(action.get("block_document_id"), str)
        and action["block_document_id"].startswith("$block:")
    }

    if not refs:
        return spec

    ref_to_uuid = {}
    async with get_client() as client:
        for ref in refs:
            _, path = ref.split(":", 1)
            block_type, block_name = path.split("/", 1)
            block_doc = await client.read_block_document_by_name(
                block_name, block_type, include_secrets=False
            )
            ref_to_uuid[ref] = str(block_doc.id)

    for field in action_fields:
        for action in spec.get(field, []):
            if action.get("block_document_id") in ref_to_uuid:
                action["block_document_id"] = ref_to_uuid[action["block_document_id"]]

    return spec
So my recommendation: * If you only support
actions
, your simplified version is not fully safe * If you support
actions
,
actions_on_trigger
, and
actions_on_resolve
, that should be sufficient * Full recursive traversal of the whole spec tree is probably more general than necessary So the middle ground is best: targeted traversal over the known action lists. If you want, I can also help refactor this into a cleaner helper with validation for malformed
$block:type/name
references.