Spring AI Recipe: Agentic Planning with TodoWriteTool
So far, we’ve seen how an LLM can:
- answer questions
- ask clarifying questions
But one of the defining characteristics of an agent is its ability to plan and execute toward a goal.
Spring AI enables this pattern with TodoWriteTool.
NOTE: This recipe has been updated since it’s original publication to reflect changes in Spring AI 2.0.0.
Adding TodoWriteTool
The TodoWriteTool is provided by the Spring AI Agent Utils project.
Add the dependency:
implementation 'org.springaicommunity:spring-ai-agent-utils:0.7.0'Then configure it using a ChatClientBuilderCustomizer:
@Bean
ChatClientBuilderCustomizer todoWriteTool() {
return builder -> builder
.defaultTools(
TodoWriteTool.builder()
.build());
}TodoWriteTool enables the LLM to:
- create a plan (a TODO list)
- track progress through that plan
- iteratively work toward a final result
Enabling chat memory
Because planning occurs over multiple steps, chat memory is essential.
Configure it with another ChatClientBuilderCustomizer:
@Bean
ChatClientBuilderCustomizer messageChatAdviser() {
return builder ->
builder.defaultAdvisors(
MessageChatMemoryAdvisor.builder(
MessageWindowChatMemory.builder().build())
.build());
}A few important details:
- Conversation history is disabled here because memory is handled separately
MessageChatMemoryAdvisorensures the TODO list and tool interactions persist across turns
Trying it out
Run the application and ask a multi-step question:
Compare Apollo 11, Apollo 13, and Apollo 17. Break the analysis into steps and produce a final report.
Instead of answering immediately, the LLM:
- Creates a structured TODO list
- Executes each task step-by-step
- Produces a final report
While the final output is fascinating, the most interesting part is in the planning and execution process that led to the final output. Let’s see how to expose the plan’s details.
Observing the plan in action
You can attach a handler to observe the TODO list as it evolves by calling the todoEventHandler() method when building TodoWriteTool:
@Bean
ChatClientBuilderCustomizer todoWriteTool() {
return builder -> builder
.defaultTools(
TodoWriteTool.builder()
.todoEventHandler(event -> {
var todos = event.todos();
var completeCount = todos.stream()
.filter(todo ->
todo.status().equals(
TodoWriteTool.Todos.Status.completed))
.count();
var percentageComplete =
Math.round((completeCount * 100.0) / todos.size());
logger.info("Event ({}/{} : {}%):",
completeCount,
todos.size(),
percentageComplete);
todos.forEach(todoItem -> {
logger.info(" -- TODO Item: {} - {}",
todoItem.status(),
todoItem.content());
});
})
.build());
}Here is an example of the kind of output you might see:
Event (0/4 : 0%):
-- TODO Item: in_progress - Collect and write mission objectives for Apollo 11, Apollo 13, and Apollo 17
-- TODO Item: pending - Analyze challenges encountered during each mission
-- TODO Item: pending - Document outcomes and scientific returns for each mission
-- TODO Item: pending - Compare missions and summarize key differences
Event (1/4 : 25%):
-- TODO Item: completed - Collect and write mission objectives for Apollo 11, Apollo 13, and Apollo 17
-- TODO Item: in_progress - Analyze challenges encountered during each mission
-- TODO Item: pending - Document outcomes and scientific returns for each mission
-- TODO Item: pending - Compare missions and summarize key differences
Event (2/4 : 50%):
-- TODO Item: completed - Collect and write mission objectives for Apollo 11, Apollo 13, and Apollo 17
-- TODO Item: completed - Analyze challenges encountered during each mission
-- TODO Item: in_progress - Document outcomes and scientific returns for each mission
-- TODO Item: pending - Compare missions and summarize key differences
Event (3/4 : 75%):
-- TODO Item: completed - Collect and write mission objectives for Apollo 11, Apollo 13, and Apollo 17
-- TODO Item: completed - Analyze challenges encountered during each mission
-- TODO Item: completed - Document outcomes and scientific returns for each mission
-- TODO Item: in_progress - Compare missions and summarize key differencesEach step represents progress toward the final goal.
It’s important to note that planning is performed by the LLM and is therefore non-deterministic. Even with the same prompt, the generated plan may differ each time.
Why this matters
Traditional applications:
- execute predefined workflows
Agentic applications:
- generate workflows dynamically
- adapt as they go
- iterate toward a goal
A core building block for agents
This aligns with a simple definition:
Agents are LLMs with tools in a loop to achieve a goal.
In this case:
- the tool enables planning
- the LLM generates the steps
- the loop executes them
Takeaway
TodoWriteTool transforms a single prompt into a structured, multi-step process.
It’s one of the simplest ways to move from:
- “LLM-powered feature”
to:
- agentic system
What’s next
In upcoming recipes, we’ll explore how to guide and shape this behavior using agentic skills and reusable capabilities.
You can find the example code for this and other recipes at https://github.com/habuma/spring-ai-recipes.