Shubham
07/13/2026, 5:04 PMMarvin
07/13/2026, 5:04 PMMarvin
07/13/2026, 5:07 PMstart_deployment_tool
- check_deployment_run
- approve_deployment_run
- reject_deployment_run
Do not make one giant tool that does trigger + wait + approve + fetch result unless all jobs are short-lived.
FastMCP’s job:
- validate tool inputs
- create a request_id / correlation ID
- trigger the Prefect deployment
- return a normalized response to the agent
- optionally poll briefly for quick jobs
- hand off long waits to async flow via n8n
---
### 2) Prefect
Prefect should own:
- execution
- retries
- task orchestration
- pause/resume for approval
- final state/result production
This is the system of record for:
- flow_run_id
- current status
- approval pending state
- final completion / failure
For approval steps, use Prefect’s pause/resume flow behavior rather than building your own fake waiting layer inside n8n.
---
### 3) n8n
n8n should own:
- human approval UX
- waiting for external user action
- routing approval/rejection back
- optional notification fanout
n8n should not be the source of truth for deployment state. It should be the approval broker.
---
## Best overall pattern
## Pattern A: Async-first tool design
This is the best fit for agent tools.
### Tool 1: Start run
Agent calls FastMCP tool:
text
run_data_quality_check(
dataset="s3://...",
require_approval=true
)
FastMCP:
- creates request_id
- calls Prefect deployment
- passes:
- business parameters
- request_id
- callback metadata
- approval_required=true
Prefect starts run and returns flow_run_id.
FastMCP returns immediately:
json
{
"status": "started",
"flow_run_id": "....",
"request_id": "req_123",
"approval_expected": true
}
This is better than blocking the agent for long-running workflows.
---
### Tool 2: Get run status
Agent or your runtime calls:
text
get_run_status(flow_run_id="...")
Returns normalized status:
json
{
"flow_run_id": "...",
"status": "PAUSED",
"awaiting_approval": true,
"message": "Waiting for manager approval"
}
or
json
{
"flow_run_id": "...",
"status": "COMPLETED",
"result": {...}
}
---
### Tool 3: Approve run
If approval comes from agent-side workflow, expose:
text
approve_run(flow_run_id="...", approved=true, notes="looks good")
But if n8n handles approval, then n8n can call Prefect directly to resume the paused flow.
---
## Approval flow I would design
## Option 1: Prefect pauses, n8n resumes
This is the cleanest design.
### In the Prefect flow
The flow does:
1. pre-checks / preparation
2. if approval required:
- call pause_flow_run(...)
3. wait for approval payload
4. continue execution
5. produce final result
Conceptually:
python
from prefect import flow
from prefect.flow_runs import pause_flow_run
from prefect.input import RunInput
class ApprovalInput(RunInput):
approved: bool
approver: str
notes: str = ""
@flow
def my_tool_flow(payload: dict, approval_required: bool = False, request_id: str = ""):
preview = {"request_id": request_id, "summary": "planned action summary"}
if approval_required:
approval = pause_flow_run(wait_for_input=ApprovalInput)
if not approval.approved:
return {
"request_id": request_id,
"status": "rejected",
"notes": approval.notes,
}
result = do_actual_work(payload)
return {
"request_id": request_id,
"status": "completed",
"result": result,
}
### In n8nMarvin
07/13/2026, 5:07 PMstarted + flow_run_id
This hybrid pattern is ideal for agent tools.
---
### Mode 2: Asynchronous callback
For long jobs, do not keep FastMCP blocked forever.
Instead:
- Prefect completes
- Prefect automation or polling layer notifies n8n
- n8n stores/sends result back where your agent can fetch it
- FastMCP exposes get_run_status or get_run_result
---
## The tool contract I’d define
I would define normalized tool responses so the agent always sees a stable schema.
### start_deployment_tool
Input:
json
{
"deployment_name": "team/flow-deployment",
"parameters": {...},
"require_approval": true
}
Output:
json
{
"ok": true,
"request_id": "req_123",
"flow_run_id": "fr_abc",
"status": "started",
"awaiting_approval": false,
"final": false
}
Possible statuses:
- started
- running
- paused_for_approval
- completed
- failed
- rejected
---
### get_deployment_result
Output:
json
{
"ok": true,
"flow_run_id": "fr_abc",
"status": "completed",
"final": true,
"result": {...},
"error": null
}
If paused:
json
{
"ok": true,
"flow_run_id": "fr_abc",
"status": "paused_for_approval",
"final": false,
"approval_context": {
"message": "Waiting for approval from finance"
}
}
---
## Important design choices
## 1) Use correlation IDs everywhere
Every request should have:
- request_id
- flow_run_id
- optional conversation_id / agent_run_id
Pass request_id into:
- Prefect flow parameters
- Prefect tags
- logs
- n8n payloads
- FastMCP responses
That gives you traceability across all 3 systems.
---
## 2) Always use idempotency
When FastMCP creates a flow run, use an idempotency_key.
That protects you when:
- the agent retries
- n8n retries
- network timeout happens after run creation
Use a deterministic key such as:
text
agent_name:tool_name:request_id
---
## 3) Separate “tool intent” from “workflow execution”
Your agent tool should say:
- what it wants done
- whether approval is required
- whether to wait for completion
Prefect decides:
- how the work executes
- whether it pauses
- what state it is in
This separation makes the tool stable even if the flow internals evolve.
---
## 4) Keep approval payloads structured
Don’t just send "approved": true.
Use something like:
json
{
"approved": true,
"approver": "<mailto:alice@example.com|alice@example.com>",
"notes": "Reviewed and approved",
"approved_at": "2026-07-13T10:30:00Z"
}
That becomes useful later for auditing.
---
## My preferred implementation plan
## Phase 1: Basic reliable pattern
Build these first:
### FastMCP tools
- run_prefect_deployment
- get_prefect_run_status
### Prefect flow behavior
- accept request_id
- accept approval_required
- pause if approval is needed
- return structured result
### n8n behavior
- receive “approval needed” notification
- human approves/rejects
- call Prefect resume endpoint
This is enough for a working MVP.
---
## Phase 2: Better async UX
Add:
- Prefect event/webhook notifications to n8n on:
- run paused
- run completed
- run failed
- n8n updates your external store / agent memory / ticket
This avoids constant polling.
---
## Phase 3: Strong operational guarantees
Add:
- idempotency keys
- retry-safe callback handling
- result persistence outside run return for large outputs
- timeout policies
- cancellation handling
---Marvin
07/13/2026, 5:07 PMparameters
- request_id
- approval_required
- idempotency_key
### C. Prefect starts work
- if no approval needed: continue
- if approval needed: pause flow run
### D. n8n handles approval
- receives notification
- waits for human input
- resumes Prefect flow with approval payload
### E. Prefect finishes
- returns final structured result
- optionally emits completion callback/webhook
### F. FastMCP or agent fetches result
Via:
- short synchronous wait, or
- later get_run_status / get_run_result
---
## What I would not do
I would avoid these designs:
### 1) Letting n8n be the real state machine
If n8n is deciding all transitions, retries, and execution state, it becomes harder to debug than letting Prefect orchestrate.
### 2) Blocking FastMCP forever
Long waits are brittle for tool calls.
### 3) Returning unstructured raw flow state to the agent
Map Prefect states to agent-friendly statuses.
### 4) Making approval live outside the flow with no resume point
If approval is external but the flow doesn’t actually pause, you’ll end up stitching state manually.
---
## Suggested status mapping
Map Prefect states to a simpler tool schema:
- Scheduled / Pending / Running -> running
- Paused -> paused_for_approval
- Completed -> completed
- Failed -> failed
- Cancelled -> cancelled
That keeps the agent logic simple.
---
## Minimal architecture summary
Best plan:
- FastMCP starts deployments and returns flow_run_id
- Prefect owns execution and approval pause/resume
- n8n owns human approval interaction and resumes the flow
- FastMCP fetches final result via status/result tool
---
## If you want my exact recommendation in one sentence
Use Prefect as the execution + approval state machine, n8n as the human approval broker, and FastMCP as a thin tool API that starts runs and fetches results.
---
## Useful starting points
- Prefect deployments concepts: Deployments
- Prefect flows: Flows
- Prefect Python SDK / client concepts: Python API reference
- Prefect automations/events: Automations
- Prefect Cloud UI: Prefect Cloud
If you want, I can next give you one of these:
1. a reference architecture diagram
2. a FastMCP tool implementation skeleton
3. a Prefect flow example with pause/resume approval
4. an n8n workflow design for approval + callbackShubham
07/13/2026, 5:29 PMMarvin
07/13/2026, 5:29 PMMarvin
07/13/2026, 5:32 PMpause_flow_run() → keeps the run process alive
- suspend_flow_run() → stops the process/container and resumes later in a new one
So for your case, suspend is usually the right primitive, not pause.
---
## Short answer
If you resume a flow that used `pause_flow_run()`:
- it resumes from where it left off
- in the same running process/container
- so the container may need to remain alive the whole time
If you resume a flow that used `suspend_flow_run()`:
- the original process/container exits
- later, Prefect starts a new worker/container
- the flow resumes via orchestration, not by keeping Python memory alive
So for a Docker worker + approvals that can take weeks, I would design around suspend_flow_run().
---
## What this means practically
## pause_flow_run()
Think of this as:
“stop here and wait, but keep this execution environment around”That means: - same Python process - same container - local variables still exist - in-memory objects may still be there - resume is quick But: - bad fit for week-long human approval - fragile if worker/container restarts - wastes resources --- ##
suspend_flow_run()
Think of this as:
“checkpoint orchestration state, exit cleanly, and come back later”That means: - container exits - worker is free - later on approval, Prefect schedules/resumes the run - a new container starts - execution continues under Prefect orchestration But: - do not rely on local in-memory variables after suspension - you must persist what you need --- ## Important correction to my earlier suggestion For your setup, I would refine the recommendation to: - n8n handles UX + approval collection - Prefect flow uses
suspend_flow_run() for long waits
- on approval, n8n calls Prefect resume
- Docker worker starts a fresh container to continue work
So your earlier instinct was actually good — you just want Prefect to own the suspended state rather than n8n faking execution state.
---
## How resume works with suspend
With `suspend_flow_run()`:
1. Flow runs up to the suspend point
2. Prefect marks the run suspended
3. The container exits
4. Days/weeks later, approval happens
5. Someone/something calls resume
6. Prefect orchestrates the flow to continue
7. Docker worker launches a new container for that run
So no, the old container does not need to stay running.
---
## The big caveat: memory is gone
This is the most important design rule.
After `suspend_flow_run()`:
- local Python variables are not something you should depend on
- open DB connections are gone
- temporary files in the container are gone
- anything only in memory is gone
So before suspending, persist what you’ll need later:
- parameters
- intermediate artifacts
- generated plan
- approval payload context
- object references / IDs
- task results
---
## Best practice for long approvals
## Use suspend_flow_run() if approval can take days/weeks
That is the safest pattern for Docker/Kubernetes/ephemeral workers.
### Good flow shape
text
prepare work
persist plan/context/artifacts
suspend for approval
resume later
load persisted context
execute approved action
return result
---
## Example mental model
### Bad pattern
python
@flow
def my_flow():
plan = build_big_plan()
suspend_flow_run()
execute_plan(plan)
Why bad:
- plan is just a local variable
- after resume in a new container, you should not assume it’s still there
---
### Better pattern
python
@flow
def my_flow(request_id: str):
plan = build_big_plan()
save_plan_somewhere(request_id, plan)
suspend_flow_run()
approved_plan = load_plan_somewhere(request_id)
execute_plan(approved_plan)
That is the right model for long-lived approvals.
---Marvin
07/13/2026, 5:32 PMsuspend_flow_run(), you should also think about result persistence.
Especially with ephemeral infrastructure, you generally want:
- persisted task results
- remote storage for results/artifacts if needed
Examples:
- S3
- GCS
- Azure Blob
- database
- object store
Because a new container won’t have access to the old container’s local filesystem.
---
## My recommendation for your architecture
Given your setup:
- AI agent calls FastMCP
- FastMCP triggers Prefect deployment
- Prefect flow prepares an action plan and suspends
- n8n sends approval UI to user
- on approval, n8n resumes the Prefect flow run
- Docker worker launches a new container
- flow loads persisted context and performs the direct action
- FastMCP can fetch final result later
This gives you:
- no long-running approval containers
- clear audit trail
- reliable execution handoff
- proper orchestration ownership in Prefect
---
## So does it “start from where it left off”?
### With pause_flow_run()
Yes, basically from where it left off in the same process.
### With suspend_flow_run()
Logically yes from workflow perspective, but not by preserving Python memory/process.
It resumes under orchestration, with a new container/process. So architecturally, treat it as:
- “continue the workflow”
- not “continue the same Python stack frame with all local memory intact”
That distinction matters a lot.
---
## My concrete advice for you
For approvals that may take weeks:
- Do not use pause_flow_run()
- Use suspend_flow_run()
- persist all needed context before suspend
- assume resumed execution is on a new container
- let n8n call Prefect resume when user approves
---
## Suggested split for your exact case
### FastMCP tools
- start_action_request
- get_action_status
- get_action_result
### Prefect deployment
- validate request
- generate plan
- persist plan/context
- suspend_flow_run()
- on resume, reload plan/context
- perform direct action
- persist final result
### n8n
- approval UI
- reminder/escalation
- resume/reject callback into Prefect
That’s probably the cleanest version of your original idea.
---
## Useful docs
- Flows: Prefect flows
- Deployments: Deployments
- Python API reference: Python API
- Prefect Cloud: Prefect Cloud
If you want, next I can give you:
1. a sample Prefect flow using suspend_flow_run() for approval
2. a FastMCP tool contract for start/status/result
3. an n8n-to-Prefect approval/resume designShubham
07/13/2026, 5:39 PMMarvin
07/13/2026, 5:39 PMMarvin
07/13/2026, 5:41 PMflow_run_id and UI URL
- send URL to user/n8n/Slack/email
- user opens run page
- clicks Resume
- fills approval input if required
This works well if you want humans to approve directly in Prefect UI.
---
### 2) Python SDK / API
Programmatically resume the flow run with input:
python
from prefect.client.orchestration import get_client
from uuid import UUID
async with get_client() as client:
await client.resume_flow_run(
flow_run_id=UUID("your-flow-run-id"),
run_input={
"approved": True,
"notes": "Approved from external UX"
}
)
The callable signature is:
text
resume_flow_run(flow_run_id: UUID, run_input: dict[str, Any] | None = None)
This is the best fit if:
- n8n collects approval
- then n8n or FastMCP resumes the run programmatically
---
### 3) Direct REST API
Prefect exposes a resume endpoint:
text
POST /flow_runs/{id}/resume
So if n8n wants to call Prefect directly, this is a clean pattern too.
---
## Can you get the unique URL/ID before suspend?
Yes.
A flow run gets its ID when it is created, so you can capture:
- flow_run_id
- flow run UI URL
before the flow reaches suspend_flow_run().
That means your approval message can include a link immediately.
---
## Practical pattern for your case
### When FastMCP starts the deployment
Return something like:
json
{
"flow_run_id": "2b5c...",
"request_id": "req_123",
"ui_url": "<https://app.prefect.cloud/account/.../workspace/.../runs/flow-run/2b5c...>"
}
Then n8n can:
- store that URL
- send it in Slack/email/UI
- let the approver click into Prefect directly
---
## Can the UI show a form for approval?
Yes, if you suspend/pause with expected input.
Using a RunInput schema lets Prefect UI render an input form on resume.
Conceptually:
python
from prefect import flow
from prefect.flow_runs import suspend_flow_run
from prefect.input import RunInput
class ApprovalInput(RunInput):
approved: bool
notes: str = ""
@flow
async def my_flow():
approval = await suspend_flow_run(wait_for_input=ApprovalInput)
if not approval.approved:
return {"status": "rejected", "notes": approval.notes}
return {"status": "approved"}
Then in the Prefect UI, the user can resume with structured input.
---
## Which approach should you choose?
## Option A: Approve in Prefect UI
Use this if:
- you want minimal custom UX
- approvers can access Prefect
- Prefect UI is acceptable as the approval interface
Flow:
- FastMCP starts run
- capture flow_run_id + UI URL
- send URL to approver
- approver resumes from Prefect UI form
---
## Option B: Approve in your own UX / n8n
Use this if:
- approval must happen in your own app
- users should not access Prefect directly
- you want a custom approval experience
Flow:
- FastMCP starts run
- flow suspends
- n8n stores flow_run_id
- approval happens in your UX
- n8n calls Prefect resume_flow_run(...)
This is probably closer to your original design.
---
## For Docker worker + long wait, my preferred pattern
I’d use:
- suspend_flow_run(wait_for_input=ApprovalInput) inside the flow
- keep flow_run_id
- optionally generate/share the Prefect UI link
- either:
- let user resume in Prefect UI, or
- have n8n resume via API
This gives you both:
- no long-running container
- optional built-in approval form in Prefect UI
---
## Caveats
### 1) Self-hosted vs Cloud URL
In Prefect Cloud, the UI URL points to app.prefect.cloud.Marvin
07/13/2026, 5:41 PMbefore suspend can I get the unique url/id to resume from ui formYes — you can get and store the
flow_run_id as soon as the run is created, and you can share the run’s UI URL for human approval in Prefect UI. If the suspended flow expects input, the UI can present a resume form.
---
## Useful docs
- Flows: Flows
- Deployments: Deployments
- Python API reference: Python API reference
- Prefect Cloud UI: Prefect Cloud
If you want, I can next give you:
1. a code snippet to generate/store the Prefect UI run URL
2. a full suspend/resume approval flow example
3. an n8n HTTP call example for resuming the flow runShubham
07/13/2026, 6:04 PMMarvin
07/13/2026, 6:05 PMMarvin
07/13/2026, 6:08 PMtext
{PREFECT_UI_URL}/flow-runs/flow-run/{flow_run_id}
Example:
text
<https://prefect.mycompany.internal/flow-runs/flow-run/2b5c9d8e-1234-4567-89ab-cdef01234567>
If your UI is local/dev:
text
<http://localhost:4200/flow-runs/flow-run/2b5c9d8e-1234-4567-89ab-cdef01234567>
Docker worker type does not change this URL.
The URL depends only on:
- your UI base URL
- the flow_run_id
---
## Resume without input
If your flow is suspended and does not require input, the user just opens that run page and clicks Resume.
Programmatically, that is:
python
from prefect.client.orchestration import get_client
from uuid import UUID
async with get_client() as client:
await client.resume_flow_run(
flow_run_id=UUID("2b5c9d8e-1234-4567-89ab-cdef01234567")
)
No run_input needed.
---
## How to get the URL inside the flow
You can access the current run UI URL from runtime context:
python
from prefect import flow
from prefect.runtime.flow_run import ui_url
@flow
def my_flow():
print(ui_url)
So before suspending, you can:
- log it
- store it
- email it
- send it to n8n
---
## Recommended pattern for your use case
### Step 1: Start run
FastMCP triggers the deployment and gets flow_run_id.
### Step 2: Prefect flow sends approval email
Before suspend_flow_run(), send an email containing:
- action summary
- run URL
- maybe request ID
### Step 3: User clicks run URL
They land on the Prefect run page and click Resume.
### Step 4: Flow continues
After resume, the Docker worker starts a new container and continues execution.
### Step 5: Send confirmation email
At the end of the resumed flow, send another email saying the action completed.
---
## Example flow shape
python
from prefect import flow
from prefect.flow_runs import suspend_flow_run
from prefect.runtime.flow_run import ui_url
@flow
async def approval_action_flow():
# prepare action
action_summary = "Delete 25 stale records"
# send approval email here with ui_url
send_email(
subject="Approval needed",
body=f"Please review and resume this run:\n{ui_url}\n\nAction: {action_summary}"
)
# suspend until user resumes in UI
await suspend_flow_run()
# after resume, do the action
do_action()
# send confirmation email after action
send_email(
subject="Action completed",
body=f"Action completed successfully.\nRun: {ui_url}"
)
---
## Sending the confirmation email
For self-hosted Prefect, simplest is to send the email directly from the flow code after resume.
That is usually easier than building server-side automations.
You can use:
- SMTP library
- SendGrid
- any internal mail API
- Prefect notification blocks if that matches your setup
---
## Simple SMTP example
python
import smtplib
from email.mime.text import MIMEText
def send_email(subject: str, body: str):
msg = MIMEText(body)
msg["Subject"] = subject
msg["From"] = "<mailto:noreply@mycompany.com|noreply@mycompany.com>"
msg["To"] = "<mailto:approver@mycompany.com|approver@mycompany.com>"
with smtplib.SMTP("<http://smtp.mycompany.com|smtp.mycompany.com>", 587) as server:
server.starttls()
server.login("smtp-user", "smtp-password")
server.send_message(msg)
Then in flow:
python
from prefect import flow
from prefect.flow_runs import suspend_flow_run
from prefect.runtime.flow_run import ui_url
@flow
async def approval_action_flow():
send_email(
subject="Approval required",
body=f"Resume this flow run from the UI:\n{ui_url}"
)
await suspend_flow_run()
# action after approval
# ... do work ...
send_email(
subject="Action completed",
body=f"The action has completed successfully.\nRun URL: {ui_url}"
)
---
## Important caveat with ui_url
For self-hosted deployments, make sure your environment is configured so Prefect knows the UI base URL correctly.Marvin
07/13/2026, 6:08 PMPREFECT_UI_URL
- PREFECT_API_URL
Otherwise generated URLs may be missing or point somewhere wrong.
---
## If you want a one-click email approval
Prefect UI link is great for “open and resume manually”.
But if you want an email button like:
- Approve
- Reject
then that usually means:
- email links go to your app/n8n endpoint
- your app/n8n calls Prefect resume_flow_run(...)
That gives a smoother approval UX than asking users to open Prefect UI.
---
## Direct answer
### Resume URL format on on-prem:
text
<https://your-prefect-ui/flow-runs/flow-run/{flow_run_id}>
### Does Docker worker affect it?
No.
### Resume without input?
User opens the run page and clicks Resume, or call:
python
resume_flow_run(flow_run_id=...)
### Send confirmation mail after resumed action?
Best simple approach: send the email directly from the flow code after the resumed action completes.
---
## Relevant docs
- Flows: Flows
- Python API reference: Python API reference
- Prefect Cloud / UI reference point: Prefect UI
If you want, I can give you:
1. a full working Prefect flow example with suspend + email + resume URL
2. a FastMCP starter tool that returns flow_run_id and ui_url
3. an n8n approval email pattern with approve link calling Prefect resume APIShubham
07/14/2026, 3:19 AMMarvin
07/14/2026, 3:20 AMMarvin
07/14/2026, 3:20 AMpython
@flow
async def my_flow():
step1()
await suspend_flow_run()
step2()
and you resume via API on a Docker worker:
- step1() is not supposed to rerun as normal flow progress
- the resumed run continues to step2()
- but it does so in a new container
So from your business logic point of view, it resumes from where it waited.
---
## Important practical caveat
Even though it resumes from the suspend point, you should still design as if:
- local variables are gone
- memory is gone
- temp files in container are gone
- open connections are gone
So this is good:
python
@flow
async def my_flow(request_id: str):
plan_id = save_plan_to_db(request_id)
await suspend_flow_run()
plan = load_plan_from_db(plan_id)
execute_plan(plan)
And this is bad:
python
@flow
async def my_flow():
plan = build_plan()
await suspend_flow_run()
execute_plan(plan)
because after resume on a new Docker container, plan should not be something you rely on being in memory.
---
## Simple answer
### pause_flow_run()
- resumes in same process
- continues exactly there
- container stays alive
### suspend_flow_run()
- resumes after that wait point
- but in a new process/container
- so persist anything needed before suspend
---
## For API resume specifically
If you call:
python
await client.resume_flow_run(flow_run_id=...)
for a flow that used `suspend_flow_run()`:
- Prefect marks it for resumption
- Docker worker picks it up
- new container starts
- execution continues from after the suspend point
---
## Bottom line
It resumes from the wait point, not from the start — but on a fresh Docker container, so don’t depend on in-memory state.
If you want, I can draw the exact execution timeline for:
- trigger
- pre-suspend steps
- suspend
- API resume
- post-resume steps on Docker workerBring your towel and join one of the fastest growing data communities. Welcome to our second-generation open source orchestration platform, a completely rethought approach to dataflow automation.
Powered by