diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,8 @@
-## [_Unreleased_](freckle-kafka-v0.0.0.2...main)
+## [_Unreleased_](freckle-kafka-v0.0.0.3...main)
+
+## [v0.0.0.3](https://github.com/freckle/freckle-app/compare/freckle-kafka-v0.0.0.2...freckle-kafka-v0.0.0.3)
+
+Add `produce`
 
 ## [v1.21.1.0](https://github.com/freckle/freckle-app/compare/freckle-kafka-v0.0.0.1...freckle-kafka-v0.0.0.2)
 
diff --git a/freckle-kafka.cabal b/freckle-kafka.cabal
--- a/freckle-kafka.cabal
+++ b/freckle-kafka.cabal
@@ -1,6 +1,6 @@
 cabal-version:      1.18
 name:               freckle-kafka
-version:            0.0.0.2
+version:            0.0.0.3
 license:            MIT
 license-file:       LICENSE
 maintainer:         Freckle Education
@@ -48,7 +48,7 @@
         base >=4.16.4.0 && <5,
         bytestring >=0.11.4.0,
         containers >=0.6.5.1,
-        freckle-env >=0.0.1.1,
+        freckle-env >=0.0.1.2,
         hs-opentelemetry-sdk >=0.0.3.6,
         hw-kafka-client >=5.0.0,
         lens >=5.1.1,
diff --git a/library/Freckle/App/Kafka/Producer.hs b/library/Freckle/App/Kafka/Producer.hs
--- a/library/Freckle/App/Kafka/Producer.hs
+++ b/library/Freckle/App/Kafka/Producer.hs
@@ -9,6 +9,7 @@
   , HasKafkaProducerPool (..)
   , createKafkaProducerPool
   , produceKeyedOn
+  , produce
   ) where
 
 import Prelude
@@ -136,30 +137,44 @@
   -> NonEmpty value
   -> (value -> key)
   -> m ()
-produceKeyedOn prTopic values keyF = traced $ do
-  logDebugNS "kafka" $ "Producing Kafka events" :# ["events" .= values]
+produceKeyedOn prTopic values keyF = produce prTopic (NE.map mkProducerRecord values)
+ where
+  mkProducerRecord value =
+    ProducerRecord
+      { prTopic
+      , prPartition = UnassignedPartition
+      , prKey = Just $ BSL.toStrict $ encode $ keyF value
+      , prValue = Just $ BSL.toStrict $ encode value
+      , prHeaders = mempty
+      }
+
+-- | Produce/Emit a message to a Kafka topic
+produce
+  :: ( MonadUnliftIO m
+     , MonadLogger m
+     , MonadTracer m
+     , MonadReader env m
+     , HasKafkaProducerPool env
+     )
+  => TopicName
+  -> NonEmpty ProducerRecord
+  -> m ()
+produce prTopic events = traced $ do
+  logDebugNS "kafka" $
+    "Producing Kafka events" :# ["events" .= NE.map show events]
   view kafkaProducerPoolL >>= \case
     NullKafkaProducerPool -> pure ()
     KafkaProducerPool producerPool ->
       withRunInIO $ \run ->
         Pool.withResource producerPool $ \producer ->
-          for_ @NonEmpty values $ \value -> do
-            mError <- liftIO $ produceMessage producer $ mkProducerRecord value
+          for_ @NonEmpty events $ \event -> do
+            mError <- liftIO $ produceMessage producer event
             for_ @Maybe mError $ \e ->
               run $
                 logErrorNS "kafka" $
                   "Failed to send event"
                     :# ["error" .= T.pack (show e)]
  where
-  mkProducerRecord value =
-    ProducerRecord
-      { prTopic
-      , prPartition = UnassignedPartition
-      , prKey = Just $ BSL.toStrict $ encode $ keyF value
-      , prValue = Just $ BSL.toStrict $ encode value
-      , prHeaders = mempty
-      }
-
   traced =
     inSpan
       "kafka.produce"
diff --git a/package.yaml b/package.yaml
--- a/package.yaml
+++ b/package.yaml
@@ -1,5 +1,5 @@
 name: freckle-kafka
-version: 0.0.0.2
+version: 0.0.0.3
 maintainer: Freckle Education
 category: Database
 github: freckle/freckle-app
