llamaIndex - ✅(Solved) Fix [Feature Request]: Support Composition Root style DI — decouple step signatures from resource construction [1 pull requests, 1 comments, 2 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
run-llama/llama_index#20900Fetched 2026-04-08 00:30:19
View on GitHub
Comments
1
Participants
2
Timeline
5
Reactions
0
Author
Participants
Timeline (top)
labeled ×2closed ×1commented ×1cross-referenced ×1

Fix Action

Fix / Workaround

ConcernImpact
CouplingSteps know how dependencies are created, not just what they need
Separation of ConcernsOrchestration mixed with wiring/configuration
Environment switchingSwapping InMemoryStoreRedisStore requires editing workflow source code
TestingMocking means patching factories inside annotations rather than passing different instances
LifecycleUnclear whether resource is per-step, per-run, or singleton
AspectPer-param Resource(factory=...)Typed deps context
Adding a dependencyModify signature + factoryAdd field to dataclass
Type safety / discoverabilityScattered across annotationsSingle container, IDE-friendly
TestingPatch individual factoriesPass different deps instance
Runtime context (user ID, etc.)Awkward via ResourceNatural field on deps
  • 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

PR fix notes

PR #20923: feat: Composition Root style DI

Description (problem / solution / changelog)

Description

Adds Composition Root style dependency injection for workflows so step signatures are decoupled from resource construction and wiring happens at the call site.

Summary of changes:

  • WorkflowContext[DepsT] — Type alias for step parameters so steps receive a single deps parameter typed as the workflow’s dependency container.
  • deps_resource() — Resource descriptor used by WorkflowContext; its factory reads the current workflow’s deps from a contextvar set during run() / arun().
  • DepsScope — Enum (RUN, STEP, SINGLETON) for optional lifecycle scope (default RUN).
  • DepsWorkflow[DepsT] — Workflow base that accepts deps and optional deps_scope in init and overrides run() / arun() to set the contextvar so steps receive the same deps instance.

Fixes #20900

New Package?

Did I fill in the tool.llamahub section in the pyproject.toml and provide a detailed README.md for my new integration or package?

  • Yes
  • No

Version Bump?

Did I bump the version in the pyproject.toml file of the package I am updating? (Except for the llama-index-core package)

  • Yes
  • No

Type of Change

Please delete options that are not relevant.

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update

How Has This Been Tested?

Your pull-request will likely not be merged unless it is covered by some form of impactful unit testing.

  • I added new unit tests to cover this change
  • I believe this change is already covered by existing unit tests

Suggested Checklist:

  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • I have added Google Colab support for the newly added notebooks.
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • I ran uv run make format; uv run make lint to appease the lint gods

Changed files

  • llama-index-core/llama_index/core/workflow/__init__.py (modified, +10/-0)
  • llama-index-core/llama_index/core/workflow/workflow_deps.py (added, +138/-0)
  • llama-index-core/tests/workflow/test_workflow_deps.py (added, +79/-0)

Code Example

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

---

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

---

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)

---

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))

---

workflow = MyWorkflow(deps=MyDeps(...), deps_scope=DepsScope.RUN)  # RUN | STEP | SINGLETON
RAW_BUFFERClick to expand / collapse

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:

ConcernImpact
CouplingSteps know how dependencies are created, not just what they need
Separation of ConcernsOrchestration mixed with wiring/configuration
Environment switchingSwapping InMemoryStoreRedisStore requires editing workflow source code
TestingMocking means patching factories inside annotations rather than passing different instances
LifecycleUnclear 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

AspectPer-param Resource(factory=...)Typed deps context
Adding a dependencyModify signature + factoryAdd field to dataclass
Type safety / discoverabilityScattered across annotationsSingle container, IDE-friendly
TestingPatch individual factoriesPass different deps instance
Runtime context (user ID, etc.)Awkward via ResourceNatural 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

extent analysis

Fix Plan

Step 1: Update Workflow Signature

Update the workflow signature to accept a deps parameter of type WorkflowContext[MyDeps].

class MyWorkflow(Workflow[MyDeps]):
    @step
    async def first(self, ev: StartEvent, deps: WorkflowContext[MyDeps]) -> StopEvent:
        ...

Step 2: Create Typed Dependency Container

Create a dataclass to represent the typed dependency container.

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

Step 3: Wire Dependencies at Composition Root

Update the composition root to create an instance of MyDeps and pass it to the workflow.

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

Step 4: Update Testing Code

Update the testing code to create a mock instance of MyDeps and pass it to the workflow.

workflow = MyWorkflow(deps=MyDeps(
    memory=MockMemory(),
    llm=MockLLM(),
    customer_id=999,
))

Step 5: Optional: Explicit Lifecycle Scope

Update the composition root to specify the lifecycle scope for the dependencies.

workflow = MyWorkflow(deps=MyDeps(...), deps_scope=DepsScope.RUN)

Verification

To verify that the fix worked, run the workflow with the updated dependencies and check that the expected behavior is observed. You can also add logging or debugging statements to verify that the dependencies are being used correctly.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING