Launch-Free 3 months Builder plan-
Pixel art lobster working at a computer terminal with email — llamaindex reactagent deprecated migration

llamaindex reactagent deprecated: how to migrate to the new workflow-based agent

LlamaIndex deprecated ReActAgent, AgentRunner, and FunctionCallingAgent. Here's the full migration path with before/after code examples.

7 min read
Ian Bussières
Ian BussièresCTO & Co-founder

LlamaIndex v0.11 shipped a deprecation warning that's now impossible to ignore: llama_index.core.agent.ReActAgent is deprecated, along with AgentRunner, ReActAgentWorker, and FunctionCallingAgent. If you're running any of these in production, your code still works today. But the clock is ticking, and the replacement API is different enough that you can't just swap an import path.

I migrated three production agents last month. The process took about an hour per agent once I understood the new model. Here's everything I learned.

LlamaIndex deprecated agent classes and their replacements#

  • llama_index.core.agent.ReActAgentllama_index.core.agent.workflow.ReActAgent
  • llama_index.core.agent.AgentRunnerllama_index.core.agent.workflow.AgentWorkflow
  • llama_index.core.agent.ReActAgentWorker → removed (no direct replacement)
  • llama_index.core.agent.FunctionCallingAgentllama_index.core.agent.workflow.FunctionAgent
  • llama_index.core.agent.FunctionCallingAgentWorker → removed (no direct replacement)
  • llama_index.core.agent.StructuredPlannerAgentllama_index.core.agent.workflow.AgentWorkflow with custom steps

The "Worker" classes are gone entirely. The new architecture doesn't separate runners from workers. Instead, everything lives in a unified workflow model where each agent is a self-contained workflow with steps.

Why LlamaIndex made this change#

The old agent architecture had a fundamental problem: the AgentRunner/AgentWorker split created unnecessary complexity without giving developers meaningful control over execution flow. You'd subclass ReActAgentWorker to customize reasoning, then wrap it in an AgentRunner that mostly just called .run(). Two abstractions doing one job.

The new workflow-based system collapses this into a single class that exposes execution as a directed graph of steps. Each step is a function that receives context and returns either a result or a pointer to the next step. This makes it possible to inject custom logic between reasoning cycles, add retry behavior, or branch execution based on intermediate results.

Conversation memory also benefits from this model. Because context flows explicitly through the step graph rather than being stored as mutable state on the agent instance, you get cleaner isolation between runs and fewer subtle bugs from shared state leaking between sessions.

It's not just a rename. The internal execution model changed from a loop-based pattern (think/act/observe repeated until done) to a step-based graph. Your tools still work the same way. Your prompts still work the same way. But how the agent orchestrates calls between them is architecturally different.

Before and after: basic ReActAgent migration#

Here's what a typical ReActAgent looked like with the old API:

from llama_index.core.agent import ReActAgent
from llama_index.core.tools import FunctionTool
from llama_index.llms.openai import OpenAI

def multiply(a: int, b: int) -> int:
    """Multiply two integers."""
    return a * b

tool = FunctionTool.from_defaults(fn=multiply)
llm = OpenAI(model="gpt-4o")

agent = ReActAgent.from_tools([tool], llm=llm, verbose=True)
response = agent.chat("What is 6 times 7?")
print(response)
And here's the same agent with the new workflow-based API:

from llama_index.core.agent.workflow import ReActAgent
from llama_index.core.tools import FunctionTool
from llama_index.llms.openai import OpenAI

def multiply(a: int, b: int) -> int:
    """Multiply two integers."""
    return a * b

tool = FunctionTool.from_defaults(fn=multiply)
llm = OpenAI(model="gpt-4o")

agent = ReActAgent(tools=[tool], llm=llm)
response = await agent.run("What is 6 times 7?")
print(response)
Three differences to notice:

1. The import path changes from `llama_index.core.agent` to `llama_index.core.agent.workflow`
2. `from_tools()` is replaced by direct instantiation with `tools=` as a keyword argument
3. `agent.chat()` becomes `await agent.run()`, which means your calling code must be async

