LangGraph Deep Agents Subagents: task, Fork and Async
A subagent is a context quarantine. LangGraph Deep Agents subagents let the main agent hand a heavy, noisy job — reading forty files, running a long search, grinding through a diff — to a separate agent, and get back only the conclusion. The parent's context window never sees the intermediate tool calls. That is the whole thesis of this part: delegate when the work is heavy and only the conclusion matters; keep the work inline when the parent needs the details.
LangGraph Deep Agents, a 10-part series. ← Part 7 · Part 9 →
This post covers the task tool, the built-in general-purpose subagent, custom subagents, isolated versus fork mode, async subagents you can launch, check, update and cancel, opt-in planning with write_todos, and how to stream subagent progress. Everything below is taken from the public subagents docs, async subagents docs and the harness docs.
How does the task tool delegate work in LangGraph Deep Agents?
Delegation is a tool call. The main agent calls task() with a subagent_type naming the subagent and a description of the job. The subagent runs to completion, and its final answer comes back as the tool result. Everything it did along the way stays inside the subagent.
The main agent can issue several task calls in one turn, and they run in parallel. That is the cheap way to fan out: three independent research questions, three task calls, one turn. With an interpreter attached, the agent can also dispatch dynamic subagents from code.
The mental model is a manager with a stack of sticky notes. The manager writes "find out why the build is slow" on a note, hands it over, and gets back one paragraph. The manager does not want the forty log excerpts the engineer read to write that paragraph.
What is the general-purpose subagent?
You get one subagent for free. The SDK automatically adds a synchronous general-purpose subagent unless you supply one with that name. It has the filesystem tools by default and uses the main agent's model unless overridden.
This covers the common case where the agent just wants a clean context for a side quest — "summarize these files" — without you defining anything. To replace it, pass your own subagent with "name": "general-purpose". To turn it off, the docs set general_purpose_subagent=GeneralPurposeSubagentProfile(enabled=False) on the active harness profile and pass no synchronous subagents.
How do you define custom LangGraph Deep Agents subagents?
A custom subagent is a dict passed in subagents. Three keys do most of the work:
name— the identifier used intask()calls.description— the main agent reads this to decide when to delegate, so write it like a routing rule, not a job title.system_prompt— required for isolated subagents, because it is not inherited from the parent.
Optional keys tune the rest: tools (inherited by default; when set, it replaces the inherited list entirely), model (a "provider:model" string or a chat model object), middleware, interrupt_on (needs a checkpointer), skills, response_format for structured output, and permissions, which replaces the parent's permissions rather than merging with them.
from typing import Literal
from deepagents import create_deep_agent
from tavily import TavilyClient
tavily_client = TavilyClient() # reads its key from the environment
def internet_search(
query: str,
max_results: int = 5,
topic: Literal["general", "news", "finance"] = "general",
include_raw_content: bool = False,
):
"""Run a web search"""
return tavily_client.search(
query,
max_results=max_results,
include_raw_content=include_raw_content,
topic=topic,
)
research_subagent = {
"name": "research-agent",
"description": "Used to research more in depth questions",
"system_prompt": "You are a great researcher",
"tools": [internet_search],
"model": "openai:gpt-5.5", # Optional override, defaults to main agent model
}
subagents = [research_subagent]
agent = create_deep_agent(
model="google_genai:gemini-3.6-flash",
subagents=subagents,
)
The tool is a plain Python function with a docstring; this one wraps the Tavily search client, as in the docs. Note the description: "Used to research more in depth questions" is short, but in your own code write it like a routing rule.
If you already have a compiled LangGraph graph, wrap it as a CompiledSubAgent(name=..., description=..., runnable=custom_graph). The graph needs a state key called "messages", since that is how the result flows back.
Isolated vs fork mode: which should a subagent use?
Every subagent has a mode, and the choice is about what context it starts with.
Isolated (the default) sees only the task description. That is the quarantine in its strictest form: the subagent cannot be confused by the parent's history, and the parent pays nothing for it. The cost is that the task description must carry everything the subagent needs.
Fork ("mode": "fork") inherits the parent's conversation history. The trailing delegation call is replaced by the task description plus a continuation preamble, so the fork picks up "where we are" without you restating it. Forks cannot call task themselves and must finish the work alone; skills is rejected on forks; and a system_prompt is appended as an addendum rather than replacing anything. Fork mode requires deepagents>=0.7.13 and is in beta.
Rule of thumb: use isolated when the job is self-contained and you can describe it in a paragraph; use fork when re-explaining the conversation would cost more than inheriting it — a reviewer that needs to know what was just discussed, for example.
How do async subagents work, and can you steer them mid-flight?
Synchronous subagents block the parent until they finish. Async subagents do not. They run as background tasks on an Agent Protocol server, and the supervisor gets a set of tools from AsyncSubAgentMiddleware:
start_async_task— starts a background task and returns its task ID immediately.check_async_task— returns the current status and, when complete, the result.update_async_task— sends new instructions to a running task.cancel_async_task— stops it.list_async_tasks— lists every tracked task with live status.
That update_async_task tool is the interesting one: the supervisor can redirect a long-running job ("skip the European sources, focus on the US") without killing and restarting it.
from deepagents import AsyncSubAgent, create_deep_agent
async_subagents = [
AsyncSubAgent(
name="researcher",
description="Research agent for information gathering and synthesis",
graph_id="researcher",
# No url → ASGI transport (co-deployed in the same deployment)
),
AsyncSubAgent(
name="coder",
description="Coding agent for code generation and review",
graph_id="coder",
# url="https://coder-deployment.langsmith.dev" # Optional: HTTP transport for remote
),
]
agent = create_deep_agent(
model="google_genai:gemini-3.6-flash",
subagents=async_subagents,
)
AsyncSubAgent takes name, description and graph_id (the graph or assistant ID on the server). Omit url and it uses ASGI transport; set it and it goes over HTTP, with optional headers. It also supports mode (isolated or fork). Newer releases (deepagents>=0.7.30) add update_strategy ("interrupt" by default, or "enqueue"), a webhook called when a run finishes, and notify_parent.
Do LangGraph Deep Agents still plan with write_todos?
Only if you ask. Starting in v0.7, task planning is opt-in; earlier versions included the planning middleware by default. To give the agent its write_todos tool, pass TodoListMiddleware:
from deepagents import create_deep_agent
from langchain.agents.middleware import TodoListMiddleware
agent = create_deep_agent(
model="google_genai:gemini-3.6-flash",
middleware=[TodoListMiddleware()],
)
Todos carry pending, in_progress or completed status and persist in agent state. The harness docs recommend it for long multistep tasks, for less capable models that benefit from an explicit accountability tool, and for UIs that stream progress. Planning and delegation pair naturally: the todo list is the manager's whiteboard, and each item that is heavy becomes a task call.
How do you stream subagent progress?
The parent's stream can interleave its own messages with subagent events. Using agent.stream_events(..., version="v3"), iterate stream.interleave("messages", "subagents"):
for name, item in stream.interleave("messages", "subagents"):
if name == "messages":
print("[coordinator]", item.text)
else:
print(f"[{item.name}] started")
Each subagent handle exposes .name, .messages, .tool_calls and .output, and the docs' full example also reads .status, which is enough to render a per-subagent panel in a UI while the context quarantine stays intact for the model.
When should you not delegate?
Delegation is not free. Every subagent is another model call with its own prompt, and isolated subagents only know what you wrote in the task description. If the parent needs the intermediate details to make its next decision, a subagent just hides them. Delegate when the work is heavy and only the conclusion matters; otherwise keep it inline.
FAQ
Is the general-purpose subagent always present?
Yes, unless you supply your own subagent named general-purpose or disable it via the harness profile with no synchronous subagents passed.
Can a forked subagent spawn its own subagents?
No. Forks cannot use task; they must finish the work themselves.
Why doesn't my agent have write_todos anymore?
Since v0.7 planning is opt-in. Add TodoListMiddleware() to middleware.
