<@ULVA73B9P> if I have the following orchestration...
# marvin-ai
m
@Marvin if I have the following orchestration happening: Flow that calls .map to start an array of tasks. Those tasks spawn a new flow. Will the first flow wait on all child flows to complete before marking it as complete?
m
thought for 377.6 seconds
Short answer: not automatically. In Prefect 3.x: -
task.map([...])
returns a list of futures. The parent flow only waits for futures that you: - explicitly wait on (
.wait()
or
.result()
), or - include in the flow’s return value, or - chain into downstream tasks that the flow waits on. If your mapped tasks call a subflow: - Calling a subflow from within a task is synchronous for that task — the task won’t finish until the subflow finishes. - However, the parent flow will only wait if it’s waiting on those task futures (per the rules above). If you submit mapped tasks and ignore the futures, the flow can finish before those task/subflow runs complete. To ensure the parent waits for all child subflows started by mapped tasks, do one of:
Copy code
futs = my_task.map(items)
futs.wait()  # waits for all tasks (and thus their subflows)
or return the futures:
Copy code
return my_task.map(items)  # flow will wait as it resolves returned futures
or chain them into downstream work the flow awaits. Clarifying question: when you say the tasks “spawn a new flow,” are you calling another @flow directly inside the task, or triggering a deployment with
run_deployment
? If it’s deployments, you can control waiting with the
timeout
parameter (e.g., wait by default/with
timeout=None
, or fire-and-forget with
timeout=0
).
m
@Marvin If I have the following:
Copy code
@flow
def c():
    sleep(5)
    return "done"

@task
def b():
    return c()


@flow(
    name="test-flow",
)
def a():
    b()
How would I ensure a wait for complete of all of c?
m
thought for 60.5 seconds
In your example,
a()
calls the task
b()
and then exits without using the returned future, so it may finish before
b
(and therefore
c
) completes. Use one of these patterns to ensure
a
waits for
c
to finish: 1) Return the task call (simplest)
Copy code
@flow
def a():
    return b()  # Prefect will wait and resolve this before marking the flow complete
2) Explicitly wait on the task future
Copy code
@flow
def a():
    fut = b()   # returns a PrefectFuture
    fut.wait()  # block until b (and its subflow c) completes
Or, if you need the value:
Copy code
@flow
def a():
    result = b().result()
    return result
3) Call the subflow directly from the flow (bypasses the task wrapper)
Copy code
@flow
def a():
    return c()  # subflow calls are synchronous; a waits for c to finish
Because
b
returns
c()
, waiting on
b
ensures the subflow
c
has fully completed.
m
@Marvin Do you know why the "runs" view in the UI doesn't load anymore when I made the recommended change above?
m
thought for 205.6 seconds
Most likely because the flow is now returning a task future/state (e.g.,
return b()
), which makes the flow’s result a nested State structure instead of simple data. The UI “Runs” list can struggle to render unexpectedly nested State payloads and may appear to not load. Two safe fixes: - Return plain data, not the future/state:
Copy code
@flow
def a():
    return b().result()  # ensures the flow returns "done", not a State
- Or wait but return something simple:
Copy code
@flow
def a():
    fut = b()
    fut.wait()           # waits for c
    return "ok"          # simple return value for the UI
Alternatively, call the subflow directly:
Copy code
@flow
def a():
    return c()           # returns "done" directly
Could you share which exact change you made (e.g.,
return b()
vs
b().result()
), your Prefect version, and whether the single run detail page loads? If you can, also check your browser dev tools Network/Console when opening the Runs page for any API errors—those clues will confirm this is a result-payload rendering issue.
m
@Marvin Why is the "Graph" tab always empty? The "Logs" tab populate but I have nothing in the "Graph" tab
m
thought for 141.0 seconds
The Graph tab only renders when the flow run has TaskRun/SubflowRun records to draw. If there are no task/subflow nodes meeting the query criteria, you’ll see logs but an empty graph. Common reasons it’s empty: - No @task calls executed. A flow that just logs/prints or calls plain Python functions won’t create TaskRun records, so the graph is empty. - Early return before tasks start. If the flow exits before any task begins (tasks remain PENDING), the query excludes them and the graph is empty. - Tasks never reached a start/expected_start time. The graph query filters out nodes that have neither start_time nor expected_start_time. - Only subflow called directly without a task wrapper and no other tasks. Direct subflow calls will render as a node only if they’re linked via a parent task run; otherwise you may not see structure. - Custom/unsupported task runner that doesn’t report TaskRuns. - Very large graphs filtered by time window (rare) or capped by server limits. How to verify/fix: - Ensure you’re actually executing tasks:
Copy code
from prefect import flow, task

  @task
  def t(x): return x + 1

  @flow
  def f():
      futs = t.map([1,2,3])  # creates TaskRun records
      futs.wait()
      return futs.result()
