Building a Personal Fitness Tracker in One Conversation
The request was simple:
I want to track my workouts and nutrition. I’m tracking lifting sessions (upper body and lower body days), and my meals throughout the day (calories, protein, fiber, and carbs). I have a goal of 150g of protein every day.
One conversation later, I had a full workspace: log workouts by typing a few lines, log meals with macro breakdowns, get a daily summary against the 150g protein goal, and generate personalized workout plans that rotate exercises based on what you’ve done recently , all accessible from Telegram. Here’s exactly how Friday built it, tool call by tool call.
Step 1: Loading the right skills
Before writing a single line of config, Friday loaded the platform skills that govern workspace design: the FSM job authoring rules, signal schema conventions, and Python agent SDK patterns. These act as strict playbooks that prevent the most common runtime failures.
Skill → load_skill("writing-workspace-jobs")
Loaded FSM job authoring guide — initial state trigger matching, outputTo / inputFrom chaining, emit/transition naming, and the most common silent-failure patterns.
Skill → load_skill("writing-workspace-signals")
Loaded signal authoring guide — HTTP signal schemas, form field rendering from schema.properties, cron validation, path collision rules.
Step 2: Designing the workspace
Four capabilities needed to exist: log a workout session, log a meal with macros, get a daily summary, and generate a fresh workout plan based on history. Each one maps to a signal + agent + job triple.
🏋️ Log Workout (llm + memory_save)
Accepts workout type (upper/lower), exercises with weights, and optional notes. Writes a structured [WORKOUT] entry to the notes memory store with date and all exercise details.
🥗 Log Meal (llm + memory_save)
Accepts meal name, calories, protein, fiber, and carbs. Writes a timestamped [MEAL] entry to memory. Format is consistent so the summary agent can parse and sum it reliably.
📋 Generate Workout Plan (llm + memory_read)
Reads recent [WORKOUT] entries from memory. Determines the next workout type (opposite of last). Identifies exercises done in the last 2–3 sessions and builds a fresh 5–6 exercise plan avoiding repeats, covering all major muscle groups, with sets, reps, and RPE.
📊 Daily Summary (llm + memory_read)
Reads all [MEAL] and [WORKOUT] entries from today. Sums macros (calories, protein, carbs, fiber). Compares protein against the 150g goal and reports how much remains — or confirms the goal was hit.
Step 3: Creating the workspace
Friday created a fresh workspace and opened a draft to stage all changes atomically — nothing goes live until validation passes.
Tool Call → create_workspace({ name: "Fitness Tracker" })
workspace.id: square_zucchini
path: ~/.atlas/workspaces/Fitness Tracker
Tool Call → begin_draft(workspaceId: "square_zucchini")
Opened workspace.yml.draft. All upserts stage into the draft until publish_draft is called. Nothing can break the live workspace mid-build.
Step 4: Building the four agents
All four agents were upserted in parallel — no need to serialize independent writes.
Tool Call → upsert_agent("log-workout-agent", { type: "llm", tools: ["memory_save", "memory_read"] })
Workout logger. Records a structured [WORKOUT - <type>] entry with date, exercise names, weights, and notes. Reads prior entries first so it can include context in its confirmation response.
Tool Call → upsert_agent("log-meal-agent", { type: "llm", tools: ["memory_save", "memory_read"] })
Meal logger. Writes a [MEAL] entry in a fixed format: date time: meal — cal kcal | P: Xg | F: Xg | C: Xg. Consistent format is critical — the summary agent parses these entries to compute daily totals.
Tool Call → upsert_agent("daily-summary-agent", { type: "llm", tools: ["memory_read", "memory_save"] })
Daily summary. Reads all [MEAL] and [WORKOUT] entries from today, sums the macros, compares protein to the 150g goal, and produces a structured report.
Tool Call → upsert_agent("workout-planner-agent", { type: "llm", tools: ["memory_read"] })
Intelligent workout planner. Reads recent [WORKOUT] entries, determines the opposite of the last session type, cross-references exercises from the last 2–3 sessions, and generates a fresh plan. Read-only — no memory_save so it can't accidentally overwrite history.
Optional preferences field threads through: "no barbell today", "keep it under 45 min", "hypertrophy focus" — all honored in the plan output.
Step 5: Defining the signals
Three HTTP signals, each with a typed JSON schema that renders as a form in the Run dialog. The schema is what makes the difference between a button that fires a job and a button that collects meaningful input.
Signal: /log-workout
workout_type (required) — Upper Body or Lower Body
exercises (required) — e.g. "Bench 3x8 185lb, OHP 3x10 115lb"
notes — Optional: energy level, PRs, etc.
Signal: /log-meal
meal_name (required) — e.g. "Chicken and rice"
calories (required) — Calories (kcal)
protein (required) — Protein in grams
carbs (required) — Carbohydrates in grams
fiber (required) — Fiber in grams
Signal: /generate-workout-plan
preferences — Optional: e.g. "no barbell", "short session"
Step 6: Wiring the jobs and publishing
Four single-step FSM jobs — each one routes a signal to its agent and uses outputTo to surface the result back to the caller.
idle
└─ log-workout ──▶ run
agentId: log-workout-agent
outputTo: workout-log-result
type: finalidle
└─ log-meal ──▶ run
agentId: log-meal-agent
outputTo: meal-log-result
type: finalidle
└─ daily-summary ──▶ run
agentId: daily-summary-agent
outputTo: daily-summary-result
type: finalidle
└─ generate-workout-plan ──▶ run
agentId: workout-planner-agent
outputTo: workout-plan-result
type: finalTool Call → validate_workspace(workspaceId: "square_zucchini")
status: ✓ ok · errors: [ ] · warnings: [ ]
Result → publish_draft → success. Draft atomically promoted to live config. Workspace is running.
Step 7: One more thing — adding the workout planner
The initial workspace was live. Then came a follow-up in the same conversation:
I also want you to generate workout plans when I request them, alternating upper and lower body workouts and prioritizing exercises I haven’t done in the last 2–3 sessions.
Friday opened a new draft, added the workout-planner-agent and generate-workout-plan signal and job, validated (clean), and published — all in one turn.
The planner reads the memory store to find recent [WORKOUT] entries, determines upper vs lower based on what ran last, cross-references exercises from the last 2–3 sessions of the same type, and generates a plan that avoids repeats while hitting all major muscle groups.
Why read-only for the planner? The workout-planner-agent only has memory_read in its tools whitelist — not memory_save. A planner shouldn't write to history; only actual logged sessions should. Keeping the tool list minimal is what makes the tool list meaningful.
Step 8: Using it live — from Telegram
The real test: the workspace ran in production the same day it was built, accessed from Telegram.
Friday computed the macros from scratch — running a Python script to estimate calories and grams from raw food descriptions — then fired the log-meal job directly. No form needed when you're on Telegram; Friday parsed the natural language and filled in the fields.
Memory across surfaces. The workout logged from the playground and the meal logged from Telegram both write to the same notes memory store on the square_zucchini workspace. The daily summary sees everything, regardless of where each entry came from.
What you actually get
Four signals you can fire from any surface — the platform UI, Telegram, or the HTTP API:
Log Workout — type your exercises with weights and notes. Saved to memory with a consistent format, timestamped, ready for the planner to read.
Log Meal — enter macros directly, or describe food in plain language and let Friday calculate them for you. Each entry is written to memory in a parseable format.
Generate Workout Plan — reads your history, determines upper vs lower, finds what you did recently, and generates a fresh 5–6 exercise plan with sets, reps, and coaching cues. Gets better as you log more.
Daily Summary — reads every meal and workout from today, sums your macros, and shows where you stand against the 150g protein goal.
The memory store accumulates over time. After a few weeks, the planner has a real rotation to work from. The data is yours, local, and not dependent on any third-party fitness app.
Want to try this out yourself? Download Friday Studio and import the “Fitness Tracker” space to get started.




