diff --git a/go.mod b/go.mod
index b8461cd55d2c97ac440dcd625e4b58039f80d775..1b73d8a9122c7b78f46c5fbecc29e46910167b76 100644
--- a/go.mod
+++ b/go.mod
@@ -64,6 +64,7 @@ require (
 	github.com/go-pg/zerochecker v0.2.0 // indirect
 	github.com/go-stack/stack v1.8.1 // indirect
 	github.com/golang/protobuf v1.5.2 // indirect
+	github.com/gorilla/websocket v1.4.2 // indirect
 	github.com/hbollon/go-edlib v1.6.0 // indirect
 	github.com/inconshreveable/log15 v3.0.0-testing.3+incompatible // indirect
 	github.com/inconshreveable/log15/v3 v3.0.0-testing.5 // indirect
@@ -76,6 +77,7 @@ require (
 	github.com/olekukonko/tablewriter v0.0.5 // indirect
 	github.com/pborman/uuid v1.2.1 // indirect
 	github.com/rivo/uniseg v0.1.0 // indirect
+	github.com/slack-go/slack v0.15.0 // indirect
 	github.com/smartystreets/goconvey v1.7.2 // indirect
 	github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
 	github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect
diff --git a/go.sum b/go.sum
index cb29536ba98516e6d496bdc8d9ad14ac2fb890c0..286892cadb8eb13b942594b3d00d2ca65de08c97 100644
--- a/go.sum
+++ b/go.sum
@@ -106,6 +106,7 @@ github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSM
 github.com/go-stack/stack v1.8.1 h1:ntEHSVwIt7PNXNpgPmVfMrNhLtgjlmnZha2kOpuRiDw=
 github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4=
 github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
+github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
 github.com/golang-jwt/jwt/v4 v4.4.3 h1:Hxl6lhQFj4AnOX6MLrsCb/+7tCj7DxP7VA+2rDIq5AU=
 github.com/golang-jwt/jwt/v4 v4.4.3/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
@@ -131,6 +132,7 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
 github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
 github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
 github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
@@ -138,6 +140,8 @@ github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
 github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
 github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
+github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
+github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
 github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=
 github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=
 github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE=
@@ -223,6 +227,8 @@ github.com/scylladb/termtables v0.0.0-20191203121021-c4c0b6d42ff4/go.mod h1:C1a7
 github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
 github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
 github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
+github.com/slack-go/slack v0.15.0 h1:LE2lj2y9vqqiOf+qIIy0GvEoxgF1N5yLGZffmEZykt0=
+github.com/slack-go/slack v0.15.0/go.mod h1:hlGi5oXA+Gt+yWTPP0plCdRKmjsDxecdHxYQdlMQKOw=
 github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs=
 github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
 github.com/smartystreets/goconvey v1.7.2 h1:9RBaZCeXEQ3UselpuwUQHltGVXvdwm6cv1hgR6gDIPg=
diff --git a/slack_utils/slack_utils.go b/slack_utils/slack_utils.go
new file mode 100644
index 0000000000000000000000000000000000000000..1d35bc19786454f2fb49995eea71d867a62ecf9e
--- /dev/null
+++ b/slack_utils/slack_utils.go
@@ -0,0 +1,130 @@
+package slack_utils
+
+import (
+	"fmt"
+	"github.com/samber/lo"
+	"github.com/slack-go/slack"
+	"gitlab.bob.co.za/bob-public-utils/bobgroup-go-utils/errors"
+	"gitlab.bob.co.za/bob-public-utils/bobgroup-go-utils/logs"
+	"reflect"
+	"regexp"
+)
+
+type SlackField struct {
+	Label string
+	Value any
+}
+
+type SlackClient struct {
+	Client *slack.Client
+}
+
+func GetClient(apiKey string) *SlackClient {
+	return &SlackClient{
+		Client: slack.New(apiKey),
+	}
+}
+
+func (s *SlackClient) SendAlert(message string, channel string, parentMessageTimestamp ...string) string {
+	if s == nil || s.Client == nil {
+		// If the slack client isn't set, log the message to Raygun instead
+		logs.ErrorMsg(message)
+		return ""
+	}
+
+	slackMessageOptions := []slack.MsgOption{
+		slack.MsgOptionText(message, false),
+	}
+
+	if len(parentMessageTimestamp) > 0 {
+		slackMessageOptions = append(slackMessageOptions, slack.MsgOptionTS(parentMessageTimestamp[0]))
+	}
+
+	return s.postMessage(channel, slackMessageOptions)
+}
+
+func (s *SlackClient) SendAlertWithFields(message string, channel string, slackFields []SlackField, parentMessageTimestamp ...string) string {
+	if s == nil || s.Client == nil {
+		// If the slack client isn't set, log the message to Raygun instead
+		slackFieldMap := slackFieldsToMap(slackFields)
+		logs.ErrorWithFields(slackFieldMap, errors.Error(message))
+		return ""
+	}
+
+	slackFieldsPerMessage := lo.Chunk(slackFields, 50) // Slack has a limit of 50 blocks per message
+
+	var messageTimestamp string
+	for i, slackFieldsForMessage := range slackFieldsPerMessage {
+		messageText := message
+		if len(slackFieldsPerMessage) > 1 {
+			messageText = fmt.Sprintf("%s (%d/%d)", message, i+1, len(slackFieldsPerMessage))
+		}
+
+		blocks := []slack.Block{slack.NewSectionBlock(slack.NewTextBlockObject(
+			"plain_text",
+			messageText,
+			hasEmoji(messageText),
+			false),
+			nil,
+			nil,
+		),
+		}
+
+		slackFieldsChunked := lo.Chunk(slackFieldsForMessage, 2)
+
+		for _, slackFieldsChunk := range slackFieldsChunked {
+			fieldBlocksForChunk := []*slack.TextBlockObject{}
+			for _, slackField := range slackFieldsChunk {
+				slackFieldValue := slackField.Value
+				// if slackField.Value is a pointer, dereference it
+				if reflect.ValueOf(slackField.Value).Kind() == reflect.Ptr {
+					slackFieldValue = reflect.ValueOf(slackField.Value).Elem().Interface()
+				}
+
+				text := fmt.Sprintf("*%s*\n%v", slackField.Label, slackFieldValue)
+				fieldBlocksForChunk = append(fieldBlocksForChunk, slack.NewTextBlockObject(
+					"mrkdwn",
+					text,
+					false,
+					false,
+				))
+			}
+
+			blocks = append(blocks, slack.NewSectionBlock(nil, fieldBlocksForChunk, nil))
+		}
+
+		slackMessageOptions := []slack.MsgOption{
+			slack.MsgOptionBlocks(blocks...),
+		}
+
+		if len(parentMessageTimestamp) > 0 {
+			slackMessageOptions = append(slackMessageOptions, slack.MsgOptionTS(parentMessageTimestamp[0]))
+		}
+
+		messageTimestamp = s.postMessage(channel, slackMessageOptions)
+	}
+
+	// Return the last message timestamp
+	return messageTimestamp
+}
+
+func (s *SlackClient) postMessage(channel string, messageOptions []slack.MsgOption) string {
+	_, messageTimestamp, err := s.Client.PostMessage(channel, messageOptions...)
+	if err != nil {
+		logs.ErrorWithMsg("Error sending slack: %v+\n", err)
+	}
+	return messageTimestamp
+}
+
+func hasEmoji(message string) bool {
+	emojiRegex := regexp.MustCompile(`:[a-zA-Z0-9_]+:`)
+	return emojiRegex.MatchString(message)
+}
+
+func slackFieldsToMap(fields []SlackField) map[string]any {
+	slackFieldsMap := make(map[string]any)
+	for _, field := range fields {
+		slackFieldsMap[field.Label] = field.Value
+	}
+	return slackFieldsMap
+}