Skip to content

Injection Patterns

This page explains how fastapi-taskflow integrates with FastAPI's dependency injection system and which pattern fits your situation.

Why injection matters

FastAPI's BackgroundTasks type gets special treatment. When FastAPI sees that annotation in a route signature, it creates the instance directly, bypassing the normal dependency injection graph entirely. That means dependency_overrides cannot intercept it.

fastapi-taskflow needs to intercept that moment so the injected instance is a ManagedBackgroundTasks instead of a plain BackgroundTasks. That is what install() does: it replaces the class reference FastAPI uses internally so every injection point in your app receives a managed instance.

There are three ways to wire this up. All three produce a ManagedBackgroundTasks instance and behave identically at runtime.

Pattern 1: Native BackgroundTasks annotation

This is the recommended starting point for existing codebases. No import changes, no signature changes.

Requires auto_install=True on TaskAdmin, or a call to task_manager.install(app).

from fastapi import BackgroundTasks

@app.post("/signup")
def signup(email: str, background_tasks: BackgroundTasks):
    task_id = background_tasks.add_task(send_email, address=email)
    return {"task_id": task_id}

Because install() patches the class FastAPI uses to create the instance, your annotation does not need to change. The object you receive is already a ManagedBackgroundTasks.

Pattern 2: Explicit ManagedBackgroundTasks annotation

Requires auto_install=True or task_manager.install(app).

from fastapi_taskflow import ManagedBackgroundTasks

@app.post("/webhook")
def webhook(background_tasks: ManagedBackgroundTasks):
    task_id = background_tasks.add_task(process_webhook, {"event": "order.created"})
    return {"task_id": task_id}

Use this when you want the annotation to communicate intent clearly, or when your type checker needs to know that add_task returns a str (the task UUID).

Why install() is still required

Annotating ManagedBackgroundTasks alone is not enough. FastAPI ignores the subclass and injects the base type anyway. The patch from install() is still what makes the injected object a managed instance.

Pattern 3: Explicit Depends

This pattern does not need install() at all. It goes through the normal FastAPI dependency graph, so no process-wide patch is applied.

from fastapi import Depends
from fastapi_taskflow import TaskManager

task_manager = TaskManager()

@app.post("/signup")
def signup(email: str, tasks=Depends(task_manager.get_tasks)):
    task_id = tasks.add_task(send_email, address=email)
    return {"task_id": task_id}

Choose this when you cannot apply the process-wide patch, for example when you run multiple FastAPI apps in a single process and only want managed injection on one of them.

Pattern summary

Pattern install() required Type checker sees return type
background_tasks: BackgroundTasks Yes No (base type)
background_tasks: ManagedBackgroundTasks Yes Yes
tasks=Depends(task_manager.get_tasks) No Only if annotated explicitly

Multi-level dependencies

FastAPI lets you declare BackgroundTasks at multiple levels: in a route, in a dependency, and in sub-dependencies. fastapi-taskflow supports the same, with behaviour that depends on which pattern you use.

With install() active

FastAPI creates one BackgroundTasks instance per request and reuses it at every injection point. Because install() replaces the class used to create that instance, every BackgroundTasks annotation at every level receives the same ManagedBackgroundTasks object.

def notify_service(background_tasks: BackgroundTasks):
    background_tasks.add_task(send_notification, ...)   # managed

@app.post("/signup")
def signup(background_tasks: BackgroundTasks, svc=Depends(notify_service)):
    background_tasks.add_task(send_email, ...)          # managed, same instance

With Depends(task_manager.get_tasks)

FastAPI caches dependency results within a request. If multiple levels declare Depends(task_manager.get_tasks), FastAPI calls get_tasks once and passes the same instance everywhere.

def notify_service(tasks=Depends(task_manager.get_tasks)):
    tasks.add_task(send_notification, ...)

@app.post("/signup")
def signup(tasks=Depends(task_manager.get_tasks), svc=Depends(notify_service)):
    tasks.add_task(send_email, ...)   # same instance as notify_service receives

The mixed case to avoid

Mixing patterns without install() causes silent data loss

If a sub-dependency uses Depends(task_manager.get_tasks) but the route declares BackgroundTasks without install() active, the two are different objects. Tasks added through the native annotation are not tracked.

# install() is NOT active in this example

def notify_service(tasks=Depends(task_manager.get_tasks)):
    tasks.add_task(send_notification, ...)   # managed, tracked

@app.post("/signup")
def signup(background_tasks: BackgroundTasks, svc=Depends(notify_service)):
    background_tasks.add_task(send_email, ...)   # NOT managed, no UUID, no retries

Both task lists share the same underlying Starlette list, so both tasks run. The problem is silent: the route-level call bypasses the managed wrapper, so that task gets no UUID, no retry tracking, and no dashboard visibility.

The fix is to either activate install() so the route annotation is also managed, or switch the route to Depends(task_manager.get_tasks) to be consistent throughout.

Using install() without the dashboard

If you are not mounting TaskAdmin, call install() directly on your app:

from fastapi_taskflow import TaskManager
from fastapi import FastAPI

task_manager = TaskManager()
app = FastAPI()
task_manager.install(app)

Process-wide scope

install() modifies a module-level reference inside fastapi.dependencies.utils. It applies to every route in the process, not just the app you pass in. In a single-app deployment this is a non-issue. If you run multiple FastAPI apps in the same process and only want managed injection on one of them, use Pattern 3 instead.

Type checker behaviour

BackgroundTasks.add_task() is typed to return None. If your type checker flags that a variable assigned from add_task() is None, switch to the ManagedBackgroundTasks annotation (Pattern 2) or add an explicit annotation:

from fastapi_taskflow import ManagedBackgroundTasks

@app.post("/order")
def create_order(tasks: ManagedBackgroundTasks):
    task_id: str = tasks.add_task(process_order, order_id=42)
    return {"task_id": task_id}

Tip

Pattern 2 is the cleanest option when you care about return-type accuracy in your IDE. The behaviour at runtime is identical to Pattern 1.