That third point trips people up. If your existing codebase is synchronous, you'll need to wrap calls in asyncio.run() or refactor the calling layer to be async. There's no synchronous .chat() method on the new agents. If you're running inside a web framework like FastAPI, you're likely already in an async context and this transition is painless. For Django or Flask codebases without async support, asyncio.run() works but watch out for nested event loop errors if you're calling from within an existing loop.

Migrating RAG query engine tools#

If you're using QueryEngineTool with your ReActAgent for RAG workflows, the good news is that tool definitions don't change at all:

from llama_index.core.agent.workflow import ReActAgent
from llama_index.core.tools import QueryEngineTool

query_tool = QueryEngineTool.from_defaults(
    query_engine=index.as_query_engine(),
    name="knowledge_base",
    description="Search the knowledge base for information about our products"
)

agent = ReActAgent(tools=[query_tool], llm=llm)
response = await agent.run("What's our refund policy?")

FunctionTool, QueryEngineTool, and any custom BaseTool subclass all work unchanged. The tool interface is stable. Only the agent orchestration layer moved.

Passing system prompts and context#

The old API accepted system_prompt as a constructor argument:


# Old API
agent = ReActAgent.from_tools(tools, llm=llm, system_prompt="You are a helpful assistant.")

The new API uses the same parameter name, just passed directly to the constructor:


# New API
agent = ReActAgent(tools=tools, llm=llm, system_prompt="You are a helpful assistant.")

For dynamic context that changes between runs, the new workflow model supports a context parameter in .run():

response = await agent.run(
    "Summarize recent activity",
    ctx={"user_id": "abc123", "session": session_data}
)

This replaces the old pattern of mutating agent state between calls or recreating the agent with different prompts.

Handling FunctionCallingAgent migration#

If you were using FunctionCallingAgent instead of ReActAgent, the migration is nearly identical. The replacement is FunctionAgent:

from llama_index.core.agent.workflow import FunctionAgent

agent = FunctionAgent(tools=[tool], llm=llm)
response = await agent.run("What is 6 times 7?")

The behavioral difference: FunctionAgent uses the LLM's native function calling (tool_choice in OpenAI's API), while ReActAgent uses prompt-based reasoning with a think/act/observe loop. If your LLM supports native function calling well (GPT-4o, Claude 3.5+), FunctionAgent tends to be faster and cheaper. If you need the agent to show its reasoning chain, stick with ReActAgent.

Testing migration parity in production#

Don't just swap the code and ship. Run both versions in parallel for a few days. Here's a minimal shadow-testing approach:

import asyncio
from llama_index.core.agent.workflow import ReActAgent as NewReActAgent
from llama_index.core.agent import ReActAgent as OldReActAgent  # still works, just deprecated

async def shadow_test(query: str, tools, llm):
    old_agent = OldReActAgent.from_tools(tools, llm=llm)
    new_agent = NewReActAgent(tools=tools, llm=llm)

    old_response = old_agent.chat(query)
    new_response = await new_agent.run(query)

    if str(old_response) != str(new_response):
        log_discrepancy(query, old_response, new_response)

    return new_response

In my experience, outputs are functionally equivalent for straightforward tool use. Where I saw divergence was in multi-step reasoning chains where the agent needed 3+ tool calls to reach an answer. The new workflow engine occasionally takes a different path to the same result, which is fine, but worth monitoring if your downstream logic depends on specific intermediate outputs. Log the number of tool calls made per query alongside the final answer — if that count shifts significantly, it's a signal to inspect whether the new engine is short-circuiting steps it shouldn't be.

What version introduced the new system#

The workflow-based agent system landed in LlamaIndex v0.11.0. The deprecation warnings appeared in v0.11.2. As of v0.11.14 (current at time of writing), the old classes still function but emit DeprecationWarning on every instantiation. There's no announced removal date, but the documentation has fully shifted to the new API. All examples, tutorials, and quickstarts reference llama_index.core.agent.workflow exclusively.