- If you call subflows, call them from a task or from the flow but also have at least one task so there’s a parent/child relationship to render. - Check a specific run’s Graph tab (not just the list) and confirm TaskRuns exist under the “Tasks” section of the run details. - Confirm your Prefect version and server/UI are up to date. - If you still see nothing, open browser dev tools on the run page and check for errors fetching GET /flow_runs/{id}/graph-v2. That response will indicate why there are no nodes. If you can share a minimal flow that reproduces the empty graph (your current flow code and Prefect version), I can point out exactly why it yields no nodes and how to adjust it.
m
@Marvin Minimal flow:
Copy code
@flow
def c():
   sleep(5)
   return "done"

@task
def b():
    return c(integration=integration)


@flow(
    name="a",
)
def a():
    get_run_logger().info("Starting Loopio sync flow.")

    connected_integrations = tasks.b()

    fut = b.map()

    fut.wait()

    return fut.result()
m
thought for 64.5 seconds
Thanks for the snippet — a few things here can make the Graph tab look empty. What the Graph tab shows - It renders TaskRun and linked SubflowRun nodes. If your run doesn’t create TaskRun records (or subflows aren’t linked), the graph will be empty even though logs exist. What I see in your example -
c()
is a flow with no tasks inside; it just sleeps. Its own Graph tab will be empty by design. If you want a node in
c
, wrap the work in a task. - You’re calling
c()
from inside a task (
b
). Even if that does create a subflow run, you generally won’t see a separate “c” node in the parent flow’s graph when the subflow is invoked from within a task. The graph will show the task (
b
) node, not a separate subflow node for
c
. - In
a()
, you call
tasks.b()
, which looks like a different function than your
@task def b(...)
above. If
tasks.b()
isn’t decorated with
@task
, it won’t create any TaskRun nodes. - The mapped function
start_etl_for_connected_integration
must be an
@task
. If it’s a plain function,
.map
on it won’t create TaskRuns, and the graph will be empty. How to make the graph populate - Ensure anything you want to appear as a node is an
@task
(or a subflow called from a flow). - If
c
is meant to be a subflow, call it from the flow
a
(not from
b
) or put its work in a task. Example A: keep c as a flow but give it a task to render, and call it from the flow
Copy code
from time import sleep
from prefect import flow, task, get_run_logger

@task
def wait_task():
    sleep(5)

@flow
def c():
    wait_task()

@task
def start_etl_for_connected_integration(integration):
    # do work here
    return f"done {integration}"

@flow(name="a")
def a():
    logger = get_run_logger()
    <http://logger.info|logger.info>("Starting Loopio sync flow.")

    connected_integrations = ["int-1", "int-2"]  # or produce via a task

    # Call subflow directly from the flow (shows as a node)
    c()

    futs = start_etl_for_connected_integration.map(integration=connected_integrations)
    futs.wait()
    return futs.result()
Example B: if c doesn’t need to be a flow, make it a task
Copy code
from time import sleep
from prefect import flow, task, get_run_logger

@task
def c():
    sleep(5)

@task
def start_etl_for_connected_integration(integration):
    # do work here
    return f"done {integration}"

@flow(name="a")
def a():
    get_run_logger().info("Starting Loopio sync flow.")

    connected_integrations = ["int-1", "int-2"]  # or produce via a task

    # This will appear in the graph
    c()

    futs = start_etl_for_connected_integration.map(integration=connected_integrations)
    futs.wait()
    return futs.result()
Debug checklist - Is
start_etl_for_connected_integration
decorated with
@task
? If not, it won’t show. - Is
tasks.b()
actually your
@task def b(...)
? If not, it won’t show. - On the run details page, do you see TaskRuns listed under “Tasks”? If not, the graph will be empty. - In browser dev tools, check the response of GET /flow_runs/{id}/graph-v2 — if nodes is an empty list, the API isn’t finding any TaskRuns for that run. If you can share the definitions of
tasks.b
and
start_etl_for_connected_integration
, and confirm your Prefect version, I can pinpoint the exact reason your graph is empty.
m
@Marvin here is a sample:
Copy code
@task
def task_a(val):
    return val + 1

@task
def task_b(val):
    return val * 2


@flow
def c(target):
    result1 = task_a(target)
    result2 = task_b(result1)
    return result2

@task
def b(target):
    # do work here
    return c(target)

@flow(name="a")
def a():
    connected_integrations = ["int-1", "int-2"]  # or produce via a task
    futs = b.map(connected_integrations)
    futs.wait()
    return futs.result()
m
thought for 69.7 seconds
Thanks for the concrete sample — that helps. What you should expect with that code - Flow c’s own Graph: should show two nodes with an edge task_a -> task_b. - Flow a’s Graph: you’ll see one node per mapped call to b. You will not see c’s inner tasks on a’s graph because c is being called from inside a task. To see c’s DAG, click into the subflow run for c. Why your Graph might be empty - Mapping call looks positional; prefer keyword mapping so Prefect definitely constructs TaskRun records:
Copy code
futs = b.map(target=connected_integrations)
- If b isn’t actually the same @task you showed (e.g., calling something like tasks.b that’s not decorated), there will be no TaskRun nodes for a. - If c has no tasks (in your earlier examples it only slept), its Graph will be empty by design. - If nothing in a creates TaskRun records (e.g., b isn’t a task or mapping didn’t run), the a Graph will be empty even though logs exist. How to make the Graph clearly populate - Keep your sample but use keyword mapping:
Copy code
@task
  def task_a(val): return val + 1

  @task
  def task_b(val): return val * 2

  @flow
  def c(target):
      r1 = task_a(target)
      r2 = task_b(r1)
      return r2

  @task
  def b(target):
      return c(target)

  @flow(name="a")
  def a():
      connected_integrations = ["int-1", "int-2"]
      futs = b.map(target=connected_integrations)  # keyword arg
      futs.wait()
      return futs.result()
