How to create a tool-calling agent in Haystack

Tool calling gives a chat model a controlled way to request Python behavior instead of pretending that it completed an external action. A Haystack Agent coordinates that exchange by passing registered tool definitions to a compatible chat generator, invoking the requested function, and returning the function result to the conversation.

The @tool decorator derives a tool name and parameter schema from a typed Python function. The Agent component then manages the message loop, while max_agent_steps limits how many generator and tool turns one request may consume.

The local generator used here requests one fixed order lookup, which exposes the complete call loop without an API key or network model. Replace it with a tool-capable chat generator when calls should be selected by a model; the decorated function and Agent wiring stay the same.

Steps to create a Haystack tool-calling agent:

  1. Create order_agent.py with the Haystack imports.
    from typing import Annotated
     
    from haystack import component
    from haystack.components.agents import Agent
    from haystack.dataclasses import ChatMessage, ToolCall
    from haystack.tools import tool
  2. Add the order-status tool below the imports.
    @tool
    def get_order_status(order_id: Annotated[str, "Order ID to look up"]) -> str:
        """Return the fulfillment status for one order."""
        return f"Order {order_id} is packed and waiting for courier pickup."

    The docstring describes the tool to the generator, and Annotated describes the order_id argument in the generated tool schema.

  3. Define the deterministic chat generator below get_order_status().
    @component
    class DemoToolCallingChatGenerator:
        @component.output_types(replies=list[ChatMessage])
        def run(self, messages: list[ChatMessage], tools=None):
            if messages[-1].is_from("user"):
                return {
                    "replies": [
                        ChatMessage.from_assistant(
                            tool_calls=[
                                ToolCall(
                                    tool_name="get_order_status",
                                    arguments={"order_id": "A100"},
                                    id="call_1",
                                )
                            ]
                        )
                    ]
                }
     
            tool_result = messages[-1].tool_call_result.result
            return {
                "replies": [
                    ChatMessage.from_assistant(
                        f"I checked order A100: {tool_result}"
                    )
                ]
            }

    The first reply requests get_order_status. The next reply receives the tool result from Agent and converts it into the final assistant text.

  4. Instantiate the agent below DemoToolCallingChatGenerator.
    agent = Agent(
        chat_generator=DemoToolCallingChatGenerator(),
        tools=[get_order_status],
        system_prompt="Use tools when order status is requested.",
        max_agent_steps=4,
    )
  5. Send the order question through the agent.
    result = agent.run(
        messages=[ChatMessage.from_user("What is the status of order A100?")]
    )
  6. Add result formatting below the agent.run() call.
    for message in result["messages"]:
        if message.tool_calls:
            call = message.tool_calls[0]
            print(f"tool call: {call.tool_name}({call.arguments})")
        elif message.tool_call_result:
            print(f"tool result: {message.tool_call_result.result}")
     
    print(f"final answer: {result['last_message'].text}")
  7. Review the completed order_agent.py source.
    order_agent.py
    from typing import Annotated
     
    from haystack import component
    from haystack.components.agents import Agent
    from haystack.dataclasses import ChatMessage, ToolCall
    from haystack.tools import tool
     
     
    @tool
    def get_order_status(order_id: Annotated[str, "Order ID to look up"]) -> str:
        """Return the fulfillment status for one order."""
        return f"Order {order_id} is packed and waiting for courier pickup."
     
     
    @component
    class DemoToolCallingChatGenerator:
        @component.output_types(replies=list[ChatMessage])
        def run(self, messages: list[ChatMessage], tools=None):
            if messages[-1].is_from("user"):
                return {
                    "replies": [
                        ChatMessage.from_assistant(
                            tool_calls=[
                                ToolCall(
                                    tool_name="get_order_status",
                                    arguments={"order_id": "A100"},
                                    id="call_1",
                                )
                            ]
                        )
                    ]
                }
     
            tool_result = messages[-1].tool_call_result.result
            return {
                "replies": [
                    ChatMessage.from_assistant(
                        f"I checked order A100: {tool_result}"
                    )
                ]
            }
     
     
    agent = Agent(
        chat_generator=DemoToolCallingChatGenerator(),
        tools=[get_order_status],
        system_prompt="Use tools when order status is requested.",
        max_agent_steps=4,
    )
     
    result = agent.run(
        messages=[ChatMessage.from_user("What is the status of order A100?")]
    )
     
    for message in result["messages"]:
        if message.tool_calls:
            call = message.tool_calls[0]
            print(f"tool call: {call.tool_name}({call.arguments})")
        elif message.tool_call_result:
            print(f"tool result: {message.tool_call_result.result}")
     
    print(f"final answer: {result['last_message'].text}")
  8. Run order_agent.py to verify the tool-calling loop.
    $ python order_agent.py
    tool call: get_order_status({'order_id': 'A100'})
    tool result: Order A100 is packed and waiting for courier pickup.
    final answer: I checked order A100: Order A100 is packed and waiting for courier pickup.

    The output shows the registered tool name and argument, the value returned by that tool, and the final answer that incorporates the same value.