How to Build a CLI Reminder Application in GO

October 21, 2025
Written by
Temitope Taiwo Oyedele
Contributor
Opinions expressed by Twilio contributors are their own
Reviewed by

How to Build a CLI Reminder Application in GO

Staying organized and on top of our tasks is important, which is why sometimes the simplest solutions are the most effective.

And one of these simple tools is a reminder app that can help us manage tasks effectively by letting us set reminders for important events without the need for a graphical interface.

So, in this tutorial, we’ll build a CLI reminder app using Go.

Prerequisites

To follow along with this article, you'll need the following:

  • Basic knowledge of Go and its concepts
  • Go version 1.22 or higher
  • Your preferred IDE or text editor

Project scope

Here’s how the app will work. The CLI reminder app will allow users to schedule reminders with natural language inputs like "in 10 minutes" or specific times like "15:30". Once the time is up or reached, the app triggers a notification and a sound alert.

You can add as many reminders as you want. You can also fetch the list of reminders, as they get saved in a database. What's more, reminders are deleted automatically once they've been fulfilled.

Create the project

First, create a new directory for your project wherever you store your Go apps, and change into it by running the following commands:

mkdir cli_reminder
cd cli_reminder

Next, you need to initialize a new Go module by running this command:

go mod init

The app needs a database to store reminders. For this project we'll use SQLite. So install the Go-SQLite3 driver to interact with the SQLite database by running the following command in your terminal:

go get github.com/mattn/go-sqlite3

Build the application

Now, it's time to write the code, which will be stored in one file, main.go. Create the main.go file inside your project's top-level directory, and add the following code to the file:

package main

import (
  "database/sql"
  "fmt"
  "log"
  "os"
  "os/exec"
  "runtime"
  "strconv"
  "strings"
  "time"
  _ "github.com/mattn/go-sqlite3"
)

func InitializeDatabase() *sql.DB {
  db, err := sql.Open("sqlite3", "reminders.db")
  if err != nil {
    log.Fatal("Failed to open database:", err)
  }

  _, err = db.Exec(`
    CREATE TABLE IF NOT EXISTS reminders (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      time DATETIME NOT NULL,
      message TEXT NOT NULL,
      status TEXT NOT NULL DEFAULT 'pending'
    )
  `)
  if err != nil {
    log.Fatal("Failed to create table:", err)
  }

  return db
}

The InitializeDatabase() initializes the SQLite database, connects to the database file (r eminders.db) and creates a table named "reminders" with the following columns for "id","time","message", and "status".

Add a reminder

Next, you need to create a function that adds a reminder to the database. In main.go, right after the InitializeDatabase() function, add the following code:

func AddReminder(db * sql.DB, reminderTime time.Time, message string) int64 {
    result, err := db.Exec("INSERT INTO reminders (time, message, status) VALUES (?, ?, 'pending')", reminderTime, message)
    if err != nil {
        log.Println("Failed to add reminder:", err)
        return 0
    }
    id, _ := result.LastInsertId()
    fmt.Println("Reminder added to database with ID:", id)
    return id
}

This function adds a new reminder to the database. It stores the reminder’s time, message, and default pending status. Àfterward, it returns the newly added reminder's unique ID.

Fetch upcoming reminders

Sometimes, you might want to view the list of reminders in the database that has yet to take place to ensure you’ve added all the reminders that you need to have a productive day. To achieve this, you need to create a function to help with that.

Right after the AddReminder() function, add this code:

func GetUpcomingReminders(db * sql.DB)([] struct {
    ID int
    Time time.Time
    Message string
}, error) {
    rows, err := db.Query("SELECT id, time, message FROM reminders WHERE status = 'pending' AND time > ?", time.Now())
    if err != nil {
        return nil, err
    }
    defer rows.Close()

    var reminders[] struct {
        ID int
        Time time.Time
        Message string
    }

    for rows.Next() {
        var id int
        var reminderTime time.Time
        var message string
        err = rows.Scan( & id, & reminderTime, & message)
        if err != nil {
            return nil, err
        }
        reminders = append(reminders, struct {
            ID int
            Time time.Time
            Message string
        } {
            ID: id,
            Time: reminderTime,
            Message: message,
        })
    }

    return reminders, nil
}

The code above retrieves reminders with their status set to "pending", and scheduled time is in the future. The results are returned as a slice of structs containing the ID, time, and message of each one.

Delete a reminder

Right after the GetUpcomingReminders() function,add the following code:

func DeleteReminder(db * sql.DB, id int) {
    _, err := db.Exec("DELETE FROM reminders WHERE id = ?", id)
    if err != nil {
        log.Println("Failed to delete reminder:", err)
    } else {
        fmt.Printf("Reminder with ID %d deleted.\n", id)
    }
}

The code removes a reminder from the database using its unique ID, ensuring old or unnecessary reminders are cleared out.

Parse a natural language time

You next need to create a function that will be responsible for converting user-friendly, natural language inputs into a time.Time object, which the program will use to schedule reminders. It will support both relative times (e.g., "in 10 minutes") and absolute times (e.g., "15:30"). With this flexibility, users will be able to set reminders in a way that feels intuitive.

