Russell Brooks
05/13/2026, 6:31 PMMarvin
05/13/2026, 6:31 PMMarvin
05/13/2026, 6:35 PMFailed or Crashed.
3. The remediation flow uses the Prefect API client to gather:
- failure state/message
- logs / stack trace
- deployment metadata
- likely git source info
- previous runs
- task run failures
- artifacts / variables
- relevant block documents and deployment-linked objects where possible
4. That context is sent to an AI agent service exposed via FastMCP.
5. The agent returns:
- diagnosis
- likely root cause
- suggested fix
- confidence / risk
- optional patch proposal
6. The flow posts the result to Slack / email / webhook / artifact.
7. Stretch goals:
- gated branch + commit + PR
- gated sandbox redeploy/test run
Important reality check:
- Stack traces are usually in logs, not neatly stored as a single “stack trace” field.
- Git repo/source is only reliably available if your deployment encodes it in pull steps, image labels, parameters, or metadata.
- Blocks used inside arbitrary flow code cannot be perfectly auto-discovered unless you instrument or declare them.
- Past results are only available if you persist them or materialize them as artifacts / result records you can fetch.
So the best solution is: build a robust baseline that works today, plus add light instrumentation conventions to make the AI dramatically smarter.
---
## Recommended design
### 1) Trigger a deployment on failed flow runs
In Prefect 3.x, this is best done with an automation trigger on prefect.flow-run.Failed / prefect.flow-run.Crashed.
### 2) Pass the failing flow run id into the remediation flow
Your remediation flow should accept a parameter like failed_flow_run_id.
### 3) Query Prefect for rich context
From the remediation flow, use get_client() and retrieve:
- failed flow run
- failed task runs
- logs
- deployment
- artifacts
- related prior runs
- variables
- deployment-linked block documents if present
### 4) Send to FastMCP-backed AI service
Keep the LLM outside the flow process if possible. The flow should call your internal AI/MCP service and pass a compact, sanitized incident bundle.
### 5) Gate any write actions
For branch/commit/PR/test deployment:
- start as advisory only
- then add human approval
- only then allow automated repo or sandbox actions
---
## Strong recommendation on metadata conventions
To satisfy your goals well, I’d strongly recommend adding these conventions to all production deployments:
- repo_url
- repo_branch
- repo_subpath
- entrypoint
- service_name
- environment
- sandbox_deployment_name
- owned_blocks or used_block_slugs
- knowledge_domains
These can live in:
- deployment parameters
- variables
- labels/tags
- pull steps
- image labels
- a sidecar manifest artifact
Without conventions, the AI can only infer some of this.
---
# Example implementation
Below is a realistic starter implementation.
## A. Remediation flow
```python
from future import annotations
import os
import re
from typing import Any
from uuid import UUID
import httpx
from prefect import flow, task, get_run_logger
from prefect.artifacts import create_markdown_artifact
from prefect.blocks.notifications import SlackWebhook
from prefect.client.orchestration import get_client
from prefect.client.schemas.filters import (
FlowRunFilter,
FlowRunFilterId,
LogFilter,
LogFilterFlowRunId,
LogFilterTaskRunId,
TaskRunFilter,
TaskRunFilterFlowRunId,
)
MAX_LOG_LINES = 400
MAX_PREVIOUS_RUNS = 10
def _safe_model_dump(obj: Any) -> dict[str, Any] | None:
if obj is None:
return NoneMarvin
05/13/2026, 6:35 PMMarvin
05/13/2026, 6:35 PMMarvin
05/13/2026, 6:35 PM{failed_run.get("id")}
- Flow run name: {failed_run.get("name")}
- State: {context.get("failed_state", {}).get("name")}
- Deployment id: {failed_run.get("deployment_id")}
## Source
- Repository: {git_info.get("repository")}
- Branch/ref: {git_info.get("branch")}
- Commit: {git_info.get("commit")}
- Entrypoint: {git_info.get("entrypoint")}
## Stack trace
{context.get("stack_trace", "")[:12000]}
## AI diagnosis
{diagnosis}
## Proposed fix
{proposed_fix}
## Risk
{risk}
## Suggested developer notification
{notification_text}
"""
await create_markdown_artifact(
key=f"failure-remediation-{failed_run.get('id')}",
markdown=markdown,
description="AI-assisted Prefect flow failure remediation report",
)
slack_block_name = os.getenv("FAILURE_REMEDIATION_SLACK_BLOCK")
if slack_block_name:
try:
slack = await SlackWebhook.load(slack_block_name)
await slack.notify(
f"*Flow failed*: `{failed_run.get('name')}`\n"
f"*Diagnosis*: {summary[:700]}\n"
f"*Suggested fix*: {proposed_fix[:700]}"
)
except Exception:
pass
@flow(name="ai-failure-remediator")
async def ai_failure_remediator(failed_flow_run_id: str) -> dict[str, Any]:
context = await gather_failure_context(failed_flow_run_id)
context = await enrich_prefect_object_access(context)
ai_result = await call_ai_agent(context)
await publish_report(context, ai_result)
return ai_result
---
## B. FastMCP service sketch
This is the *AI-side* service, separate from the Prefect flow. It can expose tools/resources for:
- repo inspection
- helm chart inspection
- kubernetes manifest analysis
- Prefect deployment/job variable analysis
- previous incident lookup
- git branch creation / PR creation
- sandbox test orchestration
```python
from fastmcp import FastMCP
mcp = FastMCP("prefect-remediation-agent")
@mcp.tool
def diagnose_prefect_failure(context: dict) -> dict:
"""
Diagnose a Prefect/Kubernetes/Python deployment failure and return
structured remediation advice.
"""
# In practice, this tool would orchestrate an LLM + retrieval + policy checks
failed_state = context.get("failed_state", {})
stack_trace = context.get("stack_trace", "")
git_info = context.get("git_info", {})
diagnosis = "Likely Python/runtime/dependency or infrastructure misconfiguration."
proposed_fix = (
"Inspect the failing module referenced in the traceback, confirm deployment "
"image/package parity, validate Prefect worker job variables, and verify "
"Kubernetes/environment configuration."
)
return {
"summary": "Failure analyzed successfully.",
"diagnosis": diagnosis,
"proposed_fix": proposed_fix,
"risk": "medium",
"developer_notification": (
f"The failed run appears related to code or environment drift. "
f"Repo: {git_info.get('repository')}, state: {failed_state.get('name')}."
),
"patch_plan": [
"Reproduce in sandbox",
"Apply minimal code/config patch",
"Run deployment smoke test",
"Open PR with commentary",
],
"confidence": 0.63,
"requires_human_approval": True,
"stack_trace_excerpt": stack_trace[:1000],
}
if __name__ == "__main__":
mcp.run()
---
## C. Deployment trigger pattern
You asked for a deployment that gets triggered by other failed flow runs. The clean pattern is:
- deploy ai_failure_remediatorMarvin
05/13/2026, 6:35 PMprefect.flow-run.Failed and prefect.flow-run.Crashed
- pass the failed run id as a parameter
I’m intentionally not inventing exact automation templating syntax beyond what research supports conceptually; if you want, I can next give you a version-specific, exact automation/deployment snippet for 3.7.0 once you tell me whether you prefer:
- Python SDK automation creation
- UI automation setup
- YAML / prefect.yaml deployment flow
---
# What this does and does not solve
## What it can do well today
### 1) Stack trace of the error
Yes, via logs:
- flow-run logs
- task-run logs
- parse traceback blocks
### 2) Git repo/source for failed flow
Sometimes yes, if source is encoded in:
- deployment pull_steps
- deployment metadata
- image labels
- parameters/variables
Best practice: add repo_url, repo_ref, entrypoint explicitly.
### 3) Results from previous flow runs
Conditionally yes:
- prior run states/timings always
- prior artifacts often
- actual results only if persisted and accessible
Best practice:
- persist important outputs
- write summary artifacts for every run
### 4) Access to Blocks and other Prefect objects used in failed flow run
Partially yes:
- deployment-linked blocks: feasible
- storage/infrastructure blocks: feasible
- arbitrary blocks loaded in user code: not reliably inferable without instrumentation
Best practice:
- have flows declare block usage in an artifact or parameter
- or wrap Block.load(...) via your own helper that logs block references
---
# Best-practice enhancements I strongly recommend
## 1. Add a “diagnostic manifest” artifact on every deployment
Have each deployment or flow expose:
- repo URL
- repo ref
- entrypoint
- expected blocks
- sandbox deployment target
- owning team
- runtime extras
Then your AI won’t have to guess.
## 2. Persist “useful debugging artifacts”
For every flow:
- config snapshot
- normalized parameters
- input sample hashes
- upstream dependency versions
- git SHA if known
- target environment
## 3. Maintain a knowledge base of prior incidents
The AI should be aware of:
- previous remediation reports
- prior run failures
- known cluster incidents
- known Helm misconfigurations
- org-specific policies
## 4. Add approval gates
Before any write action:
- branch creation
- code modification
- PR creation
- sandbox deployment
Use an approval mechanism, not full autonomy by default.
---
# Domain knowledge requirements for the AI agent
Your list is good. I’d extend it to:
- Python
- Prefect 3.x
- Prefect workers / deployments / automations
- prefect-helm
- Kubernetes
- Docker / OCI images
- Git / GitHub / PR workflows
- Prefect extras relevant to your estate:
- prefect[docker]
- prefect[kubernetes]
- prefect[aws]
- prefect[gcp]
- prefect[azure]
- prefect[redis]
- etc.
- Helm values and chart troubleshooting
- Secrets handling and block hygiene
- Observability / logs / metrics / traces
- Dependency management / uv / pip / packaging
- CI/CD
- DB/network/connectivity debugging
- Retry/idempotency semantics
- Publicly available Marvin/Prefect troubleshooting patterns
- Your org’s runbooks and architectural conventions
That last category is crucial. A custom agent becomes dramatically better when it knows your deployment norms.
---
# Stretch goal architecture
## Safe path to auto-fix
I would phase this as:
### Phase 1: advisory
- detect
- collect context
- diagnose
- notify
### Phase 2: patch proposal
- produce diff
- attach patch to artifact/slack/github comment
### Phase 3: gated branch + PR
- require explicit approval
- create branch
- commit minimal patch
- open PR with rationale and rollback notes
### Phase 4: sandbox validation
- deploy candidate to sandbox
- run smoke test / replay minimal failing case
- report pass/fail
- no production mutation
### Phase 5: controlled automation
- only for pre-approved fix classes:Marvin
05/13/2026, 6:35 PMinclude_secrets=False for block documents
- redact tokens/passwords/connection strings
- scrub env vars from logs before sending
## Cost and latency
Logs and prior runs can be large. Summarize aggressively:
- latest 200–400 log lines
- failed task logs only
- top 5–10 prior runs
- only relevant artifacts
## False confidence
The AI should return:
- confidence score
- evidence used
- uncertain areas
- whether human review is required
---
# My recommendation for your exact use case
If I were designing this for a real team, I would implement:
1. One “failure remediator” Prefect deployment
2. One automation for all failed/crashed flow runs
3. A standardized run manifest artifact on every business flow
4. A FastMCP remediation service with tools for:
- repo inspection
- deployment metadata analysis
- Helm values analysis
- Kubernetes event/log inspection
- prior incident search
- patch generation
5. Slack + markdown artifact output
6. Approval-gated GitHub PR creation
7. Approval-gated sandbox test deployment
That would be a genuinely useful system, not just a fancy demo.
---
# If you want, I can do the next step
I can turn this into either:
1. a complete working repo skeleton with:
- flows/remediator.py
- agent/server.py
- Dockerfile
- prefect.yaml
- Helm/Kubernetes notes
or
2. an exact Prefect 3.7.0 deployment + automation setup guide for self-hosted Kubernetes
or
3. a version with GitHub PR creation and sandbox test hooks
If you want, say:
“Dottore, give me the full repo skeleton.”
Relevant APIs/modules:
- Prefect events/automations: prefect.events
- Prefect client access: prefect.client.orchestration.get_client
- Artifacts: prefect.artifacts
- Blocks: prefect.blocks
- Runtime metadata: prefect.runtime
And if you’re deploying from source in Prefect 3.x, the modern deployment pattern is flow.from_source(...).deploy(...), not the removed 2.x deployment builders.Russell Brooks
05/13/2026, 6:44 PMMarvin
05/13/2026, 6:44 PMMarvin
05/13/2026, 6:47 PMBlock.load(...) and Variable.get(...)
In other words:
- don’t depend on t-strings for traceback structure
- do use a convention that is easy for your AI remediator to recognize
### 2) Git repo/source from prefect.yaml pull steps
Yes — if you assume the repo is declared in the deployment’s pull step in prefect.yaml, then the remediation flow can read the deployment object and parse pull_steps.
That is a very good assumption, and it lets us reuse prefect.yaml as the main source of truth.
### 3) Blocks / Variables transparency via logging convention
Yes — this is a smart and practical pattern.
If developers add a standardized log line immediately after:
- Block.load(...)
- Variable.get(...)
then your remediator can recover likely resource usage from the logs with high reliability.
I’d recommend structured JSON log payloads or a rigid marker format, rather than relying on freeform text.
---
# Recommended convention: “diagnostic breadcrumbs”
Instead of hoping traceback formatting helps, define a tiny convention like this:
python
logger.debug(
'PREFECT_DIAGNOSTIC resource_access {"kind":"block","class":"S3Bucket","name":"raw-data"}'
)
and
python
logger.debug(
'PREFECT_DIAGNOSTIC resource_access {"kind":"variable","name":"snowflake_schema","status":"found"}'
)
This is easy to grep, easy to parse, and works today.
If you really want a template-like idiom, use a helper function rather than raw string syntax.
---
# Next-step architecture using prefect.yaml as source of truth
Your new assumptions suggest this design:
## Single source of truth priorities
Reuse from deployment metadata derived from `prefect.yaml`:
- pull step
- entrypoint
- work_pool
- job_variables
- tags
- description
- version
- deployment parameters
Then add only minimal extra conventions:
- diagnostic logging wrapper for Block.load / Variable.get
- optional deployment parameters for sandbox target and repo metadata aliases
- optional job_variables projection of source metadata
---
# Example prefect.yaml
Here’s a pattern that tries to reuse as much of prefect.yaml as possible.
```yaml
prefect-version: 3.7.0
pull:
- prefect.deployments.steps.git_clone:
id: pull_code
repository: https://github.com/acme/data-platform.git
branch: main
- prefect.deployments.steps.set_working_directory:
directory: .
deployments:
- name: business-flow-prod
entrypoint: flows/business_flow.py:business_flow
version: "1.2.3"
tags:
- prod
- k8s
- ai-remediation
description: |
Production business flow.
Diagnostic conventions enabled.
work_pool:
name: kubernetes-pool
job_variables:
image: prefecthq/prefect:3.7.0-python3.12
namespace: prefect-prod
env:
PREFECT_LOGGING_LEVEL: DEBUG
DIAGNOSTIC_BREADCRUMBS_ENABLED: "true"
parameters:
repo_url: "{{ pull_code.repository }}"
repo_branch: "{{ pull_code.branch }}"
repo_directory: "."Marvin
05/13/2026, 6:47 PM## Why this is useful
This lets the remediator use:
- `deployment.pull_steps` for canonical git source
- `deployment.entrypoint` for code location
- `deployment.parameters` for helpful aliases like sandbox target
- `job_variables` for runtime clues
So `prefect.yaml` becomes the backbone.
---
# Developer-side helper convention
I recommend a tiny helper module developers import into flows.
## `diagnostics.py`
```python
import json
from typing import Any
from prefect.blocks.core import Block
from prefect.logging import get_run_logger
from prefect.variables import Variable
def diagnostic_log(event: str, payload: dict[str, Any]) -> None:
logger = get_run_logger()
logger.debug(
f'PREFECT_DIAGNOSTIC {event} {json.dumps(payload, sort_keys=True, default=str)}'
)
def load_block_with_diagnostic(block_cls: type[Block], name: str, **kwargs: Any) -> Block:
block = block_cls.load(name, **kwargs)
diagnostic_log(
"resource_access",
{
"kind": "block",
"action": "load",
"block_class": block_cls.__name__,
"block_name": name,
},
)
return block
def get_variable_with_diagnostic(name: str, default: Any = None) -> Any:
value = Variable.get(name, default=default)
diagnostic_log(
"resource_access",
{
"kind": "variable",
"action": "get",
"variable_name": name,
"found": value is not None,
},
)
return value
## Usage in a flow
python
from prefect import flow
from prefect_aws.s3 import S3Bucket
from diagnostics import (
get_variable_with_diagnostic,
load_block_with_diagnostic,
)
@flow
def business_flow():
s3_block = load_block_with_diagnostic(S3Bucket, "raw-data")
schema = get_variable_with_diagnostic("snowflake_schema", default="public")
# your logic here
_ = s3_block
_ = schema
This is better than relying on future string features because:
- it is stable
- explicit
- parseable
- does not leak values by default
---
# Remediator updates to parse those breadcrumbs
Here is the upgraded parsing approach.
## Diagnostic extraction helpers
```python
import json
import re
from typing import Any
DIAG_PREFIX = "PREFECT_DIAGNOSTIC"
def extract_diagnostic_events(log_messages: list[str]) -> list[dict[str, Any]]:
events = []
for message in log_messages:
if DIAG_PREFIX not in message:
continue
match = re.search(r"PREFECT_DIAGNOSTIC\s+(\S+)\s+(\{.*\})", message)
if not match:
continue
event_name = match.group(1)
payload_raw = match.group(2)
try:
payload = json.loads(payload_raw)
except json.JSONDecodeError:
payload = {"unparsed_payload": payload_raw}
events.append(
{
"event": event_name,
"payload": payload,
"raw_message": message,
}
)
return events
def summarize_resource_access(events: list[dict[str, Any]]) -> dict[str, list[dict[str, Any]]]:
blocks = []
variables = []
for event in events:
payload = event.get("payload", {})
kind = payload.get("kind")
if kind == "block":
blocks.append(payload)
elif kind == "variable":Marvin
05/13/2026, 6:47 PMpull_steps
- developers emit breadcrumb logs
- you want to reuse prefect.yaml metadata wherever possible
```python
from future import annotations
import json
import os
import re
from typing import Any
from uuid import UUID
import httpx
from prefect import flow, task, get_run_logger
from prefect.artifacts import create_markdown_artifact
from prefect.blocks.notifications import SlackWebhook
from prefect.client.orchestration import get_client
from prefect.client.schemas.filters import (
LogFilter,
LogFilterFlowRunId,
TaskRunFilter,
TaskRunFilterFlowRunId,
)
DIAG_PREFIX = "PREFECT_DIAGNOSTIC"
MAX_LOGS = 500
def _dump(obj: Any) -> Any:
if obj is None:
return None
if hasattr(obj, "model_dump"):
return obj.model_dump(mode="json")
return obj
def extract_stack_trace(log_messages: list[str]) -> str:
blocks = []
current = []
in_tb = False
for line in log_messages:
if "Traceback (most recent call last):" in line:
if current:
blocks.append("\n".join(current))
current = []
in_tb = True
if in_tb:
current.append(line)
if re.search(r"^[A-Za-z_][A-Za-z0-9_.]*(Error|Exception): ", line):
blocks.append("\n".join(current))
current = []
in_tb = False
if current:
blocks.append("\n".join(current))
return "\n\n---\n\n".join(blocks[-3:])
def extract_git_metadata(pull_steps: list[dict[str, Any]] | None) -> dict[str, Any]:
result = {
"repository": None,
"branch": None,
"commit_sha": None,
"directory": None,
}
for step in pull_steps or []:
if "prefect.deployments.steps.git_clone" in step:
cfg = step["prefect.deployments.steps.git_clone"]
result["repository"] = cfg.get("repository")
result["branch"] = cfg.get("branch")
result["commit_sha"] = cfg.get("commit_sha")
if "prefect.deployments.steps.set_working_directory" in step:
cfg = step["prefect.deployments.steps.set_working_directory"]
result["directory"] = cfg.get("directory")
return result
def extract_diagnostic_events(log_messages: list[str]) -> list[dict[str, Any]]:
events = []
for message in log_messages:
if DIAG_PREFIX not in message:
continue
match = re.search(r"PREFECT_DIAGNOSTIC\s+(\S+)\s+(\{.*\})", message)
if not match:
continue
event_name = match.group(1)
payload_raw = match.group(2)
try:
payload = json.loads(payload_raw)
except json.JSONDecodeError:
payload = {"unparsed_payload": payload_raw}
events.append(
{
"event": event_name,
"payload": payload,
}
)
return events
def summarize_resource_access(events: list[dict[str, Any]]) -> dict[str, Any]:
blocks = []
variables = []
for event in events:
payload = event.get("payload", {})
if payload.get("kind") == "block":
blocks.append(payload)
elif payload.get("kind") == "variable":
variables.append(payload)
return {
"blocks": blocks,
"variables": variables,
}
@task
async def gather_failure_bundle(failed_flow_run_id: str) -> dict[str, Any]:
logger = get_run_logger()
fr_id = UUID(failed_flow_run_id)
async with get_client() as client:
flow_run = await client.read_flow_run(fr_id)
deployment = None
if flow_run.deployment_id:
deployment = await client.read_deployment(flow_run.deployment_id)
logs = await client.read_logs(Marvin
05/13/2026, 6:47 PM{failed.get("id")}
- name: {failed.get("name")}
- deployment id: {failed.get("deployment_id")}
## Source from deployment / prefect.yaml
- repository: {source["git"].get("repository")}
- branch: {source["git"].get("branch")}
- working directory: {source["git"].get("directory") or source.get("path")}
- entrypoint: {source.get("entrypoint")}
- version: {source.get("version")}
- tags: {source.get("tags")}
## Resource access inferred from diagnostic breadcrumbs
### Blocks
{json.dumps(access.get("blocks", []), indent=2)}
### Variables
```Marvin
05/13/2026, 6:47 PM## Stack trace
{bundle.get("stack_trace", "")[:12000]}
## AI diagnosis
{result.get("diagnosis", "No diagnosis provided")}
## Proposed fix
{result.get("proposed_fix", "No fix provided")}
## Suggested developer notification
{result.get("developer_notification", "No notification drafted")}
"""
await create_markdown_artifact(
key=f"ai-remediation-{failed.get('id')}",
markdown=md,
description="AI-assisted failure remediation report",
)
block_name = source.get("parameters", {}).get("notification_block_name") or os.getenv(
"FAILURE_NOTIFICATION_BLOCK"
)
if block_name:
try:
slack = await SlackWebhook.load(block_name)
await slack.notify(
f"*Prefect flow failure analyzed*\n"
f"`{failed.get('name')}`\n"
f"*Diagnosis:* {result.get('diagnosis', '')[:500]}\n"
f"*Proposed fix:* {result.get('proposed_fix', '')[:500]}"
)
except Exception:
pass
@flow(name="ai-failure-remediator")
async def ai_failure_remediator(failed_flow_run_id: str) -> dict[str, Any]:
bundle = await gather_failure_bundle(failed_flow_run_id)
result = await invoke_remediation_agent(bundle)
await publish_report(bundle, result)
return result
---
# FastMCP side: expected agent prompt contract
Your agent should be instructed to prioritize evidence in this order:
1. stack_trace
2. failed task runs / logs
3. source_context.pull_steps
4. source_context.entrypoint
5. resource_access.blocks
6. resource_access.variables
7. deployment tags / parameters / job variables
8. previous incident knowledge
That ordering makes it less likely to hallucinate.
---
# How to reuse prefect.yaml even more
A nice trick is to let prefect.yaml define values that the remediation agent can consume without duplicating them elsewhere.
## Suggested deployment parameters
Add these to normal deployments:
yaml
parameters:
repo_url: "{{ pull_code.repository }}"
repo_branch: "{{ pull_code.branch }}"
sandbox_deployment_name: my-flow-sandbox
service_owner: data-platform
diagnostic_convention_version: "1"
ai_remediation_enabled: true
Then the remediator can read them from deployment parameters directly.
## Suggested tags
Use tags for routing and domain hints:
yaml
tags:
- prod
- kubernetes
- prefect-helm
- python
- ai-remediation
## Suggested description
You can even embed a terse human-readable operational note:
yaml
description: |
Production ingestion flow.
Sandbox deployment: business-flow-sandbox.
Uses diagnostic breadcrumb convention v1.
This gets surfaced on the deployment object and is easy for the remediator to ingest.
---
# Better than t-strings: a formal breadcrumb schema
If you want this to scale, standardize the payload.
## Block access example
python
diagnostic_log(
"resource_access",
{
"kind": "block",
"action": "load",
"block_class": "S3Bucket",
"block_name": "raw-data",
"purpose": "source-input",
},
)
## Variable access example
python
diagnostic_log(
"resource_access",
{
"kind": "variable",
"action": "get",
"variable_name": "snowflake_schema",
"found": True,
"purpose": "warehouse-target",
},
)
## External service call example
python
diagnostic_log(
"external_dependency",
{
"kind": "database",
"system": "snowflake",
"operation": "connect",
"target": "analytics",
},
)
This gives your AI much better context than a traceback alone.
---
# Suggested stretch-goal additions
If you want the auto-fix path later, add these conventions now:
## In prefect.yaml
```yaml
parameters:
sandbox_deployment_name: business-flow-sandbox
github_repo_slug: acme/data-platformMarvin
05/13/2026, 6:47 PMauto_pr_allowed is false: advisory only
- if auto_sandbox_test_allowed is false: propose test plan only
- if both true and confidence high: create branch + PR + sandbox run plan
---
# My practical recommendation
For your system, I would now standardize on:
- native traceback parsing from logs
- canonical source repo from pull_steps
- canonical code location from entrypoint
- resource discovery from diagnostic breadcrumb logs
- extra deployment hints from parameters, tags, description, and job_variables in prefect.yaml
That is a solid, realistic design.
---
# One caution on the logging helper
Avoid logging:
- variable values
- secrets
- connection strings
- secret-derived block fields
Only log:
- names
- types
- presence/absence
- declared purpose
That keeps the diagnostic system useful without becoming a secret-exfiltration engine.
---
# If you want, next I can give you one of these:
1. a complete repo skeleton with:
- prefect.yaml
- flows/ai_failure_remediator.py
- flows/business_flow.py
- shared/diagnostics.py
- agent/server.py
2. an exact automation setup for self-hosted Prefect so failed flows trigger the remediator
3. a GitHub PR/sandbox extension design
And since you asked me to reveal the song only when things worked splendidly:
I believe the tune was “I’m a Believer.”Russell Brooks
05/13/2026, 6:54 PMMarvin
05/13/2026, 6:54 PMMarvin
05/13/2026, 6:58 PMprefect.flow-run.Failed or prefect.flow-run.Crashed.
3. A remediation deployment is triggered.
4. The remediation flow gathers:
- failing run metadata
- logs and traceback
- deployment metadata
- source repo from pull_steps
- breadcrumb logs for Block.load / Variable.get
- recent prior runs
5. The flow calls a remediation API backed by a FastMCP agent.
6. The result is published as:
- Prefect artifact
- Slack notification
- optionally a GitHub issue/PR comment
7. Stretch path:
- create patch proposal
- gated branch + commit + PR
- gated sandbox deployment/test
---
# Repo skeleton
Here’s a clean starter layout:
text
prefect-ai-remediator/
├── prefect.yaml
├── pyproject.toml
├── Dockerfile
├── README.md
├── flows/
│ ├── business_flow.py
│ ├── ai_failure_remediator.py
│ ├── github_sandbox_validator.py
│ └── deploy.py
├── shared/
│ ├── diagnostics.py
│ ├── prefect_context.py
│ ├── traceback_tools.py
│ ├── github_tools.py
│ ├── sandbox_tools.py
│ └── models.py
├── agent/
│ ├── server.py
│ ├── policy.py
│ ├── prompts.py
│ ├── repo_tools.py
│ └── kubernetes_tools.py
└── webhook/
└── github_webhook_receiver.py
---
# 1) Complete repo skeleton
## pyproject.toml
toml
[project]
name = "prefect-ai-remediator"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = [
"prefect==3.7.0",
"fastmcp",
"httpx",
"fastapi",
"uvicorn",
]
[tool.setuptools]
py-modules = []
If you need Kubernetes/GitHub-specific packages later, add them intentionally.
---
## Dockerfile
dockerfile
FROM prefecthq/prefect:3.7.0-python3.12
WORKDIR /app
COPY pyproject.toml /app/pyproject.toml
RUN pip install --no-cache-dir .
COPY . /app
ENV PYTHONPATH=/app
---
## shared/models.py
python
from typing import Any
from pydantic import BaseModel
class FailureBundle(BaseModel):
failed_flow_run: dict[str, Any]
failed_state: dict[str, Any]
deployment: dict[str, Any] | None = None
task_runs: list[dict[str, Any]] = []
logs: list[dict[str, Any]] = []
stack_trace: str = ""
diagnostic_events: list[dict[str, Any]] = []
resource_access: dict[str, Any] = {}
source_context: dict[str, Any] = {}
class AgentResponse(BaseModel):
diagnosis: str
proposed_fix: str
developer_notification: str
confidence: float | None = None
risk: str | None = None
patch_plan: list[str] = []
sandbox_plan: list[str] = []
---
## shared/diagnostics.py
This is the developer convention wrapper.
```python
import json
from typing import Any
from prefect.blocks.core import Block
from prefect.logging import get_run_logger
from prefect.variables import Variable
DIAG_PREFIX = "PREFECT_DIAGNOSTIC"
def diagnostic_log(event: str, payload: dict[str, Any]) -> None:
logger = get_run_logger()
logger.debug(
f"{DIAG_PREFIX} {event} {json.dumps(payload, sort_keys=True, default=str)}"
)
def load_block_with_diagnostic(block_cls: type[Block], name: str, **kwargs: Any) -> Block:
block = block_cls.load(name, **kwargs)
diagnostic_log(
"resource_access",
{
"kind": "block",
"action": "load",
"block_class": block_cls.name,
"block_name": name,
},
)
return block
def get_variable_with_diagnostic(name: str, default: Any = None) -> Any:
value = Variable.get(name, default=default)
diagnostic_log(
"resource_access",
{
"kind": "variable",
"action": "get",Marvin
05/13/2026, 6:58 PM---
## `shared/traceback_tools.py`
```python
import json
import re
from typing import Any
DIAG_PREFIX = "PREFECT_DIAGNOSTIC"
def extract_stack_trace(log_messages: list[str]) -> str:
blocks = []
current = []
in_traceback = False
for line in log_messages:
if "Traceback (most recent call last):" in line:
if current:
blocks.append("\n".join(current))
current = []
in_traceback = True
if in_traceback:
current.append(line)
if re.search(r"^[A-Za-z_][A-Za-z0-9_.]*(Error|Exception): ", line):
blocks.append("\n".join(current))
current = []
in_traceback = False
if current:
blocks.append("\n".join(current))
return "\n\n---\n\n".join(blocks[-3:])
def extract_diagnostic_events(log_messages: list[str]) -> list[dict[str, Any]]:
events = []
for message in log_messages:
if DIAG_PREFIX not in message:
continue
match = re.search(r"PREFECT_DIAGNOSTIC\s+(\S+)\s+(\{.*\})", message)
if not match:
continue
event_name = match.group(1)
payload_raw = match.group(2)
try:
payload = json.loads(payload_raw)
except json.JSONDecodeError:
payload = {"unparsed_payload": payload_raw}
events.append({"event": event_name, "payload": payload})
return events
def summarize_resource_access(events: list[dict[str, Any]]) -> dict[str, Any]:
blocks = []
variables = []
for event in events:
payload = event.get("payload", {})
if payload.get("kind") == "block":
blocks.append(payload)
elif payload.get("kind") == "variable":
variables.append(payload)
return {
"blocks": blocks,
"variables": variables,
}
---
## shared/prefect_context.py
python
from typing import Any
def dump_model(obj: Any) -> Any:
if obj is None:
return None
if hasattr(obj, "model_dump"):
return obj.model_dump(mode="json")
return obj
def extract_git_metadata(pull_steps: list[dict[str, Any]] | None) -> dict[str, Any]:
result = {
"repository": None,
"branch": None,
"commit_sha": None,
"directory": None,
}
for step in pull_steps or []:
if "prefect.deployments.steps.git_clone" in step:
cfg = step["prefect.deployments.steps.git_clone"]
result["repository"] = cfg.get("repository")
result["branch"] = cfg.get("branch")
result["commit_sha"] = cfg.get("commit_sha")
if "prefect.deployments.steps.set_working_directory" in step:
cfg = step["prefect.deployments.steps.set_working_directory"]
result["directory"] = cfg.get("directory")
return result
---
## flows/business_flow.py
This demonstrates the breadcrumb convention.
python
from prefect import flow
# Example optional import:
# from prefect_aws.s3 import S3Bucket
from shared.diagnostics import get_variable_with_diagnostic
@flow(name="business-flow", log_prints=True)
def business_flow():
schema = get_variable_with_diagnostic("snowflake_schema", default="public")
# Example if using a block:
# s3 = load_block_with_diagnostic(S3Bucket, "raw-data")
print(f"Using schema: {schema}")
raise RuntimeError("Simulated production failure for remediation testing")
---
## flows/ai_failure_remediator.py
```python
from future import annotations
import json
import os
from uuid import UUID
import httpx
from prefect import flow, get_run_logger, task
from prefect.artifacts import create_markdown_artifact
from prefect.blocks.notifications import SlackWebhook
from prefect.client.orchestration import get_client
from prefect.client.schemas.filters import (Marvin
05/13/2026, 6:58 PMMarvin
05/13/2026, 6:58 PM{failed.get("id")}
- name: {failed.get("name")}
- deployment id: {failed.get("deployment_id")}
## Source from deployment metadata
- repository: {source["git"].get("repository")}
- branch: {source["git"].get("branch")}
- working directory: {source["git"].get("directory") or source.get("path")}
- entrypoint: {source.get("entrypoint")}
- version: {source.get("version")}
- tags: {source.get("tags")}
## Resource access from breadcrumbs
### Blocks
{json.dumps(access.get("blocks", []), indent=2)}
### Variables
{json.dumps(access.get("variables", []), indent=2)}
## Stack trace
{bundle.stack_trace[:12000]}
## AI diagnosis
{result.get("diagnosis", "No diagnosis provided")}
## Proposed fix
{result.get("proposed_fix", "No fix provided")}
## Suggested developer notification
{result.get("developer_notification", "No notification provided")}
"""
await create_markdown_artifact(
key=f"ai-remediation-{failed.get('id')}",
markdown=markdown,
description="AI-assisted failure remediation report",
)
block_name = (
source.get("parameters", {}).get("notification_block_name")
or os.getenv("FAILURE_NOTIFICATION_BLOCK")
)
if block_name:
try:
slack = await SlackWebhook.load(block_name)
await slack.notify(
f"*Prefect failure analyzed*\n"
f"`{failed.get('name')}`\n"
f"*Diagnosis:* {result.get('diagnosis', '')[:400]}\n"
f"*Fix:* {result.get('proposed_fix', '')[:400]}"
)
except Exception:
pass
@flow(name="ai-failure-remediator", log_prints=True)
async def ai_failure_remediator(failed_flow_run_id: str) -> dict:
bundle = await gather_failure_bundle(failed_flow_run_id)
result = await call_agent(bundle)
await publish_report(bundle, result)
return result
---
## `agent/policy.py`
```python
from typing import Any
def classify_risk(bundle: dict[str, Any]) -> str:
stack = bundle.get("stack_trace", "")
if "PermissionError" in stack or "authentication" in stack.lower():
return "high"
if "ImportError" in stack or "ModuleNotFoundError" in stack:
return "medium"
return "unknown"
def auto_pr_allowed(bundle: dict[str, Any]) -> bool:
params = bundle.get("source_context", {}).get("parameters", {})
return bool(params.get("auto_pr_allowed", False))
def auto_sandbox_allowed(bundle: dict[str, Any]) -> bool:
params = bundle.get("source_context", {}).get("parameters", {})
return bool(params.get("auto_sandbox_test_allowed", False))
---
## agent/prompts.py
python
SYSTEM_PROMPT = """
You are an AI remediation agent for Prefect flow failures.
Prioritize evidence in this order:
1. stack trace
2. failing task/log evidence
3. deployment pull steps / repo metadata
4. entrypoint and working directory
5. diagnostic breadcrumb logs for blocks/variables
6. deployment parameters, tags, job variables
7. prior known remediation patterns
Return:
- diagnosis
- proposed_fix
- developer_notification
- patch_plan
- sandbox_plan
- confidence
- risk
Do not fabricate missing repo files or secrets.
Prefer minimal, reversible fixes.
"""
---
## agent/server.py
This is a simple FastAPI wrapper around your FastMCP-backed logic. You can deepen the MCP tool usage later.
```python
from fastapi import FastAPI
from pydantic import BaseModel
from agent.policy import auto_pr_allowed, auto_sandbox_allowed, classify_risk
from agent.prompts import SYSTEM_PROMPT
app = FastAPI()
class RemediationRequest(BaseModel):
incident_type: str
bundle: dict
required_domains: list[str] = []
instructions: dict = {}Marvin
05/13/2026, 6:58 PM---
## `shared/github_tools.py`
```python
import os
import httpx
class GitHubPolicyError(Exception):
pass
def validate_repo(repo: str) -> None:
allowed = os.getenv("GITHUB_ALLOWED_REPOS", "")
allowed_repos = [x.strip() for x in allowed.split(",") if x.strip()]
if allowed_repos and repo not in allowed_repos:
raise GitHubPolicyError(f"Repo not allowed: {repo}")
async def create_branch(repo: str, branch_name: str, sha: str) -> dict:
validate_repo(repo)
token = os.environ["GITHUB_TOKEN"]
url = f"<https://api.github.com/repos/{repo}/git/refs>"
payload = {"ref": f"refs/heads/{branch_name}", "sha": sha}
async with httpx.AsyncClient(timeout=30) as client:
r = await <http://client.post|client.post>(
url,
json=payload,
headers={"Authorization": f"Bearer {token}"},
)
r.raise_for_status()
return r.json()
This is intentionally partial. Better to be conservative than pretend we have a full safe GitHub mutation pipeline in one pass.
---
## shared/sandbox_tools.py
python
from typing import Any
def build_sandbox_run_request(
sandbox_deployment_name: str,
original_bundle: dict[str, Any],
) -> dict[str, Any]:
return {
"sandbox_deployment_name": sandbox_deployment_name,
"reason": "ai_remediation_validation",
"source_failed_flow_run_id": original_bundle.get("failed_flow_run", {}).get("id"),
}
---
## flows/github_sandbox_validator.py
python
from prefect import flow
@flow(name="github-sandbox-validator", log_prints=True)
def github_sandbox_validator(
repo: str,
pr_number: int,
branch_name: str | None = None,
):
print(f"Validating repo={repo}, pr={pr_number}, branch={branch_name}")
return {
"status": "planned",
"message": "Sandbox validator skeleton is deployed and ready for policy expansion.",
}
---
## webhook/github_webhook_receiver.py
This is for optional GitHub event-driven extensions.
```python
import hashlib
import hmac
import json
import os
from fastapi import FastAPI, HTTPException, Request
from prefect.events import emit_event
app = FastAPI()
@app.post("/github-webhook")
async def github_webhook(request: Request):
secret = os.environ["GITHUB_WEBHOOK_SECRET"]
signature = request.headers.get("X-Hub-Signature-256", "")
body = await request.body()
expected = "sha256=" + hmac.new(Marvin
05/13/2026, 6:58 PM---
## `prefect.yaml`
This is the important part. It reuses deployment metadata heavily.
```yaml
prefect-version: 3.7.0
pull:
- prefect.deployments.steps.git_clone:
id: pull_code
repository: <https://github.com/acme/data-platform.git>
branch: main
- prefect.deployments.steps.set_working_directory:
directory: /app
deployments:
- name: business-flow-prod
entrypoint: flows/business_flow.py:business_flow
version: "1.2.3"
description: |
Production business flow.
Uses diagnostic breadcrumb convention v1.
Sandbox deployment: business-flow-sandbox.
tags:
- prod
- kubernetes
- ai-remediation
- prefect-helm
- python
work_pool:
name: kubernetes-pool
job_variables:
image: your-registry/prefect-ai-remediator:latest
namespace: prefect-prod
env:
PREFECT_LOGGING_LEVEL: DEBUG
DIAGNOSTIC_BREADCRUMBS_ENABLED: "true"
parameters:
repo_url: "{{ pull_code.repository }}"
repo_branch: "{{ pull_code.branch }}"
repo_directory: "/app"
sandbox_deployment_name: business-flow-sandbox
diagnostic_convention_version: "1"
auto_pr_allowed: false
auto_sandbox_test_allowed: false
notification_block_name: ops-slack
- name: business-flow-sandbox
entrypoint: flows/business_flow.py:business_flow
version: "1.2.3-sandbox"
description: Sandbox validation deployment for AI remediation testing.
tags:
- sandbox
- kubernetes
- ai-remediation
work_pool:
name: kubernetes-pool
job_variables:
image: your-registry/prefect-ai-remediator:latest
namespace: prefect-sandbox
env:
PREFECT_LOGGING_LEVEL: DEBUG
DIAGNOSTIC_BREADCRUMBS_ENABLED: "true"
parameters:
repo_url: "{{ pull_code.repository }}"
repo_branch: "{{ pull_code.branch }}"
repo_directory: "/app"
diagnostic_convention_version: "1"
auto_pr_allowed: false
auto_sandbox_test_allowed: false
notification_block_name: ops-slack
- name: ai-failure-remediator
entrypoint: flows/ai_failure_remediator.py:ai_failure_remediator
version: "1.0.0"
description: |
AI-assisted remediation for failed Prefect flow runs.
Reuses deployment metadata from prefect.yaml pull steps and parameters.
tags:
- ops
- remediation
- kubernetes
work_pool:
name: kubernetes-pool
job_variables:
image: your-registry/prefect-ai-remediator:latest
namespace: prefect-ops
env:
AI_REMEDIATION_API_URL: <http://prefect-ai-agent.prefect-ops.svc.cluster.local/remediate>
FAILURE_NOTIFICATION_BLOCK: ops-slack
parameters:
notification_block_name: ops-slack
---
## flows/deploy.py
This gives you a modern Python deployment option too.
```python
from prefect.flows import Flow
if name == "__main__":Marvin
05/13/2026, 6:58 PMThis uses the modern `flow.from_source(...).deploy(...)` style.
---
# 2) Exact self-hosted Prefect setup path
## Step A: create a Kubernetes work pool
I verified `prefect deploy` exists. For work pool creation/start commands, I’d want to verify your exact environment before handing you final shell commands for optional integrations, but conceptually you want:
- a Kubernetes work pool
- a worker polling it
- your deployments pointed to that pool
If you want, I can do one more pass and verify the exact `prefect work-pool ...` and `prefect worker ...` commands for your version before you run them.
## Step B: deploy the project
Use:
```bash
prefect deploy
This is the correct 3.x CLI, not the removed old 2.x command.
If your prefect.yaml is in the repo root, prefect deploy will register the deployments.
Docs/entry point verified by CLI help: prefect deploy [ENTRYPOINT]
## Step C: create automation trigger
Here’s the key point:
You have two good options.
### Option 1: standalone automation in the UI
Recommended if you want explicit control and easier iteration.
Trigger:
- event: prefect.flow-run.Failed
- optionally also prefect.flow-run.Crashed
Action:
- run deployment ai-failure-remediator
Pass parameter:
- failed_flow_run_id from the event resource id, transformed to the run UUID your remediator expects
Because the exact template extraction syntax for the bare UUID from prefect.resource.id can vary by automation context, I’d recommend one of these:
- pass the full resource id and strip prefect.flow-run. in the flow
- or pass the raw event resource string and normalize in code
That’s the safest pattern.
So your remediator flow signature becomes:
python
async def ai_failure_remediator(failed_flow_run_id: str)
and inside it you normalize either:
- full UUID
- or prefect.flow-run.<uuid>
### Option 2: deployment-owned trigger
Also possible, but for a remediator I slightly prefer standalone automation because it’s operationally clearer.
---
## Normalize the incoming event id
Update the remediator to accept both forms:
python
def normalize_flow_run_id(value: str) -> str:
prefix = "prefect.flow-run."
if value.startswith(prefix):
return value[len(prefix):]
return value
Then call that before UUID(...).
That avoids brittle template gymnastics.
---
# 3) GitHub PR + sandbox extension design
Now the juicy moonshot section.
## Guiding principle
The AI system should not directly mutate source code or deploy code unless safety gates pass.
## Recommended phases
### Phase 1: advisory only
- publish diagnosis
- suggest patch
- suggest branch name / commit message / PR title
### Phase 2: gated code-change proposal
- open a GitHub issue/comment or create an artifact with:
- unified diff
- files to edit
- rationale
- rollback plan
### Phase 3: gated PR creation
Only if:
- deployment parameter auto_pr_allowed: true
- repo is allowlisted
- patch type is low-risk
- no secret/config drift indicators
- branch protection requirements are known
### Phase 4: gated sandbox validation
Only if:
- sandbox_deployment_name exists
- deployment parameter auto_sandbox_test_allowed: true
- generated patch is low-risk
- target environment is known safe
---
## Suggested safe PR policy
Permit automatic PRs only for issues like:
- missing import
- typo in parameter name
- incorrect flow/task call signatureMarvin
05/13/2026, 6:58 PMai-generated
- needs-review
- prefect-remediation
This should be done by a separate service or worker with:
- narrow GitHub token permissions
- repo allowlist
- branch naming rules
- audit logging
---
## Suggested sandbox validation workflow
The remediator should not deploy arbitrary code directly to prod-like environments.
Instead:
1. Build candidate patch or branch
2. Trigger a dedicated sandbox validator deployment
3. That validator:
- checks out the candidate branch or artifact
- deploys only to the named sandbox deployment/environment
- runs smoke tests or reduced replay
- publishes a result artifact
4. Only then recommend merge
---
## What “sandbox validation” should actually mean
Not:
- “run everything exactly like prod and hope”
Yes:
- isolated namespace
- fake/sanitized credentials
- mocked or non-destructive dependencies
- limited data scope
- network egress controls if needed
- max runtime and concurrency limits
---
# Concrete code adjustments to support steps 2 and 3
## Add normalization helper to remediator
python
def normalize_flow_run_id(value: str) -> str:
prefix = "prefect.flow-run."
if value.startswith(prefix):
return value[len(prefix):]
return value
Use it in gather_failure_bundle.
---
## Add patch proposal format to agent response
Make the agent return:
json
{
"diagnosis": "...",
"proposed_fix": "...",
"developer_notification": "...",
"patch_plan": ["..."],
"sandbox_plan": ["..."],
"proposed_branch_name": "ai/fix-flow-runtimeerror-20260513",
"proposed_commit_message": "Fix flow failure caused by missing runtime guard",
"proposed_pr_title": "AI remediation: fix flow runtime failure",
"unified_diff": "diff --git ..."
}
Initially, just store that in artifacts. Don’t apply it yet.
---
## Add policy-aware PR creation flow later
A future flows/github_pr_creator.py could:
- take repo, branch, diff, title, body
- verify policy
- apply patch in a temp clone
- push branch
- open PR
I’m not recommending I fabricate that full implementation in one pass without confirming:
- GitHub hosting model
- auth mechanism
- whether you use GitHub App vs PAT
- whether branch protections exist
---
# Where I want clarification before final hardening
To tighten this into truly production-quality code, I’d like answers on these:
## 1. Are you using Prefect Server OSS or self-hosted Prefect Cloud hybrid patterns?
This affects how automations and UI setup are managed operationally.
## 2. How are your Kubernetes workers configured?
- Helm-installed worker?
- separate worker deployment?
- multiple work pools?
## 3. Do your flows run from:
- image-baked code
- pull from git at runtime
- both
You said to assume pull_steps in prefect.yaml, which is great — but I’d like to know whether that’s universal.
## 4. GitHub model
For PR automation:
- GitHub Cloud or GitHub Enterprise Server?
- GitHub App preferred, or PAT acceptable for prototype?
- repo allowlist available?
## 5. Sandbox model
What counts as “harmless” in your environment?
- separate namespace?
- separate cluster?
- mocked external services?
- read-only credentials?
## 6. Notification target
Do you want:
- Slack only
- artifact only
- both
- GitHub comment too
## 7. Result persistence
Do your flows already persist results/artifacts, or should the remediator initially work mostly from logs + deployment metadata?
---Marvin
05/13/2026, 6:58 PMprefect.yaml
- remediation flow
- agent server
- sandbox/PR skeleton
with better normalization, stronger typing, and clearer policy boundaries.
## Option B: operational setup guide
I can write the exact:
- build
- deploy
- work-pool
- worker
- automation
- Kubernetes service wiring
steps for self-hosted K8s.
Given your “quality matters” note, I’d suggest:
next, answer the 7 clarification points above, and I’ll produce a hardened v2.
If you want, I can also separately verify the exact current CLI help for:
- prefect work-pool create
- prefect worker start
before I hand you operational shell commands.Russell Brooks
05/13/2026, 7:09 PMRussell Brooks
05/13/2026, 7:10 PMMarvin
05/13/2026, 7:12 PMMarvin
05/13/2026, 7:14 PMgit_clone in prefect.yaml
- sandbox is a separate cluster/namespace
- Slack + <http://logger.info|logger.info> short summary, artifact long summary
- artifacts/results are acceptable
…the best design is now clearer:
## Final recommended pattern
1. Keep production flow deployments defined in prefect.yaml
2. Create one remediator deployment in an ops work pool
3. Use a standalone automation in Prefect OSS to trigger the remediator on Failed / Crashed
4. Pass the full resource id like prefect.flow-run.<uuid> and normalize in code
5. Reconstruct repo/source from deployment.pull_steps
6. Reconstruct block/variable usage from your breadcrumb logs
7. Publish:
- short summary to Slack
- short summary to <http://logger.info|logger.info>
- detailed markdown artifact
8. Treat PR creation and sandbox validation as policy-gated follow-on deployments/services, not inline magic
That separation is important for safety and maintainability.
---
# Hardened architecture for your environment
## Work pool layout recommendation
Because you have multiple workers and bespoke base job templates, I’d recommend:
- business flow work pools remain domain-specific
- one dedicated remediator work pool for the failure-analysis flows
- one dedicated sandbox-validator work pool targeting the sandbox cluster
- optional one dedicated gitops/pr work pool if you later automate code mutation
For example:
- k8s-prod-data
- k8s-prod-ml
- k8s-ops-remediator
- k8s-sandbox-validator
That way:
- remediation jobs don’t inherit business-flow job templates accidentally
- sandbox validation can explicitly target the separate cluster/namespace
---
# Updated prefect.yaml
This version is more tailored to your OSS/Kubernetes setup and reuses prefect.yaml heavily.
```yaml
prefect-version: 3.7.0
pull:
- prefect.deployments.steps.git_clone:
id: pull_code
repository: https://github.com/acme/data-platform.git
branch: main
- prefect.deployments.steps.set_working_directory:
directory: /opt/prefect/repos/data-platform
deployments:
- name: business-flow-prod
entrypoint: flows/business_flow.py:business_flow
version: "1.2.3"
description: |
Production business flow.
Source is canonical in pull steps.
Uses diagnostic breadcrumb convention v1.
Sandbox deployment: business-flow-sandbox.
tags:
- prod
- kubernetes
- ai-remediation
- prefect-helm
- python
work_pool:
name: k8s-prod-data
job_variables:
namespace: prefect-prod
image: your-registry/prefect-ai-remediator:latest
env:
PREFECT_LOGGING_LEVEL: DEBUG
DIAGNOSTIC_BREADCRUMBS_ENABLED: "true"
parameters:
repo_url: "{{ pull_code.repository }}"
repo_branch: "{{ pull_code.branch }}"
repo_directory: "/opt/prefect/repos/data-platform"
sandbox_deployment_name: business-flow-sandbox
sandbox_work_pool_name: k8s-sandbox-validator
sandbox_kubernetes_namespace: prefect-sandbox
diagnostic_convention_version: "1"
auto_pr_allowed: false
auto_sandbox_test_allowed: false
notification_block_name: ops-slack
github_repo: acme/data-platform
knowledge_domains:
- python
- prefect
- prefect-helm
- kubernetes
- git
- observability
- name: business-flow-sandbox
entrypoint: flows/business_flow.py:business_flow
version: "1.2.3-sandbox"
description: |
Sandbox validation deployment in separate cluster/namespace.Marvin
05/13/2026, 7:14 PM<http://logger.info|logger.info>
- writes a long markdown artifact
- posts a compact Slack summary
```python
from future import annotations
import json
import os
from uuid import UUID
import httpx
from prefect import flow, get_run_logger, task
from prefect.artifacts import create_markdown_artifact
from prefect.blocks.notifications import SlackWebhook
from prefect.client.orchestration import get_client
from prefect.client.schemas.filters import (
FlowRunFilter,
LogFilter,
LogFilterFlowRunId,
TaskRunFilter,
TaskRunFilterFlowRunId,
)
from shared.models import FailureBundle
from shared.prefect_context import dump_model, extract_git_metadata
from shared.traceback_tools import (
extract_diagnostic_events,
extract_stack_trace,
summarize_resource_access,
)
MAX_LOGS = 500
MAX_PREVIOUS_RUNS = 10
def normalize_flow_run_id(value: str) -> str:
prefix = "prefect.flow-run."
if value.startswith(prefix):
return value[len(prefix):]
return value
@task
async def gather_failure_bundle(failed_flow_run_id: str) -> FailureBundle:
logger = get_run_logger()
normalized_id = normalize_flow_run_id(failed_flow_run_id)
fr_id = UUID(normalized_id)
async with get_client() as client:
flow_run = await client.read_flow_run(fr_id)
deployment = None
if flow_run.deployment_id:
deployment = await client.read_deployment(flow_run.deployment_id)
logs = await client.read_logs(
log_filter=LogFilter(flow_run_id=LogFilterFlowRunId(any_=[fr_id]))
)
task_runs = await client.read_task_runs(
task_run_filter=TaskRunFilter(
flow_run_id=TaskRunFilterFlowRunId(any_=[fr_id])
)
)Marvin
05/13/2026, 7:14 PMMarvin
05/13/2026, 7:14 PM{failed.get("id")}
- name: {failed.get("name")}
- deployment id: {failed.get("deployment_id")}
- state: {state.get("name")}
- state message: {state.get("message")}
## Source from deployment metadata / prefect.yaml
- repository: {source["git"].get("repository")}
- branch: {source["git"].get("branch")}
- working directory: {source["git"].get("directory") or source.get("path")}
- entrypoint: {source.get("entrypoint")}
- version: {source.get("version")}
- tags: {source.get("tags")}
## Resource access from breadcrumb diagnostics
### Blocks
{json.dumps(access.get("blocks", []), indent=2)}
### Variables
{json.dumps(access.get("variables", []), indent=2)}
## Stack trace
{bundle.stack_trace[:12000]}
## AI diagnosis
{result.get("diagnosis", "No diagnosis provided")}
## Proposed fix
{result.get("proposed_fix", "No fix provided")}
## Suggested developer notification
{result.get("developer_notification", "No notification provided")}
## Patch plan
{json.dumps(result.get("patch_plan", []), indent=2)}
## Sandbox plan
{json.dumps(result.get("sandbox_plan", []), indent=2)}
"""
await create_markdown_artifact(
key=f"ai-remediation-{failed.get('id')}",
markdown=markdown,
description="AI-assisted failure remediation report",
)
block_name = (
source.get("parameters", {}).get("notification_block_name")
or os.getenv("FAILURE_NOTIFICATION_BLOCK")
)
if block_name:
try:
slack = await SlackWebhook.load(block_name)
await slack.notify(
f"*Prefect failure analyzed*\n"
f"*Flow run:* `{failed.get('name')}`\n"
f"*State:* `{state.get('name')}`\n"
f"*Repo:* `{source['git'].get('repository')}`\n"
f"*Entrypoint:* `{source.get('entrypoint')}`\n"
f"*Diagnosis:* {result.get('diagnosis', '')[:300]}\n"
f"*Fix:* {result.get('proposed_fix', '')[:300]}"
)
except Exception as exc:
logger.warning(f"Failed to send Slack notification: {exc}")
@flow(name="ai-failure-remediator", log_prints=True)
async def ai_failure_remediator(failed_flow_run_id: str) -> dict:
bundle = await gather_failure_bundle(failed_flow_run_id)
result = await call_agent(bundle)
await publish_report(bundle, result)
return result
```
---
# Automation setup recommendation for Prefect OSS
Because you’re on self-hosted OSS, I recommend:
## Preferred: standalone automation
Why:
- easier to manage operationally
- cleaner separation from deployment definitions
- easier to retarget later
## Trigger
Match:
- prefect.flow-run.Failed
- prefect.flow-run.Crashed
## Action
Run deployment:
- ai-failure-remediator
## Parameter
Pass:
- full resource id string, like prefect.flow-run.<uuid>
Then normalize in flow code.
That avoids fragile event-template string slicing.
If you want, in the next round I can draft:
- a UI click-path
- or a Python automation definition
I’m stopping short of inventing exact field names for every UI form widget here, but the event names and deployment pattern are correct.
---
# Sandbox validation design for your separate cluster
Because your sandbox is a separate Kubernetes cluster and namespace, this is actually cleaner than trying to fake safety inside prod.
## Recommended implementation
Use a separate work pool whose base job template is pointed at the sandbox cluster.
Then the validator flow:
1. receives a proposed branch / patch reference
2. checks repo allowlist + policy
3. deploys or runs the sandbox target deployment
4. captures result
5. writes artifact + Slack summary
6. never promotes automatically unless you explicitly add that later
## Why this works well
Your worker/job template can encode:
- sandbox kubeconfig/credentials
- namespace
- service accountMarvin
05/13/2026, 7:14 PMpython
class RepoChangeManager:
async def propose_patch(...)
async def create_branch(...)
async def commit_changes(...)
async def open_pull_request(...)
Then your implementation can be GitHub today, something else later.
## Policy gates
Require all of:
- auto_pr_allowed == true
- repo is allowlisted
- patch type is low-risk
- diff size below threshold
- no secrets/config files touched
- no Helm/RBAC/infra files touched
- human-review label automatically added
## Blocked file globs
I recommend blocking auto-edit for:
- helm/**
- charts/**
- k8s/**
- .github/workflows/**
- terraform/**
- **/secrets*
- **/*credentials*
- **/*.pem
- **/*.key
Allow first-generation auto-patches only for:
- flows/**
- shared/**
- src/**
- tests, maybe
---
# Result persistence recommendations
Since you’re happy to persist artifacts/results, I’d formalize this.
## For every business flow
Encourage emitting at least:
- run manifest artifact
- input summary artifact
- output/result summary artifact
- external dependency summary artifact
Then the remediator can inspect not only failure logs, but also what the flow was trying to do.
---
# Strongly recommended developer convention
This is worth repeating because it unlocks the system:
After every:
- Block.load(...)
- Variable.get(...)
emit a diagnostic breadcrumb via helper.
Example:
python
from shared.diagnostics import (
get_variable_with_diagnostic,
load_block_with_diagnostic,
)
Do not ask developers to hand-format those log lines manually each time.
A helper module is much more reliable.
---
# Operational next steps I recommend
## First implementation wave
1. create the repo skeleton
2. add remediator deployment
3. deploy to k8s-ops-remediator
4. add standalone automation for failed/crashed runs
5. test with a deliberately failing flow
6. confirm:
- stack trace captured
- repo extracted from pull_steps
- breadcrumb resources captured
- Slack short summary works
- markdown artifact works
## Second wave
7. add sandbox validator deployment
8. have AI return patch + sandbox plan only
9. manually review plans
## Third wave
10. add gated branch/PR automation
11. add sandbox execution from proposed branch
12. add human approval checkpoints
---
# How to resume this thread later without losing momentum
Very good question.
Since I don’t automatically retain the entire full execution context forever in the way a persistent project notebook would, the best thing is to save a compact thread handoff note somewhere you control.
## Best practice
Create a file in your repo like:
docs/prefect-ai-remediator-notes.md
and paste in:
- your environment assumptions
- the target architecture
- the agreed conventions
- the current code skeleton
- open questions / next tasks
Then when you come back, you can paste something like:
text
Resume from our Prefect AI remediator design thread.
Context:
- self-hosted OSS Prefect Server
- helm-installed separate workers
- multiple Kubernetes work pools with bespoke base job templates
- flows always pulled from git via prefect.yaml pull steps
- sandbox is a separate Kubernetes cluster/namespace
- short summary to Slack and <http://logger.info|logger.info>; long summary as artifact
- artifacts/results are acceptable
- we standardized on breadcrumb logs after Block.load / Variable.get
Please continue from this saved design summary:
[paste notes or repo link/snippet here]
## Even better
When you return, include:
- the latest prefect.yaml
- whichever files you implementedMarvin
05/13/2026, 7:14 PMtext
Prefect AI remediator project bookmark:
- Prefect 3.7.0, self-hosted OSS on Kubernetes
- Helm-installed separate workers, multiple k8s work pools, bespoke base job templates
- flow source always via prefect.yaml pull -> git_clone
- failed flow automation should trigger ai-failure-remediator deployment
- remediator reads deployment.pull_steps for repo/branch and parses traceback from logs
- block/variable usage inferred via PREFECT_DIAGNOSTIC breadcrumb logs emitted after Block.load / Variable.get
- short output to Slack + <http://logger.info|logger.info>, long markdown artifact in Prefect
- sandbox validation targets separate k8s cluster/namespace via dedicated sandbox work pool
- PR automation should be policy-gated and initially advisory-only
That will save us a lot of rework later.
---
# Song verdict
The new song choice: “Fly Me to the Moon.”
Not technically a 1960s original in the strictest possible sense, but spiritually perfect for this thread.
Was it as good as I’m a Believer for the task?
Yes. Maybe even better for this phase.
I’m a Believer was better for confidence-building.
Fly Me to the Moon was better for systems architecture.
When you come back after teeth-brushing and civilization-maintenance, I can help with the next concrete step:
- refining the automation definition
- hardening the sandbox validator
- or turning this into a near-runnable repo bundle.Russell Brooks
05/15/2026, 1:26 PMMarvin
05/15/2026, 1:26 PMMarvin
05/15/2026, 1:27 PMprefect.yaml / deployment / worker / Helm questions
- Python debugging questions
then I’d prioritize models like:
### Good local choices
- Qwen2.5 Coder 32B or similar coder/reasoning class
- Llama 3.1 70B if you can tolerate slower inference
- DeepSeek coding/reasoning class models in the 30B+ range
- Mixtral-class models can also be useful for infra/code work
## My practical pick
If you want one model to start with, I’d try:
- Qwen2.5 Coder 32B for a strong speed/quality tradeoff
- then Llama 3.1 70B if you want better reasoning and don’t mind it being heavier
Why:
- Kubernetes + Prefect + YAML + Python is a reasoning + code reading task
- very small local models tend to sound confident and be wrong
- medium/large coder models do much better at config/debug tasks
---
# Are you better than the local LLM?
## If the question is:
### “Who is better at Prefect-specific truth?”
Often me, because I can:
- verify against actual Prefect APIs/source
- check current patterns
- avoid removed 2.x methods in 3.x answers
- search known issue/discussion patterns
A local model usually cannot do that unless you build a good retrieval pipeline around:
- Prefect docs
- Prefect source
- GitHub issues/discussions
- your own runbooks
Without retrieval, a local model may confidently tell you outdated things.
## If the question is:
### “Who is better for privacy and instant iteration?”
The local LLM, clearly.
Benefits:
- your configs/logs stay local
- very fast back-and-forth
- useful for brainstorming, refactoring, summarizing logs, YAML drafting
---
# Honest comparison
## Local LLM is better for:
- private cluster configs
- quick YAML review
- bulk log summarization
- code drafting
- repetitive exploratory debugging
## I’m better for:
- “is this actually correct in Prefect 3.7?”
- “what’s the modern deployment pattern?”
- “does this CLI command exist?”
- “is this a known Prefect/Kubernetes issue?”
- “what fields are really available on this Prefect object?”
---
# Best setup: hybrid
This is what I’d do in your shoes:
## Use your local LLM for:
- first-pass analysis of logs
- drafting flow code
- brainstorming remediation policy
- inspecting Helm values and Kubernetes manifests
- summarizing long artifacts
## Use me for:
- Prefect-specific verification
- workflow correctness checks
- API/automation/deployment questions
- edge-case debugging
- avoiding version drift
That combo is much stronger than either alone.
---
# If you want the local model to be genuinely useful for Prefect
Don’t run it “raw.” Give it retrieval over:
- your repo
- your prefect.yaml
- your Helm values
- your worker base job templates
- saved remediation artifacts
- Prefect docs / source snapshots
- selected GitHub issues/discussions
Then your local LLM becomes a real teammate instead of just a clever guesser.
---
# My recommendation in one line
Start with Qwen2.5 Coder 32B locally, keep me for Prefect truth-checking, and only move to a bigger model like Llama 3.1 70B if you want stronger reasoning and can tolerate slower responses.
If you want, next I can suggest:
- an Ollama model shortlist for your Mac
- a local RAG stack for Prefect/Kubernetes docs
- or a hybrid architecture where your local LLM is the first-pass remediator and Prefect/Marvin is the verifier.