I’m terrible at estimating how long things take. That “quick 30-minute task” somehow devours two hours. And by the time I realize it, my carefully planned day is in ruins.
So I decided to build an AI agent to do what I can’t: estimate task durations realistically, look at my actual calendar, and schedule everything so it actually fits. Better yet — it learns from my mistakes.
In this post, I’ll walk through building a productivity agent with LangChain. Along the way, we’ll explore how LangChain’s tool-calling works under the hood, and create something genuinely useful.
The plan
Core Features:
1. Time estimation – LLM infers how long each task takes
2. Calendar integration – reads your available time slots
3. Smart scheduling – fits tasks into slots, splitting if needed
4. Priority management – respects your priorities but suggests swaps
5. Learning loop – tracks actual vs estimated time to improve predictions
Architecture we’ll need:
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ UI/Input │────▶│ LLM Agent │────▶│ Calendar │
│ (todos) │ │ (LangChain)│ │ API │
└─────────────┘ └──────┬──────┘ └─────────────┘
│
┌─────▼─────┐
│ Storage │
│(estimates,│
│ actuals) │
└───────────┘
What is LangChain?
LangChain is a framework for building applications with LLMs. Its key concepts are:
- Models – wrappers around LLMs (OpenAI, Anthropic, etc.)
- Tools – functions the LLM can call (like checking your calendar)
- Agents – LLMs that decide which tools to use and when
- Chains – sequences of steps piped together
For our scheduling agent, we’ll mainly use Tools and Agents — the LLM will decide when to estimate time, check the calendar, or schedule a task.
So, What Even Is a Tool?
In LangChain-land, a “tool” is just a Python function that an LLM can ask to use. That’s it. No magic.
You create one with the @tool decorator:
from langchain_core.tools import tool
@tool
def estimate_duration(task_description: str) -> int:
"""Estimate how many minutes a task will take."""
return 30 # placeholder for nowprint(estimate_duration.name)
print(estimate_duration.description)estimate_duration
Estimate how many minutes a task will take.
The LLM sees the function name and docstring — that’s how it decides when to use it. Think of the docstring as the tool’s sales pitch.
The Two-Step Dance
Here’s the part that confused me at first: the LLM never actually runs your code.
When you ask “How long will writing a blog post take?”, the LLM responds with something like:
“I’d like to call
estimate_durationwith'write a blog post'”
That’s it. It’s a request, not an execution. Your code has to actually run the function and feed the result back. It’s a polite back-and-forth — the LLM proposes, you execute.
This is where agents come in, but we’ll get there.
Setting Up the Model
First, let’s connect to an LLM. LangChain supports many providers — we’ll use Anthropic’s Claude.
You’ll need an API key from Anthropic’s console. Set it as an environment variable:
export ANTHROPIC_API_KEY="your-key-here"from langchain_anthropic import ChatAnthropic
llm = ChatAnthropic(model="claude-sonnet-4-20250514")
# Quick sanity check to make sure it's working:
response = llm.invoke("Say hello in exactly 5 words")
print(response.content)Hello there, how are you?
We’re in business. Now let’s give this LLM some superpowers.
llm_with_tools = llm.bind_tools([estimate_duration])
response = llm_with_tools.invoke("How long will it take to write a blog post?")
print(response.tool_calls)[{'name': 'estimate_duration', 'args': {'task_description': 'write a blog post'}, 'id': 'toolu_01VzcaBjFXpStLFXhBAgp87h', 'type': 'tool_call'}]
What just happened?
The LLM didn’t actually run your function. Instead, it said: “I want to call estimate_duration with this argument.”
Look at what came back:
{
'name': 'estimate_duration', # which tool to call
'args': {'task_description': 'write a blog post'}, # with what input
'id': 'toolu_015...', # unique ID for this call
'type': 'tool_call'
}This is the key insight: tool calling is a two-step dance:
- LLM decides — “I should use this tool with these arguments”
- Your code executes — you actually run the function and feed the result back
The LLM is essentially saying “please call this function for me” — it can’t run Python itself!
This is where Agents come in. An agent automates this loop: ask LLM → run tool → feed result back → repeat.
Enter the Agent
Remember that two-step dance? Ask the LLM, run the function, feed the result back? Yeah, I don’t want to do that manually every time. That’s exactly the kind of tedious loop computers were invented for.
LangChain’s create_react_agent handles this for us. “ReAct” stands for Reason + Act — the agent thinks about what to do, calls a tool, observes the result, and keeps going until it has an answer.
from langchain.agents import create_agent
# Combine your tools
tools = [estimate_duration] # add more tools here as you build them
# Create the agent
agent = create_react_agent(llm, tools)
# Now you can invoke it and it handles the full loop
result = agent.invoke({"messages": [("user", "How long will writing a blog post take?")]})
print(result["messages"][-1].content)/tmp/ipykernel_575/3338827541.py:8: LangGraphDeprecatedSinceV10: create_react_agent has been moved to `langchain.agents`. Please update your import to `from langchain.agents import create_agent`. Deprecated in LangGraph V1.0 to be removed in V2.0.
agent = create_react_agent(llm, tools)
Based on the estimation, writing a blog post will take approximately **30 minutes**.
However, this is a general estimate and the actual time can vary significantly depending on factors such as:
- The length and complexity of the blog post
- Your writing experience and speed
- Whether you need to do research
- The amount of editing and revision required
- Whether you're including images, links, or other media
A simple, short blog post might take less time, while a detailed, well-researched article could take several hours.
That’s it. No manual back-and-forth. The agent sees my question, realizes it needs the estimate_duration tool, calls it, gets “30 minutes,” and wraps everything into a friendly response.
It’s like having a very polite assistant who knows when to check their reference materials instead of just making stuff up.