Right after the function that deletes a reminder, add the following code:

func ParseTime(input string)(time.Time, error) {
    now := time.Now()
    if strings.HasPrefix(input, "in ") {
        parts := strings.Fields(strings.TrimPrefix(input, "in "))
        if len(parts) < 2 || (parts[1] != "minutes" && parts[1] != "hours" && parts[1] != "minute" && parts[1] != "hour") {
            return time.Time {}, fmt.Errorf("invalid format for 'in X time units'")
        }
        durationValue, err := strconv.Atoi(parts[0])
        if err != nil || durationValue <= 0 {
            return time.Time {}, fmt.Errorf("invalid duration value")
        }

        if parts[1] == "minutes" || parts[1] == "minute" {
            return now.Add(time.Duration(durationValue) * time.Minute), nil
        } else if parts[1] == "hours" || parts[1] == "hour" {
            return now.Add(time.Duration(durationValue) * time.Hour), nil
        }

        if len(parts) > 2 && parts[2] == "and" && len(parts) > 4 && (parts[4] == "minutes" || parts[4] == "minute") {
            minutes, err := strconv.Atoi(parts[3])
            if err != nil || minutes < 0 {
                return time.Time {}, fmt.Errorf("invalid minute value")
            }
            return now.Add(time.Duration(durationValue) * time.Hour + time.Duration(minutes) * time.Minute), nil
        }
    }

    timeParts := strings.Split(input, ":")
    if len(timeParts) == 2 {
        hour, err := strconv.Atoi(timeParts[0])
        if err != nil || hour < 0 || hour > 23 {
            return time.Time {}, fmt.Errorf("invalid hour in time")
        }

        minute, err := strconv.Atoi(timeParts[1])
        if err != nil || minute < 0 || minute > 59 {
            return time.Time {}, fmt.Errorf("invalid minute in time")
        }

        return time.Date(now.Year(), now.Month(), now.Day(), hour, minute, 0, 0, now.Location()), nil
    }

    return time.Time{}
}

Add an alert sound

Next up, you need to add a sound so that whenever a reminder pops up, it is followed by an alert sound, as this is the main reason why we create a reminder in the first place. So, right after the ParseTime() function, add the following code:

func PlayBeep() {
    repeats := 2
    for i := 0; i < repeats; i++ {
        switch runtime.GOOS {
            case "windows":
                cmd := exec.Command("powershell", "-c", `Add-Type -AssemblyName presentationCore; (New-Object System.Media.SoundPlayer "C:\\Windows\\Media\\ding.wav").PlaySync();`)
			err := cmd.Run()
                if err != nil {
                    fmt.Println("Failed to play beep sound:", err)
                }
            case "darwin":
                cmd := exec.Command("afplay", "/System/Library/Sounds/Glass.aiff")
                err := cmd.Run()
                if err != nil {
                    fmt.Println("Failed to play sound on macOS:", err)
                }
            case "linux":
                cmd := exec.Command("aplay", "/usr/share/sounds/alsa/Front_Center.wav")
                err := cmd.Run()
                if err != nil {
                    fmt.Println("Failed to play sound on Linux:", err)
                }
            default:
                fmt.Print("\a")
        }
        time.Sleep(300 * time.Millisecond)
    }
}

This function generates a sound to alert users when a reminder is due. It uses platform-specific commands to play sounds on Windows, macOS, and Linux, PowerShell, afplay, and aplay, respectively.

Display the notification

You next need to create a function for displaying desktop notifications to alert users when a reminder is due, a key component of the reminder application. This function is designed to work across the three major operating systems, Linux, macOS, and Windows.

Right after the PlayBeep() function, add the following code:

func ShowNotification(title, message string) {
    switch runtime.GOOS {
        case "linux":
            cmd := exec.Command("notify-send", title, message)
            _ = cmd.Run()
        case "darwin":
            cmd := exec.Command("osascript", "-e", fmt.Sprintf(`display notification "%s" with title "%s"`, message, title))
            _ = cmd.Run()
        case "windows":
            cmd := exec.Command("powershell", "-c", `[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime].CreateToastNotifier().Show((New-Object Windows.UI.Notifications.ToastNotification (New-Object Windows.Data.Xml.Dom.XmlDocument)))`)
            _ = cmd.Run()
        default:
            fmt.Println("Unsupported OS for notifications.")
    }
}

This function displays a pop-up notification to remind the user. It uses system commands like notify-send for Linux, osascript for macOS, and PowerShell (Windows) for platform compatibility.

Add the main function

You're almost done! Now, you just need to create the main() function that serves as the entry point for the reminder application. It orchestrates the overall workflow, handling database initialization, processing pending reminders, and managing user interactions for adding or viewing reminders.

Right after the ShowNotification() function, add the following code:

