I'm a bit of a fitness guy.
In college, I worked at a running store. I built relationships with people in the fitness industry and got my toes wet with coaching. I trained people for years to build speed and endurance and helped them get comfortable with running.
When I graduated college and started working in the tech industry, I continued to coach for a couple more years. Eventually, that tapered off as I focused on work. However, I ended up leading a strength-training group at my company for about 6 years until the pandemic hit.
Once we all started working from home, I built a mobile app to continue sending out workouts remotely so we could train virtually and do the same workouts together.
But again, that tapered off after a couple of years and now I just train myself. I have over a decade of experience building personalized training plans. But that's a lot of work, especially if it's just for me. I now have two kids, a small farm, work at a startup, and run Ready, Set, Cloud where I write weekly blog posts and newsletters, and release a biweekly podcast.
I'm a bit busy.
But not working out isn't an option. It's part of my morning routine.
In episode 2 of my podcast, I talk with Ran Isenberg about platform engineering. Ran says part of the responsibilities of a platform engineering team is to reduce cognitive load from developers. That sounded exactly like what I needed with my fitness routine. I needed someone, or something, to make it easier on me when building my workout schedule.
ChatGPT is trending right now. It consolidates information from all over the internet to build a legitimate learning model. Why not use it to reduce cognitive load and build my workouts?
Wouldn't hurt to give it a try, right?
tl;dr - you can skip straight to the GitHub repo if you want to try it yourself!
Workout Variance
Every winter I go through a strength training phase. I lift weights five days a week and work a different muscle group every day. I cycle through arms, chest, back, shoulders, and legs. I also throw in a cardio-based warmup and cooldown into the mix and always try to get in some abs.
At first, I wanted to see how well ChatGPT could come up with a workout. So I went to the website and asked it to write me one.
This answer got me very excited. It was as good (if not better than) a workout I would have come up with, and it only took a few seconds to create exactly what I was looking for!
One thing I've learned about exercise is that variety is the spice of life. Not only does it help to keep you mentally fresh but changing up the workout routine can also have dramatic results physically by preventing you from plateauing. With this in mind, I decided to add some variance to the query I sent ChatGPT.
I wanted the ability to have a workout randomly generated that was either a circuit, superset, or standard workout. On top of that, I wanted a workout that would randomly pick from the equipment I had available. One day I could be using kettlebells and the next I could be using barbells.
So I parameterized the workout types and available equipment and built a small randomizer that would pick and choose a workout type and subset of equipment every day. Ultimately my query to ChatGPT ended up looking like this:
let muscleGroups = ['chest', 'arm', 'shoulder', 'back', 'leg'];
let workoutTypes = ['as a circuit', 'in supersets', 'as a standard workout'];
let equipment = [{ type: 'barbells', threshold: .9 }, { type: 'dumbbells', threshold: .75 }, { type: 'kettlebells', threshold: .45 }];
// Logic to randomly pick a muscle group, workout type, and set of equipment omitted for brevity
const request = `Create a ${muscleGroup} workout for strength training that uses ${joinWithAnd(equipmentToUse)} structured ${workoutType}.`;
For randomly selecting equipment, I had a random number generated between 0 and 1. If that random number was lower than the provided threshold, it was included in the workout.
With all this in place, I had a great way to build high-quality workouts that cycled through all my available equipment and gave me some much-needed variance.
Let's talk a bit about how it works.
Serverless Solution
Since I tend to do everything serverless, it made sense to build a solution powered by Lambda, DynamoDB, EventBridge, and Step Functions. These services give me an event-driven architecture that could scale up if I decided to turn this into a SaaS offering. For now, I'll settle with the free-scale nature of the application where usage is so little I will never get charged for anything.
This solution is powered by two Step Functions:
- Generate Weekly Schedule - Randomizes which muscle groups, workout types, and equipment will be on each day and gets the workout from ChatGPT
- Daily Workout Notifier - Sends me an email in the evening with the workout details for the next day
Every Sunday the weekly scheduler will run, randomly decide what the workouts will be, ask ChatGPT to generate them, and save them to the DynamoDB. I then get an email so I know what to expect in the upcoming week.
Every weekday the workout notifier runs, "pops" a workout from DynamoDB (more on this in a sec), and sends the details in an email so I have it for the next day. All I have to do is open up the email in the morning and I'm good to go!
Data Model
I was recently educated by Pete Naylor in his blog post about optimizing DynamoDB secondary indexes. It was a real eye-opener that made me wonder if I've been doing things wrong for a long time.
A practice I am trying to shy away from is "GSI overloading" where you shove everything in a GSI and hope for the best. That feels (and is) confusing to maintain over time and doesn't provide any real benefits. So I came up with this data model for the workout schedule.
The partition key is the date and the sort key is either workout
or schedule
to reflect which entity they represent. I have a GSI named FutureSchedule
with hash key of IsScheduledInFuture
and range key of ScheduledDate
.
This GSI is intended to be sparse, meaning not everything is going to be projected into it. Only records that are scheduled in the future will show up, making it a breeze to figure out what the next workout is.
To get tomorrow's workout, I have a query in my state machine that looks like this:
"Get Next Workout": {
"Type": "Task",
"Parameters": {
"TableName": "${TableName}",
"IndexName": "FutureSchedule",
"KeyConditionExpression": "#future = :future",
"ExpressionAttributeNames": {
"#future": "IsScheduledInFuture"
},
"ExpressionAttributeValues": {
":future": {
"S": "true"
}
},
"Limit": 1
},
"Resource": "${DynamodbQuery}",
"Next": "Has Next Workout?"
}
This pulls the first item (see Limit: 1
) out of the GSI. All items in this index are sorted by the scheduled date, so the first item to be returned is the one for tomorrow!
Now, before you tell me this pattern creates a hot partition, I am aware that it could. But it won't. The data is accessed once per day and is scoped to a single record. You start having issues with hot partitions when you cross 3000 RCUs or 1000 WCUs per second.
After loading the data in the state machine and sending out the email, I remove the IsScheduledInFuture
and ScheduledDate
values from the workout record so it is removed from the GSI and doesn't return in a future query. Easy!
3rd Party APIs
This entire solution runs on some outstanding 3rd party APIs. First, of course, is the OpenAI API that enables us to communicate with ChatGPT. This API allows a decent amount of configuration to fine-tune the results you get back from the next-gen AI. You can adjust the level of creativity (I think of it as workout difficulty) with the temperature
setting. The higher the temperature, the more creative of an answer you receive.
You also can limit the number of tokens in your response. The OpenAI API is not free. Depending on the AI model you use, you are charged anywhere from $0.0004 to $0.02 per 1000 tokens. Tokens are pieces of words and depending on how wordy your response is you could use anywhere from 30 to 1500 tokens in a single call. So we want to limit how much it's charging you per request by providing a value in the max_tokens
parameter.
The workout generator uses the most powerful AI model available via the API - Davinci. This model is close to (but not exactly) the one you use when you interact with ChatGPT on the website. This ends up being the most expensive part of the entire solution.
Since the OpenAI API is the most expensive part, I don't want to run unnecessary queries. We already know that "everything fails all the time," so when I built the state machines for retry I opted to use Momento to cache the ChatGPT responses. This enables me to omit an expensive query by implementing a read-aside (more like query-aside) cache.
First, I check my cache in Momento to see if I've queried OpenAI for a specific workout. If I have, great! I short-circuit the call and return the cached response. If I haven't, then I make the call to the OpenAI API. Momento has a 50GB free tier per month so I pay nothing to use this serverless caching service.
Finally, I use the SendGrid API to send the workout email to myself. I could have used Amazon SES, but I already had SendGrid fully functional and configured to send emails in response to an EventBridge event as part of my newsletter platform. I just use putEvents
with a DetailType of Send Email, pass the html
, subject
, and to
fields in the Detail, and boom - email sent. SendGrid also has a great free tier that allows me to send up to 100 emails per day at no charge.
Summary
The world we live in today is amazing. I had an idea to offload workout creation to AI and two days later I was receiving automated emails with high-quality, high-variance workouts using equipment I have in my home gym.
I was able to move quickly by going serverless, but not so fast where I can't maintain it in the future. I've also left the door open for this to be a fully viable SaaS offering. It wouldn't be too much work to add Cognito into the mix, add configurable user settings in DynamoDB, and toss in EventBridge Schedules to dynamically send workouts at a configurable time to anyone who wants to sign up.
Software today is highly composable, whether you're sticking completely with your cloud vendor services, using 3rd party offerings like OpenAI, Momento, or Sendgrid, or even writing your own modules. With APIs, you can link together a wealth of services to build any crazy integration you think of.
I am thrilled to take "workout building" off my plate. It's something I've enjoyed for a long time but I'm ready to have someone else do it for me. This was a fun project to build and I hope you take it and run with it. It's fully open-source and available for a PR!
Happy coding!
Top comments (4)
Allen, this is absolutely awesome!
Thank you very much!
Love this idea Allen!
Fantastic Allen...How you took a real problem and imparted Technology to it in a pretty logical fashion...Your thought process is pretty good...A lot of takeaways from this post!