Shubham
10/26/2025, 1:49 PMMarvin
10/26/2025, 1:49 PMMarvin
10/26/2025, 1:55 PMpause_flow_run(wait_for_input=..., key="...") or suspend_flow_run(wait_for_input=..., key="...")
- Programmatic resume: resume_flow_run(flow_run_id, run_input={...})
- Getting the current run id: get_run_context().flow_run.id
Docs:
- Interactive (human-in-the-loop) workflows
- REST API: Resume flow run
- prefect-email integration
Example: email + suspend + external approval callback
Flow code (suspend and wait for a boolean approval, then continue):
from uuid import UUID
from prefect import flow, suspend_flow_run, get_run_logger
from prefect.context import get_run_context
from prefect_email.message import email_send_message
from prefect_email.credentials import EmailServerCredentials
@flow
def approval_flow(to_email: str):
logger = get_run_logger()
# Get the current flow run id for the callback link
run_id = str(get_run_context().flow_run.id)
# Build approval links to your external app
approve_url = f"<https://myapp.example.com/approve?run_id={run_id}&approved=true>"
reject_url = f"<https://myapp.example.com/approve?run_id={run_id}&approved=false>"
# Send the email (configure a Block named "my-smtp-creds" in the UI first)
creds = EmailServerCredentials.load("my-smtp-creds")
email_send_message(
subject="Approval needed",
msg=f"Please approve this run.\nApprove: {approve_url}\nReject: {reject_url}",
email_server_credentials=creds,
email_to=to_email,
)
# Suspend and wait for input; frees infra while waiting
# Use a key so the external callback can target this input
approved: bool | None = suspend_flow_run(
wait_for_input=bool,
key="approval",
timeout=7 * 24 * 3600 # 7 days
)
if approved:
<http://logger.info|logger.info>("Approved, continuing workflow...")
# ... do the thing ...
else:
<http://logger.info|logger.info>("Rejected (or no response in time).")
# ... handle rejection ...
if __name__ == "__main__":
approval_flow("<mailto:approver@example.com|approver@example.com>")
External approval endpoint (FastAPI example):
from uuid import UUID
from fastapi import FastAPI, HTTPException, Query
from prefect import resume_flow_run
app = FastAPI()
@app.get("/approve")
def approve(
run_id: UUID = Query(...),
approved: bool = Query(...),
token: str = Query(...) # validate a shared secret or signed JWT!
):
# TODO: validate token to prevent unauthorized approvals
try:
# Provide input keyed by the same `key` used in suspend_flow_run(...)
resume_flow_run(flow_run_id=run_id, run_input={"approval": approved})
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
return {"status": "ok"}
cURL example using the REST API (non-Python systems)
curl -X POST \
-H "Authorization: Bearer $PREFECT_API_KEY" \
-H "Content-Type: application/json" \
"$PREFECT_API_URL/api/flow-runs/<flow_run_id>/resume" \
-d '{"run_input": {"approval": true}}'
Notes and tips
- Use suspend_flow_run for long waits; it tears down infra and you must call resume_flow_run to continue.
- For short waits, pause_flow_run works similarly and will poll until input is available or timeout.
- The key argument lets you correlate inputs when there are multiple pauses in a flow.Marvin
10/26/2025, 1:55 PMfrom pydantic import BaseModel
class Approval(BaseModel):
approved: bool
comment: str | None = None
decision = suspend_flow_run(wait_for_input=Approval, key="approval")
# External app would then pass:
# resume_flow_run(run_id, run_input={"approval": {"approved": true, "comment": "LGTM"}})
- If you prefer manual approval via the Prefect UI, users can click Resume on the flow run and enter input directly.
Questions to tailor this for you
- Which external app will issue the callback (and in what language)?
- How long might approvals take (minutes vs. hours/days)? That determines pause vs. suspend.
- Do you want a simple boolean approval, or a structured payload (e.g., comment, amount, approver id)?
- Will you host your own approval endpoint, or should we use a Prefect Automation + Webhook pattern?Shubham
10/26/2025, 1:59 PMShubham
10/26/2025, 2:03 PMMarvin
10/26/2025, 2:03 PMMarvin
10/26/2025, 2:03 PMfrom uuid import UUID
import os, time
from pydantic import BaseModel
from prefect import flow, suspend_flow_run, get_run_logger
from prefect.context import get_run_context
from prefect_email.message import email_send_message
from prefect_email.credentials import EmailServerCredentials
class Approval(BaseModel):
approved: bool
channel: str | None = None # "email"|"slack"|"teams"|...
actor: str | None = None # who approved
comment: str | None = None
APP_BASE = os.getenv("APP_BASE", "<https://approvals.example.com>") # your callback base URL
def approval_links(run_id: str, key: str="approval"):
# token should be a signed value that encodes run_id/key and expires
# for brevity, we insert a placeholder
token = f"SIGNED.{run_id}.{key}"
approve = f"{APP_BASE}/approve?token={token}&approved=true"
reject = f"{APP_BASE}/approve?token={token}&approved=false"
return approve, reject
@flow
def hitl_flow(to_email: str):
logger = get_run_logger()
run_id = str(get_run_context().flow_run.id)
approve_url, reject_url = approval_links(run_id)
# Send email with clickable approve/reject links
creds = EmailServerCredentials.load("my-smtp-creds")
email_send_message(
subject="Approval needed",
msg=(
"Please review and approve this run.\n\n"
f"Approve: {approve_url}\n"
f"Reject: {reject_url}\n"
),
email_server_credentials=creds,
email_to=to_email,
)
# Suspend for up to 7 days and wait for structured input
decision: Approval | None = suspend_flow_run(
wait_for_input=Approval,
key="approval",
timeout=7 * 24 * 3600, # 7 days
)
if decision and decision.approved:
<http://logger.info|logger.info>(f"Approved by {decision.actor or decision.channel}. Proceeding.")
# ... continue downstream steps ...
else:
<http://logger.info|logger.info>("Rejected or no response. Taking fallback path.")
# ... handle rejection or timeout ...
Callback service (FastAPI) example
- Verifies the token, extracts run_id and key, then resumes the flow with a structured payload
```
import os
from uuid import UUID
from fastapi import FastAPI, HTTPException, Query
from prefect import resume_flow_run
SECRET = os.getenv("APPROVAL_TOKEN_SECRET", "change-me")
def verify_and_decode(token: str) -> dict:
# Replace with proper JWT/HMAC verification and expiration checks
# Expected to return {"run_id": "...", "key": "..."}
try:
_, run_id, key = token.split(".")
return {"run_id": run_id, "key": key}
except Exception:Marvin
10/26/2025, 2:03 PM