How Agents Plan Tasks with To-Do Lists

-

all of us do naturally and often. In our personal lives, we regularly keep to-do lists to organise holidays, errands, and all the things in between. 

At work, we depend on task trackers and project plans to maintain teams aligned. For developers, it’s also common to go away comments within the code as reminders for future changes.

Unsurprisingly, LLM agents also profit from clear to-do lists to guide their planning.

To-do lists help agents plan and track complex tasks more effectively, making them especially useful for multi-tool coordination and long-running operations where progress must be visible.

Coding agents like OpenAI Codex, Cline, and Claude Code (which I take advantage of often) are prime examples of this idea. 

They break complex requests into an initial set of steps, organize them as a to-do list with dependencies, and update the plan in real time as tasks are accomplished or recent information arises.

Example of a plan generated by Claude Code agent (Claude-4.5-sonnet) on the right-hand pane of Cursor | Image by creator

This clarity enables agents to handle long sequences of actions, coordinate across different tools, and track progress in an comprehensible manner.

In this text, we dive into how agents utilize to-do list capabilities, analyze the underlying components of the planning process, and display its implementation with LangChain.

Contents

(1) Example Scenario for Planning Agent
(2) Key Components of To-Do Capabilities
(3) Putting it together in a Middleware

The accompanying code is obtainable in this GitHub repo.


(1) Example Scenario for Planning Agent

Let’s have a look at the instance scenario to anchor our walkthrough.

We are going to arrange a single agent to plan a travel itinerary and execute the booking tasks. The agent has access to :


Here is the code to implement our planning agent in LangChain:

We input a user query and think about the to-do list resulting from the agent’s planning:

To-do list generated by the travel planning agent

Using structured note-taking via to-do lists enables agents to take care of persistent memory outside the context window. This strategy improves an agent’s ability to administer and retain relevant context over time.


The code setup is easy: create_agent creates the LLM agent instance, we pass within the system prompt, select the model (GPT-5.1), and link the tools.

What’s noteworthy is the TodoListMiddleware() object that’s passed into the middleware parameter.

Firstly, what’s LangChain’s middleware?

Because the name suggests, it’s a middle layer that enables you to run custom code before and after LLM calls.

Consider middleware as a programmable layer that permits us to inject code to observe, adjust, or extend its behavior.

It gives us control and visibility over agents’ behaviors without changing their core logic. It might probably be used to rework prompts and outputs, manage retries or early exits, and apply safeguards (e.g., guardrails, PII checks).

TodoListMiddleware is a built-in middleware that specifically provides to-do list management capabilities to agents. Next, we explore how the TodoListMiddleware works under the hood.


(2) Key Components of To-Do Capabilities

A planning agent’s to-do list management capabilities boil all the way down to these 4 key components:

  1. To-do task item
  2. List of to-do items
  3. A tool that writes and updates the to-do list
  4. To-do system prompt update

The TodoListMiddleware brings these elements together to enable an agent’s to-do list capabilities.

Let’s take a more in-depth have a look at each component and the way it’s implemented within the to-do middleware code.

(2.1) To-do task item

A to-do item is the smallest unit in a to-do list, representing a single task. It’s represented by two fields: task description and current status.

In LangChain, that is modeled as a Todo type, defined using TypedDict:

The content field represents the outline of the duty that the agent must do next, while the status tracks whether the duty has not been began (pending), being worked on (in_progress), or finished (accomplished).

Here is an example of a to-do item:

{
   "content": "Compare flight options from Singapore to Tokyo",
   "status": "accomplished"
},

(2.2) List of to-do items

Now that we’ve defined the structure of a Todo item, we explore how a set of to-do items is stored and tracked as a part of the general plan.

We define a State object (PlanningState) to capture the plan as a list of to-do items, which will probably be updated as tasks are performed:

The todos field is optional (NotRequired), meaning it could be absent when first initialized (i.e., the agent may not yet have any tasks in its plan).

OmitFromInput signifies that todos is managed internally by the middleware and shouldn’t be provided directly as user input.

Here is an example of a to-do list:

todos: list[Todo] = [
    {
        "content": "Research visa requirements for Singapore passport holders visiting Japan",
        "status": "completed"
    },
    {
        "content": "Compare flight options from Singapore to Tokyo",
        "status": "in_progress"
    },
    {
        "content": "Book flights and hotels once itinerary is finalized",
        "status": "pending"
    }
]

(2.3) Tool to write down and update to-dos

With the fundamental structure of the to-do list established, we now need a tool for the LLM agent to administer and update it as tasks get executed.

Here is the usual technique to define our tool (write_todos):

The write_todos tool returns a Command that instructs the agent to update its to-do list and append a message recording the change.

While the write_todos structure is easy, the magic lies in the outline (WRITE_TODOS_TOOL_DESCRIPTION) of the tool.

When the agent calls the tool, the tool description serves because the critical additional prompt, guiding it on how one can use it accurately and what inputs to offer.

Here is LangChain’s (pretty lengthy) expression of the tool description:

We are able to see that the outline is very structured and precise, defining when and how one can use the tool, task states, and management rules. 

It also provides clear guidelines for tracking complex tasks, breaking them into clear steps, and updating them systematically.


(2.4) System prompt update

The ultimate element of organising the to-do capability is updating the agent’s system prompt.

It is completed by injecting WRITE_TODOS_SYSTEM_PROMPT into the important prompt, explicitly informing the agent that the write_todos tool exists. 

It guides the agent on when and why to make use of the tool, provides context for complex, multi-step tasks, and descriptions best practices for maintaining and updating the to-do list:


(3) Putting it together in a Middleware

Finally, all 4 key components come together in a single class called TodoListMiddleware, which packages the to-do capabilities right into a cohesive flow for the agent:

  • Define PlanningState to trace tasks as a part of a to-do list
  • Dynamically create write_todos tool for updating the list and making it accessible to the LLM
  • Inject WRITE_TODOS_SYSTEM_PROMPT to guide the agent’s planning and reasoning

The WRITE_TODOS_SYSTEM_PROMPT is injected through the middleware’s wrap_model_call (and awrap_model_call) method, which appends it to the agent’s system message for each model call, as shown below:


Wrapping it up

Identical to humans, agents use to-do lists to interrupt down complex problems, stay organized, and adapt in real time, enabling them to resolve problems more effectively and accurately.

Through LangChain’s middleware implementation, we also gain deeper insight into how planned tasks might be structured, tracked, and executed by agents.

Take a look at this GitHub repo for the code implementation.

ASK ANA

What are your thoughts on this topic?
Let us know in the comments below.

0 0 votes
Article Rating
guest
0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments

Share this article

Recent posts

0
Would love your thoughts, please comment.x
()
x