One more thing for agent builders#

If you're building agents that need to interact with external services (sending emails, calling APIs, managing state), the migration is a good moment to reconsider which capabilities you bundle as tools versus which you delegate to infrastructure. For example, agents that need email can use something like LobsterMail to self-provision inboxes without you wiring up SMTP as a custom tool. The fewer low-level tools your agent juggles, the fewer things break when you swap orchestration layers.


Frequently asked questions

What is the new import path for ReActAgent in LlamaIndex after deprecation?

The new path is from llama_index.core.agent.workflow import ReActAgent. The old from llama_index.core.agent import ReActAgent still works but emits deprecation warnings.

Why did LlamaIndex deprecate the original ReActAgent class?

The old AgentRunner/AgentWorker architecture created unnecessary abstraction layers without giving developers meaningful control over execution flow. The new workflow-based model unifies everything into a step-based graph that's easier to customize and extend.

Which other agent classes were deprecated alongside ReActAgent?

AgentRunner, ReActAgentWorker, FunctionCallingAgent, FunctionCallingAgentWorker, and StructuredPlannerAgent were all deprecated. The Worker classes have no direct replacements since the runner/worker split no longer exists.

Is the deprecated ReActAgent still functional in recent LlamaIndex versions?

Yes, as of v0.11.14 it still works but emits a DeprecationWarning on every instantiation. No removal date has been announced, but all official documentation now uses the new API exclusively.

What is the architectural difference between the old ReActAgent and the new workflow-based ReActAgent?

The old version used a loop-based pattern (think/act/observe repeated until completion). The new version models execution as a directed graph of steps, where each step can branch, retry, or inject custom logic between reasoning cycles.

Do I need to change my tool definitions when migrating to the new ReActAgent?

No. FunctionTool, QueryEngineTool, and custom BaseTool subclasses all work unchanged with the new API. Only the agent orchestration layer changed.

How do I pass a system prompt to the new workflow-based ReActAgent?

Pass it directly to the constructor: ReActAgent(tools=tools, llm=llm, system_prompt="Your prompt here"). For dynamic per-run context, use the ctx parameter in .run().

Can I still use the from_tools() pattern with the new workflow-based ReActAgent?

No. The new API uses direct instantiation with keyword arguments: ReActAgent(tools=[...], llm=llm). The from_tools() class method doesn't exist on the new class.

What replaced AgentRunner in the new LlamaIndex agent architecture?

llama_index.core.agent.workflow.AgentWorkflow replaces AgentRunner. It serves as the general-purpose workflow container when you need custom multi-step agent logic beyond what ReActAgent or FunctionAgent provide out of the box.

How do I handle the async requirement in the new agent API?

The new .run() method is async-only. Wrap calls in asyncio.run() for synchronous codebases, or refactor your calling layer to be async. There is no synchronous .chat() equivalent on the new agents.

Does the new workflow ReActAgent support RAG query engine tools?

Yes. QueryEngineTool works identically with the new API. Pass it in the tools list and the agent will use it for retrieval the same way the old version did.

How do I verify output parity between my old and new ReActAgent?

Run both versions in parallel using shadow testing. Compare outputs for a representative set of queries over several days. Watch for divergence in multi-step reasoning chains where the new workflow engine may take different intermediate paths to the same result.

What LlamaIndex version introduced the workflow-based agent system?

The workflow-based agents shipped in LlamaIndex v0.11.0. Deprecation warnings for the old classes appeared in v0.11.2.

Are there breaking changes in response format or streaming after migration?

The response object structure is similar, but streaming uses a different pattern. The new API yields workflow events rather than token-by-token callbacks. Check the async for event in agent.run_stream() pattern in the current docs.

Where is the official LlamaIndex migration guide for deprecated agent classes?

LlamaIndex maintains a deprecated terms page in their documentation at docs.llamaindex.ai. The workflow agent examples in their official docs serve as the de facto migration guide, showing the new patterns for each use case.

Related posts