Build a Custom Workout Companion Using Twilio Functions and Airtable

Person into fitness planning out a custom workout companion to build
December 07, 2022
Written by
Dainyl Cua
Twilion
Reviewed by

In honor of the holiday spirit of gift-giving, I decided to use my software development skills to come up with an application that can help my partner with their workouts! By texting a number, they can get their workout routine, get helpful instructions for each workout, and even get videos and gifs to help them even more!

In this tutorial, you’ll use both Twilio Functions and Airtable to create a workout companion number for your own use.

Prerequisites

For this tutorial, you will need:

Set up your Airtable and get your API key

Head over to your Airtable dashboard and create a new base, then give it a suitable name (I named mine new-workout-plan). Next, create an empty table with the following headers:

  • Exercise (single line text)
  • Description (long text)
  • Video Demonstration (single line text)
  • Gif Demonstration (single line text)
  • Sets (single line text)

Duplicate this table for every other workout routine you’d like to include. Populate the cells with the relevant data, ensuring that all the fields are filled. In case you’d like to copy my data instead of populating the cells on your own, check out this GitHub repository I made which contains the .csv files for three routines.

Airtable - "Push Muscle Workouts" table populated with data, located in the "New Workout Plan" base

Next, you will need to get your API key to read the data from the table you just populated. Keep your table open; in a new tab, head over to the Airtable account page and generate your own API key under the API header. Keep this page open for later, as you will need to provide your Twilio Function with this API key as an environment variable.

Finally, you will also need your Base ID and Table IDs. In your Airtable base, look at the URL.

Airtable ID infographic showing ID locations in the URL, provided by Airtable - Understanding Airtable IDs

Base IDs will start with app, Table IDs will start with tbl, and View IDs will start with viw. You will only need your Base IDs and Table IDs for this tutorial.

Set up your Twilio Function

From your Twilio Console, click the Explore Products label on the left-hand sidebar. Then, click on Developer tools in the menu on the left, and then click on the Function and Assets card label. Click on the blue button labeled Create Service, enter a suitable Service Name (I named mine new-workout-plan), and then hit the blue button labeled Next. You should end up on a page like the one below.

Twilio Function - Empty function

Click the blue button labeled Add on the top of the screen, and rename the path to /main. Next, click on the button labeled Environment Variables under the Settings header in the bottom left of the screen. Copy and paste your Airtable API Key into the Value input box.

Then, click the Key input box and enter AIRTABLE_API_KEY, then click the button labeled Add. Add your Base ID and name the key AIRTABLE_BASE_ID. Also, add your Table IDs and name the keys after their respective table names.

Twilio Function - Environment Variables tab open with multiple Airtable environment variable set to a hidden Value

Finally, click on Dependencies, located below Environment Variables. You will need to add the node-fetch module, specifically version 2.x. Enter node-fetch in the Module input box and enter 2.x in the Value input box. Then, click the button labeled Add.

If you don’t see the other modules, don’t worry! They will be automatically imported once your function is deployed.

Twilio Function - Dependencies tab with node-fetch and other Twilio module imports

Now, you’re ready to write your code!

Write your code

The code will consist of three parts:

  • Give the user instructions on how to start the application
  • Prompt the user for the desired workout routine
  • Provide the workout routine with instructions and visual references

Give the user instructions and prompt them for their desired routine

Head back to your /main path and replace the preexisting code with the following code below:

exports.handler = async function(context, event, callback) {
  // Initialize response, twiml, and parse message body
  let response = new Twilio.Response();
  let twiml = new Twilio.twiml.MessagingResponse();
  let body = parseInt(event.Body);

  // Initialize stage and tableNames
  let stage = event.request.cookies.stage || "day";
  let tableNames = ["Push Muscles", "Pull Muscles", "Leg Muscles"]

  // Set response
  response
        .setBody(twiml.toString())
        .appendHeader('Content-Type', 'text/xml');

  return callback(null, response);
};

