ਬਲਜੀਤ ਸਿੰਘ

Nutrition and workout tracker

A Telegram agent that logs my meals and workouts


Overview

I had been working out consistently for almost a year, but I wasn't seeing much change. Because I wasn't tracking my meals or workouts, I had no idea whether I was hitting my calorie and protein goals or improving my workouts over time.

I built this Telegram fitness tracker to log my meals and workouts and tell me where I stand against my targets.

What it does

It logs meals and workouts, and answers questions about nutrition or training, like whether a snack fits into today's remaining calories or how to adjust my next leg session after skipping the last one. It also sends me nudges to stay consistent.

The key features I built it for:

Meal logging

I can log meals in natural language, and the tracker records their calories and macros against my daily targets.

0:00 / 0:00

Workout logging

I can describe a workout in my own words, and the tracker turns it into structured exercises, sets, reps, and weights.

0:00 / 0:00

Nudges

I receive nudges like weekly summaries and reminders that help me stay consistent.

Weekly summary
Under-target streak
Unlogged day reminder
Lock-screen notifications

How it works

Every message passes through four components before the tracker responds.

  • Claude: Understands the request and decides which tool to use.
  • Tools: Perform specific tasks such as logging meals or retrieving workout history.
  • Application: The code that executes the requested tool, validates the result, and prepares the response.
  • SQL Database: Stores every meal and workout and acts as the source of truth.

Nutrition

Nutrition logging flow I describe a dish over Telegram. Claude decides to look up the dish. If a match is found, Claude proposes the saved macros; otherwise it estimates them. Either way I confirm before anything is logged. Once confirmed, Claude logs the meal, writes the reply, the app totals the day, and I receive the confirmation on Telegram. Match not found Match found
TELEGRAM I describe a dish
CLAUDE Decides what to do Reads the message, decides which tool to call
APPLICATION + SQL Looks up the dish Calls the tool, checks whether the dish has been saved before
CLAUDE Estimates the macros and proposes Estimates calories and macros, proposes a total
APPLICATION + SQL Proposes saved macros Retrieves the saved calories and macros
TELEGRAM I confirm Nothing gets logged until I confirm
CLAUDE Decides what to do Reads the confirmation, decides which tool to call
APPLICATION + SQL Logs the meal and replies Calls the tool, saves the dish if new, sums today's totals, and replies
TELEGRAM I receive the confirmation Confirms the meal, today's totals, and what's left
Simplified Nutrition Logging Flow

Each dish begins with a lookup against previously logged dishes. If a match exists, the saved calories and macros are retrieved. Otherwise, Claude estimates the calories and macros, which I can adjust before confirming. Once confirmed, the dish is logged, daily totals are updated, and the response is returned to Telegram.

Workout

Workout logging flow I describe a workout over Telegram. Claude structures it and I confirm before anything is logged. Once confirmed, Claude logs the workout, checks for trends against recent history, writes a coaching note when there's something worth saying, and I receive the confirmation on Telegram.
TELEGRAM I describe a workout e.g. "Bench press 3×8 @ 30kgs, OHP 3×10 @ 30kgs, Pec deck 3x12..."
CLAUDE Decides what to do Reads the message, structures the workout, and proposes it.
TELEGRAM I confirm Nothing gets logged until I confirm.
CLAUDE Decides what to do Reads the confirmation, decides which tool to call
APPLICATION + SQL Logs the workout and checks for trends Calls the tool, logs it, checks recent history for trends
CLAUDE Writes the coaching note Flags whatever the recent trend shows, stays quiet when there's nothing worth saying
TELEGRAM I receive the confirmation Confirms the workout, and a coaching note if there's one worth giving
Simplified Workout Logging Flow

I describe my workout in shorthand. Claude interprets it as a workout logging request, structures the exercises and sets, and asks me to confirm them. After I confirm it, the workout is written to the database. Recent history for each exercise is then retrieved and compared against previous sessions. If there's enough evidence of a consistent trend, Claude adds a coaching note to the response.