- Graph for a: 2 task nodes (b[0], b[1]). - Graph for each c run: task_a -> task_b. If you want subflow c to appear as nodes in a’s Graph, call it from the flow (not inside a task):
Copy code
@flow(name="a")
def a():
    connected_integrations = ["int-1", "int-2"]
    for integ in connected_integrations:
        c(integ)  # subflow nodes will appear on a’s graph
Note: flows don’t support .map — use a loop. Quick debug checklist - In the run details for a, do you see TaskRuns listed? If not, the graph will be empty. - Confirm
b
is the same @task you defined (not a different module’s function). - Open your browser dev tools on the run page and check the response for GET /flow_runs/{id}/graph-v2. If
nodes: []
, the API isn’t finding any TaskRuns for that run, which usually means the task wasn’t invoked as a Prefect task (or mapping didn’t submit). If this still shows an empty Graph, share your Prefect version and a specific run ID; we can inspect what the API returns for
/flow_runs/{id}/graph-v2
and pinpoint the cause.
m
@Marvin I am not actually seeing an API call to graph. Is this just a bug in the UI?
m
thought for 43.2 seconds
It might be, but there are a couple likely causes that can make it look like a UI bug: Most common root causes - No graph request is fired because the page errors earlier. If the run detail page throws a JS error while parsing the run’s state/result, the Graph component never mounts and you won’t see a GET /flow_runs/{id}/graph-v2 request. - Returning nested State objects. In your sample,
c
returns
result2
which is a task future; Prefect resolves that into a State. Since
b
returns
c(target)
and you map
b
, the task runs may end up returning State objects nested inside State records. That can trip up the UI before it even asks for the graph. - Version mismatch (self-hosted). If your UI and API versions don’t match, the UI may not call the v2 graph endpoint (or may fail before it does). What to try right now 1) Make sure flows/tasks return plain data, not futures/states, to avoid nested State payloads:
Copy code
@flow
def c(target):
    r1 = task_a(target)
    r2 = task_b(r1)
    return r2.result()  # ensure c returns data, not a future/state

@task
def b(target):
    return c(target)    # now returns plain data
2) Confirm mapped tasks are actually Prefect tasks:
Copy code
futs = b.map(target=connected_integrations)  # keyword mapping
futs.wait()
return futs.result()
With the change above, you should see TaskRuns for b on a’s graph, and c’s inner graph on c’s run page. 3) Check the Network/Console on the run page: - In browser dev tools, select the Graph tab for a specific run, then look for: - GET /flow_runs/<run_id>/graph-v2 - Any Console errors before the graph request fires (these usually point to result/state parsing issues). 4) If self-hosted, confirm versions match: -
prefect version
on the server - UI build version (footer or /about) - If they differ, upgrade to the latest 3.x on both and try again. If after returning plain data you still don’t see a graph request, please share: - Prefect version and whether you’re on Cloud or self-hosted - A specific run ID and a screenshot or text of any Console error - The Network response (or lack thereof) for /flow_runs/<id>/graph-v2 With that, I can tell you whether it’s a UI bug we should escalate or a data/compatibility issue.
m
@Marvin index-Cfbo6U5e.es:2 Uncaught (in promise) Error: CanvasRenderer is not yet implemented Is this is a known issue?
m
thought for 40.8 seconds
Thanks for grabbing the console error. “CanvasRenderer is not yet implemented” is coming from the front-end graph renderer and it explains why you’re not seeing any request to /flow_runs/{id}/graph-v2 — the Graph component is erroring before it mounts. I couldn’t find an existing report of this in our tracker, so this looks like a UI bug. A few quick things to try while we investigate: - Hard refresh to bust the cached UI assets (Ctrl/Cmd+Shift+R), or open the UI in a fresh Incognito window. If self-hosted, clear your browser cache and reload. - Try a different browser (Chrome vs Firefox) to rule out a browser-specific rendering/regression. - Reduce the repro: run a tiny flow with a single @task and open its Graph tab. If that works, the error may be data-dependent. If it persists, could you share: - Cloud or self-hosted? Prefect version(s) if self-hosted (server/api and UI build). - Browser and OS versions. - Whether the error happens on every run or only certain runs. - The full console stack trace around “CanvasRenderer is not yet implemented”. - A minimal flow that reproduces it. With that, I can file a bug for the UI team. You can also open one directly here: Create a Prefect bug report. In the meantime, you can still use the Logs and Tasks/States views to monitor your runs; this issue is limited to the Graph rendering path.