Bug
The official LlamaIndex starter templates deep_research and financial_report both end with a module-level singleton:
# last line of the file
workflow = create_workflow()
create_workflow() builds a Workflow subclass whose __init__ stores per-call state as instance attributes:
# packages/create-llama/templates/components/use-cases/python/deep_research/workflow.py
class DeepResearchWorkflow(Workflow):
def __init__(self, ...):
self.context_nodes = []
self.memory = SimpleComposableMemory.from_defaults(...)
# ... self.user_request set later in prepare step
LlamaIndex's Workflow.run(...) permits multiple concurrent calls on the same instance, but every step in these templates writes through self.xxx. Two concurrent requests share self.memory, self.context_nodes, and self.user_request — tenant A's query, retrieved doc nodes, and chat history are visible to tenant B's response, and vice versa.
Any project scaffolded via npx create-llama … that picks these templates and deploys with even a single uvicorn worker handling async concurrency (the default in llamaindex-server) inherits the leak.
Affected files (HEAD 97a7d9bc)
The sibling files under packages/create-llama/templates/components/agents/python/…/workflows/ define the same Workflow subclass with the same instance-attr layout (the use-cases/ copies just bake in the module-level singleton on top).
Reproducer
We replicate the exact pattern (Workflow subclass + __init__ instance attrs + module-level singleton) using llama-index-core==0.14.22 — the same library version the templates target. The full template can't be imported standalone (heavy LlamaCloud / pgvector / spacy deps), but the bug is in the pattern, not in any specific step body, so a faithful replication is sufficient:
# repro_real.py
import asyncio
from llama_index.core.workflow import (
Workflow, Context, StartEvent, StopEvent, Event, step,
)
class StartReq(StartEvent):
user_msg: str
class RetrievedEvent(Event):
pass
# Mirrors DeepResearchWorkflow's instance-attr layout (workflow.py:138-149)
class DeepResearchWorkflow(Workflow):
def __init__(self, **kw):
super().__init__(**kw)
self.context_nodes = []
self.memory = []
self.user_request = None
@step
async def prepare(self, ctx, ev: StartReq) -> RetrievedEvent:
self.user_request = ev.user_msg # line 149
self.memory.append({"role": "user", "content": ev.user_msg}) # lines 158-165
self.context_nodes.extend([f"doc_for_{ev.user_msg}"]) # lines 179-180
await asyncio.sleep(0.05)
return RetrievedEvent()
@step
async def finalize(self, ctx, ev: RetrievedEvent) -> StopEvent:
return StopEvent(result={
"saw_user_request": self.user_request,
"saw_memory": list(self.memory),
"saw_context_nodes": list(self.context_nodes),
})
workflow = DeepResearchWorkflow(timeout=10) # mirrors the template's line 588
async def main():
a, b = await asyncio.gather(
workflow.run(user_msg="TENANT_A_secret_aaaa"),
workflow.run(user_msg="TENANT_B_unrelated_bbbb"),
)
print("Run A saw:", a)
print("Run B saw:", b)
asyncio.run(main())
Output:
Run A saw: {'saw_user_request': 'TENANT_B_unrelated_bbbb',
'saw_memory': [{'role':'user','content':'TENANT_A_secret_aaaa'},
{'role':'user','content':'TENANT_B_unrelated_bbbb'}],
'saw_context_nodes': ['doc_for_TENANT_A_secret_aaaa',
'doc_for_TENANT_B_unrelated_bbbb']}
Run B saw: {'saw_user_request': 'TENANT_B_unrelated_bbbb', ...same merged state...}
Both concurrent .run() calls observe each other's user_request, memory, and context_nodes. The "last-writer wins" race on self.user_request plus the share-and-append on self.memory / self.context_nodes is a cross-tenant data leak.
Expected behavior
Either:
- The templates instantiate a fresh
Workflow per request (move the singleton out, document create_workflow() as the per-request factory). Llama-index-server's WorkflowFactory already has the right API for this, but the template ships the singleton.
- Or the templates use Workflow's
Context.set/get (which is per-run) instead of self.xxx instance attrs for memory, context_nodes, and user_request.
Both fixes preserve correctness; the first is the smaller diff (one line per template).
Bug
The official LlamaIndex starter templates
deep_researchandfinancial_reportboth end with a module-level singleton:create_workflow()builds aWorkflowsubclass whose__init__stores per-call state as instance attributes:LlamaIndex's
Workflow.run(...)permits multiple concurrent calls on the same instance, but every step in these templates writes throughself.xxx. Two concurrent requests shareself.memory,self.context_nodes, andself.user_request— tenant A's query, retrieved doc nodes, and chat history are visible to tenant B's response, and vice versa.Any project scaffolded via
npx create-llama …that picks these templates and deploys with even a single uvicorn worker handling async concurrency (the default in llamaindex-server) inherits the leak.Affected files (HEAD
97a7d9bc)packages/create-llama/templates/components/use-cases/python/deep_research/workflow.pyworkflow = create_workflow()self.memory(138-140),self.context_nodes(138, 180),self.user_request(149)packages/create-llama/templates/components/use-cases/python/financial_report/workflow.pyworkflow = create_workflow()self.memory,self.user_requestThe sibling files under
packages/create-llama/templates/components/agents/python/…/workflows/define the sameWorkflowsubclass with the same instance-attr layout (theuse-cases/copies just bake in the module-level singleton on top).Reproducer
We replicate the exact pattern (Workflow subclass +
__init__instance attrs + module-level singleton) usingllama-index-core==0.14.22— the same library version the templates target. The full template can't be imported standalone (heavy LlamaCloud / pgvector / spacy deps), but the bug is in the pattern, not in any specific step body, so a faithful replication is sufficient:Output:
Both concurrent
.run()calls observe each other'suser_request,memory, andcontext_nodes. The "last-writer wins" race onself.user_requestplus the share-and-append onself.memory/self.context_nodesis a cross-tenant data leak.Expected behavior
Either:
Workflowper request (move the singleton out, documentcreate_workflow()as the per-request factory). Llama-index-server'sWorkflowFactoryalready has the right API for this, but the template ships the singleton.Context.set/get(which is per-run) instead ofself.xxxinstance attrs formemory,context_nodes, anduser_request.Both fixes preserve correctness; the first is the smaller diff (one line per template).