Russell Brooks
05/10/2026, 6:50 AMMarvin
05/10/2026, 6:50 AMMarvin
05/10/2026, 6:50 AM[10:15 PM] are clearly output contamination and should not have been in the scripts. Sorry about that.
If you want this cleaned up properly, the revised version should follow these rules:
* use only YAML + yq for shell processing
* use Python only where you explicitly want it, assuming a YAML library is already installed
* remove jq
* remove create_project.sh
* remove pre-commit steps I cannot verify
* ensure no stray timestamp/log artifacts appear anywhere
If you'd like, I can rewrite the whole solution cleanly now. A good cleaned-up structure would look like this:
* prefect.yaml
* generate_files.sh using only yq
* optional generate_files.py using a YAML library
* any supporting YAML config files only if truly necessary
For example, if the shell script’s job is to read values from YAML and create/update files, the pattern should be something like:
bash
#!/usr/bin/env bash
set -euo pipefail
CONFIG_FILE="${1:-config.yaml}"
project_name="$(yq -r '.project.name' "$CONFIG_FILE")"
module_name="$(yq -r '.project.module_name' "$CONFIG_FILE")"
entrypoint="$(yq -r '.project.entrypoint' "$CONFIG_FILE")"
mkdir -p "$project_name"
cat > "$project_name/prefect.yaml" <<EOF
name: $project_name
deployments:
- name: default
entrypoint: $entrypoint
EOF
cat > "$project_name/__init__.py" <<EOF
# $module_name package
EOF
And the Python version, assuming PyYAML or similar is installed, should be similarly minimal:
python
from pathlib import Path
import yaml
import sys
config_file = Path(sys.argv[1] if len(sys.argv) > 1 else "config.yaml")
config = yaml.safe_load(config_file.read_text())
project_name = config["project"]["name"]
module_name = config["project"]["module_name"]
entrypoint = config["project"]["entrypoint"]
project_dir = Path(project_name)
project_dir.mkdir(parents=True, exist_ok=True)
(project_dir / "prefect.yaml").write_text(
yaml.safe_dump(
{
"name": project_name,
"deployments": [
{
"name": "default",
"entrypoint": entrypoint,
}
],
},
sort_keys=False,
)
)
(project_dir / "__init__.py").write_text(f"# {module_name} package\n")
If you want, send me the current version of the files or the exact desired output structure, and I’ll rewrite the whole thing cleanly in one pass with:
* no jq
* no extra helper script
* no unverifiable pre-commit bits
* no timestamp contamination
* cleaner YAML-first approach
If you paste the original spec or the files I should fix, I’ll produce a tidy replacement.Russell Brooks
05/10/2026, 6:54 AMMarvin
05/10/2026, 6:54 AMMarvin
05/10/2026, 6:54 AMautomations.yaml, not prefect.yaml.
So the cleaned-up version should be based on:
* automations.yaml as the source of truth
* yq for shell-only YAML processing
* optional Python script using an installed YAML library
* no jq
* no create_project.sh
* no pre-commit step
* no stray timestamp contamination
A much more appropriate shell pattern would be:
bash
#!/usr/bin/env bash
set -euo pipefail
AUTOMATIONS_FILE="${1:-automations.yaml}"
count="$(yq '.automations | length' "$AUTOMATIONS_FILE")"
for i in $(seq 0 $((count - 1))); do
name="$(yq -r ".automations[$i].name" "$AUTOMATIONS_FILE")"
description="$(yq -r ".automations[$i].description" "$AUTOMATIONS_FILE")"
echo "Upserting automation: $name"
# Replace this echo with the actual Prefect CLI/API call you want to use
yq -o=json ".automations[$i]" "$AUTOMATIONS_FILE"
done
And the Python equivalent would be:
python
from pathlib import Path
import sys
import yaml
automations_file = Path(sys.argv[1] if len(sys.argv) > 1 else "automations.yaml")
data = yaml.safe_load(automations_file.read_text())
for automation in data.get("automations", []):
name = automation.get("name", "<unnamed>")
print(f"Upserting automation: {name}")
print(yaml.safe_dump(automation, sort_keys=False))
A typical automations.yaml structure might be:
yaml
automations:
- name: example-automation
description: Example automation
enabled: true
trigger:
type: event
match:
prefect.resource.id: prefect.flow-run.*
actions:
- type: do-nothing
If you want, I can now rewrite the full tidy solution for automations management specifically, for example:
* automations.yaml
* a cleaned upsert_automations.sh using only yq
* optionally upsert_automations.py
* and, if needed, the exact Prefect CLI or API call wired in correctly
If you want that, I can produce the final cleaned version in one go.Russell Brooks
05/10/2026, 6:55 AMMarvin
05/10/2026, 6:55 AMMarvin
05/10/2026, 6:57 AMautomations.yaml, using:
* automations.yaml as the source of truth
* shell with YAML + yq only
* optional Python version using an installed YAML library
* no jq
* no create_project.sh
* no pre-commit
* no contaminated timestamps/log fragments
Also, one important note from Prefect 3.x:
* prefect automation create exists
* prefect automation update exists
* prefect automation inspect exists
* but there is no built-in CLI upsert, so we implement upsert by:
1. looking up an existing automation by name
2. extracting its id
3. calling create or update as appropriate
Because update requires an automation ID, the cleanest robust approach is Python. I’m including both:
* a shell version that uses prefect automation inspect + yq
* a Python version that is easier to maintain
---
1) Example automations.yaml
This format keeps all automations in one file:
yaml
automations:
- name: notify-on-failure
description: Notify when a flow run fails
enabled: true
trigger:
type: event
posture: Reactive
expect:
- prefect.flow-run.Failed
threshold: 1
actions:
- type: do-nothing
- name: cancel-on-running
description: Example automation for running flow runs
enabled: true
trigger:
type: event
posture: Reactive
expect:
- prefect.flow-run.Running
threshold: 1
actions:
- type: cancel-flow-run
You can replace the triggers/actions with your real automations.
---
2) Shell version: upsert_automations.sh
This version uses only:
* bash
* yq
* prefect
It:
* reads automations.yaml
* loops through each automation
* writes a temporary single-automation YAML file
* tries to inspect by name
* uses yq to parse the returned YAML/JSON
* runs create or update
bash
#!/usr/bin/env bash
set -euo pipefail
AUTOMATIONS_FILE="${1:-automations.yaml}"
if ! command -v yq >/dev/null 2>&1; then
echo "Error: yq is required but not installed." >&2
exit 1
fi
if ! command -v prefect >/dev/null 2>&1; then
echo "Error: prefect is required but not installed." >&2
exit 1
fi
if [ ! -f "$AUTOMATIONS_FILE" ]; then
echo "Error: file not found: $AUTOMATIONS_FILE" >&2
exit 1
fi
count="$(yq '.automations | length' "$AUTOMATIONS_FILE")"
if [ "$count" = "0" ]; then
echo "No automations found in $AUTOMATIONS_FILE"
exit 0
fi
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
for i in $(seq 0 $((count - 1))); do
name="$(yq -r ".automations[$i].name" "$AUTOMATIONS_FILE")"
if [ -z "$name" ] || [ "$name" = "null" ]; then
echo "Error: automation at index $i is missing a name" >&2
exit 1
fi
echo "Processing automation: $name"
single_file="$tmpdir/automation-$i.yaml"
yq ".automations[$i]" "$AUTOMATIONS_FILE" > "$single_file"
inspect_file="$tmpdir/inspect-$i.yaml"
if prefect automation inspect "$name" > "$inspect_file" 2>/dev/null; then
automation_id="$(yq -r '.id' "$inspect_file")"
if [ -n "$automation_id" ] && [ "$automation_id" != "null" ]; then
echo "Updating existing automation: $name ($automation_id)"
prefect automation update --id "$automation_id" --from-file "$single_file"
else
echo "Error: automation '$name' was found but no id could be parsed" >&2
exit 1
fi
else
echo "Creating new automation: $name"
prefect automation create --from-file "$single_file"
fi
done
echo "Done."
---
3) Python version: upsert_automations.py
This is the version I’d recommend using in practice.
It:
* reads automations.yaml
* checks for an existing automation by name
* updates if found
* creates if not found
This assumes:
* Python 3.14-compatible YAML library installed
* Prefect installed and authenticated against your workspace
```python
#!/usr/bin/env python3
from pathlib import Path
import sys
import yamlMarvin
05/10/2026, 6:57 AM---
*4) Usage*
*Shell version:*
```bash
chmod +x upsert_automations.sh
./upsert_automations.sh automations.yaml
Python version:
bash
python upsert_automations.py automations.yaml
---
5) Recommendation
If you want the most maintainable solution, use the Python version.
Why:
* no fragile CLI output parsing
* cleaner update logic
* easier to validate YAML before applying
* easier to extend later with dry-run, diffing, filtering, etc.
The shell version works, but it depends on prefect automation inspect output being parseable the way we expect.
---
6) Caveat about name matching
For upsert-by-name, there is one practical caveat:
* this assumes automation names are unique enough in your environment for Automation.read(name=...) or prefect automation inspect <name> to identify the intended automation
If you want, I can do one more pass and produce an even more polished version with any of these added:
* --dry-run
* --only NAME
* --verbose
* safer handling for duplicate names
* validation of required keys in automations.yaml
If you want, I can generate that as a final “production-ready” version next.Russell Brooks
05/10/2026, 7:45 AMMarvin
05/10/2026, 7:45 AMMarvin
05/10/2026, 7:45 AMRussell Brooks
05/11/2026, 10:11 AMupsert_automations.sh above from* Yesterday at 8:55 AM, the line with automation_id="$(yq -r '.id' "$inspect_file") errors out as it cannot find an id. This is because in the original user defined automations.yaml there was also no id specified -- nor does there need to be. Can you rewrite just the upsert_automations.sh to handle the id better, or perhaps work around using the id at that point in the script's logic?`Marvin
05/11/2026, 10:11 AMMarvin
05/11/2026, 10:11 AMid does not come from your source `automations.yaml`; it has to come from the existing automation already in Prefect.
So the shell script needs to be more careful about how it obtains that ID from prefect automation inspect.
A cleaner approach is:
* keep your source file free of id
* inspect the existing automation by name
* extract id from the inspect output only if it exists
* if inspect succeeds but no id is parseable, fall back to create failure with a clear error instead of silently breaking
Here’s a rewritten just-the-script version:
bash
#!/usr/bin/env bash
set -euo pipefail
AUTOMATIONS_FILE="${1:-automations.yaml}"
if ! command -v yq >/dev/null 2>&1; then
echo "Error: yq is required but not installed." >&2
exit 1
fi
if ! command -v prefect >/dev/null 2>&1; then
echo "Error: prefect is required but not installed." >&2
exit 1
fi
if [ ! -f "$AUTOMATIONS_FILE" ]; then
echo "Error: file not found: $AUTOMATIONS_FILE" >&2
exit 1
fi
count="$(yq '.automations | length' "$AUTOMATIONS_FILE")"
if [ "$count" = "0" ]; then
echo "No automations found in $AUTOMATIONS_FILE"
exit 0
fi
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
for i in $(seq 0 $((count - 1))); do
name="$(yq -r ".automations[$i].name" "$AUTOMATIONS_FILE")"
if [ -z "$name" ] || [ "$name" = "null" ]; then
echo "Error: automation at index $i is missing a name" >&2
exit 1
fi
echo "Processing automation: $name"
single_file="$tmpdir/automation-$i.yaml"
yq ".automations[$i]" "$AUTOMATIONS_FILE" > "$single_file"
inspect_file="$tmpdir/inspect-$i.yaml"
if prefect automation inspect "$name" > "$inspect_file" 2>/dev/null; then
automation_id="$(yq -r '.id // ""' "$inspect_file" 2>/dev/null || true)"
if [ -n "$automation_id" ]; then
echo "Updating existing automation: $name ($automation_id)"
prefect automation update --id "$automation_id" --from-file "$single_file"
else
echo "Error: automation '$name' appears to exist, but no id could be parsed from 'prefect automation inspect' output." >&2
echo "Inspect output was:" >&2
cat "$inspect_file" >&2
exit 1
fi
else
echo "Creating new automation: $name"
prefect automation create --from-file "$single_file"
fi
done
echo "Done."
A couple of key fixes here:
* id is not expected in your input YAML
* extraction uses:
bash
yq -r '.id // ""'
so it won’t explode just because .id is absent
* if inspect succeeds but no id is present, the script now fails with a clear diagnostic and prints the inspect output
If you want an even more robust workaround, the best shell-only strategy is to avoid depending on the shape of inspect output entirely by using the CLI only for existence detection and switching to Python for the actual update. But if you want to stay in shell, the above is the cleanest correction to the current logic.
If you want, I can also produce a more defensive shell version that:
* first tries inspect
* then tries to parse either YAML or JSON-shaped output
* and only updates when it can positively identify a single automation ID.Russell Brooks
05/11/2026, 10:14 AMMarvin
05/11/2026, 10:14 AMMarvin
05/11/2026, 10:17 AMid in your source YAML.
The most defensive shell-only approach is:
1. read each automation from automations.yaml
2. look it up with prefect automation inspect "<name>" --json
3. treat the result as a list when looking up by name
4. if:
* 0 matches: create
* 1 match: update using that returned ID
* 1 matches: fail clearly because the name is ambiguous That avoids requiring
id in the original YAML and handles the actual CLI behavior more robustly.
Here is the rewritten upsert_automations.sh only:
bash
#!/usr/bin/env bash
set -euo pipefail
AUTOMATIONS_FILE="${1:-automations.yaml}"
if ! command -v yq >/dev/null 2>&1; then
echo "Error: yq is required but not installed." >&2
exit 1
fi
if ! command -v prefect >/dev/null 2>&1; then
echo "Error: prefect is required but not installed." >&2
exit 1
fi
if [ ! -f "$AUTOMATIONS_FILE" ]; then
echo "Error: file not found: $AUTOMATIONS_FILE" >&2
exit 1
fi
count="$(yq -r '.automations | length' "$AUTOMATIONS_FILE")"
if [ "$count" = "0" ]; then
echo "No automations found in $AUTOMATIONS_FILE"
exit 0
fi
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
for i in $(seq 0 $((count - 1))); do
name="$(yq -r ".automations[$i].name" "$AUTOMATIONS_FILE")"
if [ -z "$name" ] || [ "$name" = "null" ]; then
echo "Error: automation at index $i is missing a name" >&2
exit 1
fi
echo "Processing automation: $name"
single_file="$tmpdir/automation-$i.yaml"
yq ".automations[$i]" "$AUTOMATIONS_FILE" > "$single_file"
inspect_json="$tmpdir/inspect-$i.json"
if prefect automation inspect "$name" --json > "$inspect_json" 2>/dev/null; then
match_count="$(yq -r 'length' "$inspect_json")"
case "$match_count" in
0)
echo "Creating new automation: $name"
prefect automation create --from-file "$single_file"
;;
1)
automation_id="$(yq -r '.[0].id // ""' "$inspect_json")"
if [ -z "$automation_id" ]; then
echo "Error: exactly one automation matched '$name', but no id was returned." >&2
echo "Inspect output:" >&2
cat "$inspect_json" >&2
exit 1
fi
echo "Updating existing automation: $name ($automation_id)"
prefect automation update --id "$automation_id" --from-file "$single_file"
;;
*)
echo "Error: multiple automations matched name '$name'; refusing to guess." >&2
echo "Matching automations:" >&2
yq -r '.[] | " - \(.name) [\(.id)]"' "$inspect_json" >&2
exit 1
;;
esac
else
echo "Creating new automation: $name"
prefect automation create --from-file "$single_file"
fi
done
echo "Done."
Why this is better:
* no id required in automations.yaml
* uses Prefect as the source of truth for existing IDs
* uses --json for structured parsing
* correctly handles the fact that name-based inspect returns a list
* safely refuses to update if the name is ambiguous
Your automations.yaml can stay like this, with no `id`:
yaml
automations:
- name: notify-on-failure
description: Notify when a flow run fails
enabled: true
trigger:
type: event
posture: Reactive
expect:
- prefect.flow-run.Failed
threshold: 1
actions:
- type: do-nothing
If you want, I can also give you a second version of this script with a --strict / --replace mode, for example:
* `--strict`: fail if automation does not already exist
* `--replace`: delete duplicates and recreate one clean automation
That can help if duplicate names are common in your workspace.Russell Brooks
05/11/2026, 10:18 AMMarvin
05/11/2026, 10:18 AMMarvin
05/11/2026, 10:18 AMupsert_automations.sh using only YAML + yq, with no JSON.
It still does robust upsert logic without requiring id in your source `automations.yaml`:
* read each automation from automations.yaml
* inspect by name
* parse the inspect output as YAML
* if 0 matches: create
* if 1 match: extract returned id from Prefect's inspect output and update
* if multiple matches: fail clearly
bash
#!/usr/bin/env bash
set -euo pipefail
AUTOMATIONS_FILE="${1:-automations.yaml}"
if ! command -v yq >/dev/null 2>&1; then
echo "Error: yq is required but not installed." >&2
exit 1
fi
if ! command -v prefect >/dev/null 2>&1; then
echo "Error: prefect is required but not installed." >&2
exit 1
fi
if [ ! -f "$AUTOMATIONS_FILE" ]; then
echo "Error: file not found: $AUTOMATIONS_FILE" >&2
exit 1
fi
count="$(yq -r '.automations | length' "$AUTOMATIONS_FILE")"
if [ "$count" = "0" ]; then
echo "No automations found in $AUTOMATIONS_FILE"
exit 0
fi
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
for i in $(seq 0 $((count - 1))); do
name="$(yq -r ".automations[$i].name" "$AUTOMATIONS_FILE")"
if [ -z "$name" ] || [ "$name" = "null" ]; then
echo "Error: automation at index $i is missing a name" >&2
exit 1
fi
echo "Processing automation: $name"
single_file="$tmpdir/automation-$i.yaml"
yq ".automations[$i]" "$AUTOMATIONS_FILE" > "$single_file"
inspect_file="$tmpdir/inspect-$i.yaml"
if prefect automation inspect "$name" --yaml > "$inspect_file" 2>/dev/null; then
match_count="$(yq -r 'length' "$inspect_file" 2>/dev/null || echo 0)"
case "$match_count" in
0)
echo "Creating new automation: $name"
prefect automation create --from-file "$single_file"
;;
1)
automation_id="$(yq -r '.[0].id // ""' "$inspect_file")"
if [ -z "$automation_id" ]; then
echo "Error: exactly one automation matched '$name', but no id was returned." >&2
echo "Inspect output:" >&2
cat "$inspect_file" >&2
exit 1
fi
echo "Updating existing automation: $name ($automation_id)"
prefect automation update --id "$automation_id" --from-file "$single_file"
;;
*)
echo "Error: multiple automations matched name '$name'; refusing to guess." >&2
echo "Matching automations:" >&2
yq -r '.[] | " - \(.name) [\(.id)]"' "$inspect_file" >&2
exit 1
;;
esac
else
echo "Creating new automation: $name"
prefect automation create --from-file "$single_file"
fi
done
echo "Done."
A couple of important notes:
* this assumes prefect automation inspect "$name" --yaml returns a YAML list for name-based lookup
* the id is taken from Prefect's returned automation definition, not from your source YAML
* your source automations.yaml should not contain id
Example source file:
yaml
automations:
- name: notify-on-failure
description: Notify when a flow run fails
enabled: true
trigger:
type: event
posture: Reactive
expect:
- prefect.flow-run.Failed
threshold: 1
actions:
- type: do-nothing
If you want, I can make this even more defensive by handling both possible YAML shapes from `inspect`:
* a list of matches
* or a single YAML object
That would make it safer across CLI output variations.Russell Brooks
05/11/2026, 10:33 AMMarvin
05/11/2026, 10:33 AMMarvin
05/11/2026, 10:33 AMyaml
block_document_id: *block_document_id
but not the corresponding anchor definition from the top-level definitions, so once each automation is split into its own temporary file, that alias becomes undefined.
The fix is to resolve/inline the alias value when writing each single automation file.
Here’s the updated upsert_automations.sh that keeps your top-level definitions.block_document_id and substitutes that concrete value into each extracted automation before calling prefect automation create/update.
bash
#!/usr/bin/env bash
set -euo pipefail
AUTOMATIONS_FILE="${1:-automations.yaml}"
if ! command -v yq >/dev/null 2>&1; then
echo "Error: yq is required but not installed." >&2
exit 1
fi
if ! command -v prefect >/dev/null 2>&1; then
echo "Error: prefect is required but not installed." >&2
exit 1
fi
if [ ! -f "$AUTOMATIONS_FILE" ]; then
echo "Error: file not found: $AUTOMATIONS_FILE" >&2
exit 1
fi
count="$(yq -r '.automations | length' "$AUTOMATIONS_FILE")"
if [ "$count" = "0" ]; then
echo "No automations found in $AUTOMATIONS_FILE"
exit 0
fi
shared_block_document_id="$(yq -r '.definitions.block_document_id // ""' "$AUTOMATIONS_FILE")"
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
for i in $(seq 0 $((count - 1))); do
name="$(yq -r ".automations[$i].name" "$AUTOMATIONS_FILE")"
if [ -z "$name" ] || [ "$name" = "null" ]; then
echo "Error: automation at index $i is missing a name" >&2
exit 1
fi
echo "Processing automation: $name"
single_file="$tmpdir/automation-$i.yaml"
if [ -n "$shared_block_document_id" ]; then
yq "
.automations[$i]
| walk(
if type == \"!!map\" and has(\"block_document_id\") and .block_document_id == \"*block_document_id\"
then .block_document_id = \"$shared_block_document_id\"
else .
end
)
" "$AUTOMATIONS_FILE" > "$single_file"
else
yq ".automations[$i]" "$AUTOMATIONS_FILE" > "$single_file"
fi
inspect_file="$tmpdir/inspect-$i.yaml"
if prefect automation inspect "$name" --yaml > "$inspect_file" 2>/dev/null; then
match_count="$(yq -r 'length' "$inspect_file" 2>/dev/null || echo 0)"
case "$match_count" in
0)
echo "Creating new automation: $name"
prefect automation create --from-file "$single_file"
;;
1)
automation_id="$(yq -r '.[0].id // ""' "$inspect_file")"
if [ -z "$automation_id" ]; then
echo "Error: exactly one automation matched '$name', but no id was returned." >&2
echo "Inspect output:" >&2
cat "$inspect_file" >&2
exit 1
fi
echo "Updating existing automation: $name ($automation_id)"
prefect automation update --id "$automation_id" --from-file "$single_file"
;;
*)
echo "Error: multiple automations matched name '$name'; refusing to guess." >&2
echo "Matching automations:" >&2
yq -r '.[] | " - \(.name) [\(.id)]"' "$inspect_file" >&2
exit 1
;;
esac
else
echo "Creating new automation: $name"
prefect automation create --from-file "$single_file"
fi
done
echo "Done."
With this expected input shape:
yaml
definitions:
block_document_id: &block_document_id 11111111-2222-3333-4444-555555555555
automations:
- name: notify-on-failure
description: Notify when a flow run fails
enabled: true
trigger:
type: event
posture: Reactive
expect:
- prefect.flow-run.Failed
threshold: 1
actions:
- type: send-notification
block_document_id: *block_document_id
subject: Flow failed
body: Something failed
What changed:
* the script reads:
bash
.definitions.block_document_idMarvin
05/11/2026, 10:33 AMblock_document_id alias usage with the actual value
* that means each temporary per-automation YAML file is self-contained and no longer depends on the original anchor existing at top level
One caveat:
Depending on your `yq`/YAML parser behavior, aliases may already be resolved before yq sees them, or may not appear literally as "*block_document_id". Since you said the current script errors on the alias, this replacement is probably the right fix for your case.
If you want, I can make this more general so it copies all top-level definitions values into each extracted automation, not just block_document_id. That would be cleaner if you plan to reuse more anchors later.Russell Brooks
05/11/2026, 10:38 AMMarvin
05/11/2026, 10:38 AMMarvin
05/11/2026, 10:39 AMwalk(...) is the problem here, and we can avoid it entirely.
Also, for your use case, we don't actually need to manipulate aliases manually if we extract the automation through yq from the original file in a way that resolves the value. The safer fix is to merge the top-level definition value into any notification action that needs block_document_id without using walk.
Here is a rewritten upsert_automations.sh that should work with yq v4.47.2 and avoids walk completely:
bash
#!/usr/bin/env bash
set -euo pipefail
AUTOMATIONS_FILE="${1:-automations.yaml}"
if ! command -v yq >/dev/null 2>&1; then
echo "Error: yq is required but not installed." >&2
exit 1
fi
if ! command -v prefect >/dev/null 2>&1; then
echo "Error: prefect is required but not installed." >&2
exit 1
fi
if [ ! -f "$AUTOMATIONS_FILE" ]; then
echo "Error: file not found: $AUTOMATIONS_FILE" >&2
exit 1
fi
count="$(yq -r '.automations | length' "$AUTOMATIONS_FILE")"
if [ "$count" = "0" ]; then
echo "No automations found in $AUTOMATIONS_FILE"
exit 0
fi
shared_block_document_id="$(yq -r '.definitions.block_document_id // ""' "$AUTOMATIONS_FILE")"
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
for i in $(seq 0 $((count - 1))); do
name="$(yq -r ".automations[$i].name" "$AUTOMATIONS_FILE")"
if [ -z "$name" ] || [ "$name" = "null" ]; then
echo "Error: automation at index $i is missing a name" >&2
exit 1
fi
echo "Processing automation: $name"
single_file="$tmpdir/automation-$i.yaml"
if [ -n "$shared_block_document_id" ]; then
yq "
.automations[$i]
| .actions |= map(
if .type == \"send-notification\" and ((.block_document_id // \"\") == \"\" or .block_document_id == \"*block_document_id\") then
. + {\"block_document_id\": \"$shared_block_document_id\"}
else
.
end
)
" "$AUTOMATIONS_FILE" > "$single_file"
else
yq ".automations[$i]" "$AUTOMATIONS_FILE" > "$single_file"
fi
inspect_file="$tmpdir/inspect-$i.yaml"
if prefect automation inspect "$name" --yaml > "$inspect_file" 2>/dev/null; then
match_count="$(yq -r 'length' "$inspect_file" 2>/dev/null || echo 0)"
case "$match_count" in
0)
echo "Creating new automation: $name"
prefect automation create --from-file "$single_file"
;;
1)
automation_id="$(yq -r '.[0].id // ""' "$inspect_file")"
if [ -z "$automation_id" ]; then
echo "Error: exactly one automation matched '$name', but no id was returned." >&2
echo "Inspect output:" >&2
cat "$inspect_file" >&2
exit 1
fi
echo "Updating existing automation: $name ($automation_id)"
prefect automation update --id "$automation_id" --from-file "$single_file"
;;
*)
echo "Error: multiple automations matched name '$name'; refusing to guess." >&2
echo "Matching automations:" >&2
yq -r '.[] | " - \(.name) [\(.id)]"' "$inspect_file" >&2
exit 1
;;
esac
else
echo "Creating new automation: $name"
prefect automation create --from-file "$single_file"
fi
done
echo "Done."
This version does the alias workaround by:
* reading the shared value from:
bash
.definitions.block_document_id
* when writing each single automation file, it updates .actions
* for any action with:
yaml
type: send-notification
it injects block_document_id if missing or if it looks like the unresolved alias case
So your source file can still look like:
```yaml
definitions:
block_document_id: &block_document_id 11111111-2222-3333-4444-555555555555
automations:
- name: notify-on-failure
enabled: true
trigger:
type: event
posture: Reactive
expect:
- prefect.flow-run.Failed
threshold: 1
actions:
- type: send-notification
block_document_id: *block_document_idMarvin
05/11/2026, 10:39 AMyq already resolves the alias before this mapping runs, that’s fine — the condition still works because it only fills block_document_id when it is empty or matches the unresolved alias string pattern.
If you want, I can make this one step cleaner and more future-proof by rewriting it so that it applies all shared values from definitions into extracted automations, not just block_document_id.Russell Brooks
05/11/2026, 10:41 AMMarvin
05/11/2026, 10:41 AMMarvin
05/11/2026, 10:41 AMyq expression inside Bash. Let's avoid that completely.
Here is a cleaner version of upsert_automations.sh that:
* uses only YAML + yq
* does not use walk
* does not use a multiline if ... then ... else expression inside a shell string
* resolves definitions.block_document_id into notification actions in a simpler, yq v4.47.2-friendly way
bash
#!/usr/bin/env bash
set -euo pipefail
AUTOMATIONS_FILE="${1:-automations.yaml}"
if ! command -v yq >/dev/null 2>&1; then
echo "Error: yq is required but not installed." >&2
exit 1
fi
if ! command -v prefect >/dev/null 2>&1; then
echo "Error: prefect is required but not installed." >&2
exit 1
fi
if [ ! -f "$AUTOMATIONS_FILE" ]; then
echo "Error: file not found: $AUTOMATIONS_FILE" >&2
exit 1
fi
count="$(yq -r '.automations | length' "$AUTOMATIONS_FILE")"
if [ "$count" = "0" ]; then
echo "No automations found in $AUTOMATIONS_FILE"
exit 0
fi
shared_block_document_id="$(yq -r '.definitions.block_document_id // ""' "$AUTOMATIONS_FILE")"
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
for i in $(seq 0 $((count - 1))); do
name="$(yq -r ".automations[$i].name" "$AUTOMATIONS_FILE")"
if [ -z "$name" ] || [ "$name" = "null" ]; then
echo "Error: automation at index $i is missing a name" >&2
exit 1
fi
echo "Processing automation: $name"
single_file="$tmpdir/automation-$i.yaml"
yq ".automations[$i]" "$AUTOMATIONS_FILE" > "$single_file"
if [ -n "$shared_block_document_id" ]; then
yq -i '(.actions[] | select(.type == "send-notification") | .block_document_id) = "'"$shared_block_document_id"'"' "$single_file"
fi
inspect_file="$tmpdir/inspect-$i.yaml"
if prefect automation inspect "$name" --yaml > "$inspect_file" 2>/dev/null; then
match_count="$(yq -r 'length' "$inspect_file" 2>/dev/null || echo 0)"
case "$match_count" in
0)
echo "Creating new automation: $name"
prefect automation create --from-file "$single_file"
;;
1)
automation_id="$(yq -r '.[0].id // ""' "$inspect_file")"
if [ -z "$automation_id" ]; then
echo "Error: exactly one automation matched '$name', but no id was returned." >&2
echo "Inspect output:" >&2
cat "$inspect_file" >&2
exit 1
fi
echo "Updating existing automation: $name ($automation_id)"
prefect automation update --id "$automation_id" --from-file "$single_file"
;;
*)
echo "Error: multiple automations matched name '$name'; refusing to guess." >&2
echo "Matching automations:" >&2
yq -r '.[] | " - \(.name) [\(.id)]"' "$inspect_file" >&2
exit 1
;;
esac
else
echo "Creating new automation: $name"
prefect automation create --from-file "$single_file"
fi
done
echo "Done."
Why this should work better:
* first extract the automation as-is:
bash
yq ".automations[$i]" "$AUTOMATIONS_FILE" > "$single_file"
* then patch the extracted YAML in-place:
bash
yq -i '(.actions[] | select(.type == "send-notification") | .block_document_id) = ...'
* this avoids the complex inline expression that was causing the lexer/parser error
This assumes your notification actions live under top-level actions, which matches Prefect automation definitions like:
yaml
automations:
- name: notify-on-failure
actions:
- type: send-notification
block_document_id: *block_document_id
subject: Flow failed
If some automations may have no actions, or you want this to be extra defensive, I can give you one final hardened version that safely handles:
* missing actions
* empty actions
* no definitions.block_document_id
* multiple notification actions per automationRussell Brooks
05/11/2026, 10:52 AMMarvin
05/11/2026, 10:53 AMMarvin
05/11/2026, 10:53 AMbash
yq ".automations[$i]" "$AUTOMATIONS_FILE" > "$single_file"
still tries to parse/extract a node that contains *block_document_id, but without carrying the anchor definition along in the extracted fragment. So the alias breaks at extraction time.
The fix is to stop extracting the aliased node directly and instead rebuild the automation while substituting the top-level value during extraction.
Here is the corrected upsert_automations.sh, still using only YAML + yq:
bash
#!/usr/bin/env bash
set -euo pipefail
AUTOMATIONS_FILE="${1:-automations.yaml}"
if ! command -v yq >/dev/null 2>&1; then
echo "Error: yq is required but not installed." >&2
exit 1
fi
if ! command -v prefect >/dev/null 2>&1; then
echo "Error: prefect is required but not installed." >&2
exit 1
fi
if [ ! -f "$AUTOMATIONS_FILE" ]; then
echo "Error: file not found: $AUTOMATIONS_FILE" >&2
exit 1
fi
count="$(yq -r '.automations | length' "$AUTOMATIONS_FILE")"
if [ "$count" = "0" ]; then
echo "No automations found in $AUTOMATIONS_FILE"
exit 0
fi
shared_block_document_id="$(yq -r '.definitions.block_document_id // ""' "$AUTOMATIONS_FILE")"
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
for i in $(seq 0 $((count - 1))); do
name="$(yq -r ".automations[$i].name" "$AUTOMATIONS_FILE")"
if [ -z "$name" ] || [ "$name" = "null" ]; then
echo "Error: automation at index $i is missing a name" >&2
exit 1
fi
echo "Processing automation: $name"
single_file="$tmpdir/automation-$i.yaml"
{
yq -n \
--arg name "$(yq -r ".automations[$i].name" "$AUTOMATIONS_FILE")" \
--arg description "$(yq -r ".automations[$i].description // \"\"" "$AUTOMATIONS_FILE")" \
--argjson enabled "$(yq ".automations[$i].enabled // true" "$AUTOMATIONS_FILE")" \
--arg block_document_id "$shared_block_document_id" \
'
.name = $name |
.description = $description |
.enabled = $enabled |
.trigger = load("'"$AUTOMATIONS_FILE"'").automations['"$i"'].trigger |
.actions = (
load("'"$AUTOMATIONS_FILE"'").automations['"$i"'].actions
| map(
if .type == "send-notification" then
.block_document_id = (
if (.block_document_id == null or .block_document_id == "") then
$block_document_id
else
.block_document_id
end
)
else
.
end
)
)
'
} > "$single_file"
inspect_file="$tmpdir/inspect-$i.yaml"
if prefect automation inspect "$name" --yaml > "$inspect_file" 2>/dev/null; then
match_count="$(yq -r 'length' "$inspect_file" 2>/dev/null || echo 0)"
case "$match_count" in
0)
echo "Creating new automation: $name"
prefect automation create --from-file "$single_file"
;;
1)
automation_id="$(yq -r '.[0].id // ""' "$inspect_file")"
if [ -z "$automation_id" ]; then
echo "Error: exactly one automation matched '$name', but no id was returned." >&2
echo "Inspect output:" >&2
cat "$inspect_file" >&2
exit 1
fi
echo "Updating existing automation: $name ($automation_id)"
prefect automation update --id "$automation_id" --from-file "$single_file"
;;
*)
echo "Error: multiple automations matched name '$name'; refusing to guess." >&2
echo "Matching automations:" >&2
yq -r '.[] | " - \(.name) [\(.id)]"' "$inspect_file" >&2
exit 1
;;
esac
else
echo "Creating new automation: $name"
prefect automation create --from-file "$single_file"
fi
done
echo "Done."
What this changes:
* it does not dump .automations[$i] directly anymore
* instead it constructs a fresh per-automation YAML document
* it pulls in:
* nameMarvin
05/11/2026, 10:53 AMdescription
* enabled
* trigger
* actions
* and for send-notification actions it fills in block_document_id from:
yaml
definitions:
block_document_id: ...
So your source can remain:
yaml
definitions:
block_document_id: &block_document_id 11111111-2222-3333-4444-555555555555
automations:
- name: notify-on-failure
description: Notify when a flow run fails
enabled: true
trigger:
type: event
posture: Reactive
expect:
- prefect.flow-run.Failed
threshold: 1
actions:
- type: send-notification
block_document_id: *block_document_id
subject: Flow failed
body: Something failed
One important caveat:
This version assumes your automations use the standard top-level fields:
* name
* description
* enabled
* trigger
* actions
If your automations also include fields like:
* owner_resource
* actions_on_trigger
* actions_on_resolve
* tags
then I should expand the script to preserve those too. If you want, I can do that next and make it preserve all supported automation fields, still without using JSON.Russell Brooks
05/11/2026, 10:56 AMMarvin
05/11/2026, 10:56 AMMarvin
05/11/2026, 10:56 AMyq v4.47.2 does not support --arg like jq, and I should not have used it. Sorry.
The simplest compatible fix is to stop trying to reconstruct YAML inside yq with injected shell args, and instead let yq fully resolve anchors/aliases at read time by using the explode operator.
For Mike Farah yq v4, that is explode(.).
So the robust fix is:
* resolve anchors from the full source document first
* then extract each automation from that fully expanded document
* then continue with the existing create/update logic
Here is the corrected upsert_automations.sh for `yq v4.47.2`:
bash
#!/usr/bin/env bash
set -euo pipefail
AUTOMATIONS_FILE="${1:-automations.yaml}"
if ! command -v yq >/dev/null 2>&1; then
echo "Error: yq is required but not installed." >&2
exit 1
fi
if ! command -v prefect >/dev/null 2>&1; then
echo "Error: prefect is required but not installed." >&2
exit 1
fi
if [ ! -f "$AUTOMATIONS_FILE" ]; then
echo "Error: file not found: $AUTOMATIONS_FILE" >&2
exit 1
fi
count="$(yq -r '.automations | length' "$AUTOMATIONS_FILE")"
if [ "$count" = "0" ]; then
echo "No automations found in $AUTOMATIONS_FILE"
exit 0
fi
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
expanded_file="$tmpdir/automations.expanded.yaml"
yq 'explode(.)' "$AUTOMATIONS_FILE" > "$expanded_file"
for i in $(seq 0 $((count - 1))); do
name="$(yq -r ".automations[$i].name" "$expanded_file")"
if [ -z "$name" ] || [ "$name" = "null" ]; then
echo "Error: automation at index $i is missing a name" >&2
exit 1
fi
echo "Processing automation: $name"
single_file="$tmpdir/automation-$i.yaml"
yq ".automations[$i]" "$expanded_file" > "$single_file"
inspect_file="$tmpdir/inspect-$i.yaml"
if prefect automation inspect "$name" --yaml > "$inspect_file" 2>/dev/null; then
match_count="$(yq -r 'length' "$inspect_file" 2>/dev/null || echo 0)"
case "$match_count" in
0)
echo "Creating new automation: $name"
prefect automation create --from-file "$single_file"
;;
1)
automation_id="$(yq -r '.[0].id // ""' "$inspect_file")"
if [ -z "$automation_id" ]; then
echo "Error: exactly one automation matched '$name', but no id was returned." >&2
echo "Inspect output:" >&2
cat "$inspect_file" >&2
exit 1
fi
echo "Updating existing automation: $name ($automation_id)"
prefect automation update --id "$automation_id" --from-file "$single_file"
;;
*)
echo "Error: multiple automations matched name '$name'; refusing to guess." >&2
echo "Matching automations:" >&2
yq -r '.[] | " - \(.name) [\(.id)]"' "$inspect_file" >&2
exit 1
;;
esac
else
echo "Creating new automation: $name"
prefect automation create --from-file "$single_file"
fi
done
echo "Done."
Why this works better for your case:
* explode(.) expands YAML anchors and aliases up front
* so this:
yaml
definitions:
block_document_id: &block_document_id 11111111-2222-3333-4444-555555555555
and this:
yaml
block_document_id: *block_document_id
become a normal concrete scalar value in the expanded file
* then extracting .automations[$i] no longer fails on unknown anchors
So your original automations.yaml can stay exactly as written with aliases.
If you want, I can also give you a slightly hardened variant that first checks whether explode(.) is supported in the installed yq and prints a more helpful error if not.Russell Brooks
05/11/2026, 11:30 AMMarvin
05/11/2026, 11:30 AMMarvin
05/11/2026, 11:30 AMyq in CI is not Mike Farah yq v4 behaving the way we expected, even if locally you have v4.47.2. In CI, you're almost certainly hitting a different yq implementation/wrapper, which is why explode(.) is being handed through to jq.
So the right move is to avoid anchor/alias handling inside yq entirely.
Since your specific requirement is just:
* top-level
yaml
definitions:
block_document_id: &block_document_id ...
* reused in automations as
yaml
block_document_id: *block_document_id
we can make the script robust by:
1. reading the concrete shared value from definitions.block_document_id
2. creating each per-automation YAML by textually replacing the alias token
3. never asking yq to resolve YAML anchors
This keeps the workflow YAML-first and yq-based, but avoids parser-specific alias expansion behavior.
Here is the rewritten `upsert_automations.sh`:
bash
#!/usr/bin/env bash
set -euo pipefail
AUTOMATIONS_FILE="${1:-automations.yaml}"
if ! command -v yq >/dev/null 2>&1; then
echo "Error: yq is required but not installed." >&2
exit 1
fi
if ! command -v prefect >/dev/null 2>&1; then
echo "Error: prefect is required but not installed." >&2
exit 1
fi
if [ ! -f "$AUTOMATIONS_FILE" ]; then
echo "Error: file not found: $AUTOMATIONS_FILE" >&2
exit 1
fi
count="$(yq -r '.automations | length' "$AUTOMATIONS_FILE")"
if [ "$count" = "0" ]; then
echo "No automations found in $AUTOMATIONS_FILE"
exit 0
fi
shared_block_document_id="$(yq -r '.definitions.block_document_id // ""' "$AUTOMATIONS_FILE")"
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
for i in $(seq 0 $((count - 1))); do
name="$(yq -r ".automations[$i].name" "$AUTOMATIONS_FILE")"
if [ -z "$name" ] || [ "$name" = "null" ]; then
echo "Error: automation at index $i is missing a name" >&2
exit 1
fi
echo "Processing automation: $name"
single_file="$tmpdir/automation-$i.yaml"
yq ".automations[$i]" "$AUTOMATIONS_FILE" > "$single_file"
if [ -n "$shared_block_document_id" ]; then
sed "s|\\*block_document_id|$shared_block_document_id|g" "$single_file" > "$single_file.tmp"
mv "$single_file.tmp" "$single_file"
fi
inspect_file="$tmpdir/inspect-$i.yaml"
if prefect automation inspect "$name" --yaml > "$inspect_file" 2>/dev/null; then
match_count="$(yq -r 'length' "$inspect_file" 2>/dev/null || echo 0)"
case "$match_count" in
0)
echo "Creating new automation: $name"
prefect automation create --from-file "$single_file"
;;
1)
automation_id="$(yq -r '.[0].id // ""' "$inspect_file")"
if [ -z "$automation_id" ]; then
echo "Error: exactly one automation matched '$name', but no id was returned." >&2
echo "Inspect output:" >&2
cat "$inspect_file" >&2
exit 1
fi
echo "Updating existing automation: $name ($automation_id)"
prefect automation update --id "$automation_id" --from-file "$single_file"
;;
*)
echo "Error: multiple automations matched name '$name'; refusing to guess." >&2
echo "Matching automations:" >&2
yq -r '.[] | " - \(.name) [\(.id)]"' "$inspect_file" >&2
exit 1
;;
esac
else
echo "Creating new automation: $name"
prefect automation create --from-file "$single_file"
fi
done
echo "Done."
Important caveat:
This line still depends on your yq being able to extract the per-automation YAML fragment at all:
bash
yq ".automations[$i]" "$AUTOMATIONS_FILE" > "$single_file"
If that extraction itself fails in CI because the alias is unresolved during parse, then the only reliable fix is to stop using YAML anchors for this field in the source file, or switch to a preprocessing approach before yq reads the document.Marvin
05/11/2026, 11:30 AMyaml
*block_document_id
with the concrete value from definitions.block_document_id, and then run all yq queries against that preprocessed file.
That version would avoid alias parsing issues entirely. If you want, I can write that next — and that is probably the best final answer for mixed local/CI environments.Russell Brooks
05/11/2026, 11:32 AMMarvin
05/11/2026, 11:32 AMMarvin
05/11/2026, 11:32 AMyaml
definitions:
block_document_id: &block_document_id ...
2. create a preprocessed copy of automations.yaml
3. replace every literal *block_document_id in that copy with the actual value
4. run all subsequent yq operations against the preprocessed file
That avoids depending on YAML anchor support in whichever yq happens to be installed in CI.
Here is the rewritten `upsert_automations.sh`:
bash
#!/usr/bin/env bash
set -euo pipefail
AUTOMATIONS_FILE="${1:-automations.yaml}"
if ! command -v yq >/dev/null 2>&1; then
echo "Error: yq is required but not installed." >&2
exit 1
fi
if ! command -v prefect >/dev/null 2>&1; then
echo "Error: prefect is required but not installed." >&2
exit 1
fi
if [ ! -f "$AUTOMATIONS_FILE" ]; then
echo "Error: file not found: $AUTOMATIONS_FILE" >&2
exit 1
fi
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
preprocessed_file="$tmpdir/automations.preprocessed.yaml"
shared_block_document_id="$(yq -r '.definitions.block_document_id // ""' "$AUTOMATIONS_FILE")"
cp "$AUTOMATIONS_FILE" "$preprocessed_file"
if [ -n "$shared_block_document_id" ]; then
sed "s|\\*block_document_id|$shared_block_document_id|g" "$preprocessed_file" > "$preprocessed_file.tmp"
mv "$preprocessed_file.tmp" "$preprocessed_file"
fi
count="$(yq -r '.automations | length' "$preprocessed_file")"
if [ "$count" = "0" ]; then
echo "No automations found in $preprocessed_file"
exit 0
fi
for i in $(seq 0 $((count - 1))); do
name="$(yq -r ".automations[$i].name" "$preprocessed_file")"
if [ -z "$name" ] || [ "$name" = "null" ]; then
echo "Error: automation at index $i is missing a name" >&2
exit 1
fi
echo "Processing automation: $name"
single_file="$tmpdir/automation-$i.yaml"
yq ".automations[$i]" "$preprocessed_file" > "$single_file"
inspect_file="$tmpdir/inspect-$i.yaml"
if prefect automation inspect "$name" --yaml > "$inspect_file" 2>/dev/null; then
match_count="$(yq -r 'length' "$inspect_file" 2>/dev/null || echo 0)"
case "$match_count" in
0)
echo "Creating new automation: $name"
prefect automation create --from-file "$single_file"
;;
1)
automation_id="$(yq -r '.[0].id // ""' "$inspect_file")"
if [ -z "$automation_id" ]; then
echo "Error: exactly one automation matched '$name', but no id was returned." >&2
echo "Inspect output:" >&2
cat "$inspect_file" >&2
exit 1
fi
echo "Updating existing automation: $name ($automation_id)"
prefect automation update --id "$automation_id" --from-file "$single_file"
;;
*)
echo "Error: multiple automations matched name '$name'; refusing to guess." >&2
echo "Matching automations:" >&2
yq -r '.[] | " - \(.name) [\(.id)]"' "$inspect_file" >&2
exit 1
;;
esac
else
echo "Creating new automation: $name"
prefect automation create --from-file "$single_file"
fi
done
echo "Done."
Why this is the safest version for CI:
* no reliance on YAML anchor expansion support
* no explode(.)
* no walk(...)
* no --arg
* no JSON
* only yq, sed, bash, and prefect
Expected input still looks like:
yaml
definitions:
block_document_id: &block_document_id 11111111-2222-3333-4444-555555555555
automations:
- name: notify-on-failure
description: Notify when a flow run fails
enabled: true
trigger:
type: event
posture: Reactive
expect:
- prefect.flow-run.Failed
threshold: 1
actions:
- type: send-notification
block_document_id: *block_document_id
subject: Flow failed
body: Something failed
One caution:Marvin
05/11/2026, 11:32 AMsed replacement, the script may need escaping. For UUID-style values, this version is fine.
If you want, I can do one final refinement and make the sed replacement fully escaped/safe even for unusual values.Russell Brooks
05/11/2026, 11:43 AMMarvin
05/11/2026, 11:43 AMMarvin
05/11/2026, 11:43 AMbash
source scripts/upsert_automations.sh automation.yaml
then the script should resolve the YAML path relative to the script location, not the current working directory.
Also, since you're sourcing it, the script should avoid exit where possible, because exit would terminate the parent shell. Using return when sourced is safer.
Here is the rewritten version that:
* supports a relative path argument like automation.yaml
* resolves that path relative to the directory containing upsert_automations.sh
* still works if you pass an absolute path
* safely handles being sourced
bash
#!/usr/bin/env bash
set -euo pipefail
if [[ "${BASH_SOURCE[0]}" != "${0}" ]]; then
_UPSERT_AUTOMATIONS_SOURCED=1
else
_UPSERT_AUTOMATIONS_SOURCED=0
fi
_die() {
echo "Error: $*" >&2
if [ "$_UPSERT_AUTOMATIONS_SOURCED" -eq 1 ]; then
return 1
else
exit 1
fi
}
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
AUTOMATIONS_ARG="${1:-automations.yaml}"
case "$AUTOMATIONS_ARG" in
/*)
AUTOMATIONS_FILE="$AUTOMATIONS_ARG"
;;
*)
if [ -f "$AUTOMATIONS_ARG" ]; then
AUTOMATIONS_FILE="$(cd "$(dirname "$AUTOMATIONS_ARG")" && pwd)/$(basename "$AUTOMATIONS_ARG")"
else
AUTOMATIONS_FILE="$SCRIPT_DIR/../$AUTOMATIONS_ARG"
fi
;;
esac
if ! command -v yq >/dev/null 2>&1; then
_die "yq is required but not installed." || return 1
fi
if ! command -v prefect >/dev/null 2>&1; then
_die "prefect is required but not installed." || return 1
fi
if [ ! -f "$AUTOMATIONS_FILE" ]; then
_die "file not found: $AUTOMATIONS_FILE" || return 1
fi
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' RETURN
preprocessed_file="$tmpdir/automations.preprocessed.yaml"
shared_block_document_id="$(yq -r '.definitions.block_document_id // ""' "$AUTOMATIONS_FILE")"
cp "$AUTOMATIONS_FILE" "$preprocessed_file"
if [ -n "$shared_block_document_id" ]; then
sed "s|\\*block_document_id|$shared_block_document_id|g" "$preprocessed_file" > "$preprocessed_file.tmp"
mv "$preprocessed_file.tmp" "$preprocessed_file"
fi
count="$(yq -r '.automations | length' "$preprocessed_file")"
if [ "$count" = "0" ]; then
echo "No automations found in $AUTOMATIONS_FILE"
return 0 2>/dev/null || exit 0
fi
for i in $(seq 0 $((count - 1))); do
name="$(yq -r ".automations[$i].name" "$preprocessed_file")"
if [ -z "$name" ] || [ "$name" = "null" ]; then
_die "automation at index $i is missing a name" || return 1
fi
echo "Processing automation: $name"
single_file="$tmpdir/automation-$i.yaml"
yq ".automations[$i]" "$preprocessed_file" > "$single_file"
inspect_file="$tmpdir/inspect-$i.yaml"
if prefect automation inspect "$name" --yaml > "$inspect_file" 2>/dev/null; then
match_count="$(yq -r 'length' "$inspect_file" 2>/dev/null || echo 0)"
case "$match_count" in
0)
echo "Creating new automation: $name"
prefect automation create --from-file "$single_file"
;;
1)
automation_id="$(yq -r '.[0].id // ""' "$inspect_file")"
if [ -z "$automation_id" ]; then
echo "Error: exactly one automation matched '$name', but no id was returned." >&2
echo "Inspect output:" >&2
cat "$inspect_file" >&2
return 1 2>/dev/null || exit 1
fi
echo "Updating existing automation: $name ($automation_id)"
prefect automation update --id "$automation_id" --from-file "$single_file"
;;
*)
echo "Error: multiple automations matched name '$name'; refusing to guess." >&2
echo "Matching automations:" >&2
yq -r '.[] | " - \(.name) [\(.id)]"' "$inspect_file" >&2
return 1 2>/dev/null || exit 1
;;
esac
else
echo "Creating new automation: $name"
prefect automation create --from-file "$single_file"
fi
done
echo "Done."
return 0 2>/dev/null || exit 0
What changed:
* resolves the script directory with:
```bashMarvin
05/11/2026, 11:43 AM* if you pass a relative file like `automation.yaml`, it first checks the current directory
* if not found there, it falls back to:
```bash
"$SCRIPT_DIR/../$AUTOMATIONS_ARG"
which matches your layout where YAML is one directory above scripts/
* _die() uses return if sourced, exit otherwise
So with this structure:
text
project/
automations.yaml
scripts/
upsert_automations.sh
this works:
bash
source scripts/upsert_automations.sh automations.yaml
and so does:
bash
bash scripts/upsert_automations.sh automations.yaml
One small note: you wrote automation.yaml singular in your example, while earlier we've used automations.yaml. Either is fine as long as the file exists.
If you want, I can do one final cleanup pass to make this shorter and a bit less defensive now that we've converged on the requirements.Russell Brooks
05/11/2026, 11:46 AMMarvin
05/11/2026, 11:46 AMMarvin
05/11/2026, 11:46 AMbash
source scripts/upsert_automations.sh automations.yaml
This version resolves the YAML path relative to the calling location first, and if not found, relative to the script directory's parent.
bash
#!/usr/bin/env bash
set -euo pipefail
AUTOMATIONS_ARG="${1:-automations.yaml}"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
if [[ "$AUTOMATIONS_ARG" = /* ]]; then
AUTOMATIONS_FILE="$AUTOMATIONS_ARG"
elif [[ -f "$AUTOMATIONS_ARG" ]]; then
AUTOMATIONS_FILE="$(pwd)/$AUTOMATIONS_ARG"
elif [[ -f "$SCRIPT_DIR/../$AUTOMATIONS_ARG" ]]; then
AUTOMATIONS_FILE="$SCRIPT_DIR/../$AUTOMATIONS_ARG"
else
echo "Error: file not found: $AUTOMATIONS_ARG" >&2
return 1 2>/dev/null || exit 1
fi
if ! command -v yq >/dev/null 2>&1; then
echo "Error: yq is required but not installed." >&2
return 1 2>/dev/null || exit 1
fi
if ! command -v prefect >/dev/null 2>&1; then
echo "Error: prefect is required but not installed." >&2
return 1 2>/dev/null || exit 1
fi
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
preprocessed_file="$tmpdir/automations.preprocessed.yaml"
shared_block_document_id="$(yq -r '.definitions.block_document_id // ""' "$AUTOMATIONS_FILE")"
cp "$AUTOMATIONS_FILE" "$preprocessed_file"
if [ -n "$shared_block_document_id" ]; then
sed "s|\\*block_document_id|$shared_block_document_id|g" "$preprocessed_file" > "$preprocessed_file.tmp"
mv "$preprocessed_file.tmp" "$preprocessed_file"
fi
count="$(yq -r '.automations | length' "$preprocessed_file")"
if [ "$count" = "0" ]; then
echo "No automations found in $AUTOMATIONS_FILE"
return 0 2>/dev/null || exit 0
fi
for i in $(seq 0 $((count - 1))); do
name="$(yq -r ".automations[$i].name" "$preprocessed_file")"
if [ -z "$name" ] || [ "$name" = "null" ]; then
echo "Error: automation at index $i is missing a name" >&2
return 1 2>/dev/null || exit 1
fi
echo "Processing automation: $name"
single_file="$tmpdir/automation-$i.yaml"
yq ".automations[$i]" "$preprocessed_file" > "$single_file"
inspect_file="$tmpdir/inspect-$i.yaml"
if prefect automation inspect "$name" --yaml > "$inspect_file" 2>/dev/null; then
match_count="$(yq -r 'length' "$inspect_file" 2>/dev/null || echo 0)"
case "$match_count" in
0)
echo "Creating new automation: $name"
prefect automation create --from-file "$single_file"
;;
1)
automation_id="$(yq -r '.[0].id // ""' "$inspect_file")"
if [ -z "$automation_id" ]; then
echo "Error: exactly one automation matched '$name', but no id was returned." >&2
cat "$inspect_file" >&2
return 1 2>/dev/null || exit 1
fi
echo "Updating existing automation: $name ($automation_id)"
prefect automation update --id "$automation_id" --from-file "$single_file"
;;
*)
echo "Error: multiple automations matched name '$name'; refusing to guess." >&2
yq -r '.[] | " - \(.name) [\(.id)]"' "$inspect_file" >&2
return 1 2>/dev/null || exit 1
;;
esac
else
echo "Creating new automation: $name"
prefect automation create --from-file "$single_file"
fi
done
echo "Done."
return 0 2>/dev/null || exit 0
This should work with a layout like:
text
repo/
automations.yaml
scripts/
upsert_automations.sh
and invoked as:
bash
source scripts/upsert_automations.sh automations.yaml
or:
bash
bash scripts/upsert_automations.sh automations.yaml
If you want, I can also make it only resolve relative to the repo root / parent of scripts, instead of trying both locations.Russell Brooks
05/11/2026, 12:18 PMMarvin
05/11/2026, 12:18 PMMarvin
05/11/2026, 12:18 PMsource, [[ ... ]], and BASH_SOURCE was not POSIX. Sorry.
If you want this to be POSIX sh compatible, then the script should:
* use #!/bin/sh
* use . instead of source
* avoid [[ ... ]]
* avoid Bash arrays and Bash-only variables like BASH_SOURCE
* use only portable shell syntax
There is one important limitation though:
when a script is sourced in POSIX shell, there is no portable way for the script itself to know its own file path
So if you require:
sh
. scripts/upsert_automations.sh automations.yaml
and want automations.yaml resolved relative to the script location, that is not portable POSIX shell behavior.
What is portable is one of these:
* run the script, not source it:
sh
sh scripts/upsert_automations.sh automations.yaml
* or pass an explicit path:
sh
. scripts/upsert_automations.sh ../automations.yaml
Given that, here is the clean POSIX-compatible version that works when executed:
sh
#!/bin/sh
set -eu
AUTOMATIONS_ARG=${1:-automations.yaml}
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
case "$AUTOMATIONS_ARG" in
/*)
AUTOMATIONS_FILE=$AUTOMATIONS_ARG
;;
*)
if [ -f "$AUTOMATIONS_ARG" ]; then
AUTOMATIONS_FILE=$(pwd)/$AUTOMATIONS_ARG
elif [ -f "$SCRIPT_DIR/../$AUTOMATIONS_ARG" ]; then
AUTOMATIONS_FILE=$SCRIPT_DIR/../$AUTOMATIONS_ARG
else
echo "Error: file not found: $AUTOMATIONS_ARG" >&2
exit 1
fi
;;
esac
if ! command -v yq >/dev/null 2>&1; then
echo "Error: yq is required but not installed." >&2
exit 1
fi
if ! command -v prefect >/dev/null 2>&1; then
echo "Error: prefect is required but not installed." >&2
exit 1
fi
tmpdir=$(mktemp -d)
trap 'rm -rf "$tmpdir"' EXIT HUP INT TERM
preprocessed_file=$tmpdir/automations.preprocessed.yaml
shared_block_document_id=$(yq -r '.definitions.block_document_id // ""' "$AUTOMATIONS_FILE")
cp "$AUTOMATIONS_FILE" "$preprocessed_file"
if [ -n "$shared_block_document_id" ]; then
sed "s|\\*block_document_id|$shared_block_document_id|g" "$preprocessed_file" > "$preprocessed_file.tmp"
mv "$preprocessed_file.tmp" "$preprocessed_file"
fi
count=$(yq -r '.automations | length' "$preprocessed_file")
if [ "$count" = "0" ]; then
echo "No automations found in $AUTOMATIONS_FILE"
exit 0
fi
i=0
while [ "$i" -lt "$count" ]; do
name=$(yq -r ".automations[$i].name" "$preprocessed_file")
if [ -z "$name" ] || [ "$name" = "null" ]; then
echo "Error: automation at index $i is missing a name" >&2
exit 1
fi
echo "Processing automation: $name"
single_file=$tmpdir/automation-$i.yaml
yq ".automations[$i]" "$preprocessed_file" > "$single_file"
inspect_file=$tmpdir/inspect-$i.yaml
if prefect automation inspect "$name" --yaml > "$inspect_file" 2>/dev/null; then
match_count=$(yq -r 'length' "$inspect_file" 2>/dev/null || echo 0)
case "$match_count" in
0)
echo "Creating new automation: $name"
prefect automation create --from-file "$single_file"
;;
1)
automation_id=$(yq -r '.[0].id // ""' "$inspect_file")
if [ -z "$automation_id" ]; then
echo "Error: exactly one automation matched '$name', but no id was returned." >&2
cat "$inspect_file" >&2
exit 1
fi
echo "Updating existing automation: $name ($automation_id)"
prefect automation update --id "$automation_id" --from-file "$single_file"
;;
*)
echo "Error: multiple automations matched name '$name'; refusing to guess." >&2
yq -r '.[] | " - \(.name) [\(.id)]"' "$inspect_file" >&2
exit 1
;;
esac
else
echo "Creating new automation: $name"
prefect automation create --from-file "$single_file"
fi
i=$((i + 1))
done
echo "Done."
Recommended invocation:
sh
sh scripts/upsert_automations.sh automations.yaml
If you truly need to dot-source it in POSIX shell:
```shMarvin
05/11/2026, 12:18 PM