This code turns the function asynchronous, then initializes the response, twiml, and body variables. response and twiml are responsible for sending messages and updating the state of your application.

The body variable takes the received text message and turns the response into a number. The stage variable tracks the user’s progress through the application and will be stored in a cookie that will persist on the server as the user sends text messages.

If there are no cookies (i.e. the user has texted the number for the first time or cookies have been cleared prior to texting), then stage is set to “day”. The tableNames variable contains the table names of the tables that you created earlier. Finally, the response variable’s properties get set and then the callback function is sent.

You can replace the names in the tableNames variable with the names of your tables, in case you populated your Airtable base with your own data.

Find the highlighted line in the above code sample, just above the setting of the response variable parameters and below the tableNames variable, and then copy and paste the following code in that space:

// Message tree
  switch(stage) {
        case "day":
          if(body > 0 && body <= tableNames.length) {
            twiml.message(`You selected ${tableNames[body-1]}.\nBe sure you have water, towel, and your dumbbells. Take no more than 2 minutes between sets, maintain proper form, and have fun!\n\nRespond with any message to start.`)
            response
              .setCookie("routine", body.toString())
              .setCookie("stage", "routine")

          } else {
            let message = "Hi! What workout would you like to do today? Please respond with the corresponding number.\n\n"
            for(let i=0; i<tableNames.length; i++) {
              message += `${i+1}. ${tableNames[i]}\n`
            }
            twiml.message(message)
            
          }
          response
            .setBody(twiml.toString())
            .appendHeader('Content-Type', 'text/xml')

          return callback(null, response);

        case "routine":
          twiml.redirect({
            method: "POST"
          }, "/routine")
  }

This code handles the logic that is responsible for sending your user the correct text messages. When a user sends a message to the app for the first time, they will receive a reply displaying the workouts stored in your tableNames variable in a numbered list. If they send a valid number (in this case, numbers 1, 2, or 3), then the user will be sent a message prompting them to prepare the necessary equipment and to respond with a message to start their workout.

As you can see from the other case, “routine”, there is another path to add: /routine. Click the button labeled Save below the code editor before moving on and adding this new path.

Provide instructions and visual references

Click the blue button labeled Add on the top of the screen, and rename the newly created path to /routine. Replace the preexisting code with the lines of code below:

const fetch = require('node-fetch')

exports.handler = async function(context, event, callback) {
  // Initialize response, twiml, and parse message body
  let response = new Twilio.Response();
  let twiml = new Twilio.twiml.MessagingResponse();
  let body = event.Body;

  // Initialize the necessary variables using data from cookies
  let tables = {
        "1": context.AIRTABLE_PUSH_TABLE_ID,
        "2": context.AIRTABLE_PULL_TABLE_ID,
        "3": context.AIRTABLE_LEG_TABLE_ID,
  };
  let routine = event.request.cookies.routine;
  let tableId = tables[routine];
  let idString = event.request.cookies.idString || "";
  let workout = parseInt(event.request.cookies.workout) || 0;

        response
          .setBody(twiml.toString())
          .appendHeader('Content-Type', 'text/xml')

  return callback(null, response)
};

The code will first initialize the fetch variable which will utilize the node-fetch module to fetch data from Airtable. The response, twiml, and body variables are then initialized just like in the previous step. The tables variable will be used to get the table IDs that you set earlier. If you added more ID environment variables, be sure to add them to the tables variable.

The routine variable is initialized to the cookie you set in the previous part, and the tableId variable is initialized to the selected ID. idString is initialized to either the idString cookie if one exists, or an empty string if not. Similarly, workout is initialized to the workout cookie parsed as a number if one exists, or a zero if not. Finally, the response variable’s properties get set and then the callback function is sent.

In the highlighted line, below the initialization of the workout variable, add the following code:

