diff --git a/bindings/java/src/org/sleuthkit/datamodel/SleuthkitCase.java b/bindings/java/src/org/sleuthkit/datamodel/SleuthkitCase.java
index 03f6651654479baa3cec364b3f1b75e8381aeda4..37a171fdf9e46dfea4762dfb97865817e3633188 100644
--- a/bindings/java/src/org/sleuthkit/datamodel/SleuthkitCase.java
+++ b/bindings/java/src/org/sleuthkit/datamodel/SleuthkitCase.java
@@ -82,6 +82,7 @@
 import org.sleuthkit.datamodel.IngestJobInfo.IngestJobStatusType;
 import org.sleuthkit.datamodel.IngestModuleInfo.IngestModuleType;
 import org.sleuthkit.datamodel.SleuthkitJNI.CaseDbHandle.AddImageProcess;
+import org.sleuthkit.datamodel.TimelineManager.TimelineEventAddedEvent;
 import org.sleuthkit.datamodel.TskData.DbType;
 import org.sleuthkit.datamodel.TskData.FileKnown;
 import org.sleuthkit.datamodel.TskData.ObjectType;
@@ -13939,6 +13940,7 @@ public static final class CaseDbTransaction {
 		// Score changes are stored as a map keyed by objId to prevent duplicates.
 		private Map<Long, ScoreChange> scoreChangeMap = new HashMap<>();
 		private List<Host> hostsAdded = new ArrayList<>();
+		private List<TimelineEventAddedEvent> timelineEvents = new ArrayList<>();
 		private List<OsAccount> accountsChanged = new ArrayList<>();
 		private List<OsAccount> accountsAdded = new ArrayList<>();
 		private List<TskEvent.MergedAccountsPair> accountsMerged = new ArrayList<>();
@@ -13986,6 +13988,16 @@ CaseDbConnection getConnection() {
 		void registerScoreChange(ScoreChange scoreChange) {
 			scoreChangeMap.put(scoreChange.getObjectId(), scoreChange);
 		}
+		
+		/**
+		 * Register timeline event to be fired when transaction finishes.
+		 * @param timelineEvent The timeline event.
+		 */
+		void registerTimelineEvent(TimelineEventAddedEvent timelineEvent) {
+			if (timelineEvent != null) {
+				timelineEvents.add(timelineEvent);
+			}
+		}
 
 		/**
 		 * Saves a host that has been added as a part of this transaction.
@@ -14084,6 +14096,11 @@ public void commit() throws TskCoreException {
 						sleuthkitCase.fireTSKEvent(new TskEvent.AggregateScoresChangedEvent(entry.getKey(), ImmutableSet.copyOf(entry.getValue())));
 					}
 				}
+				if (!timelineEvents.isEmpty()) {
+					for (TimelineEventAddedEvent evt : timelineEvents) {
+						sleuthkitCase.fireTSKEvent(evt);
+					}
+				}
 				if (!hostsAdded.isEmpty()) {
 					sleuthkitCase.fireTSKEvent(new TskEvent.HostsAddedTskEvent(hostsAdded));
 				}
diff --git a/bindings/java/src/org/sleuthkit/datamodel/TimelineManager.java b/bindings/java/src/org/sleuthkit/datamodel/TimelineManager.java
index cf560705d3ad91e16ead90f9087e334191a62ec2..0b215868d60e5d045037766c16c7dee15ea31f7f 100644
--- a/bindings/java/src/org/sleuthkit/datamodel/TimelineManager.java
+++ b/bindings/java/src/org/sleuthkit/datamodel/TimelineManager.java
@@ -26,6 +26,7 @@
 import java.sql.SQLException;
 import java.sql.Statement;
 import java.sql.Types;
+import java.text.MessageFormat;
 import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -48,6 +49,7 @@
 import static org.sleuthkit.datamodel.CollectionUtils.isNotEmpty;
 import static org.sleuthkit.datamodel.CommManagerSqlStringUtils.buildCSVString;
 import org.sleuthkit.datamodel.SleuthkitCase.CaseDbConnection;
+import org.sleuthkit.datamodel.SleuthkitCase.CaseDbTransaction;
 import static org.sleuthkit.datamodel.SleuthkitCase.escapeSingleQuotes;
 
 /**
@@ -790,6 +792,58 @@ private Optional<TimelineEvent> addOtherEventDesc(BlackboardArtifact artifact) t
 
 		return addArtifactEvent(evtWDesc, evtType, artifact);
 	}
+	
+	
+	/**
+	 * Adds a timeline event to the database in a transaction.
+	 * @param eventType The event type.
+	 * @param shortDesc The short description.
+	 * @param medDesc The medium description.
+	 * @param longDesc The long description.
+	 * @param dataSourceId The data source id of the event.
+	 * @param contentId The content id of the event.
+	 * @param artifactId The artifact id of the event (can be null).
+	 * @param time Unix epoch offset time of the event in seconds.
+	 * @param hashHit True if a hash hit.
+	 * @param tagged True if tagged.
+	 * @param trans The transaction.
+	 * @return The added event.
+	 * @throws TskCoreException 
+	 */
+	@Beta
+	public TimelineEvent addTimelineEvent(
+			TimelineEventType eventType, String shortDesc, String medDesc, String longDesc,
+			long dataSourceId, long contentId, Long artifactId, long time,
+			boolean hashHit, boolean tagged,
+			CaseDbTransaction trans
+	) throws TskCoreException {
+		caseDB.acquireSingleUserCaseWriteLock();
+		try {
+			Long descriptionID = addEventDescription(dataSourceId, contentId, artifactId,
+					longDesc, medDesc, shortDesc, hashHit, tagged, trans.getConnection());
+
+			if (descriptionID == null) {
+				descriptionID = getEventDescription(dataSourceId, contentId, artifactId, longDesc, trans.getConnection());
+			}
+			if (descriptionID != null) {
+				long eventID = addEventWithExistingDescription(time, eventType, descriptionID, trans.getConnection());
+				TimelineEvent timelineEvt = new TimelineEvent(eventID, descriptionID, contentId, artifactId, time, eventType,
+						longDesc, medDesc, shortDesc, hashHit, tagged);
+				
+				trans.registerTimelineEvent(new TimelineEventAddedEvent(timelineEvt));
+				return timelineEvt;
+			} else {
+				throw new TskCoreException(MessageFormat.format(
+						"Failed to get event description for [shortDesc: {0}, dataSourceId: {1}, contentId: {2}, artifactId: {3}]",
+						shortDesc, dataSourceId, contentId, artifactId == null ? "<null>" : artifactId));
+			}
+		} catch (DuplicateException dupEx) {
+			logger.log(Level.WARNING, "Attempt to make duplicate", dupEx);
+			return null;
+		} finally {
+			caseDB.releaseSingleUserCaseWriteLock();
+		}
+	}
 
 	/**
 	 * Add an event of the given type from the given artifact to the database.