How to use Command in Tool without using ToolNode

Hi @neel

thanks for you reply and feedback. This is how I understant what you are asking about:

Q1 - Confirm custom node behavior: “My custom tool node invokes multiple tools sequentially (loop) without handing over control during execution - correct?”

Yes. Your custom node processes tool calls in sequence and only returns control when the node returns. In your snippet, state is updated and ToolMessages are collected, then a single update is returned. Control flow changes only when the node returns a Command (node-level). See Command semantics:

Q2 - Sequential node with Command short-circuit: “If I add the Command-based snippet and multiple tools each return a Command, will control hand over to the first tool’s goto (short‑circuit)?”

Yes, if you immediately return Command(...) upon the first Command, the node short‑circuits and hands control to that goto. Example branch inside your node:

if isinstance(result, Command):
    merged = {"messages": state["messages"] + new_messages}
    if result.update:
        merged.update(result.update)
    return Command(update=merged, goto=result.goto)

To run all tools then decide, collect commands and choose a winner after the loop.

Q3 - Native ToolNode execution model: “Does the original ToolNode call all tools simultaneously, and hand over control to each tool’s goto?”

Yes, ToolNode executes tool calls in parallel (async: asyncio.gather | sync: thread/executor map) and then combines outputs. If tools return Command, ToolNode returns a list that can include Commands and/or ToolMessages. The runtime honors Command.goto (potentially fanning out). Source excerpts:

Q4 - How do multiple control branches merge into one?

They don’t auto-merge. Branches converge only where your graph topology brings them together, and your state reducers define how updates combine (e.g., messages channel using list-add). Use conditional edges or a join node to converge.

With “merging”, you may be referring to BSP (Bulk Synchronous Parallel).

Relation to BSP/Pregel: LangGraph’s execution is step-based with fan-out and fan-in. Commands (with multiple Send targets) spawn parallel tasks. The runtime applies updates and advances at step boundaries, similar to BSP supersteps. The codebase even uses Pregel terminology.

Important differences: BSP implies a strict global barrier each superstep. LangGraph enforces step boundaries but doesn’t auto-merge branches- you must converge them via graph topology and channel reducers. ToolNode’s concurrent tool-calls are a “mini-superstep” inside the node, then results are combined.

Fan-in example (map-reduce style)

Fan-out with Send to process items in parallel, then fan-in by returning to a join node whose reducer appends results.

from typing import Annotated, TypedDict
import operator
from langgraph.types import Send
from langgraph.graph import StateGraph, START, END


class OverallState(TypedDict):
    subjects: list[str]
    # Fan-in happens via list-add reducer on "jokes"
    jokes: Annotated[list[str], operator.add]


def router(state: OverallState):
    # Fan-out: create one Send per subject
    return [Send("generate_joke", {"subject": s}) for s in state["subjects"]]


def generate_joke(state: dict):
    subj = state["subject"]
    return {"jokes": [f"Joke about {subj}"]}


builder = StateGraph(OverallState)
builder.add_conditional_edges(START, router)
builder.add_node("generate_joke", generate_joke)
builder.add_edge("generate_joke", END)  # Fan-in at END; list-add reducer appends
graph = builder.compile()

# Invoking with two subjects results in both jokes appended
graph.invoke({"subjects": ["cats", "dogs"], "jokes": []})
# => {"subjects": ["cats", "dogs"], "jokes": ["Joke about cats", "Joke about dogs"]}

Optional: Fan-in with Command.goto

from langgraph.types import Command


def router_with_command(state: OverallState):
    sends = [Send("generate_joke", {"subject": s}) for s in state["subjects"]]
    return Command(goto=sends)  # multiple branches; same fan-in behavior via reducer

Q5 - Is Command an upgrade to Send that can create multiple control branches?

Command generalizes Send. Send sends a message to a node. Command can include update and goto, where goto may be a single node, a sequence of nodes, a Send, or a sequence of Sends. Returning multiple Sends via Command.goto can create multiple branches. See local sources:

ToolNode validates and combines Commands across tool calls, including merging parent-graph Sends into a single parent Command: tool_node.py combine logic.