diff --git a/backend/allowance_planner.db3.backup.3 b/backend/allowance_planner.db3.backup.3
deleted file mode 100644
index 380cafd..0000000
Binary files a/backend/allowance_planner.db3.backup.3 and /dev/null differ
diff --git a/backend/api_test.go b/backend/api_test.go
index dec630e..7959b17 100644
--- a/backend/api_test.go
+++ b/backend/api_test.go
@@ -15,8 +15,9 @@ const (
 func startServer(t *testing.T) *httpexpect.Expect {
 	config := ServerConfig{
 		Datasource: ":memory:",
-		Addr:       ":0",
-		Started:    make(chan bool),
+		//Datasource: "test.db",
+		Addr:    ":0",
+		Started: make(chan bool),
 	}
 	go start(t.Context(), &config)
 	<-config.Started
@@ -284,6 +285,54 @@ func TestCreateTask(t *testing.T) {
 	responseWithUser.Value("id").Number().IsEqual(2)
 }
 
+func TestCreateScheduleTask(t *testing.T) {
+	e := startServer(t)
+
+	// Create a new task without assigned user
+	requestBody := map[string]interface{}{
+		"name":     "Test Task",
+		"reward":   100,
+		"schedule": "0 */5 * * * *",
+	}
+
+	response := e.POST("/tasks").
+		WithJSON(requestBody).
+		Expect().
+		Status(201). // Expect Created status
+		JSON().Object()
+
+	requestBody["schedule"] = "every 5 seconds"
+	e.POST("/tasks").WithJSON(requestBody).Expect().Status(400)
+
+	// Verify the response has an ID
+	response.ContainsKey("id")
+	response.Value("id").Number().IsEqual(1)
+
+	e.GET("/tasks").Expect().Status(200).JSON().Array().Length().IsEqual(1)
+
+	// Get task
+	result := e.GET("/task/1").Expect().Status(200).JSON().Object()
+	result.Value("id").IsEqual(1)
+	result.Value("name").IsEqual("Test Task")
+	result.Value("schedule").IsEqual("0 */5 * * * *")
+	result.Value("reward").IsEqual(100)
+	result.Value("assigned").IsNull()
+
+	// Complete the task
+	e.POST("/task/1/complete").Expect().Status(200)
+
+	// Set expires date to 1 second in the past
+	db.db.Query("update tasks set next_run = ? where id = 1").Bind(time.Now().Add(10 * -time.Minute).Unix()).MustExec()
+
+	// Verify a new task is created
+	newTask := e.GET("/task/2").Expect().Status(200).JSON().Object()
+	newTask.Value("id").IsEqual(2)
+	newTask.Value("name").IsEqual("Test Task")
+	newTask.Value("schedule").IsEqual("0 */5 * * * *")
+	newTask.Value("reward").IsEqual(100)
+	newTask.Value("assigned").IsNull()
+}
+
 func TestDeleteTask(t *testing.T) {
 	e := startServer(t)
 
diff --git a/backend/db.go b/backend/db.go
index 4c0aaf3..71b7d9d 100644
--- a/backend/db.go
+++ b/backend/db.go
@@ -3,6 +3,7 @@ package main
 import (
 	"errors"
 	"fmt"
+	"github.com/adhocore/gronx"
 	"log"
 	"math"
 	"time"
@@ -313,10 +314,20 @@ func (db *Db) CreateTask(task *CreateTaskRequest) (int, error) {
 	}
 	defer tx.MustRollback()
 
+	var nextRun *int64
+	if task.Schedule != nil {
+		nextRunTime, err := gronx.NextTick(*task.Schedule, false)
+		if err != nil {
+			return 0, fmt.Errorf("failed to calculate next run: %w", err)
+		}
+		nextRunTimeAsInt := nextRunTime.Unix()
+		nextRun = &nextRunTimeAsInt
+	}
+
 	// Insert the new task
 	reward := int(math.Round(task.Reward * 100.0))
-	err = tx.Query("insert into tasks (name, reward, assigned) values (?, ?, ?)").
-		Bind(task.Name, reward, task.Assigned).
+	err = tx.Query("insert into tasks (name, reward, assigned, schedule, next_run) values (?, ?, ?, ?, ?)").
+		Bind(task.Name, reward, task.Assigned, task.Schedule, nextRun).
 		Exec()
 
 	if err != nil {
@@ -340,13 +351,17 @@ func (db *Db) CreateTask(task *CreateTaskRequest) (int, error) {
 }
 
 func (db *Db) GetTasks() ([]Task, error) {
-	tasks := make([]Task, 0)
-	var err error
+	err := db.UpdateScheduledTasks()
+	if err != nil {
+		return nil, fmt.Errorf("failed to update scheduled tasks: %w", err)
+	}
 
-	for row := range db.db.Query("select id, name, reward, assigned from tasks").Range(&err) {
+	tasks := make([]Task, 0)
+
+	for row := range db.db.Query("select id, name, reward, assigned, schedule from tasks where completed is null").Range(&err) {
 		task := Task{}
 		var reward int64
-		err = row.Scan(&task.ID, &task.Name, &reward, &task.Assigned)
+		err = row.Scan(&task.ID, &task.Name, &reward, &task.Assigned, &task.Schedule)
 		task.Reward = float64(reward) / 100.0
 		if err != nil {
 			return nil, err
@@ -362,16 +377,78 @@ func (db *Db) GetTasks() ([]Task, error) {
 func (db *Db) GetTask(id int) (Task, error) {
 	task := Task{}
 
-	var reward int64
-	err := db.db.Query("select id, name, reward, assigned from tasks where id = ?").
-		Bind(id).ScanSingle(&task.ID, &task.Name, &reward, &task.Assigned)
-	task.Reward = float64(reward) / 100.0
+	err := db.UpdateScheduledTasks()
 	if err != nil {
-		return Task{}, err
+		return Task{}, fmt.Errorf("failed to update scheduled tasks: %w", err)
 	}
+
+	var reward int64
+	err = db.db.Query("select id, name, reward, assigned, schedule from tasks where id = ? and completed is null").
+		Bind(id).ScanSingle(&task.ID, &task.Name, &reward, &task.Assigned, &task.Schedule)
+	if err != nil {
+		return task, err
+	}
+	task.Reward = float64(reward) / 100.0
 	return task, nil
 }
 
+func (db *Db) UpdateScheduledTasks() error {
+	type ScheduledTask struct {
+		ID       int
+		Schedule string
+		Expires  int64
+	}
+	tasks := make([]ScheduledTask, 0)
+	var err error
+
+	for row := range db.db.Query("select id, schedule, next_run from tasks where schedule is not null").Range(&err) {
+		task := ScheduledTask{}
+		err := row.Scan(&task.ID, &task.Schedule, &task.Expires)
+		if err != nil {
+			return err
+		}
+		if time.Now().Unix() >= task.Expires {
+			tasks = append(tasks, task)
+		}
+	}
+	if err != nil {
+		return fmt.Errorf("failed to fetch scheduled tasks: %w", err)
+	}
+
+	tx, err := db.db.Begin()
+	if err != nil {
+		return err
+	}
+	defer tx.MustRollback()
+
+	for _, task := range tasks {
+		nextRun, err := gronx.NextTickAfter(task.Schedule, time.Now(), false)
+		if err != nil {
+			return fmt.Errorf("failed to calculate next run for task %d: %w", task.ID, err)
+		}
+
+		err = tx.Query("insert into tasks (name, reward, assigned, schedule, next_run) select name, reward, assigned, schedule, ? from tasks where id = ?").
+			Bind(nextRun.Unix(), task.ID).
+			Exec()
+		if err != nil {
+			return err
+		}
+
+		err = tx.Query("update tasks set schedule = null where id = ?").Bind(task.ID).Exec()
+		if err != nil {
+			return err
+		}
+
+		tx.Query("select last_insert_rowid()").MustScanSingle(&task.ID)
+		log.Printf("Task %d scheduled for %s", task.ID, nextRun)
+	}
+	if err != nil {
+		return err
+	}
+
+	return tx.Commit()
+}
+
 func (db *Db) DeleteTask(id int) error {
 	tx, err := db.db.Begin()
 	if err != nil {
@@ -453,7 +530,10 @@ func (db *Db) CompleteTask(taskId int) error {
 	}
 
 	// Remove the task
-	err = tx.Query("delete from tasks where id = ?").Bind(taskId).Exec()
+	err = tx.Query("update tasks set completed=? where id = ?").Bind(time.Now().Unix(), taskId).Exec()
+	if err != nil {
+		return err
+	}
 
 	return tx.Commit()
 }
diff --git a/backend/dto.go b/backend/dto.go
index d51831c..6b89d3c 100644
--- a/backend/dto.go
+++ b/backend/dto.go
@@ -29,7 +29,8 @@ type Task struct {
 	ID       int     `json:"id"`
 	Name     string  `json:"name"`
 	Reward   float64 `json:"reward"`
-	Assigned *int    `json:"assigned"` // Pointer to allow null
+	Assigned *int    `json:"assigned"`
+	Schedule *string `json:"schedule"`
 }
 
 type Allowance struct {
@@ -68,6 +69,7 @@ type CreateTaskRequest struct {
 	Name     string  `json:"name" binding:"required"`
 	Reward   float64 `json:"reward"`
 	Assigned *int    `json:"assigned"`
+	Schedule *string `json:"schedule"`
 }
 
 type CreateTaskResponse struct {
diff --git a/backend/go.mod b/backend/go.mod
index 8ded2c3..4e1850c 100644
--- a/backend/go.mod
+++ b/backend/go.mod
@@ -6,11 +6,12 @@ require (
 	gitea.seeseepuff.be/seeseemelk/mysqlite v0.14.0
 	github.com/gavv/httpexpect/v2 v2.17.0
 	github.com/gin-contrib/cors v1.7.5
-	github.com/gin-gonic/gin v1.10.0
+	github.com/gin-gonic/gin v1.10.1
 )
 
 require (
 	github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2 // indirect
+	github.com/adhocore/gronx v1.19.6 // indirect
 	github.com/ajg/form v1.5.1 // indirect
 	github.com/andybalholm/brotli v1.1.1 // indirect
 	github.com/bytedance/sonic v1.13.2 // indirect
@@ -49,7 +50,7 @@ require (
 	github.com/sergi/go-diff v1.3.1 // indirect
 	github.com/stretchr/testify v1.10.0 // indirect
 	github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
-	github.com/ugorji/go/codec v1.2.12 // indirect
+	github.com/ugorji/go/codec v1.2.14 // indirect
 	github.com/valyala/bytebufferpool v1.0.0 // indirect
 	github.com/valyala/fasthttp v1.62.0 // indirect
 	github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
@@ -68,10 +69,10 @@ require (
 	gopkg.in/fsnotify.v1 v1.4.7 // indirect
 	gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
-	modernc.org/libc v1.65.7 // indirect
+	modernc.org/libc v1.65.8 // indirect
 	modernc.org/mathutil v1.7.1 // indirect
 	modernc.org/memory v1.11.0 // indirect
-	modernc.org/sqlite v1.37.0 // indirect
+	modernc.org/sqlite v1.37.1 // indirect
 	moul.io/http2curl/v2 v2.3.0 // indirect
-	zombiezen.com/go/sqlite v1.4.0 // indirect
+	zombiezen.com/go/sqlite v1.4.2 // indirect
 )
diff --git a/backend/go.sum b/backend/go.sum
index 0a43417..411208b 100644
--- a/backend/go.sum
+++ b/backend/go.sum
@@ -2,6 +2,8 @@ gitea.seeseepuff.be/seeseemelk/mysqlite v0.14.0 h1:aRItVfUj48fBmuec7rm/jY9KCfvHW
 gitea.seeseepuff.be/seeseemelk/mysqlite v0.14.0/go.mod h1:cgswydOxJjMlNwfcBIXnKjr47LwXnMT9BInkiHb0tXE=
 github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2 h1:ZBbLwSJqkHBuFDA6DUhhse0IGJ7T5bemHyNILUjvOq4=
 github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2/go.mod h1:VSw57q4QFiWDbRnjdX8Cb3Ow0SFncRw+bA/ofY6Q83w=
+github.com/adhocore/gronx v1.19.6 h1:5KNVcoR9ACgL9HhEqCm5QXsab/gI4QDIybTAWcXDKDc=
+github.com/adhocore/gronx v1.19.6/go.mod h1:7oUY1WAU8rEJWmAxXR2DN0JaO4gi9khSgKjiRypqteg=
 github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
 github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
 github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
@@ -34,6 +36,8 @@ github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w
 github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
 github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
 github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
+github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
+github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
 github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
 github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
 github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
@@ -127,6 +131,8 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS
 github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
 github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
 github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
+github.com/ugorji/go/codec v1.2.14 h1:yOQvXCBc3Ij46LRkRoh4Yd5qK6LVOgi0bYOXfb7ifjw=
+github.com/ugorji/go/codec v1.2.14/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
 github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
 github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
 github.com/valyala/fasthttp v1.62.0 h1:8dKRBX/y2rCzyc6903Zu1+3qN0H/d2MsxPPmVNamiH0=
@@ -216,6 +222,8 @@ modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
 modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
 modernc.org/libc v1.65.7 h1:Ia9Z4yzZtWNtUIuiPuQ7Qf7kxYrxP1/jeHZzG8bFu00=
 modernc.org/libc v1.65.7/go.mod h1:011EQibzzio/VX3ygj1qGFt5kMjP0lHb0qCW5/D/pQU=
+modernc.org/libc v1.65.8 h1:7PXRJai0TXZ8uNA3srsmYzmTyrLoHImV5QxHeni108Q=
+modernc.org/libc v1.65.8/go.mod h1:011EQibzzio/VX3ygj1qGFt5kMjP0lHb0qCW5/D/pQU=
 modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
 modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
 modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
@@ -226,6 +234,8 @@ modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
 modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
 modernc.org/sqlite v1.37.0 h1:s1TMe7T3Q3ovQiK2Ouz4Jwh7dw4ZDqbebSDTlSJdfjI=
 modernc.org/sqlite v1.37.0/go.mod h1:5YiWv+YviqGMuGw4V+PNplcyaJ5v+vQd7TQOgkACoJM=
+modernc.org/sqlite v1.37.1 h1:EgHJK/FPoqC+q2YBXg7fUmES37pCHFc97sI7zSayBEs=
+modernc.org/sqlite v1.37.1/go.mod h1:XwdRtsE1MpiBcL54+MbKcaDvcuej+IYSMfLN6gSKV8g=
 modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
 modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
 modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
@@ -235,3 +245,5 @@ moul.io/http2curl/v2 v2.3.0/go.mod h1:RW4hyBjTWSYDOxapodpNEtX0g5Eb16sxklBqmd2RHc
 nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
 zombiezen.com/go/sqlite v1.4.0 h1:N1s3RIljwtp4541Y8rM880qgGIgq3fTD2yks1xftnKU=
 zombiezen.com/go/sqlite v1.4.0/go.mod h1:0w9F1DN9IZj9AcLS9YDKMboubCACkwYCGkzoy3eG5ik=
+zombiezen.com/go/sqlite v1.4.2 h1:KZXLrBuJ7tKNEm+VJcApLMeQbhmAUOKA5VWS93DfFRo=
+zombiezen.com/go/sqlite v1.4.2/go.mod h1:5Kd4taTAD4MkBzT25mQ9uaAlLjyR0rFhsR6iINO70jc=
diff --git a/backend/main.go b/backend/main.go
index c1481a2..514972c 100644
--- a/backend/main.go
+++ b/backend/main.go
@@ -4,7 +4,9 @@ import (
 	"context"
 	"embed"
 	"errors"
+	"fmt"
 	"gitea.seeseepuff.be/seeseemelk/mysqlite"
+	"github.com/adhocore/gronx"
 	"log"
 	"net"
 	"net/http"
@@ -436,6 +438,14 @@ func createTask(c *gin.Context) {
 		return
 	}
 
+	if taskRequest.Schedule != nil {
+		valid := gronx.IsValid(*taskRequest.Schedule)
+		if !valid {
+			c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("Invalid cron schedule: %s", *taskRequest.Schedule)})
+			return
+		}
+	}
+
 	// If assigned is not nil, check if user exists
 	if taskRequest.Assigned != nil {
 		exists, err := db.UserExists(*taskRequest.Assigned)
@@ -513,6 +523,11 @@ func putTask(c *gin.Context) {
 		c.JSON(http.StatusNotFound, gin.H{"error": "Task not found"})
 		return
 	}
+	if err != nil {
+		log.Printf("Error getting task: %v", err)
+		c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError})
+		return
+	}
 
 	err = db.UpdateTask(taskId, &taskRequest)
 	if err != nil {
diff --git a/backend/migrations/5_add_schedules.sql b/backend/migrations/5_add_schedules.sql
new file mode 100644
index 0000000..16c2807
--- /dev/null
+++ b/backend/migrations/5_add_schedules.sql
@@ -0,0 +1,3 @@
+alter table tasks add column schedule text;
+alter table tasks add column completed date;
+alter table tasks add column next_run date;
diff --git a/backend/web.go b/backend/web.go
index d8188e1..02e178b 100644
--- a/backend/web.go
+++ b/backend/web.go
@@ -71,10 +71,18 @@ func renderCreateTask(c *gin.Context) {
 		return
 	}
 
-	_, err = db.CreateTask(&CreateTaskRequest{
+	request := &CreateTaskRequest{
 		Name:   name,
 		Reward: reward,
-	})
+	}
+
+	schedule := c.PostForm("schedule")
+	if schedule != "" {
+		request.Schedule = &schedule
+	}
+	
+	_, err = db.CreateTask(request)
+
 	if err != nil {
 		renderError(c, http.StatusInternalServerError, err)
 		return
diff --git a/backend/web.gohtml b/backend/web.gohtml
index cd23150..5d838c0 100644
--- a/backend/web.gohtml
+++ b/backend/web.gohtml
@@ -81,6 +81,7 @@
 					<th>Name</th>
 					<th>Assigned</th>
 					<th>Reward</th>
+					<th>Schedule</th>
 					<th>Actions</th>
 				</tr>
 				</thead>
@@ -96,6 +97,7 @@
 							{{end}}
 						</td>
 						<td>{{.Reward}}</td>
+						<td>{{.Schedule}}</td>
 						<td>
 							<a href="/completeTask?task={{.ID}}">Mark as completed</a>
 						</td>
@@ -105,6 +107,7 @@
 							<td><label><input type="text" name="name" placeholder="Name"></label></td>
 							<td></td>
 							<td><label><input type="number" name="reward" placeholder="Reward"></label></td>
+							<td><label><input type="text" name="schedule" placeholder="Schedule"></label></td>
 							<td><input type="submit" value="Create"></td>
 						</tr>
 				</tbody>
diff --git a/common/api.yaml b/common/api.yaml
index e2fddd9..20dedbe 100644
--- a/common/api.yaml
+++ b/common/api.yaml
@@ -422,7 +422,10 @@ components:
           description: The task name
         reward:
           type: integer
-          description: The task reward, in cents
+          description: The task reward
+        schedule:
+          type: string
+          description: The schedule of the task, in cron format
         assigned:
           type: integer
           description: The user ID of the user assigned to the task