Skip to content

[Feature Request]: Support Composition Root style DI — decouple step signatures from resource construction #20900

@niits

Description

@niits

Feature Description

Workflows currently support DI via inline Resource(factory=...) in step signatures:

class MyWorkflow(Workflow):
    @step
    async def first(
        self,
        ev: StartEvent,
        memory: Annotated[Memory, Resource(factory=get_memory)],
    ) -> StopEvent:
        ...

This couples step definitions to resource construction logic.

Proposal: Adopt a typed dependency context pattern (similar to PydanticAI's deps), where each step receives a single deps parameter instead of individually-annotated resources.

1. Typed dependency container:

@dataclass
class MyDeps:
    memory: Memory
    llm: LLM
    customer_id: int

2. Steps receive deps — no factory knowledge:

class MyWorkflow(Workflow[MyDeps]):
    @step
    async def first(self, ev: StartEvent, deps: WorkflowContext[MyDeps]) -> StopEvent:
        response = await deps.llm.achat(...)
        deps.memory.put(response)
        return StopEvent(result=response)

3. Wiring at the call site (Composition Root):

workflow = MyWorkflow(deps=MyDeps(
    memory=ChatMemoryBuffer.from_defaults(token_limit=3000),
    llm=OpenAI(model="gpt-4o"),
    customer_id=123,
))

# Testing — zero changes to workflow code
workflow = MyWorkflow(deps=MyDeps(memory=MockMemory(), llm=MockLLM(), customer_id=999))

Optional: explicit lifecycle scope

workflow = MyWorkflow(deps=MyDeps(...), deps_scope=DepsScope.RUN)  # RUN | STEP | SINGLETON

Reason

The current Resource(factory=...) annotation pattern introduces several issues:

Concern Impact
Coupling Steps know how dependencies are created, not just what they need
Separation of Concerns Orchestration mixed with wiring/configuration
Environment switching Swapping InMemoryStoreRedisStore requires editing workflow source code
Testing Mocking means patching factories inside annotations rather than passing different instances
Lifecycle Unclear whether resource is per-step, per-run, or singleton

If this pattern is already achievable today, I'd appreciate a pointer to the relevant docs or examples.

Value of Feature

Aspect Per-param Resource(factory=...) Typed deps context
Adding a dependency Modify signature + factory Add field to dataclass
Type safety / discoverability Scattered across annotations Single container, IDE-friendly
Testing Patch individual factories Pass different deps instance
Runtime context (user ID, etc.) Awkward via Resource Natural field on deps

Key benefits:

  • Clean separation: steps declare what, composition root decides how
  • Testability: pass mocks at construction without patching internals
  • Environment flexibility: swap implementations via config, not code changes
  • Explicit lifecycle: caller controls resource scope

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requesttriageIssue needs to be triaged/prioritized

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions