Two tools at once
Parallel tool calls are several tool requests the model makes in one message, which the agent runs together before it calls the model again.
Last updated: 27 Sep, 2026 · LangChain 1.4
An earlier question about A17 and C40 produced two tool calls in one AI message. Each call carries its own id, and each result comes back tagged with it, so the model can tell which answer belongs to which request.
The return_direct and parallel_tool_calls options
@tool(return_direct=True) # end the run as soon as this tool returns
def my_tool(...): ...
# turn parallel calls off when binding (OpenAI, Anthropic)
model.bind_tools([t], parallel_tool_calls=False)Building the agent
Build the agent with the deterministic stand-in shop model and one lookup tool.
from langchain.agents import create_agent
from shop_model import ShopModel # a deterministic stand-in model
from tools import lookup_order # looks up one order id
agent = create_agent(ShopModel(), tools=[lookup_order])Asking about three orders
Ask about three orders in one message. The model puts all three requests in one AI message.
result = agent.invoke({"messages": [
{"role": "user", "content": "Where are A17, B22 and C40?"}
]})Matching results to calls by id
Walk the messages between the question and the final answer. Print each tool call's id, then each result tagged with the id it answers.
for message in result["messages"][1:-1]:
if message.type == "ai":
for call in message.tool_calls:
print("asked ", call["id"]) # one line per request
else:
print("result", message.tool_call_id, "->", message.text) # tagged resultThree calls in one run
The same pieces in one file. Three calls go out together and three results come back, all before the model is called a second time.
result = agent.invoke({"messages": [{"role": "user", "content": "Where are A17, B22 and C40?"}]})
for message in result["messages"][1:-1]:
if message.type == "ai":
for call in message.tool_calls:
print("asked ", call["id"])
else:
print("result", message.tool_call_id, "->", message.text)Skipping the last model call
The final answer above only repeats what the tools said. A tool marked return_direct=True ends the run the moment it returns, and its result becomes the answer.
@tool(return_direct=True)
def lookup_order(order_id: str) -> str:
"""Look up an order's shipping status by its id, such as A17."""
status = ORDERS.get(order_id)
return f"{order_id} {status}." if status else f"{order_id} is not an order we have."Run the same question and print every message with its type. The run ends on the tool results, with no AI message after them.
from langchain.agents import create_agent
from shop_model import ShopModel
from tools import lookup_order
agent = create_agent(ShopModel(), tools=[lookup_order])
result = agent.invoke({"messages": [{"role": "user", "content": "Where are A17 and C40?"}]})
for message in result["messages"]:
print(f"{message.type:<5} {message.text or len(message.tool_calls)}")What the ids matched
- Three calls, three results. All three ran before the model was called a second time, and each result is tagged with the id of the call it answers.
- Most hosted models make parallel calls by default; OpenAI and Anthropic let you switch it off with
parallel_tool_calls=Falsewhen binding tools. - return_direct ends the run on the tool result: one model call instead of two, and its text is the answer.
- With several tools in one step, the run stops only if every one of them has
return_direct; otherwise all results go back to the model.
A normal tool vs return_direct
| Normal tool | return_direct | |
|---|---|---|
| Result goes | Back to the model | Straight out as the answer |
| Model calls | Two: ask, then answer | One: ask only |
| Run ends | After the model's reply | As soon as the tool returns |
| Several tools in a step | All results go back | Stops only if all are return_direct |
When to run tools in parallel
- Looking up several items in one request instead of a round trip to the model per item.
- A tool whose result is the finished answer, such as a lookup that needs no rewording.
return_direct ends the run only if every one of them has it. Mix in one plain tool and all the results go back to the model as usual.Related
- Previous: Validation errors, and the retry
- Next: Short-term memory with a checkpointer
- Reference: create_agent and tools
- Ask about one order with
return_directon and count the messages. - Stream the
return_directagent and check which step comes last. - Print each
message.idfor the tool messages and compare them with the tool call ids.
Slow is fine. Stopping is the only problem.