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:

  1. Models – wrappers around LLMs (OpenAI, Anthropic, etc.)
  2. Tools – functions the LLM can call (like checking your calendar)
  3. Agents – LLMs that decide which tools to use and when
  4. 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 now
print(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_duration with '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:

  1. LLM decides — “I should use this tool with these arguments”
  2. 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.