func main() {
    db: = InitializeDatabase()
    defer db.Close()

    fmt.Println("Checking for pending reminders...")
    pendingReminders,
    err: = GetUpcomingReminders(db)
    if err != nil {
        log.Println("Failed to fetch pending reminders:", err)
    } else {
        for _, reminder: = range pendingReminders {
            go func(reminder struct {
                ID int
                Time time.Time
                Message string
            }) {
                now: = time.Now()
                if reminder.Time.Before(now) {
                    fmt.Printf("Skipping expired reminder: %s\n", reminder.Message)
                    DeleteReminder(db, reminder.ID)
                    return
                }
                diff: = reminder.Time.Sub(now)
                fmt.Printf("Rescheduling reminder: %s (in %v)\n", reminder.Message, diff.Round(time.Second))
                time.Sleep(diff)
                ShowNotification("Reminder", reminder.Message)
                PlayBeep()
                DeleteReminder(db, reminder.ID)
            }(reminder)
        }
    }

    if len(os.Args) < 3 {
        fmt.Println("Usage: go run main.go <time> <message>")
        fmt.Println("Fetching upcoming reminders:")
        upcomingReminders, err: = GetUpcomingReminders(db)
        if err != nil {
            log.Println("Failed to fetch reminders:", err)
            os.Exit(1)
        }
        if len(upcomingReminders) == 0 {
            fmt.Println("No upcoming reminders.")
        } else {
            for _, reminder: = range upcomingReminders {
                fmt.Printf("- [%d] %s: %s\n", reminder.ID, reminder.Time.Format("15:04"), reminder.Message)
            }
        }
        os.Exit(0)
    }

    reminderInput: = os.Args[1]
    message: = strings.Join(os.Args[2: ], " ")
    reminderTime,
    err: = ParseTime(reminderInput)
    if err != nil {
        fmt.Println("Error parsing time:", err)
        os.Exit(2)
    }

    reminderID: = AddReminder(db, reminderTime, message)
    go func() {
        now: = time.Now()
        if now.After(reminderTime) {
            DeleteReminder(db, int(reminderID))
            return
        }
        diff: = reminderTime.Sub(now)
        fmt.Printf("Reminder set for %s. Waiting for %v...\n", reminderTime.Format("15:04"), diff.Round(time.Second))
        time.Sleep(diff)
        ShowNotification("Reminder", message)
        PlayBeep()
        DeleteReminder(db, int(reminderID))
    }()
    select {}
}

The main() function initializes the database and retrieves any pending reminders. It then parses new reminder input and schedules them using goroutines, allowing the app to handle multiple reminders concurrently.

Each goroutine calculates the time difference until the reminder is due, waits for the time to arrive, and triggers a notification with an alert sound. Once the reminder is fulfilled, it is deleted from the database. The app runs continuously, processing reminders without blocking.

Test the application

Now, let's check that the code works as expected by adding a reminder by running the following command:

go run main.go "in 1 minute" "Meeting reminder"

What you’re doing is telling the program to create a reminder for a specific time. In this case, "1 minute" from now. The "in 1 minute" part specifies the time for the reminder, and "Meeting reminder" is the message that will be displayed when the reminder triggers.

This command makes the app process the time input, add the reminder to the database, and set a notification and sound alert for when that time arrives.

image showing how a reminder being added via the CLI command
image showing how a reminder being added via the CLI command

You should see that the reminder is added to the database.You should get a notification pop-up reminder after one minute.

Next, fetch a list of reminders byrunning this command:

go run main.go fetch
image showing the list of reminders
image showing the list of reminders

You should see a list of reminders displayed in your terminal.

Now, it's time to build the application and make it available everywhere.

Run this command:

go build -o reminder

This command compiles the Go code into an executable. The go build command compiles the source files in the current directory and generates a binary. The "-o" option specifies the output file name, in this case, reminder. This means the compiled application will be saved as a reminder, which can be run directly from the command line.

The next step is to make this binary accessible from anywhere in the terminal, which is accomplished by moving it to a directory that is part of your system's PATH.

To do this, run this command:

mv reminder /usr/local/bin

This moves the reminder binary to the /usr/local/bin directory, which is already included in the system’s PATH.

For Windows users, you can either manually add a directory to the PATH or move the executable to a directory that's already included in the PATH.

Now, to add a reminder, run the following command:

reminder "in 1 minute" "Stretch your legs"
Add a reminder directly from your system terminal
Add a reminder directly from your system terminal

You’ll see it gets added, and, after a minute, you get a reminder.

To fetch a list of reminders, run this command:

reminder
View the list of reminder directly from your system terminal
View the list of reminder directly from your system terminal

And now we’re done!

That’s how to build a CLI reminder application in Go

In this tutorial, you learned how to build a CLI reminder application in Go.

You can also use libraries like Cobra, which can significantly simplify the process of building robust CLI applications. Cobra provides a clean and expressive way to define commands, flags, and arguments, making it an excellent choice for enhancing the development experience. Please share if you found this helpful.

Temitope Taiwo Oyedele is a software engineer and technical writer. He likes to write about things he’s learned and experienced.

Terminal icon created by Royyan Wijaya on Flaticon. The reminder icon was created by Darius Dan on Flaticon.