Key challenges and decisions

Designing for ambiguity

When logging a meal, I simply type whatever comes naturally. Sometimes I describe the lunch in full, like "200 g black chana and 100 g rice". Other times I just type "oats" for breakfast, expecting the tracker to figure out whether I meant overnight oats or oatmeal. Some messages are straightforward, while others could be interpreted in more than one way. I wanted the tracker to understand those variations without asking unnecessary questions, while still asking for clarification when an assumption could affect the log. The table below shows how I handled those decisions.

SituationWhat it doesWhy
The dish name matches more than one saved item ("Had oats" when you've saved both "oatmeal" and "overnight oats")Shows both options with their macros for me to choose.Guessing wrong would log a different food entirely.
A never-before-seen phrase could be one dish or two items ("Had rajma chawal," nothing similar saved)Asks directly, before estimating anything, whether it's one dish or two.The answer determines how the dish gets stored.
A saved dish is re-described with different numbers than what's already logged (oats saved at 685 kcal, new ingredients add up to something else)Asks if it's the same dish, a correction, or a new variant.A wrong choice overwrites good data or creates duplicates.
A brand-new dish with no breakdown ("chicken sandwich")Asks whether to estimate or requests a breakdown, unless it's a single item like "eggs", which gets estimated directlyRecipes vary too much to default safely.

Calculating daily totals deterministically

Early on, I let Claude calculate the day's calorie and protein totals from the meals it had logged. One day Claude reported 2,101 kcal for a day that actually totaled 2,021 kcal. The calories and macros for each logged meal were correct, but the day's total wasn't. After seeing it happen a few more times, I stopped relying on Claude to calculate the totals and let the code handle it instead.

A better model would probably make this mistake less often today, but this calculation is still better handled in code. I realized the calorie and macro values for every logged meal were already known, so I shouldn't have been asking Claude to recalculate the totals. It's a deterministic operation that doesn't need Claude's reasoning.

Replacing classification with tool use

The first version of the tracker ran on Ollama, a local model I chose because it was free to run and kept my nutrition data on my own machine. Every request went through a classify-then-extract pattern: one call to figure out what I was trying to do, a second to pull out the details.

{ "intent": "log_meal" }
{ "food": "overnight oats", "quantity": 1 }

Both calls depended on the model getting that format exactly right, every time, and Ollama wasn't always reliable about it. Sometimes it added a little extra formatting around its answer, like wrapping it in ```, and that alone was enough to confuse the code, even though the answer itself was correct. When that happened, the tracker replied, "Sorry, I couldn't parse that meal. Could you rephrase?"

I switched to Claude for more reliable outputs, but the architecture stayed the same. One day I typed "log my lunch and show my remaining protein," and it logged the meal but said nothing about the protein. The architecture could only handle one request per message.

I realized I'd built the wrong architecture. I moved to Claude's native tool use, which let Claude call a function directly instead of describing its intent in text for the code to parse. It gave Claude direct access to the application's functions as tools: match_food, log_meal, get_daily_summary, and the rest. Claude could call more than one tool in the same exchange, so a message with two requests could finally be handled as a single conversation. The code stopped routing requests. It executed whatever tool Claude chose to call.

Closing thoughts

This project changed quite a bit as I built it. It still isn't the cleanest system, but it does the job I built it for.

It made me much more aware of my eating habits. I still have days where I overshoot my nutrition targets. The difference is now I know when it happens and what caused it.

Building this also deepened my understanding of AI agents. While I was building it, we were working on an agentic pipeline at work, and a colleague shared how he was approaching it. He described an agent as having a brain and a set of tools, which made me rethink the project's architecture, and so I moved from classification to Claude's native tool use. Later, I came across Anthropic describing the same idea more formally in their writing on building effective agents.

None of this would have been worth building a few years ago. I'd have needed a team, or paid for one of the dozen nutrition apps already on the market. The cost of building software has dropped enough that being curious is reason enough to build something. That's made me question what a SaaS product's moat even is anymore.


More projects