diff --git a/batch/batch.go b/batch/batch.go
new file mode 100644
index 0000000000000000000000000000000000000000..1763f04332f0f200c59a31127521b6e01f1cef0f
--- /dev/null
+++ b/batch/batch.go
@@ -0,0 +1,42 @@
+// Package batch contains the non-interactive maintenence commands.
+package batch
+
+import (
+	_ "embed"
+	"fmt"
+	"os"
+	"strings"
+	"text/template"
+)
+
+//go:embed crontab
+var crontabTemplate string
+
+// Reboot deletes all messages with `shutdown` set.
+func Reboot() int {
+	fmt.Println("TODO: Delete messages with shutdown != 0.")
+	return 0
+}
+
+// Expire deletes all messages that have hit their expiration date.
+func Expire() int {
+	fmt.Println("TODO: expire messages.")
+	return 0
+}
+
+// Install is an interactive command used to install the crontab.
+func Install() int {
+	bulletin, err := os.Executable()
+	if err != nil {
+		panic(err) // TODO: cleanup error handling.
+	}
+	crontab := &strings.Builder{}
+	template.Must(template.New("crontab").Parse(crontabTemplate)).
+		Execute(crontab, map[string]string{"Bulletin": bulletin})
+	fmt.Printf("Add this to crontab:\n\n%s\n", crontab.String())
+
+	fmt.Println("TODO: Finish installing bulletin.")
+	// TODO: Offer to add ssh key for system user.
+	// TODO: Offer to add ssh key for other user(s).
+	return 0
+}
diff --git a/batch/crontab b/batch/crontab
new file mode 100644
index 0000000000000000000000000000000000000000..c3dac074c2d8521b7f9bbe7778293c0933f93d9c
--- /dev/null
+++ b/batch/crontab
@@ -0,0 +1,2 @@
+@reboot {{ .Bulletin }} -u SYSTEM -b reboot
+@daily {{ .Bulletin }} -u SYSTEM -b expire
diff --git a/go.mod b/go.mod
index 2d648865a1f3f087bb62f87e56b60ae450421729..c6997802db3e8ff5106caf0584601998d708e186 100644
--- a/go.mod
+++ b/go.mod
@@ -6,8 +6,10 @@ require (
 	github.com/adrg/xdg v0.5.3
 	github.com/chzyer/readline v1.5.1
 	github.com/davecgh/go-spew v1.1.1
+	github.com/gdamore/tcell/v2 v2.7.1
 	github.com/golang-migrate/migrate/v4 v4.18.3
 	github.com/jmoiron/sqlx v1.4.0
+	github.com/rivo/tview v0.0.0-20250501113434-0c592cd31026
 	github.com/urfave/cli/v3 v3.3.2
 	modernc.org/sqlite v1.37.0
 )
@@ -15,7 +17,6 @@ require (
 require (
 	github.com/dustin/go-humanize v1.0.1 // indirect
 	github.com/gdamore/encoding v1.0.0 // indirect
-	github.com/gdamore/tcell/v2 v2.7.1 // indirect
 	github.com/golang-migrate/migrate v3.5.4+incompatible // indirect
 	github.com/google/uuid v1.6.0 // indirect
 	github.com/hashicorp/errwrap v1.1.0 // indirect
@@ -26,7 +27,6 @@ require (
 	github.com/mitchellh/go-wordwrap v1.0.1 // indirect
 	github.com/ncruces/go-strftime v0.1.9 // indirect
 	github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
-	github.com/rivo/tview v0.0.0-20250501113434-0c592cd31026 // indirect
 	github.com/rivo/uniseg v0.4.7 // indirect
 	go.uber.org/atomic v1.11.0 // indirect
 	golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 // indirect
diff --git a/main.go b/main.go
index 87ef32a80af81ba9b2f9f8df4bab8218db0381cd..e6acf8f7386ba4dd7d407a44e613aa45eda76519 100644
--- a/main.go
+++ b/main.go
@@ -8,6 +8,7 @@ import (
 	"os"
 
 	"git.lyda.ie/kevin/bulletin/accounts"
+	"git.lyda.ie/kevin/bulletin/batch"
 	"git.lyda.ie/kevin/bulletin/repl"
 
 	"github.com/urfave/cli/v3"
@@ -33,19 +34,26 @@ func main() {
 		},
 		Action: func(_ context.Context, cmd *cli.Command) error {
 			user := cmd.String("user")
-			batch := cmd.String("batch")
+			batchFlag := cmd.String("batch")
 
-			if batch != "" {
+			if batchFlag != "" {
 				if user != "SYSTEM" {
 					fmt.Println("ERROR: can only run batch commands as SYSTEM.")
 					os.Exit(1)
 				}
+				err := accounts.Open(user)
+				if err != nil {
+					fmt.Printf("ERROR: %s.", err)
+					os.Exit(1)
+				}
 				exitcode := 0
-				switch batch {
-				case "after-boot":
-					fmt.Println("TODO: Delete messages with shutdown != 0.")
+				switch batchFlag {
+				case "reboot":
+					exitcode = batch.Reboot()
 				case "expire":
-					fmt.Println("TODO: expire messages.")
+					exitcode = batch.Expire()
+				case "install":
+					exitcode = batch.Install()
 				default:
 					fmt.Println("ERROR: can only run batch commands as SYSTEM.")
 					exitcode = 1