Building a medication reminder workflow with accountability

By Adam Temple

In this article I ask, what’s the easiest way to achieve a smart process for taking meds? Many tools give us a reminder. That’s not tricky. But we have more complex needs:

  • Twice daily Reminder
  • Input validation
  • Escalation to a family member when meds aren’t take, but only after a certain time
  • Avoid an app download

In our case, let’s communicate with them via SMS! That check’s off one of our requirement; to avoid an app download.

Considering the SMS interactivity and the nuance that this process needs, I am using a workflow library and code to accomplish this. Here’s some tools I bypassed and why:

Business Process Management suite

Although BPM’s are quite powerful I chose not to use them here. I am not handy with Java and all desktop BPM tools are customized with Java.

Process Automation tools online

There are some great tools like processplan.com, process.st and many others. But I wanted to own the process this time. Plus, they can be quite tricky to get customized inputs and outputs out of, like custom JSON. That would slow us down.

Temporal.io

I ended up using Temporal.io. It’s a Workflow and schedule and persistence server that has bindings in Go, among other languages. You can think of it this way: As a database is to data, Temporal is to workflows. It adds all the plumbing. Here’s an overview of how I built a process in with it.

In starter/main.go you will see a few things happening:

  • Grab variables from .env
  • Create a temporal client
  • Start a web server

Also you will see a cron. Temporal is great for this! Normally one could use the system cron to execute a script to send a signal, but that isn’t so straight forward. Dialing it in here is perfect. Here’s the code:


workflowOptions := client.StartWorkflowOptions{
    ID:        "ifworker-" + str2,
    TaskQueue: "insulinFlowWorker",
    // for immediate start, remove cron
    CronSchedule: "0 10,22 * * *",
}

we, err := temporal.ExecuteWorkflow(context.Background(), workflowOptions, app.InsulinWorkflow, patient, watchers)
if err != nil {
    log.Fatalln("Unable to execute workflow", err)
}

Once the time comes, here’s what the patient sees:

Reminder to take insulin. Please report how many units you took.

This is a twice daily reminder for this particular medication. Now let’s make it interactive. We do that via SMS, a perfect solution for single-threaded, low-context tasks.

First we setup a Twilio account which gives us the power to send SMS and call webhooks once an SMS is received back on same number. Sending an SMS is easy enough, so I will show you a bit about receiving and interacting with SMS.

From your Twilio console you need to setup a webhook on the SMS receive. Meaning, once Twilio gets and SMS from our users, it fires a webhook POST request to any address we specify. On starter/main.go you can see we are using /sms url.


// Setup POST request handler
serveMux := http.NewServeMux()
serveMux.HandleFunc("/sms", app.SMSPOSTHandler(context.Background(), temporal, we.GetRunID(), we.GetID()))

Now we get a POST request whenever an SMS comes in. You can see how we parse it with app.go. Since this workflow is asking for the amount of insulin taken, we just need that integer and we can move on.


// Trigger a signal from incoming SMS
func SMSPOSTHandler(ctx context.Context, temporal client.Client, runID, wfID string) func(http.ResponseWriter, *http.Request) {
    return func(w http.ResponseWriter, req *http.Request) {
        w.Header().Set("Content-Type", "application/x-www-form-urlencoded")

// ...

        amount, _ := strconv.Atoi(req.PostForm.Get("Body"))
        if err != nil {
            log.Fatal(err)
        }

        signal := Message{
            Amount: amount,
        }
        err = temporal.SignalWorkflow(context.Background(), wfID, "", "CHANNEL_YOLO", signal)
        if err != nil {
            log.Fatalln("Error sending the Signal foo", err)
            return
        }

    }
}

The signal that this triggers is an important concept in Temporal. Signals can come from code, the command line and other workflows. They are the communication needed to interact with and update a process. In our case, we want to send in the amount of insulin taken.



Input: 10

Since they put in a proper amount, we can move on to the next step. But this is the real world. Let’s put in a couple boundaries for that number to corral our user. If the units are inputted as lower than 0 or higher than 50, they aren’t accepted. This logic is inside of InsulinWorkflow. That is our only workflow. It fires off activities, waits for signals and more. Check it out in app.go.

Now they are corralled into giving us a good input. If the amount is proper, the “watcher” sees this:

Nathan just took x units of insulin

If the amount was outside of our boundaries or 15 mins has passed, the watcher sees

Nathan didn't take their insulin

Temporal is too juicy to pass up. It was birthed from Uber, originally named Uber. Some of those original users forked it to add improvements and that became Temporal. I can’t think of a better way to write workflows with code. I have tried many, many other tools. It’s a little difficult to describe it because it does a couple of different things that keep it outside of the usual categories.

Source code: https://github.com/adamhub/InsulinReminder