Giacomo Chiarella
03/11/2026, 2:52 PMMarvin
03/11/2026, 2:52 PMMarvin
03/11/2026, 2:58 PMapi and trying (and failing) to parse it as JSON.
Root cause
- If PREFECT_API is set (e.g., PREFECT_API=http://...), Prefect treats it as the entire api settings object and tries to JSON-parse it. A plain string isn’t valid for that, so you get:
pydantic_settings.exceptions.SettingsError: error parsing value for field "api" from source "EnvFilterSettingsSource"
Fix
- Unset any PREFECT_API env var and only use PREFECT_API_URL (and optionally PREFECT_API_KEY, etc.)
Quick checks
1) Check what’s set in your shell:
env | grep PREFECT_API
If you see PREFECT_API=..., unset it:
- bash/zsh:
unset PREFECT_API
- fish:
set -e PREFECT_API
- PowerShell:
Remove-Item Env:PREFECT_API
2) Verify Prefect’s view of your config and sources:
prefect config view --show-sources
If you want to validate explicitly:
prefect config validate
Correct ways to set the API URL
- Environment variable:
export PREFECT_API_URL="<http://127.0.0.1:4200/api>"
# For Cloud:
# export PREFECT_API_URL="<https://api.prefect.cloud/api/accounts/<ACCOUNT_ID>/workspaces/<WORKSPACE_ID>>"
# export PREFECT_API_KEY="pcu_xxxxx"
- In a profile (recommended):
prefect config set PREFECT_API_URL="<http://127.0.0.1:4200/api>"
- In ~/.prefect/profiles.toml (env-style keys):
[profiles.myprofile]
PREFECT_API_URL = "<http://127.0.0.1:4200/api>"
If you’re still seeing the error after unsetting PREFECT_API, check for:
- An entry like api = "http://..." in ~/.prefect/profiles.toml. Replace it with PREFECT_API_URL = "..." or use a nested table:
[profiles.myprofile.api]
url = "<http://127.0.0.1:4200/api>"
Once cleaned up, importing should work:
from prefect import get_client
If it persists, please share:
- Output of env | grep PREFECT_API
- Output of prefect config view --show-sources (mask secrets)
Docs for reference: Settings and ProfilesGiacomo Chiarella
03/11/2026, 3:31 PMMarvin
03/11/2026, 3:31 PMMarvin
03/11/2026, 3:33 PMapi incorrectly, even though PREFECT_API_URL is correct.
Here’s how to pinpoint and fix it:
1) Check for a stray PREFECT_API env var
- This is the most common culprit.
env | grep '^PREFECT_API'
If you see PREFECT_API=..., unset it:
- bash/zsh:
unset PREFECT_API
- fish:
set -e PREFECT_API
- PowerShell:
Remove-Item Env:PREFECT_API
2) Inspect Prefect’s view of your settings and their sources
- This will show exactly where api is being set from.
prefect config view --show-sources
- Validate the active profile:
prefect config validate
Look for any source that sets api as a string (instead of api.url) — that will cause the parsing error.
3) Check config files that may set api incorrectly
- ~/.prefect/profiles.toml
- A local .env in your working directory
- A local prefect.toml or pyproject.toml
Wrong (causes your error):
- In profiles.toml:
[profiles.myprofile]
api = "http://..."
- In prefect.toml:
api = "http://..."
Right:
- Env-style in profiles.toml:
[profiles.myprofile]
PREFECT_API_URL = "<http://127.0.0.1:4200/api>"
- Or nested table in prefect.toml:
[api]
url = "<http://127.0.0.1:4200/api>"
4) Minimal sanity check in a clean shell
- Start a fresh shell, only set the URL, and import:
export PREFECT_API_URL="<http://127.0.0.1:4200/api>"
python -c "from prefect.settings import get_current_settings as g; print(g().api.url)"
python -c "from prefect import get_client; print('ok')"
If you’re still getting the error, please share:
- Output of:
env | grep '^PREFECT_API'
prefect config view --show-sources
(mask any API key), and we’ll zero in on the conflicting setting.
Reference: Settings and ProfilesGiacomo Chiarella
03/11/2026, 7:53 PMclass JsonConfig(Block):
value: Dict[str, Any]
for block_name in blocks:
print(JsonConfig.load(block_name).value)
It is strange because the block is read, it is printed but I get printed
<block-value>
<warning>
<block-value>
<warning>
<block-value>
<warning>
…
why?Marvin
03/11/2026, 7:53 PMMarvin
03/11/2026, 7:57 PMlocation /api/events/in {
proxy_pass <http://your_prefect_api_upstream>;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto https;
proxy_buffering off;
proxy_request_buffering off;
proxy_read_timeout 7d;
proxy_send_timeout 7d;
}
If you’re using Cloudflare/ALB/Traefik:
- Enable WebSockets, keep HTTP/1.1, and set sufficient idle timeouts.
2) Turn on debug to see the full handshake failure
export PREFECT_DEBUG_MODE=1
Then reproduce the warning and check the detailed log.
3) Temporarily suppress the warnings (while you fix the proxy)
- Disable event emission in-process:
from prefect.events.worker import EventsWorker
from prefect.events.clients import NullEventsClient
EventsWorker.set_client_override(NullEventsClient)
- Or reduce the log level just for the events client:
import logging
logging.getLogger("prefect.events.clients").setLevel(logging.ERROR)
4) Confirm server version matches client
- If you’re self-hosting Prefect Server, ensure it’s 3.x to match your client (3.6.4). A 2.x server won’t have the same events endpoints and will 404.
Why the warnings appear between your prints
- Block.load(...) emits an event like prefect.block.<type>.loaded. The event client attempts a WebSocket connection and logs a warning when it’s rejected (404). Since you load multiple blocks, you see a warning after each value print.
If you can share:
- Your reverse proxy/load balancer type and config for /api (especially /api/events/in and /api/events/out)
- Prefect Server version you’re running
- The output with PREFECT_DEBUG_MODE=1
I can help pinpoint the exact fix. For background, see:
- Events overview: Prefect 3 Events
- Settings and profiles (for API URL and debug mode): Settings and ProfilesGiacomo Chiarella
04/04/2026, 3:55 PM@flow(name=DAG_NAME)
def flow_entrypoint(dag_name: str, parameters: dict):
having a dictionary as one of the parametersMarvin
04/04/2026, 3:55 PMMarvin
04/04/2026, 3:59 PMfrom prefect import flow
@flow(name=DAG_NAME) # DAG_NAME must be a defined string at import time
def flow_entrypoint(dag_name: str, parameters: dict):
# use parameters like a normal dict
...
Notes and best practices:
- Dicts are supported; keep values JSON-serializable (str, int, float, bool, None, lists, nested dicts).
- There’s a default parameter size limit (~512 KB). For large configs, store them externally (file/object store) and pass a reference (path/URL).
- If you want validation and nicer UI forms, prefer a Pydantic model instead of a bare dict:
from typing import Dict, Any
from pydantic import BaseModel, Field
from prefect import flow
class Params(BaseModel):
settings: Dict[str, Any] = Field(default_factory=dict, description="Run settings")
@flow
def flow_entrypoint(dag_name: str, parameters: Params):
...
Docs:
- Flows and parameters overview: Flows concept docs
- Using Pydantic for structured parameters and UI forms: Form buildingGiacomo Chiarella
04/04/2026, 7:12 PMMarvin
04/04/2026, 7:12 PMMarvin
04/04/2026, 7:15 PMfrom prefect import flow
from prefect.deployments import run_deployment
@flow
def orchestrator():
sub_run = run_deployment(
"child-flow/production", # "flow_name/deployment_name" or a deployment UUID
parameters={"x": 1, "y": 2},
timeout=600, # wait up to 10 minutes (None waits indefinitely)
poll_interval=5, # seconds between polls
as_subflow=True, # show it as a subflow in the UI (default)
flow_run_name="child-from-parent"
)
if sub_run.state.is_failed():
raise RuntimeError(f"Child failed: {sub_run.state.message}")
return sub_run.id
Async example:
import asyncio
from prefect import flow
from prefect.deployments import arun_deployment
@flow
async def orchestrator_async():
sub_run = await arun_deployment(
"child-flow/production",
parameters={"x": 1},
timeout=300,
poll_interval=2
)
return sub_run.id
Fire-and-forget (don’t wait):
run_deployment("child-flow/production", timeout=0)
If you need the child flow’s return value, ensure the child persists its result and then fetch it via a future:
from prefect import flow
from prefect.deployments import run_deployment
from prefect.futures import PrefectFlowRunFuture
@flow
def orchestrator():
fr = run_deployment("child-flow/production", timeout=0) # create run, return immediately
future = PrefectFlowRunFuture(fr.id)
result = future.result(raise_on_failure=True) # requires the child to persist its result
return result
And set the child to persist:
from prefect import flow
@flow(persist_result=True)
def child_flow(...):
return {"ok": True}
Docs:
- API reference: run_deployment
- Async API: arun_deploymentGiacomo Chiarella
04/04/2026, 7:17 PMMarvin
04/04/2026, 7:17 PMMarvin
04/04/2026, 7:17 PM# Sync: blocks until done (or timeout)
from prefect import flow
from prefect.deployments import run_deployment
@flow
def parent():
fr = run_deployment("child/production", parameters={"x": 1}, timeout=600)
return fr.state
# Async: await, and can run several at once
import asyncio
from prefect import flow
from prefect.deployments import arun_deployment
@flow
async def parent_async():
runs = await asyncio.gather(
arun_deployment("child/a", parameters={"x": 1}),
arun_deployment("child/b", parameters={"x": 2}),
)
return [r.state for r in runs]
Docs:
- run_deployment: API reference
- arun_deployment: API referenceGiacomo Chiarella
04/04/2026, 7:21 PMMarvin
04/04/2026, 7:21 PMMarvin
04/04/2026, 7:21 PMas_subflow only controls the parent/child linking and UI lineage, not the waiting behavior.
- timeout controls waiting for both sync and async APIs:
- timeout=None (default) waits until the child run reaches a final state
- timeout=0 returns immediately (don’t wait)
- timeout=N waits up to N seconds
- as_subflow=True (default) just links the created run as a subflow of the caller so it appears under the parent in the UI and lineage.
- as_subflow=False creates an independent run. You can still wait on it the same way.
Example (wait without subflow linking):
from prefect import flow
from prefect.deployments import run_deployment
@flow
def parent():
fr = run_deployment(
"child-flow/prod",
parameters={"x": 1},
as_subflow=False, # no UI subflow link
timeout=600 # still waits up to 10 minutes
)
return fr.state
API refs:
- run_deployment: API reference
- arun_deployment: API referenceGiacomo Chiarella
04/04/2026, 7:25 PMMarvin
04/04/2026, 7:25 PMMarvin
04/04/2026, 7:26 PM# parent
run_deployment("child/deploy", as_subflow=False, tags=["via-run-deployment"])
# child
from prefect.runtime import flow_run
if "via-run-deployment" in flow_run.tags:
...
- Sentinel parameter: pass an internal flag parameter.
run_deployment("child/deploy", as_subflow=False, parameters={"_via_run_deployment": True})
- Custom naming convention: set flow_run_name with a recognizable prefix and inspect it in the child.
These are reliable ways to differentiate, since the runtime context itself does not expose the trigger source when not linked as a subflow.Giacomo Chiarella
04/04/2026, 7:33 PMMarvin
04/04/2026, 7:34 PMMarvin
04/04/2026, 7:35 PMfrom prefect import flow
from prefect.runtime.flow_run import tags
@flow
def my_flow():
current_tags = tags # list[str]
print(current_tags)
- Alternative (advanced):
from prefect.context import FlowRunContext
@flow
def my_flow():
ctx = FlowRunContext.get()
tags_set = ctx.flow_run.tags # set[str]Giacomo Chiarella
05/12/2026, 9:09 AMMarvin
05/12/2026, 9:09 AMMarvin
05/12/2026, 9:11 AMread_block_documents, Prefect is calling the API endpoint for block documents and then doing response.json(). If the response body is empty or is an HTML page, you’ll get:
text
json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)
Most likely causes
1. PREFECT_API_URL is pointing at the UI, not the API
- Cloud:
- wrong: <https://app.prefect.cloud>
- correct: <https://api.prefect.cloud/api/accounts/<account-id>/workspaces/<workspace-id>>
- Self-hosted:
- wrong: <http://localhost:4200> or <http://localhost:8080>
- correct: <http://localhost:4200/api> or <http://localhost:8080/api>
2. Missing/invalid auth
- Especially in Cloud, if PREFECT_API_KEY is missing or bad, you may get a non-JSON response depending on what’s in front of the API.
3. A proxy / ingress / auth layer is returning HTML
- For example a login page, gateway error page, etc.
Quick checks
First, print what Prefect is actually using:
python
from prefect import get_client
async with get_client() as client:
print(client.api_url)
Then test health / hello:
python
from prefect import get_client
async with get_client() as client:
print("API URL:", client.api_url)
print(await client.api_healthcheck())
Or from the terminal:
bash
curl "$PREFECT_API_URL/health"
If that returns HTML, an empty body, or a redirect page, the URL is wrong or something is intercepting the request.
What read_block_documents does
It hits the block documents API (/block_documents/filter) and expects JSON back. So if the base URL is off, this will fail exactly the way you’re seeing.
What to verify
- PREFECT_API_URL ends with /api for server/self-hosted
- For Cloud, PREFECT_API_URL uses api.prefect.cloud/.../api/accounts/.../workspaces/...
- PREFECT_API_KEY is set if you’re using Cloud
- You’re not accidentally using the browser/UI URL
Docs
- Configure API access
- Prefect API concepts
- Prefect Cloud UI
If you want, paste your PREFECT_API_URL value here with the IDs/token redacted, and I can tell you if the format is correct.Giacomo Chiarella
05/18/2026, 7:09 AMMarvin
05/18/2026, 7:09 AMMarvin
05/18/2026, 7:10 AMblock_document directly from the Prefect DB, the data column is not plain JSON. In Prefect 3.x, block document data is stored encrypted at rest, so what you’re seeing as a string is expected.
What’s happening
- block_document.data is stored as encrypted JSON
- Prefect uses a Fernet key to encrypt/decrypt it
- This applies to all block documents, not just credentials blocks
So even for non-credential blocks, raw DB reads will show an encrypted string rather than the decoded content.
How to get the actual content
You need the Prefect server encryption key, then decrypt the string and parse the JSON.
The key comes from one of these places:
- PREFECT_SERVER_ENCRYPTION_KEY environment variable, if set
- otherwise the configuration table entry for ENCRYPTION_KEY
High-level process
1. Read the encrypted data value from block_document
2. Read the Fernet key
3. Decrypt the string
4. JSON-decode the result
Example:
import json
from cryptography.fernet import Fernet
encrypted_data = "<value from block_document.data>"
fernet_key = "<your fernet key>"
fernet = Fernet(fernet_key.encode())
decrypted_bytes = fernet.decrypt(encrypted_data.encode())
block_data = json.loads(decrypted_bytes.decode())
print(block_data)
Where the key is stored
If Prefect is not using PREFECT_SERVER_ENCRYPTION_KEY, check the configuration table for the ENCRYPTION_KEY record. The value is stored like:
{
"fernet_key": "<base64-key>"
}
So you’d use the nested fernet_key value.
Important caveat
If you want to know which fields are considered secret vs non-secret for API-style masking, that comes from the block schema (block_schema.fields.secret_fields). But for raw DB decryption, you can still decrypt the whole payload without needing the schema.
Better option than reading the DB directly
If possible, use the Prefect API/SDK to read the block document instead of querying the table yourself. Prefect will handle decryption for you.
If you want, I can also give you:
- a SQL query to pull both the encrypted block row and the encryption key, or
- a complete Python script that reads from Postgres and decrypts block_document.data end-to-end.Giacomo Chiarella
06/12/2026, 3:15 PMMarvin
06/12/2026, 3:15 PMMarvin
06/12/2026, 3:17 PMPATCH /block_documents/{id} is for updating the block document's stored data and optionally its block_schema_id.
The most important thing is: the body is not the full block document object. It should be an update payload like this:
{
"data": {
"some_field": "new value"
},
"merge_existing_data": true
}
Supported fields in the PATCH body
- `data`: object of block field values to update
- `merge_existing_data`: boolean, default true
- `block_schema_id`: optional, only for migrating to another schema of the same block type
What merge_existing_data does
- true (default): partial update / merge
- `false`: replace existing stored data with exactly what you send
Example:
Current block data:
{
"a": 1,
"b": 2,
"c": 3
}
PATCH body:
{
"data": {
"b": 99
},
"merge_existing_data": true
}
Result:
{
"a": 1,
"b": 99,
"c": 3
}
If instead:
{
"data": {
"b": 99
},
"merge_existing_data": false
}
Result becomes:
{
"b": 99
}
Very common reason it “doesn’t work”
People often send the wrong shape, such as:
- the full block document returned by GET
- top-level fields that belong inside data
- a body missing the data wrapper
For example, this is wrong:
{
"some_field": "new value"
}
This is right:
{
"data": {
"some_field": "new value"
}
}
Success response
On success, Prefect returns:
204 No Content
So you won’t get an updated block back in the response body.
Secrets behavior
If the block has secret fields and you previously fetched it with obfuscated secrets, Prefect tries to preserve those secrets rather than overwrite them with masked values. So patching a block you got from the API is generally safe.
Things you cannot update with this endpoint
You cannot patch:
- name
- is_anonymous
- block_type_id
- metadata like id, created, updated
Possible failure cases
- `404`: block document ID does not exist
- `400`: invalid payload or invalid schema migration
- `409`: invalid referenced block if your data includes block references
Example curl
curl -X PATCH "http://<prefect-api>/block_documents/<BLOCK_DOCUMENT_ID>" \
-H "Content-Type: application/json" \
-d '{
"data": {
"my_field": "updated value"
},
"merge_existing_data": true
}'
If you want, paste the exact payload you’re sending to PATCH /block_documents/{id} and the response/error you get, and I can tell you exactly what’s wrong.Giacomo Chiarella
06/12/2026, 3:27 PMMarvin
06/12/2026, 3:27 PMMarvin
06/12/2026, 3:27 PM204 No Content is the expected success response for a PATCH. It does not return the updated block in the response body.
So if you're sending:
{
"data": {
...
}
}
and getting 204, Prefect accepted the update.
How to verify the change
Do a follow-up GET for that block document and inspect the returned data.
If you're using the API directly, read the same block document again after the PATCH.
A couple of important details:
- With only:
{
"data": {...}
}
Prefect uses merge_existing_data=true by default
- So your payload is merged into the existing block data, not a full replacement
- If you want to fully replace the stored config, send:
{
"data": {
...
},
"merge_existing_data": false
}
Why it may look like nothing changed
A few common reasons:
1. You expected the PATCH response to include the updated block
- It won’t; 204 means success with no body
2. You are re-reading from the DB directly
- block_document.data in the database is encrypted, so you won’t see plain values there
3. You expected omitted keys to be removed
- They are preserved unless you set merge_existing_data to false
4. You are looking at secret fields
- GET responses may obfuscate secret values
In short
- 204 = success
- Your payload shape is valid
- Re-fetch the block to confirm
- Use merge_existing_data: false if you want replacement instead of merge
If you want, I can show you the exact GET endpoint to call afterward, or help confirm whether your update is being merged vs replaced.Giacomo Chiarella
06/12/2026, 3:28 PMMarvin
06/12/2026, 3:28 PMMarvin
06/12/2026, 3:28 PMPATCH returns 204 but the block still looks unchanged in the UI, the most likely cause is this:
You are patching the block document directly, but the UI may be showing a hydrated/typed view or you're updating only a subset while merge behavior preserves the old values.
A few concrete things to check:
1. Confirm you are patching the correct block document ID
It’s easy to patch one block document and inspect another in the UI.
If you can, first GET the block document by ID, then PATCH that same ID, then GET it again.
2. Check whether your update is being merged instead of replaced
If you send:
{
"data": {
"x": 2
}
}
that defaults to:
{
"data": {
"x": 2
},
"merge_existing_data": true
}
So only x changes; everything else remains.
If the UI form derives values from other fields, it may look unchanged unless you replace the full config:
{
"data": {
"x": 2,
"y": 3
},
"merge_existing_data": false
}
3. Make sure your fields belong under data exactly as stored by the block schema
The keys in data must match the block schema fields. If the UI uses different labels than the stored field names, you may be updating a field the UI doesn’t render the way you expect.
4. Remember the DB value is encrypted
If you’re checking block_document.data in Postgres, that won’t be human-readable. You need to verify via the API/UI, not raw DB contents.
5. Secret fields may appear masked
If you update a secret-like field, the UI may still display it as masked, so it can be hard to tell whether it changed.
6. Some blocks may involve nested block references
If part of the visible config in the UI comes from a referenced block, patching this block document’s local data may not affect what you expect.
Best way to debug
Compare the block before and after with GET calls.
Before:
GET /block_documents/{id}
Patch:
PATCH /block_documents/{id}
{
"data": {
...
}
}
After:
GET /block_documents/{id}
If the GET-after response is unchanged, then one of these is true:
- wrong block ID
- wrong field names
- values are being merged and not replacing what you expect
- the visible UI values come from referenced/nested data
If the GET-after response does show the change, then it’s more likely a UI interpretation/caching issue rather than the PATCH failing.
What I’d try next
Send a very obvious change to a non-secret top-level field and force replacement:
{
"data": {
"your_field": "TEST_PATCH_VALUE_123"
},
"merge_existing_data": false
}
Then immediately GET the same block document ID.
If you want, paste:
- the block type
- your PATCH payload
- the GET response before/after
and I can tell you exactly whether the payload shape is correct for that block.Giacomo Chiarella
06/15/2026, 8:28 AMMarvin
06/15/2026, 8:28 AMMarvin
06/15/2026, 8:32 AMparameters = [] → no, not the same; it's invalid
- parameters = None → no, not the same in raw REST; it's invalid
- omitting parameters entirely → valid
- parameters = {} → valid, and functionally the same as omitting it
What the endpoint expects
parameters must be a JSON object / dictionary, not a list.
Valid:
{
"parameters": {
"x": 1
}
}
Also valid:
{
"parameters": {}
}
Invalid:
{
"parameters": []
}
Invalid:
{
"parameters": null
}
Semantics
For `POST /deployments/{deployment_id}/create_flow_run`:
- if parameters is omitted, Prefect treats it like {} and just uses the deployment’s saved parameters
- if parameters is {}, same practical result: no overrides, deployment parameters are used
- if parameters contains keys, those keys override the deployment’s parameters
So:
Equivalent in practice
{}
and
{
"parameters": {}
}
Not equivalent / invalid
{
"parameters": null
}
{
"parameters": []
}
One subtle note about the Python client
If you use the Python client and pass parameters=None, the client typically normalizes that to {} before sending. So in Python client usage, None may appear to work. But in the raw API, null is not valid for this field.
If you want, I can also show the exact request/response examples for:
- raw REST
- Python client
- what happens when deployment defaults are present.Giacomo Chiarella
06/16/2026, 9:35 AMMarvin
06/16/2026, 9:35 AMMarvin
06/16/2026, 9:38 AMScheduled
- workers just won’t pull more runs until slots open up
So there is no built-in “drop if queue full” behavior for work queues.
What you can do instead
Option 1: Use a deployment concurrency limit with CANCEL_NEW
If your goal is “don’t let excess runs wait; discard them”, this is the closest native feature.
With deployment concurrency limits:
- ENQUEUE → extra runs wait
- CANCEL_NEW → extra runs are canceled instead of waiting
This is usually the best fit if the behavior should apply per deployment.
Option 2: Add your own pre-check before creating the run
If you control the caller that creates flow runs, you can:
1. inspect queue/load
2. decide not to call create_flow_run
3. return your own “queue full” behavior
That prevents creation entirely, but it’s custom logic, not server-enforced.
Option 3: Use an automation to cancel newly scheduled runs
You could create an automation that watches for new scheduled runs and cancels them when your capacity condition is met.
This works, but it’s:
- more indirect
- eventually consistent
- less clean than deployment concurrency with CANCEL_NEW
Important distinction
- Work queue concurrency limit = controls how many runs workers receive
- Deployment concurrency limit = controls whether new runs are allowed to proceed
So if you need reject/discard semantics, look at deployment concurrency, not queue concurrency.
There weren’t any matching GitHub issues/discussions I found on a built-in work-queue “discard overflow” mode.
If helpful, I can show you:
- how to configure a deployment concurrency limit with CANCEL_NEW, or
- how to model this when multiple deployments share one queue.Giacomo Chiarella
06/16/2026, 9:50 AMMarvin
06/16/2026, 9:50 AMMarvin
06/16/2026, 9:52 AMconcurrency_limit
- collision_strategy like:
- ENQUEUE
- CANCEL_NEW
That gives you deployment-level control over flow run concurrency.
If you want one shared limit across multiple deployments
There is not a “deployment tag concurrency limit” feature analogous to task tags.
Instead, the usual alternatives are:
1. Global concurrency limits
- good when several deployments share one resource/API/db
- enforced in flow/task code using concurrency primitives
2. Separate deployment concurrency limits
- if each deployment should have its own cap
3. Work pool / work queue limits
- if you want infrastructure-side throttling
- note these do not reject new runs; they just delay execution
Important gotcha
Flow/deployment tags are mainly for organization/filtering. They do not create flow-run concurrency enforcement the way task tags do.
Rule of thumb
- limit tasks by tag → task tag concurrency limits
- limit one deployment’s flow runs → deployment concurrency limit
- limit multiple deployments sharing a resource → global concurrency limit pattern
If you want, I can show:
- an example deployment concurrency config, or
- how to implement a shared cross-deployment limit with global concurrency.Giacomo Chiarella
06/16/2026, 9:55 AMMarvin
06/16/2026, 9:55 AMMarvin
06/16/2026, 9:55 AM"my-shared-limit".
Conceptually:
Deployment A \
Deployment B > all acquire "shared-limit"
Deployment C /
Only N runs can proceed at once.
Pattern
Inside the flow or an early task, acquire a named global concurrency slot.
Example:
from prefect import flow
from prefect.concurrency.sync import concurrency
@flow
def my_flow():
with concurrency("shared-deployment-limit", occupy=1):
# protected section
do_the_work()
Then create the global limit once in Prefect.
This is the usual way to express:
- many deployments
- different queues
- one shared cap
Important behavior difference
This does not prevent flow runs from being created.
It prevents more than the allowed number from entering the protected section at the same time.
So the extra runs will wait there, rather than being rejected at queue creation time.
If you need discard instead of wait, you’d need custom logic, because shared queue/tag-level “cancel new” behavior does not exist out of the box across multiple deployments.
If you want discard semantics across multiple deployments
Your options are more custom:
1. Client-side pre-check before creating runs
- check current occupancy
- skip creating new flow runs if full
2. Automation that cancels new runs
- create runs normally
- cancel them if the shared-cap condition is exceeded
3. Custom first-step logic in the flow
- inspect current shared occupancy
- immediately exit/cancel if above threshold
But for shared throttling across deployments, global concurrency is the cleanest built-in option.
Bottom line
Given:
- multiple deployments
- cannot use one queue
- need one common limit
Use global concurrency limits in flow/task code.
If you want, I can give you a concrete example of:
- how to create the shared global limit, and
- how to structure the flow so the whole run is effectively gated by that limit.Giacomo Chiarella
06/16/2026, 10:36 AMMarvin
06/16/2026, 10:36 AMMarvin
06/16/2026, 10:36 AMtag:my_tag
So to get a tag concurrency limit via API, query the global concurrency limit by that name.
What to look up
For tag my_tag, use:
tag:my_tag
API shape
You want the global concurrency limit endpoints, not a separate “tag limit” endpoint.
If you're calling the REST API directly, the flow is generally:
1. list/filter global concurrency limits
2. find the one whose name is tag:my_tag
If you want to query by exact name, use tag:my_tag.
Important note
This is an implementation detail that is now effectively the real storage model for tag limits in Prefect 3.x.
So:
- task tag concurrency limit my_tag
- underlying global concurrency limit name tag:my_tag
Practical answer
If you see it in the UI under Concurrency → Global concurrency limits as tag:my_tag, that is the object you should read through the API.
If you want, I can give you:
- the exact REST endpoint/body to read it, or
- the Python client code to fetch tag:my_tag.Giacomo Chiarella
06/16/2026, 10:37 AMMarvin
06/16/2026, 10:37 AMMarvin
06/16/2026, 10:37 AMtag:my_tag.
Example request:
POST /api/v2/concurrency_limits/filter
Content-Type: application/json
{
"concurrency_limit": {
"name": {
"any_": ["tag:my_tag"]
}
},
"limit": 10,
"offset": 0
}
Example `curl`:
curl -X POST "https://<your-prefect-api>/api/v2/concurrency_limits/filter" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <API_KEY>" \
-d '{
"concurrency_limit": {
"name": {
"any_": ["tag:my_tag"]
}
},
"limit": 10,
"offset": 0
}'
You should get back the global concurrency limit object for that tag.
If you already know the ID, you can also read it directly by ID from the global concurrency limit endpoints.
Sorry — you were right to ask for the concrete request.Giacomo Chiarella
06/16/2026, 11:03 AM@task(name="run_task", retries=1, retry_delay_seconds=30, retry_jitter_factor=1)
def run_task(run_options: dict, wait_for_running_task: bool = True, stop_running_tasks: bool = True):
I’m running it like thisMarvin
06/16/2026, 11:03 AMMarvin
06/16/2026, 11:03 AMrun_task(...)
or
run_task.submit(...)
or however you’re invoking it, plus what behavior/error you’re seeing.
A few likely things depending on how you’re calling it:
If you call it inside a flow normally
run_task(run_options=my_dict)
that’s valid.
If you submit it asynchronously
run_task.submit(run_options=my_dict)
also valid.
If you pass positional args incorrectly
your signature is:
def run_task(
run_options: dict,
wait_for_running_task: bool = True,
stop_running_tasks: bool = True,
)
So:
- first positional arg = run_options
- second positional arg = wait_for_running_task
- third positional arg = stop_running_tasks
Example:
run_task(my_dict, False, True)
If you’re trying to set retries dynamically at call time
that won’t happen through the function arguments; retries are on the decorator/task config, not normal parameters.
One important gotcha
If this is being called outside a flow, then run_task() will execute, but .submit() requires a flow/task-run context.
Please send:
1. the invocation code
2. whether it’s inside a @flow
3. the exact error or unexpected behavior
and I’ll pinpoint it quickly.Giacomo Chiarella
06/16/2026, 11:04 AM@task(name="run_task", retries=1, retry_delay_seconds=30, retry_jitter_factor=1)
def run_task(run_options: dict, wait_for_running_task: bool = True, stop_running_tasks: bool = True):
I’m running it like this
run_task.submit(run_options, wait_for=[second], tags=tags)
but I’ve got
an unexpected keyword argument 'tags'
how can I pass tags programmatically?Marvin
06/16/2026, 11:04 AMMarvin
06/16/2026, 11:06 AMtags is not a supported argument to .submit().
So this:
run_task.submit(run_options, wait_for=[second], tags=tags)
fails because Prefect treats tags=... as if you're passing a keyword argument to the underlying Python function, and your task function does not have a tags parameter.
How to pass tags programmatically
Use the tags() context manager:
from prefect import tags
with tags(*tags):
run_task.submit(run_options, wait_for=[second])
If tags is a list like:
tags = ["foo", "bar"]
then:
with tags(*tags):
run_task.submit(run_options, wait_for=[second])
will apply those tags to that task run.
If you want tags fixed on the task object itself
You can create a task variant with `with_options()`:
tagged_run_task = run_task.with_options(tags=tags)
tagged_run_task.submit(run_options, wait_for=[second])
Difference between the two approaches
- with tags(...): → dynamic runtime context, usually best for per-call tagging
- with_options(tags=...) → creates a modified task object with those tags
Important note
If your task already has tags from the decorator, runtime tags from with tags(...) are combined with them.
Working example
from prefect import flow, task, tags
@task(name="run_task", retries=1, retry_delay_seconds=30, retry_jitter_factor=1)
def run_task(run_options: dict, wait_for_running_task: bool = True, stop_running_tasks: bool = True):
return run_options
@flow
def my_flow():
second = run_task.submit({"step": 2})
dynamic_tags = ["team-a", "special-run"]
with tags(*dynamic_tags):
third = run_task.submit({"step": 3}, wait_for=[second])
return third.result()
So the direct answer is:
with tags(*tags):
run_task.submit(run_options, wait_for=[second])
If you want, I can also show how to apply different tags to each mapped/submitted task in a loop.Giacomo Chiarella
06/16/2026, 11:07 AMMarvin
06/16/2026, 11:07 AMMarvin
06/16/2026, 11:08 AMwait_for is a special argument to Prefect’s .submit(), not an argument to your task function.
So in:
run_task.submit(run_options, wait_for=[second])
Prefect interprets:
- run_options → argument for your function
- wait_for=[second] → submission metadata telling Prefect about an upstream dependency
It does not pass wait_for into:
def run_task(run_options: dict, wait_for_running_task: bool = True, stop_running_tasks: bool = True):
That’s why it works even though your function has no wait_for parameter.
Why tags fails but wait_for works
Because .submit() explicitly supports wait_for, but does not explicitly support tags.
So:
- wait_for is consumed by Prefect itself
- tags falls through as a normal keyword argument
- then Prefect tries to pass tags to your task function
- your function does not accept tags
- result: unexpected keyword argument 'tags'
Conceptually
.submit() behaves like:
task.submit(
*task_args,
wait_for=[...], # Prefect-controlled
return_state=False, # Prefect-controlled
**task_kwargs # passed to your function
)
So your call:
run_task.submit(run_options, wait_for=[second], tags=tags)
is effectively treated like:
- special submit option: wait_for=[second]
- task kwarg: tags=tags
and that causes the error.
Rule of thumb
When using `.submit()`:
- wait_for and return_state are special Prefect submission options
- everything else should match your task function signature unless documented otherwise
If you want, I can show the mental model for .submit() vs direct function call vs .map() too.Giacomo Chiarella
06/16/2026, 1:57 PMMarvin
06/16/2026, 1:57 PMMarvin
06/16/2026, 1:59 PMPOST /api/v2/concurrency_limits/filter only takes pagination fields like:
{
"limit": 10,
"offset": 0
}
So this part:
"concurrency_limit": {
"name": {
"any_": ["tag:my_tag"]
}
}
is ignored, which is why you get all limits back.
What to use instead
Use the direct read endpoint by name:
GET /api/v2/concurrency_limits/tag:my_tag
Example with `curl`:
curl -X GET "https://<your-prefect-api>/api/v2/concurrency_limits/tag:my_tag" \
-H "Authorization: Bearer <API_KEY>"
That’s the correct way to fetch the global concurrency limit for a tag-backed limit like tag:my_tag.
Two extra notes
1. Your example also uses typographic quotes (“ ”) instead of normal JSON quotes ("). If that was copied literally, it would not be valid JSON.
2. Even with valid JSON quotes, the filter body still would not work for this endpoint because name filtering is simply not supported there.
So the short answer is:
- POST /api/v2/concurrency_limits/filter → list/paginate all
- GET /api/v2/concurrency_limits/tag:my_tag → fetch the specific tag limit
If you want, I can also show the equivalent Python client call.Giacomo Chiarella
06/17/2026, 10:13 AMMarvin
06/17/2026, 10:13 AMMarvin
06/17/2026, 10:15 AMflow_id, not the flow name directly.
So the usual way is:
1. GET /api/deployments/{deployment_id}
2. read flow_id
3. GET /api/flows/{flow_id}
4. read name
Example
Get the deployment:
curl -X GET "https://<your-prefect-api>/api/deployments/<deployment_id>" \
-H "Authorization: Bearer <API_KEY>"
That response includes something like:
{
"id": "...",
"name": "my-deployment",
"flow_id": "12345678-1234-1234-1234-123456789abc"
}
Then fetch the flow:
curl -X GET "https://<your-prefect-api>/api/flows/12345678-1234-1234-1234-123456789abc" \
-H "Authorization: Bearer <API_KEY>"
Response:
{
"id": "12345678-1234-1234-1234-123456789abc",
"name": "my-flow"
}
So the flow name is:
my-flow
Important note
The deployment response does not include flow_name directly, only flow_id.
If you want, I can also show:
- how to do this in one small Python snippet, or
- how to get a deployment by flow_name/deployment_name instead.Giacomo Chiarella
06/17/2026, 10:37 AMMarvin
06/17/2026, 10:37 AMMarvin
06/17/2026, 10:37 AMflow_name/deployment_name, or
2. just the deployment’s local name like prod
That distinction matters.
If you have the full name flow_name/deployment_name
Then you already have the flow name — it’s the part before the /.
Example:
- full deployment name: my-flow/prod
- flow name: my-flow
- deployment name: prod
If you only have the local deployment name
Example: just prod
Then there is not a direct unique lookup by deployment name alone, because deployment names are scoped to flows. Multiple flows can each have a deployment named prod.
So in that case you need to:
1. list/filter deployments
2. find the matching deployment(s) by name
3. read the flow_id
4. fetch the flow(s) by flow_id
5. read the flow name
REST approach
List deployments:
POST /api/deployments/filter
Example:
curl -X POST "https://<your-prefect-api>/api/deployments/filter" \
-H "Authorization: Bearer <API_KEY>" \
-H "Content-Type: application/json" \
-d '{
"limit": 100,
"offset": 0
}'
Then filter client-side for:
.name == "prod"
For each match, take its flow_id, then call:
GET /api/flows/{flow_id}
Example:
curl -X GET "https://<your-prefect-api>/api/flows/<flow_id>" \
-H "Authorization: Bearer <API_KEY>"
Why this is necessary
In Prefect 3.x:
- deployment objects have name
- deployment objects have flow_id
- deployment objects do not include flow_name
- and deployment name alone is not guaranteed globally unique
So the short answer
- if you have flow_name/deployment_name, split on /
- if you only have deployment_name, you must search deployments, then follow flow_id to the flow
If you want, I can give you:
- a jq one-liner for this, or
- a Python snippet that takes a deployment name and returns all matching flow names.Giacomo Chiarella
06/17/2026, 10:39 AMMarvin
06/17/2026, 10:39 AMMarvin
06/17/2026, 10:41 AMPOST /api/deployments/filter by filtering on the deployment’s name field only.
If you want all deployments whose deployment name part is my_deployment, use:
curl -X POST "https://<your-prefect-api>/api/deployments/filter" \
-H "Authorization: Bearer <API_KEY>" \
-H "Content-Type: application/json" \
-d '{
"deployments": {
"name": {
"any_": ["my_deployment"]
}
},
"limit": 100,
"offset": 0
}'
That filters on Deployment.name, not flow_name/deployment_name as a combined string.
If you want partial matching instead
Use `like_`:
curl -X POST "https://<your-prefect-api>/api/deployments/filter" \
-H "Authorization: Bearer <API_KEY>" \
-H "Content-Type: application/json" \
-d '{
"deployments": {
"name": {
"like_": "my_deployment"
}
},
"limit": 100,
"offset": 0
}'
Useful note
The response will include each matching deployment’s flow_id, so if you want the flow name too, you then call:
GET /api/flows/{flow_id}
So the direct answer is: yes, server-side filtering by deployment name alone is supported.
If you want, I can also give you a jq pipeline that takes the filtered deployments and prints flow_name/deployment_name.Giacomo Chiarella
06/18/2026, 10:38 AMMarvin
06/18/2026, 10:38 AMMarvin
06/18/2026, 10:40 AM3.6.4, prefect.workers.base imports:
from importlib_metadata import distributions
but importlib_metadata may not actually be installed in your environment, which causes:
ModuleNotFoundError: No module named 'importlib_metadata'
Best fix
Upgrade Prefect to a version where this was fixed. In newer versions, Prefect uses the stdlib import instead.
pip install --upgrade "prefect>=3.7.2"
If you need to stay on 3.6.4
Install the missing package explicitly:
pip install importlib_metadata
That should unblock the CLI immediately.
Why this happens on Python 3.11
Python 3.11 already has importlib.metadata in the standard library, so the backport package should not be necessary. But Prefect 3.6.4 still imports the backport package name in this code path.
Good next steps
1. If possible, upgrade Prefect:
pip install --upgrade prefect
2. If you must stay pinned to 3.6.4, install:
pip install importlib_metadata
3. If this is inside a service/container, rebuild or restart after installing so the prefect executable sees the updated environment.
I didn’t find an existing GitHub issue for this exact error, but I did verify the import behavior and that it was changed in later Prefect versions.
If you want, I can help you determine the safest exact upgrade target from 3.6.4, or help check whether your service is installing Prefect into a different Python environment than the one you updated.Giacomo Chiarella
06/18/2026, 11:08 AMGiacomo Chiarella
06/18/2026, 11:09 AMMarvin
06/18/2026, 11:09 AMMarvin
06/18/2026, 11:10 AMfastapi>=0.111.0,<1.0.0
So any FastAPI version in that range is compatible by Prefect’s package constraints.
Related constraints Prefect 3.7.4 also declares
- pydantic>=2.10.1,<3.0.0
- excluding 2.11.0 through 2.11.4
- starlette>=1.0.1
Recommendation
If possible, don’t pin FastAPI separately unless you have to. Let prefect==3.7.4 resolve its own compatible FastAPI version:
pip install prefect==3.7.4
If you do need to pin FastAPI manually, stay within:
fastapi>=0.111.0,<1.0.0
If you want, I can also help you find a specific FastAPI + Starlette + Pydantic combination that’s safest with Prefect 3.7.4.Giacomo Chiarella
06/18/2026, 11:13 AMMarvin
06/18/2026, 11:13 AMMarvin
06/18/2026, 11:16 AMAttributeError: 'PrefectRouter' object has no attribute 'routes'
matches that exact breakage.
What’s happening
Prefect 3.7.4 assumes older FastAPI router behavior. FastAPI 0.137.0+ changed router inclusion internals, and that breaks Prefect 3.7.4 at request time.
What to do
Best option: upgrade Prefect
Upgrade to a version that contains the compatibility fix:
pip install --upgrade "prefect>=3.7.5"
If you must stay on Prefect 3.7.4
Pin FastAPI below 0.137.0, for example:
pip install "prefect==3.7.4" "fastapi==0.136.3"
That is the safest short-term workaround.
Recommended version pairings
- prefect==3.7.4 + fastapi==0.136.3 → should work
- prefect>=3.7.5 + fastapi>=0.137.0 → should work
- prefect==3.7.4 + fastapi==0.137.0+ → broken
Check what you currently have
pip show prefect fastapi starlette
If this is in Docker / container build
Make sure your image is not installing a newer FastAPI after Prefect. If you install both explicitly, pin them together.
Example:
pip install "prefect==3.7.4" "fastapi==0.136.3"
or better:
pip install --upgrade "prefect>=3.7.5"
Related issue
There is a matching GitHub issue here:
fastapi 0.137 compatibility: AttributeError: 'PrefectRouter' object has no attribute 'routes'
And another related report:
Broken test harness due to missing routes attribute in PrefectRouter
If you want, I can help you build a fully pinned compatible set for:
- Prefect
- FastAPI
- Starlette
- PydanticGiacomo Chiarella
06/18/2026, 1:26 PMMarvin
06/18/2026, 1:26 PMMarvin
06/18/2026, 1:28 PMurlsplit(db_url_str)
Python raises:
ValueError: Invalid IPv6 URL
Most likely cause
If your DB host is IPv6, this is invalid:
<postgresql+asyncpg://user:pass@2001:db8::1:5432/prefect>
and this is valid:
<postgresql+asyncpg://user:pass@[2001:db8::1]:5432/prefect>
IPv6 addresses in URLs must be wrapped in [ and ].
What to check
Look at your Prefect server DB setting, typically:
- PREFECT_API_DATABASE_CONNECTION_URL
or whatever is setting the server database URL in your container/service.
Examples
Invalid:
<postgresql://user:password@::1:5432/prefect>
<postgresql+asyncpg://user:password@2001:db8::1:5432/prefect>
Valid:
<postgresql://user:password@[::1]:5432/prefect>
<postgresql+asyncpg://user:password@[2001:db8::1]:5432/prefect>
Why it shows up in automations
That log line is from the automation change listener, which uses a Postgres notification connection. So the main symptom appears there, but the real issue is the DB URL format.
What to do
Update your DB connection string so any IPv6 host is bracketed.
If your host is not IPv6 and is instead a hostname, then something else may be malformed in the URL. In that case, paste the URL with credentials redacted, like:
<postgresql+asyncpg://USER:***@HOST:5432/DBNAME>
and I can sanity-check it.
No matching GitHub issue/discussion showed up for this exact message, but the root cause is clear from the parser behavior and Prefect’s DB listener code.Giacomo Chiarella
06/18/2026, 1:31 PMMarvin
06/18/2026, 1:31 PMMarvin
06/18/2026, 1:31 PMPREFECT_API_DATABASE_CONNECTION_URL.
Common causes with an RDS hostname
1. Password contains special characters and is not URL-encoded
This is the most common cause.
For example, if your password contains characters like:
- @
- :
- /
- #
- ?
then a URL like this can break parsing:
<postgresql+asyncpg://user:pa:ss@word@mydb.xxxxx.us-east-1.rds.amazonaws.com:5432/prefect>
because the parser can no longer reliably tell what is password vs host vs port.
2. Extra colon or slash in the host portion
For example:
<postgresql+asyncpg://user:pass@mydb.xxxxx.us-east-1.rds.amazonaws.com:5432:5432/prefect>
3. The URL was templated incorrectly
For example, missing username/password separation or accidental concatenation.
4. Smart quotes / copied formatting
If the value was copied from somewhere with transformed punctuation, parsing can fail in strange ways.
What a valid RDS URL should look like
<postgresql+asyncpg://USERNAME:PASSWORD@mydb.xxxxx.us-east-1.rds.amazonaws.com:5432/prefect>
If the password has special characters, it must be URL-encoded.
Example:
- raw password:
p@ss:word/123
- encoded password:
p%40ss%3Aword%2F123
Then the URL becomes:
<postgresql+asyncpg://USERNAME:p%40ss%3Aword%2F123@mydb.xxxxx.us-east-1.rds.amazonaws.com:5432/prefect>
What I’d check first
Print or inspect the exact value of PREFECT_API_DATABASE_CONNECTION_URL being used by the server process, with the password redacted.
It should look like:
<postgresql+asyncpg://USER:***@HOST:5432/DBNAME>
Quick sanity checklist
- exactly one @ separating credentials from host
- exactly one :5432 port section
- no unescaped special characters in password
- no surrounding quotes accidentally included in the environment variable
- no whitespace/newline in the value
Very likely fix
If your password contains special characters, URL-encode it.
In Python, for example:
from urllib.parse import quote_plus
print(quote_plus("p@ss:word/123"))
Or if you build it manually, encode the password before inserting it into the URL.
If you want, paste the connection URL with the password redacted but preserving structure, for example:
<postgresql+asyncpg://myuser:***@mydb.xxxxx.us-east-1.rds.amazonaws.com:5432/prefect>
and I can tell you whether the shape is valid.Giacomo Chiarella
06/18/2026, 1:35 PMMarvin
06/18/2026, 1:35 PMMarvin
06/18/2026, 1:35 PMaaaaaaaa*[{%a<=*aaa
The key issue is the [ character.
In a URL, [ and ] are special because they’re used for IPv6 host notation. When they appear unescaped inside the credential section, Python’s URL parser can get confused and raise:
Invalid IPv6 URL
You also have other characters that should be encoded in a URL password, including:
- [
- %
- <
- =
So the fix is: URL-encode the password before putting it in the connection string.
Your current URL
<postgresql+asyncpg://postgres:aaaaaaaa*[{%a<=*aaa@rdsname.part1.part2.rds.amazonaws.com:5432/my_db>
Correct approach
Encode only the password portion.
In Python:
from urllib.parse import quote
password = "aaaaaaaa*[{%a<=*aaa"
print(quote(password, safe=""))
That will produce an encoded password you can safely place in the URL.
It will look something like:
aaaaaaaa%2A%5B%7B%25a%3C%3D%2Aaaa
So your fixed connection URL would be:
<postgresql+asyncpg://postgres:aaaaaaaa%2A%5B%7B%25a%3C%3D%2Aaaa@rdsname.part1.part2.rds.amazonaws.com:5432/my_db>
Why this happens
Even though the host is an RDS hostname, the parser sees special URL characters in the password and misinterprets the whole string structure before it ever gets to the host correctly.
What to do now
Replace the password in PREFECT_API_DATABASE_CONNECTION_URL with the URL-encoded version and restart Prefect.
If you want, I can encode that exact password for you directly in the full DSN format.Giacomo Chiarella
06/18/2026, 1:36 PMMarvin
06/18/2026, 1:36 PMMarvin
06/18/2026, 1:36 PMprefect.server.utilities.postgres_listener.get_pg_notify_connection()
and specifically:
urlsplit(db_url_str)
That path is used by the automations/events listener, and in 3.7.4 it’s parsing the DB URL in a way that now trips over the unescaped [ in your password.
So the short version is:
- your connection string was already technically invalid as a URL
- older Prefect versions happened not to hit this parsing path the same way
- 3.7.4 does, so the bad password encoding is now exposed
Why it may have seemed fine before
A few reasons this can happen:
1. Different code path
- normal DB engine creation may have tolerated or differently parsed the URL
- the automation listener now separately parses it for PG notify
2. Stricter parser behavior
- urllib.parse.urlsplit() is less forgiving about URL structure
- especially around [ / ], which are reserved for IPv6 literals
3. New automations/events behavior
- if the listener wasn’t active before, or wasn’t using the same helper path, you may never have hit this failure
So this is less “3.7.4 broke a valid URL” and more “3.7.4 exposed an invalid DSN that previously slipped by.”
The important takeaway
Your password must be URL-encoded in the DSN. The [ is enough to cause exactly this issue.
So even if 3.6.4 happened to work, I would still recommend fixing the connection string to:
<postgresql+asyncpg://postgres:<url-encoded-password>@rdsname.part1.part2.rds.amazonaws.com:5432/my_db>
If you want, I can give you the exact corrected DSN with your password encoded based on the string you pasted.Giacomo Chiarella
06/26/2026, 8:32 AMMarvin
06/26/2026, 8:32 AMMarvin
06/26/2026, 8:35 AMSubmitting is a state name, not a new StateType.
In Prefect 3.x, states have two layers:
1. type — one of the enum values in StateType
2. name — a display/semantic label, which can be more specific
So when you see Submitting, it is typically:
type = PENDING
name = "Submitting"
That’s why you don’t see SUBMITTING in the StateType enum.
Why Prefect does this
StateType stays small and represents the fundamental orchestration categories:
- SCHEDULED
- PENDING
- RUNNING
- COMPLETED
- FAILED
- CANCELLED
- CRASHED
- PAUSED
- CANCELLING
Then Prefect uses named sub-states for more detail.
So for example:
- Pending → type PENDING
- Submitting → type PENDING
- InfrastructurePending → type PENDING
Similarly for scheduled states, you may see names like:
- Scheduled
- AwaitingRetry
- AwaitingConcurrencySlot
all of which can still be type = SCHEDULED.
Why you see it during job execution
Submitting is used when Prefect is in the phase of handing the run off to infrastructure / worker execution machinery.
So the flow is roughly:
Scheduled -> Pending(name="Submitting") -> Pending(name="InfrastructurePending") -> Running
The exact path can vary, but Submitting is part of that transitional handoff.
Why the UI shows it
The UI/API expose both state.type and state.name, and the UI generally shows the name, because it’s more informative than just PENDING.
So you may see:
{
"type": "PENDING",
"name": "Submitting"
}
Short answer
Submitting is not missing from `StateType`; it was never meant to be there. It is a named PENDING state.
If you want, I can also give you a quick table of the common Prefect named states and their underlying StateType.Giacomo Chiarella
06/26/2026, 8:36 AMMarvin
06/26/2026, 8:36 AMMarvin
06/26/2026, 8:36 AMStateType, not the human-readable state name.
Why
State names are more specific and can change or expand over time:
- Submitting
- InfrastructurePending
- AwaitingRetry
- AwaitingConcurrencySlot
But their underlying StateType remains the stable orchestration category:
- PENDING
- SCHEDULED
- etc.
So for most programmatic logic, StateType is the more robust choice.
Good use cases for StateType
- “is this run terminal?”
- “is this run failed?”
- “is this run still waiting to execute?”
- “is this run actively running?”
When state names are useful
Use state.name only if you specifically care about a finer-grained sub-state.
For example:
- distinguish AwaitingRetry from generic Scheduled
- distinguish Submitting from other PENDING states
- detect AwaitingConcurrencySlot
So:
- business/orchestration logic → prefer StateType
- UI messaging / special-case operational branching → maybe inspect state.name
Practical advice
If you do rely on state.name, treat it as a narrower, more fragile condition than StateType.
So yes: basing your core logic on StateType is the safer design.