if(idString === "") {
        // If no idString cookie
        let workouts = [];
        let ids = [];
        let res = await fetch(`https://api.airtable.com/v0/${context.AIRTABLE_BASE_ID}/${tableId}?view=Grid%20view`, {
                method: "GET",
                headers: {
                        "Authorization": `Bearer ${context.AIRTABLE_API_KEY}`
                }
        });

        let data = await res.json();
        workouts = data.records;

        // Format and send message while pushing IDs into the ids array
        let workoutMessage = "Today's workouts are:\n\n"
        for(i=0; i<workouts.length; i++) {
                workoutMessage += `${workouts[i]["fields"]["Exercise"]}\n`
                ids.push(workouts[i]["id"])
        }

        workoutMessage += '\nRespond with "finish" to end the workout early, or anything else to go to the next workout.'
        twiml.message(workoutMessage)

// Set the idString cookie
        response.setCookie("idString", ids.toString());

This code is the first case in this path’s message tree: if idString is empty, then your code will first need to gather the workouts and exercises. The data is gathered and turned into JSON after fetching the proper Airtable API endpoint. Workout records are added to the message that will be sent to the user, and the IDs of the workouts are stored in the ids variable. The list of exercises and instructions on how to use the bot are added to twiml, and the idString cookie is set.

Below the line where you call the setCookie() method, right where your last pasted code block ended, copy and paste the following lines of code:

} else if(body === "finish" || workout === idString.split(",").length-1) {
        // If workout finishes or ends early, remove all cookies
        twiml.message(`I hope you had a great workout!`)
        response
          .removeCookie("idString")
          .removeCookie("routine")
          .removeCookie("stage")
          .removeCookie("workout")

In case the user sends “finish” as their message or they reach the end of their workout, then this code will send a goodbye message and remove all the cookies.

Finally, below the line where the workout cookie is removed, add the last lines of code:

} else {
        // If workouts have been found and idString has been set
        let ids = idString.split(",");
        let res = await fetch(`https://api.airtable.com/v0/${context.AIRTABLE_BASE_ID}/${tableId}/${ids[workout]}`, {
          method: "GET",
          headers: {
            "Authorization": `Bearer ${context.AIRTABLE_API_KEY}`
          }
        })
        let data = await res.json();
        let fields = data.fields;

        // Send necessary information and gif demonstration
        twiml.message().body(`${fields["Exercise"]}\n${fields["Video Demonstration"]}\nSets: ${fields["Sets"]}\n\n${fields["Description"]}`)
        twiml.message().media(fields["Gif Demonstration"])
    
        response
          .setCookie("workout", (workout + 1).toString())

  }

This code is responsible for fetching individual workout records for the user’s selected routine. A message is then formatted with all the necessary information, and then a gif is added to show the user how to perform the exercise. The workout cookie is incremented once the message is sent, so whenever the user responds with a message they will get the next workout in the routine.

Whew, your code is finally complete! Be sure to hit Save and then hit the blue button labeled Deploy All near the bottom of the page. The final step is adding this Twilio Function to your phone number!

Add the Function to your Twilio number

Navigate to your Active numbers page and then click on the Twilio phone number that you are using for this tutorial.

Scroll down to the Messaging section. Under the heading that says A MESSAGE COMES IN, click the dropdown menu and select Function. Next, select your Service in the dropdown menu under the SERVICE header, then select ui in the dropdown menu under the ENVIRONMENT header, and finally the function path you named earlier under the FUNCTION PATH header.

Twilio Phone Number - Messaging functionality set up

After that, hit the blue button labeled Save at the bottom of the page. You can now text your Twilio phone number and get some help in your workout!

Conversation with Twilio number where the texter is given exercise information

Conclusion

Hopefully this tutorial helps motivate you to do a bit of exercise! By the time this article comes out, my partner and I will have (hopefully) used this number to help us out. Try altering the code and data to match your personal workout plan! Or maybe you could code something for someone special in your life using Twilio’s many APIs?

Gift going through laptop

I can’t wait to see what you build next!

Dainyl Cua is a Software Engineer who loves helping others code. They would love to talk at any time and help you out. They can be reached through LinkedIn.