langchain - 💡(How to fix) Fix Support callable/dynamic system_prompt in create_agent

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…

Root Cause

Any application where the system prompt must be rebuilt before every LLM call because:

Fix Action

Fix / Workaround

This callable prompt support was one of the most powerful features of create_react_agent and has no direct equivalent in create_agent. For applications where the system prompt must change based on conversation state or external data, the only workaround is middleware:

class DynamicPromptMiddleware(AgentMiddleware):
    """Workaround: inject dynamic content via awrap_model_call."""

1. **Middleware `awrap_model_call` + `request.override(system_message=...)`** — this is our current workaround (described above). It works but separates prompt logic from the agent definition, reducing readability for prompt-heavy applications.

Code Example

class DynamicPromptMiddleware(AgentMiddleware):
    """Workaround: inject dynamic content via awrap_model_call."""

    def __init__(self, static_messages, dynamic_getter):
        self._static = static_messages
        self._getter = dynamic_getter

    async def awrap_model_call(self, request, handler):
        dynamic_msgs = await self._getter()
        combined = "\n\n".join(
            m.content for m in self._static + dynamic_msgs
            if hasattr(m, "content") and m.content
        )
        if combined:
            request = request.override(
                system_message=SystemMessage(content=combined)
            )
        return await handler(request)

---

async def prompt(state) -> list[BaseMessage]:
    # Re-fetched every LLM call — reflects tool-modified external state
    dynamic_context = await fetch_current_context()
    return system_messages + dynamic_context + list(state["messages"])

agent = create_react_agent(model=llm, tools=tools, prompt=prompt, ...)

---

agent = create_agent(
    model=llm,
    tools=tools,
    system_prompt=my_dynamic_prompt,  # callable accepted
)

---

# Option A: receives state (like create_react_agent's prompt)
async def my_dynamic_prompt(state: AgentState) -> str | SystemMessage | list[BaseMessage]:
    ...

# Option B: receives state + runtime (consistent with middleware hooks)
async def my_dynamic_prompt(state: AgentState, runtime: Runtime) -> str | SystemMessage:
    ...
RAW_BUFFERClick to expand / collapse

Checked other resources

  • This is a feature request, not a bug report or usage question.
  • I added a clear and descriptive title that summarizes the feature request.
  • I used the GitHub search to find a similar feature request and didn't find it.
  • I checked the LangChain documentation and API reference to see if this feature already exists.
  • This is not related to the langchain-community package.

Package (Required)

  • langchain
  • langchain-openai
  • langchain-anthropic
  • langchain-classic
  • langchain-core
  • langchain-cli
  • langchain-model-profiles
  • langchain-tests
  • langchain-text-splitters
  • Other / not sure / general

Feature Description

create_agent() currently accepts system_prompt as str | SystemMessage | None — a static value bound at agent creation time. The deprecated create_react_agent() accepted prompt as a callable (state) -> list[BaseMessage], allowing dynamic prompt construction on every LLM call.

This callable prompt support was one of the most powerful features of create_react_agent and has no direct equivalent in create_agent. For applications where the system prompt must change based on conversation state or external data, the only workaround is middleware:

class DynamicPromptMiddleware(AgentMiddleware):
    """Workaround: inject dynamic content via awrap_model_call."""

    def __init__(self, static_messages, dynamic_getter):
        self._static = static_messages
        self._getter = dynamic_getter

    async def awrap_model_call(self, request, handler):
        dynamic_msgs = await self._getter()
        combined = "\n\n".join(
            m.content for m in self._static + dynamic_msgs
            if hasattr(m, "content") and m.content
        )
        if combined:
            request = request.override(
                system_message=SystemMessage(content=combined)
            )
        return await handler(request)

This works but adds indirection — the prompt logic is separated from the agent definition and hidden inside middleware, making it harder to reason about what the LLM actually sees.

Use Case

Any application where the system prompt must be rebuilt before every LLM call because:

  1. Tool calls modify external state mid-conversation — e.g. a tool writes data to a database. The system prompt includes context derived from that data (available items, user progress, etc.), which changes after each tool call within a single agent turn.
  2. User/session context is fetched from an external source — instructions, phase-specific prompts, or user metadata are assembled dynamically per call.

With the old create_react_agent, this was clean:

async def prompt(state) -> list[BaseMessage]:
    # Re-fetched every LLM call — reflects tool-modified external state
    dynamic_context = await fetch_current_context()
    return system_messages + dynamic_context + list(state["messages"])

agent = create_react_agent(model=llm, tools=tools, prompt=prompt, ...)

With create_agent, this must be moved into middleware, which obscures the prompt construction from the agent definition site.

Proposed Solution

Allow system_prompt to accept a callable, consistent with how create_react_agent handled prompt:

agent = create_agent(
    model=llm,
    tools=tools,
    system_prompt=my_dynamic_prompt,  # callable accepted
)

Where the callable signature could be:

# Option A: receives state (like create_react_agent's prompt)
async def my_dynamic_prompt(state: AgentState) -> str | SystemMessage | list[BaseMessage]:
    ...

# Option B: receives state + runtime (consistent with middleware hooks)
async def my_dynamic_prompt(state: AgentState, runtime: Runtime) -> str | SystemMessage:
    ...

This would make the type: str | SystemMessage | Callable[[StateT], Awaitable[str | SystemMessage | list[BaseMessage]]] | None

Alternatives Considered

  1. Middleware awrap_model_call + request.override(system_message=...) — this is our current workaround (described above). It works but separates prompt logic from the agent definition, reducing readability for prompt-heavy applications.

  2. Use create_react_agent instead — supports callable prompt= natively, but is officially deprecated in favor of create_agent.

  3. Custom StateGraph — maximum flexibility but sacrifices all the convenience of create_agent (middleware ecosystem, built-in tool loop, etc.).

Related Issues

  • #34239 — Dynamic response_format with create_agent (same pattern: callable parameter that resolves at runtime based on state)
  • #33808 — Dynamic tool addition/removal in middleware (uses request.override(tools=...), same override pattern)
  • #33630 — system_prompt expanded from str to str | SystemMessage (natural next step: expand to callable)

Additional Context

The migration guide from create_react_agent to create_agent doesn't address the loss of callable prompt support. For applications where dynamic prompting is central to the product, this is a significant regression in developer experience when migrating from create_react_agent.

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