diff --git a/doc/content/source/actions/AddSpatioTemporalPathAction.md b/doc/content/source/actions/AddSpatioTemporalPathAction.md
new file mode 100644
index 00000000..81c74372
--- /dev/null
+++ b/doc/content/source/actions/AddSpatioTemporalPathAction.md
@@ -0,0 +1,3 @@
+# AddSpatioTemporalPathAction
+
+This action registers objects derived from [`SpatioTemporalPath`](SpatioTemporalPath/index.md) into the current problem. See the linked page for more details on the usage of the `SpatioTemporalPath` system.
diff --git a/doc/content/source/materials/ADMovingEllipsoidalHeatSource.md b/doc/content/source/materials/ADMovingEllipsoidalHeatSource.md
new file mode 100644
index 00000000..806dd44a
--- /dev/null
+++ b/doc/content/source/materials/ADMovingEllipsoidalHeatSource.md
@@ -0,0 +1,11 @@
+# ADMovingEllipsoidalHeatSource
+
+!syntax description /Materials/ADMovingEllipsoidalHeatSource
+
+## Example Input File Syntax
+
+!syntax parameters /Materials/ADMovingEllipsoidalHeatSource
+
+!syntax inputs /Materials/ADMovingEllipsoidalHeatSource
+
+!syntax children /Materials/ADMovingEllipsoidalHeatSource
diff --git a/doc/content/source/spatiotemporalpaths/CSVPiecewiseLinearSpatioTemporalPath.md b/doc/content/source/spatiotemporalpaths/CSVPiecewiseLinearSpatioTemporalPath.md
new file mode 100644
index 00000000..29dbc1d1
--- /dev/null
+++ b/doc/content/source/spatiotemporalpaths/CSVPiecewiseLinearSpatioTemporalPath.md
@@ -0,0 +1,11 @@
+# CSVPiecewiseLinearSpatioTemporalPath
+
+!syntax description /UserObjects/CSVPiecewiseLinearSpatioTemporalPath
+
+## Example Input File Syntax
+
+!syntax parameters /UserObjects/CSVPiecewiseLinearSpatioTemporalPath
+
+!syntax inputs /UserObjects/CSVPiecewiseLinearSpatioTemporalPath
+
+!syntax children /UserObjects/CSVPiecewiseLinearSpatioTemporalPath
diff --git a/doc/content/source/spatiotemporalpaths/FunctionSpatioTemporalPath.md b/doc/content/source/spatiotemporalpaths/FunctionSpatioTemporalPath.md
new file mode 100644
index 00000000..b904671c
--- /dev/null
+++ b/doc/content/source/spatiotemporalpaths/FunctionSpatioTemporalPath.md
@@ -0,0 +1,11 @@
+# FunctionSpatioTemporalPath
+
+!syntax description /UserObjects/FunctionSpatioTemporalPath
+
+## Example Input File Syntax
+
+!syntax parameters /UserObjects/FunctionSpatioTemporalPath
+
+!syntax inputs /UserObjects/FunctionSpatioTemporalPath
+
+!syntax children /UserObjects/FunctionSpatioTemporalPath
diff --git a/doc/content/source/spatiotemporalpaths/PiecewiseLinearSpatioTemporalPath.md b/doc/content/source/spatiotemporalpaths/PiecewiseLinearSpatioTemporalPath.md
new file mode 100644
index 00000000..bb959f97
--- /dev/null
+++ b/doc/content/source/spatiotemporalpaths/PiecewiseLinearSpatioTemporalPath.md
@@ -0,0 +1,11 @@
+# PiecewiseLinearSpatioTemporalPath
+
+!syntax description /UserObjects/PiecewiseLinearSpatioTemporalPath
+
+## Example Input File Syntax
+
+!syntax parameters /UserObjects/PiecewiseLinearSpatioTemporalPath
+
+!syntax inputs /UserObjects/PiecewiseLinearSpatioTemporalPath
+
+!syntax children /UserObjects/PiecewiseLinearSpatioTemporalPath
diff --git a/doc/content/source/userobjects/SpatioTemporalPathElementSubdomainModifier.md b/doc/content/source/userobjects/SpatioTemporalPathElementSubdomainModifier.md
new file mode 100644
index 00000000..412f7713
--- /dev/null
+++ b/doc/content/source/userobjects/SpatioTemporalPathElementSubdomainModifier.md
@@ -0,0 +1,11 @@
+# SpatioTemporalPathElementSubdomainModifier
+
+!syntax description /UserObjects/SpatioTemporalPathElementSubdomainModifier
+
+## Example Input File Syntax
+
+!syntax parameters /UserObjects/SpatioTemporalPathElementSubdomainModifier
+
+!syntax inputs /UserObjects/SpatioTemporalPathElementSubdomainModifier
+
+!syntax children /UserObjects/SpatioTemporalPathElementSubdomainModifier
diff --git a/doc/content/syntax/SpatioTemporalPaths/index.md b/doc/content/syntax/SpatioTemporalPaths/index.md
new file mode 100644
index 00000000..08a605f7
--- /dev/null
+++ b/doc/content/syntax/SpatioTemporalPaths/index.md
@@ -0,0 +1,32 @@
+# SpatioTemporalPath System
+
+The SpatioTemporalPath system offers flexible ways of defining spatio-temporal paths, hereinafter referred to as paths, that are often useful in additive manufacturing simulations.
+
+## Overview
+
+A spatio-temporal path is a vector-valued function of time, written as
+
+\begin{equation}
+  \begin{aligned}
+    \boldsymbol{x} = f(t)
+  \end{aligned}
+\end{equation}
+
+where $\boldsymbol{x}$ is the path front at time $t$. In addition to the path front, the path object also defines the path's moving velocity $\boldsymbol{v}$ and the path's moving direction $\boldsymbol{t}$, defined as
+
+\begin{equation}
+  \begin{aligned}
+    \boldsymbol{v} &= \dfrac{\boldsymbol{x}}{t}
+    \boldsymbol{t} &= \dfrac{\boldsymbol{v}}{\lVert\boldsymbol{v}\rVert}
+  \end{aligned}
+\end{equation}
+
+## Example Input File Syntax
+
+Spatio-temporal paths are defined under the `[SpatioTemporalPaths]` in the input file and can be referenced by other objects in the input file.
+
+!syntax list /SpatioTemporalPath objects=True actions=False subsystems=False
+
+!syntax list /SpatioTemporalPath objects=False actions=False subsystems=True
+
+!syntax list /SpatioTemporalPath objects=False actions=True subsystems=False
diff --git a/include/actions/AddSpatioTemporalPathAction.h b/include/actions/AddSpatioTemporalPathAction.h
new file mode 100644
index 00000000..dd25a0f9
--- /dev/null
+++ b/include/actions/AddSpatioTemporalPathAction.h
@@ -0,0 +1,25 @@
+/****************************************************************************/
+/*                        DO NOT MODIFY THIS HEADER                         */
+/*                                                                          */
+/* MALAMUTE: MOOSE Application Library for Advanced Manufacturing UTilitiEs */
+/*                                                                          */
+/*           Copyright 2021 - 2023, Battelle Energy Alliance, LLC           */
+/*                           ALL RIGHTS RESERVED                            */
+/****************************************************************************/
+
+#pragma once
+
+#include "MooseObjectAction.h"
+
+/**
+ * Add SpatioTemporalPath
+ */
+class AddSpatioTemporalPathAction : public MooseObjectAction
+{
+public:
+  AddSpatioTemporalPathAction(const InputParameters & params);
+
+  static InputParameters validParams();
+
+  void act() override final;
+};
diff --git a/include/interfaces/SpatioTemporalPathInterface.h b/include/interfaces/SpatioTemporalPathInterface.h
new file mode 100644
index 00000000..0377bfa3
--- /dev/null
+++ b/include/interfaces/SpatioTemporalPathInterface.h
@@ -0,0 +1,51 @@
+/****************************************************************************/
+/*                        DO NOT MODIFY THIS HEADER                         */
+/*                                                                          */
+/* MALAMUTE: MOOSE Application Library for Advanced Manufacturing UTilitiEs */
+/*                                                                          */
+/*           Copyright 2021 - 2023, Battelle Energy Alliance, LLC           */
+/*                           ALL RIGHTS RESERVED                            */
+/****************************************************************************/
+
+#pragma once
+
+#include "MooseTypes.h"
+#include "InputParameters.h"
+
+class FEProblemBase;
+class InputParameters;
+class MooseObject;
+class SpatioTemporalPath;
+
+/**
+ * Interface for objects that need to use SpatioTemporalPath
+ */
+class SpatioTemporalPathInterface
+{
+public:
+  SpatioTemporalPathInterface(const MooseObject * moose_object);
+
+  /**
+   * Get a SpatioTemporalPath with a given name
+   * @param name The name of the parameter key of the SpatioTemporalPath to retrieve
+   * @return The SpatioTemporalPath with name associated with the parameter 'name'
+   */
+  const SpatioTemporalPath & getSpatioTemporalPath(const std::string & name) const;
+
+  /**
+   * Get a SpatioTemporalPath with a given name
+   * @param name The name of the SpatioTemporalPath to retrieve
+   * @return The SpatioTemporalPath with name 'name'
+   */
+  const SpatioTemporalPath & getSpatioTemporalPathByName(const std::string & name) const;
+
+private:
+  /// Parameters of the object with this interface
+  const InputParameters & _stpi_params;
+
+  /// Reference to FEProblemBase instance
+  FEProblemBase & _stpi_feproblem;
+
+  /// Thread ID
+  const THREAD_ID _stpi_tid;
+};
diff --git a/include/materials/ADMovingEllipsoidalHeatSource.h b/include/materials/ADMovingEllipsoidalHeatSource.h
new file mode 100644
index 00000000..adaea245
--- /dev/null
+++ b/include/materials/ADMovingEllipsoidalHeatSource.h
@@ -0,0 +1,37 @@
+/****************************************************************************/
+/*                        DO NOT MODIFY THIS HEADER                         */
+/*                                                                          */
+/* MALAMUTE: MOOSE Application Library for Advanced Manufacturing UTilitiEs */
+/*                                                                          */
+/*           Copyright 2021 - 2023, Battelle Energy Alliance, LLC           */
+/*                           ALL RIGHTS RESERVED                            */
+/****************************************************************************/
+
+#pragma once
+
+#include "ADMovingHeatSource.h"
+
+/**
+ * @brief A moving ellipsoidal heat source following a SpatioTemporalPath
+ */
+class ADMovingEllipsoidalHeatSource : public ADMovingHeatSource
+{
+public:
+  static InputParameters validParams();
+
+  ADMovingEllipsoidalHeatSource(const InputParameters & parameters);
+
+protected:
+  virtual ADReal computeHeatSource() override;
+
+  /// Input power
+  const ADMaterialProperty<Real> & _P;
+  /// Length of the ellipsoid semi-axis along the path direction
+  const ADMaterialProperty<Real> & _a;
+  /// Length of the ellipsoid semi-axis perpendicular to the path direction
+  const ADMaterialProperty<Real> & _b;
+  /// Process efficienty
+  const Real _eta;
+  /// Scaling factor
+  const Real _scale;
+};
diff --git a/include/materials/ADMovingHeatSource.h b/include/materials/ADMovingHeatSource.h
new file mode 100644
index 00000000..126e03bd
--- /dev/null
+++ b/include/materials/ADMovingHeatSource.h
@@ -0,0 +1,41 @@
+/****************************************************************************/
+/*                        DO NOT MODIFY THIS HEADER                         */
+/*                                                                          */
+/* MALAMUTE: MOOSE Application Library for Advanced Manufacturing UTilitiEs */
+/*                                                                          */
+/*           Copyright 2021 - 2023, Battelle Energy Alliance, LLC           */
+/*                           ALL RIGHTS RESERVED                            */
+/****************************************************************************/
+
+#pragma once
+
+#include "Material.h"
+#include "SpatioTemporalPath.h"
+#include "SpatioTemporalPathInterface.h"
+
+/**
+ * @brief A moving heat source following a SpatioTemporalPath
+ */
+class ADMovingHeatSource : public Material, public SpatioTemporalPathInterface
+{
+public:
+  static InputParameters validParams();
+
+  ADMovingHeatSource(const InputParameters & parameters);
+
+protected:
+  virtual void computeQpProperties() override;
+  virtual ADReal computeHeatSource() = 0;
+
+  /// The path
+  const SpatioTemporalPath & _path;
+
+  /// The heat source
+  ADMaterialProperty<Real> & _volumetric_heat;
+
+  /// Tangential distance from the heat source
+  MaterialProperty<Real> & _tangential_distance;
+
+  /// Normal distance from the heat source
+  MaterialProperty<Real> & _normal_distance;
+};
diff --git a/include/spatiotemporalpaths/CSVPiecewiseLinearSpatioTemporalPath.h b/include/spatiotemporalpaths/CSVPiecewiseLinearSpatioTemporalPath.h
new file mode 100644
index 00000000..48a297e8
--- /dev/null
+++ b/include/spatiotemporalpaths/CSVPiecewiseLinearSpatioTemporalPath.h
@@ -0,0 +1,24 @@
+/****************************************************************************/
+/*                        DO NOT MODIFY THIS HEADER                         */
+/*                                                                          */
+/* MALAMUTE: MOOSE Application Library for Advanced Manufacturing UTilitiEs */
+/*                                                                          */
+/*           Copyright 2021 - 2023, Battelle Energy Alliance, LLC           */
+/*                           ALL RIGHTS RESERVED                            */
+/****************************************************************************/
+
+#pragma once
+
+#include "PiecewiseLinearSpatioTemporalPathBase.h"
+
+/**
+ * @brief Construct a piecewise linear path from a csv file
+ *
+ */
+class CSVPiecewiseLinearSpatioTemporalPath : public PiecewiseLinearSpatioTemporalPathBase
+{
+public:
+  static InputParameters validParams();
+
+  CSVPiecewiseLinearSpatioTemporalPath(const InputParameters & params);
+};
diff --git a/include/spatiotemporalpaths/FunctionSpatioTemporalPath.h b/include/spatiotemporalpaths/FunctionSpatioTemporalPath.h
new file mode 100644
index 00000000..9ec7ac38
--- /dev/null
+++ b/include/spatiotemporalpaths/FunctionSpatioTemporalPath.h
@@ -0,0 +1,35 @@
+/****************************************************************************/
+/*                        DO NOT MODIFY THIS HEADER                         */
+/*                                                                          */
+/* MALAMUTE: MOOSE Application Library for Advanced Manufacturing UTilitiEs */
+/*                                                                          */
+/*           Copyright 2021 - 2023, Battelle Energy Alliance, LLC           */
+/*                           ALL RIGHTS RESERVED                            */
+/****************************************************************************/
+
+#pragma once
+
+#include "SpatioTemporalPath.h"
+
+class Function;
+
+/**
+ * @brief A spatiotemporal path whose coordinates are specified using MOOSE functions.
+ */
+class FunctionSpatioTemporalPath : public SpatioTemporalPath
+{
+public:
+  static InputParameters validParams();
+
+  FunctionSpatioTemporalPath(const InputParameters & params);
+
+  virtual Point position(Real t) const override;
+
+protected:
+  /// The function for the x-coordinate
+  const Function * _x;
+  /// The function for the y-coordinate
+  const Function * _y;
+  /// The function for the z-coordinate
+  const Function * _z;
+};
diff --git a/include/spatiotemporalpaths/PiecewiseLinearSpatioTemporalPath.h b/include/spatiotemporalpaths/PiecewiseLinearSpatioTemporalPath.h
new file mode 100644
index 00000000..ec76be74
--- /dev/null
+++ b/include/spatiotemporalpaths/PiecewiseLinearSpatioTemporalPath.h
@@ -0,0 +1,23 @@
+/****************************************************************************/
+/*                        DO NOT MODIFY THIS HEADER                         */
+/*                                                                          */
+/* MALAMUTE: MOOSE Application Library for Advanced Manufacturing UTilitiEs */
+/*                                                                          */
+/*           Copyright 2021 - 2023, Battelle Energy Alliance, LLC           */
+/*                           ALL RIGHTS RESERVED                            */
+/****************************************************************************/
+
+#pragma once
+
+#include "PiecewiseLinearSpatioTemporalPathBase.h"
+
+/**
+ * @brief Construct a piecewise linear spatiotemporal path from discrete times and vertices.
+ */
+class PiecewiseLinearSpatioTemporalPath : public PiecewiseLinearSpatioTemporalPathBase
+{
+public:
+  static InputParameters validParams();
+
+  PiecewiseLinearSpatioTemporalPath(const InputParameters & params);
+};
diff --git a/include/spatiotemporalpaths/PiecewiseLinearSpatioTemporalPathBase.h b/include/spatiotemporalpaths/PiecewiseLinearSpatioTemporalPathBase.h
new file mode 100644
index 00000000..a442af4d
--- /dev/null
+++ b/include/spatiotemporalpaths/PiecewiseLinearSpatioTemporalPathBase.h
@@ -0,0 +1,63 @@
+/****************************************************************************/
+/*                        DO NOT MODIFY THIS HEADER                         */
+/*                                                                          */
+/* MALAMUTE: MOOSE Application Library for Advanced Manufacturing UTilitiEs */
+/*                                                                          */
+/*           Copyright 2021 - 2023, Battelle Energy Alliance, LLC           */
+/*                           ALL RIGHTS RESERVED                            */
+/****************************************************************************/
+
+#pragma once
+
+#include "SpatioTemporalPath.h"
+
+/**
+ * @brief A general piecewise linear spatiotemporal description of path.
+ *
+ * The path consists of a set of line segments. The line segments must be continuous and are stored
+ * as a series of times and vertex coordinates. The coordinate (front) and direction of the path can
+ * be queried using `operator()` with the current time.
+ */
+class PiecewiseLinearSpatioTemporalPathBase : public SpatioTemporalPath
+{
+public:
+  static InputParameters validParams();
+
+  PiecewiseLinearSpatioTemporalPathBase(const InputParameters & params);
+
+  virtual Point position(Real t) const override;
+
+  /// Get the times associated with all vertices
+  const std::vector<Real> & times() const { return _times; }
+  /// Get the times associated with all vertices
+  const std::vector<Real> & times() { return _times; }
+
+  /// Get the coordinates for all vertices
+  const std::vector<Point> & coords() const { return _coords; }
+
+  /// Get the coordinates for all vertices
+  const std::vector<Point> & coords() { return _coords; }
+
+protected:
+  /// Set coordinates from components
+  virtual void
+  setCoords(const std::vector<Real> & x, const std::vector<Real> & y, const std::vector<Real> & z);
+
+  /// Check the validity of the path
+  virtual void validate() const;
+
+  /// Get the bounding indices for time \p t
+  std::pair<unsigned int, unsigned int> getIntervalIndices(Real t) const;
+
+  /// Times associated with all vertices, in ascending order
+  std::vector<Real> _times;
+
+  /// Coordinates for all vertices
+  std::vector<Point> _coords;
+
+  /// Tolerance of query time
+  const Real _t_tol;
+
+  /// Extrapolation method
+  const MooseEnum _outside;
+};
diff --git a/include/spatiotemporalpaths/SpatioTemporalPath.h b/include/spatiotemporalpaths/SpatioTemporalPath.h
new file mode 100644
index 00000000..79da00a1
--- /dev/null
+++ b/include/spatiotemporalpaths/SpatioTemporalPath.h
@@ -0,0 +1,146 @@
+/****************************************************************************/
+/*                        DO NOT MODIFY THIS HEADER                         */
+/*                                                                          */
+/* MALAMUTE: MOOSE Application Library for Advanced Manufacturing UTilitiEs */
+/*                                                                          */
+/*           Copyright 2021 - 2023, Battelle Energy Alliance, LLC           */
+/*                           ALL RIGHTS RESERVED                            */
+/****************************************************************************/
+
+#pragma once
+
+#include "UserObject.h"
+#include "libmesh/vector_value.h"
+#include "TransientInterface.h"
+
+/**
+ * @brief General abstract description of a spatiotemporal path.
+ *
+ * Subclasses must define the query `operator()`. The coordinate (front) and direction of the path
+ * can be queried using `operator()` with the current time.
+ */
+class SpatioTemporalPath : public UserObject, public TransientInterface
+{
+public:
+  static InputParameters validParams();
+
+  SpatioTemporalPath(const InputParameters & params);
+
+  virtual void execute() override final {}
+  virtual void initialize() override final {}
+  virtual void finalize() override final {}
+  virtual void threadJoin(const UserObject &) override final {}
+  virtual void initialSetup() override;
+  virtual void timestepSetup() override;
+  virtual void meshChanged() override;
+
+  /// Update the path's current position, velocity, direction, etc.
+  virtual void update();
+
+  /// Last time the path front was updated
+  Real lastUpdated() const { return _last_updated; }
+
+  /**
+   * @brief Get the path coordinates at time \p t
+   *
+   * @param t The time to query
+   * @return Point The path coordinates at time \p t
+   */
+  virtual Point position(Real t) const = 0;
+
+  /// Get the current coordinates
+  const Point & position() const { return _current_position; }
+
+  /// Get the previous coordinates
+  const Point & previousPosition() const { return _previous_position; }
+
+  /**
+   * @brief Get the path velocity at time \p t
+   *
+   * A default implementation is provided to use finite-differencing to compute the path velocity.
+   * A subclass may choose to override this default implementation, and the input parameters
+   * "path_FD_abs_eps" and "path_FD_rel_eps" should be suppressed.
+   *
+   * @param t The time to query
+   * @return RealVectorValue The path velocity at time \p t
+   */
+  virtual RealVectorValue velocity(Real t) const;
+
+  /// Get the current velocity
+  const RealVectorValue & velocity() const { return _current_velocity; }
+
+  /// Compute the average velocity
+  RealVectorValue smoothVelocity(Real t) const;
+
+  /// Get the previous velocity
+  const RealVectorValue & previousVelocity() const { return _previous_velocity; }
+
+  /**
+   * @brief Get the path direction at time \p t
+   *
+   * A default implementation is provided to use finite-differencing to compute the path direction.
+   * A subclass may choose to override this default implementation, and the input parameters
+   * "path_FD_abs_eps" and "path_FD_rel_eps" should be suppressed.
+   *
+   * @param t The time to query
+   * @return RealVectorValue The path direction at time \p t
+   */
+  virtual RealVectorValue direction(Real t) const;
+
+  /// Get the current direction
+  const RealVectorValue & direction() const { return _current_direction; }
+
+  /// Compute the average direction
+  RealVectorValue smoothDirection(Real t) const;
+
+  /// Get the previous direction
+  const RealVectorValue & previousDirection() const { return _previous_direction; }
+
+  /// Get the tangential component of the distance between the given point \p p and the path's position at time \p t
+  Real tangentialDistance(Real t, const Point & p) const;
+
+  /// Get the tangential component of the distance between the given point \p p and the path's current position
+  Real tangentialDistance(const Point & p) const;
+
+  /// Get the normal component of the distance between the given point \p p and the path's position at time \p t
+  Real normalDistance(Real t, const Point & p) const;
+
+  /// Get the normal component of the distance between the given point \p p and the path's current position
+  Real normalDistance(const Point & p) const;
+
+protected:
+  /// Verbose?
+  const bool _verbose;
+
+private:
+  /// The absolute epsilon to use in finite-differencing
+  const Real _abs_epsilon;
+  /// The relative epsilon to use in finite-differencing
+  const Real _rel_epsilon;
+
+  /// Path update interval
+  const Real _interval;
+  /// Last time the path front was updated
+  Real & _last_updated;
+
+  /// Whether to apply direction/velocity smoothing
+  const bool _smooth;
+  /// The smoothing window
+  const Real _smooth_window;
+  /// The number of smoothing points
+  const unsigned int _smooth_points;
+
+  /// The current position
+  Point _current_position;
+  /// The current velocity
+  RealVectorValue _current_velocity;
+  /// The current direction
+  RealVectorValue _current_direction;
+
+  /// The previous position
+  Point & _previous_position;
+  /// The current velocity
+  RealVectorValue & _previous_velocity;
+  /// The current direction
+  RealVectorValue & _previous_direction;
+};
diff --git a/include/userobjects/SpatioTemporalPathElementSubdomainModifier.h b/include/userobjects/SpatioTemporalPathElementSubdomainModifier.h
new file mode 100644
index 00000000..2315906f
--- /dev/null
+++ b/include/userobjects/SpatioTemporalPathElementSubdomainModifier.h
@@ -0,0 +1,35 @@
+/****************************************************************************/
+/*                        DO NOT MODIFY THIS HEADER                         */
+/*                                                                          */
+/* MALAMUTE: MOOSE Application Library for Advanced Manufacturing UTilitiEs */
+/*                                                                          */
+/*           Copyright 2021 - 2023, Battelle Energy Alliance, LLC           */
+/*                           ALL RIGHTS RESERVED                            */
+/****************************************************************************/
+
+#pragma once
+
+#include "ElementSubdomainModifier.h"
+#include "SpatioTemporalPath.h"
+#include "SpatioTemporalPathInterface.h"
+
+class SpatioTemporalPathElementSubdomainModifier : public ElementSubdomainModifier,
+                                                   public SpatioTemporalPathInterface
+{
+public:
+  static InputParameters validParams();
+
+  SpatioTemporalPathElementSubdomainModifier(const InputParameters & parameters);
+
+protected:
+  virtual SubdomainID computeSubdomainID() override;
+
+  /// The path
+  const SpatioTemporalPath & _path;
+
+  /// Target subdomain ID
+  const SubdomainID _subdomain_id;
+
+  /// Radius
+  const Real _r;
+};
diff --git a/src/actions/AddSpatioTemporalPathAction.C b/src/actions/AddSpatioTemporalPathAction.C
new file mode 100644
index 00000000..0781d931
--- /dev/null
+++ b/src/actions/AddSpatioTemporalPathAction.C
@@ -0,0 +1,31 @@
+/****************************************************************************/
+/*                        DO NOT MODIFY THIS HEADER                         */
+/*                                                                          */
+/* MALAMUTE: MOOSE Application Library for Advanced Manufacturing UTilitiEs */
+/*                                                                          */
+/*           Copyright 2021 - 2023, Battelle Energy Alliance, LLC           */
+/*                           ALL RIGHTS RESERVED                            */
+/****************************************************************************/
+
+#include "AddSpatioTemporalPathAction.h"
+#include "FEProblem.h"
+#include "SpatioTemporalPath.h"
+
+registerMooseAction("MalamuteApp", AddSpatioTemporalPathAction, "add_userobject");
+
+InputParameters
+AddSpatioTemporalPathAction::validParams()
+{
+  return MooseObjectAction::validParams();
+}
+
+AddSpatioTemporalPathAction::AddSpatioTemporalPathAction(const InputParameters & params)
+  : MooseObjectAction(params)
+{
+}
+
+void
+AddSpatioTemporalPathAction::act()
+{
+  _problem->addObject<SpatioTemporalPath>(_type, _name, _moose_object_pars);
+}
diff --git a/src/base/MalamuteApp.C b/src/base/MalamuteApp.C
index f1191737..d71c653a 100644
--- a/src/base/MalamuteApp.C
+++ b/src/base/MalamuteApp.C
@@ -38,7 +38,9 @@ MalamuteApp::registerAll(Factory & f, ActionFactory & af, Syntax & syntax)
   Registry::registerObjectsTo(f, {"MalamuteApp"});
   Registry::registerActionsTo(af, {"MalamuteApp"});
 
-  /* register custom execute flags, action syntax, etc. here */
+  // Adds [SpatioTemporalPath] block
+  registerSyntax("EmptyAction", "SpatioTemporalPaths");
+  registerSyntaxTask("AddSpatioTemporalPathAction", "SpatioTemporalPaths/*", "add_user_object");
 }
 
 void
diff --git a/src/interfaces/SpatioTemporalPathInterface.C b/src/interfaces/SpatioTemporalPathInterface.C
new file mode 100644
index 00000000..e17f1fd0
--- /dev/null
+++ b/src/interfaces/SpatioTemporalPathInterface.C
@@ -0,0 +1,35 @@
+/****************************************************************************/
+/*                        DO NOT MODIFY THIS HEADER                         */
+/*                                                                          */
+/* MALAMUTE: MOOSE Application Library for Advanced Manufacturing UTilitiEs */
+/*                                                                          */
+/*           Copyright 2021 - 2023, Battelle Energy Alliance, LLC           */
+/*                           ALL RIGHTS RESERVED                            */
+/****************************************************************************/
+
+#include "SpatioTemporalPathInterface.h"
+#include "SubProblem.h"
+#include "MooseTypes.h"
+#include "FEProblemBase.h"
+#include "SpatioTemporalPath.h"
+
+SpatioTemporalPathInterface::SpatioTemporalPathInterface(const MooseObject * moose_object)
+  : _stpi_params(moose_object->parameters()),
+    _stpi_feproblem(*_stpi_params.getCheckedPointerParam<FEProblemBase *>("_fe_problem_base")),
+    _stpi_tid(_stpi_params.have_parameter<THREAD_ID>("_tid") ? _stpi_params.get<THREAD_ID>("_tid")
+                                                             : 0)
+{
+}
+
+const SpatioTemporalPath &
+SpatioTemporalPathInterface::getSpatioTemporalPath(const std::string & param_name) const
+{
+  return getSpatioTemporalPathByName(_stpi_params.get<std::string>(param_name));
+}
+
+const SpatioTemporalPath &
+SpatioTemporalPathInterface::getSpatioTemporalPathByName(const std::string & name) const
+{
+  std::vector<SpatioTemporalPath *> objs;
+  return _stpi_feproblem.getUserObject<SpatioTemporalPath>(name, _stpi_tid);
+}
diff --git a/src/materials/ADMovingEllipsoidalHeatSource.C b/src/materials/ADMovingEllipsoidalHeatSource.C
new file mode 100644
index 00000000..3fc3a88a
--- /dev/null
+++ b/src/materials/ADMovingEllipsoidalHeatSource.C
@@ -0,0 +1,47 @@
+/****************************************************************************/
+/*                        DO NOT MODIFY THIS HEADER                         */
+/*                                                                          */
+/* MALAMUTE: MOOSE Application Library for Advanced Manufacturing UTilitiEs */
+/*                                                                          */
+/*           Copyright 2021 - 2023, Battelle Energy Alliance, LLC           */
+/*                           ALL RIGHTS RESERVED                            */
+/****************************************************************************/
+
+#include "ADMovingEllipsoidalHeatSource.h"
+
+registerMooseObject("MalamuteApp", ADMovingEllipsoidalHeatSource);
+
+InputParameters
+ADMovingEllipsoidalHeatSource::validParams()
+{
+  auto params = ADMovingHeatSource::validParams();
+  params.addClassDescription("A moving ellipsoidal heat source following a SpatioTemporalPath");
+  params.addRequiredParam<MaterialPropertyName>("power", "Input power of the heat source.");
+  params.addRequiredParam<MaterialPropertyName>(
+      "a", "Length of the ellipsoid semi-axis along the path direction");
+  params.addRequiredParam<MaterialPropertyName>(
+      "b", "Length of the ellipsoid semi-axis perpendicular to the path direction");
+  params.addParam<Real>("efficiency", 1.0, "Process efficiency");
+  params.addParam<Real>("scale", 1.0, "Scaling factor");
+  return params;
+}
+
+ADMovingEllipsoidalHeatSource::ADMovingEllipsoidalHeatSource(const InputParameters & params)
+  : ADMovingHeatSource(params),
+    _P(getADMaterialProperty<Real>("power")),
+    _a(getADMaterialProperty<Real>("a")),
+    _b(getADMaterialProperty<Real>("b")),
+    _eta(getParam<Real>("efficiency")),
+    _scale(getParam<Real>("scale"))
+{
+}
+
+ADReal
+ADMovingEllipsoidalHeatSource::computeHeatSource()
+{
+  auto factor = 2.0 * _P[_qp] * _eta * _scale / libMesh::pi / _a[_qp] / _b[_qp];
+  auto dist_t0 = std::sqrt(2.0) * _tangential_distance[_qp] / _a[_qp];
+  auto dist_n0 = std::sqrt(2.0) * _normal_distance[_qp] / _b[_qp];
+
+  return factor * std::exp(-dist_t0 * dist_t0 - dist_n0 * dist_n0);
+}
diff --git a/src/materials/ADMovingHeatSource.C b/src/materials/ADMovingHeatSource.C
new file mode 100644
index 00000000..68bb6087
--- /dev/null
+++ b/src/materials/ADMovingHeatSource.C
@@ -0,0 +1,50 @@
+/****************************************************************************/
+/*                        DO NOT MODIFY THIS HEADER                         */
+/*                                                                          */
+/* MALAMUTE: MOOSE Application Library for Advanced Manufacturing UTilitiEs */
+/*                                                                          */
+/*           Copyright 2021 - 2023, Battelle Energy Alliance, LLC           */
+/*                           ALL RIGHTS RESERVED                            */
+/****************************************************************************/
+
+#include "ADMovingHeatSource.h"
+
+InputParameters
+ADMovingHeatSource::validParams()
+{
+  auto params = Material::validParams();
+  params.addRequiredParam<std::string>("path",
+                                       "The name of the spatio-temporal path object that describes "
+                                       "the moving path of the heat source.");
+  params.addParam<MaterialPropertyName>(
+      "volumetric_heat",
+      "volumetric_heat",
+      "The name of the material property used to store the computed heat source value.");
+  params.addParam<MaterialPropertyName>("heat_source_tangential_distance",
+                                        "heat_source_tangential_distance",
+                                        "The name of the material property used to store the "
+                                        "tangential distance from the heat source.");
+  params.addParam<MaterialPropertyName>("heat_source_normal_distance",
+                                        "heat_source_normal_distance",
+                                        "The name of the material property used to store the "
+                                        "normal distance from the heat source.");
+  return params;
+}
+
+ADMovingHeatSource::ADMovingHeatSource(const InputParameters & params)
+  : Material(params),
+    SpatioTemporalPathInterface(this),
+    _path(getSpatioTemporalPath("path")),
+    _volumetric_heat(declareADProperty<Real>("volumetric_heat")),
+    _tangential_distance(declareProperty<Real>("heat_source_tangential_distance")),
+    _normal_distance(declareProperty<Real>("heat_source_normal_distance"))
+{
+}
+
+void
+ADMovingHeatSource::computeQpProperties()
+{
+  _tangential_distance[_qp] = _path.tangentialDistance(_q_point[_qp]);
+  _normal_distance[_qp] = _path.normalDistance(_q_point[_qp]);
+  _volumetric_heat[_qp] = computeHeatSource();
+}
diff --git a/src/spatiotemporalpaths/CSVPiecewiseLinearSpatioTemporalPath.C b/src/spatiotemporalpaths/CSVPiecewiseLinearSpatioTemporalPath.C
new file mode 100644
index 00000000..370ebce3
--- /dev/null
+++ b/src/spatiotemporalpaths/CSVPiecewiseLinearSpatioTemporalPath.C
@@ -0,0 +1,63 @@
+/****************************************************************************/
+/*                        DO NOT MODIFY THIS HEADER                         */
+/*                                                                          */
+/* MALAMUTE: MOOSE Application Library for Advanced Manufacturing UTilitiEs */
+/*                                                                          */
+/*           Copyright 2021 - 2023, Battelle Energy Alliance, LLC           */
+/*                           ALL RIGHTS RESERVED                            */
+/****************************************************************************/
+
+#include "CSVPiecewiseLinearSpatioTemporalPath.h"
+#include "DelimitedFileReader.h"
+
+registerMooseObject("MalamuteApp", CSVPiecewiseLinearSpatioTemporalPath);
+
+InputParameters
+CSVPiecewiseLinearSpatioTemporalPath::validParams()
+{
+  InputParameters params = PiecewiseLinearSpatioTemporalPathBase::validParams();
+  params.addClassDescription("A piecewise linear spatiotemporal path from a csv file.");
+  params.addRequiredParam<FileName>(
+      "file", "CSV file containing information about the spatio-temporal path");
+  MooseEnum header_flag("OFF ON AUTO", "AUTO");
+  params.addParam<MooseEnum>("file_header",
+                             header_flag,
+                             "Set the header flag. ON: use the first row has header, OFF: assumes "
+                             "no header, AUTO: attempt to determine if a header exists.");
+  MooseEnum format_flag("COLUMNS ROWS", "COLUMNS");
+  params.addParam<MooseEnum>("file_format", format_flag, "Set the file format (rows vs. columns).");
+  params.addParam<bool>("file_ignore_empty_lines", true, "Ignore empty lines in the csv file.");
+  params.addParam<std::string>(
+      "file_comment", "", "Set the comment character, by default no comment character is used.");
+  params.addParam<std::string>("t", "t", "Column name for the spatio-temporal path's time");
+  params.addParam<std::string>("x", "x", "Column name for the spatio-temporal path's x-coordinate");
+  params.addParam<std::string>("y", "y", "Column name for the spatio-temporal path's y-coordinate");
+  params.addParam<std::string>("z", "z", "Column name for the spatio-temporal path's z-coordinate");
+  return params;
+}
+
+CSVPiecewiseLinearSpatioTemporalPath::CSVPiecewiseLinearSpatioTemporalPath(
+    const InputParameters & params)
+  : PiecewiseLinearSpatioTemporalPathBase(params)
+{
+  using Format = MooseUtils::DelimitedFileReader::FormatFlag;
+  using Header = MooseUtils::DelimitedFileReader::HeaderFlag;
+
+  auto csv_reader = MooseUtils::DelimitedFileReader(getParam<FileName>("file"));
+  csv_reader.setHeaderFlag(getParam<MooseEnum>("file_header").getEnum<Header>());
+  csv_reader.setFormatFlag(getParam<MooseEnum>("file_format").getEnum<Format>());
+  csv_reader.setIgnoreEmptyLines(getParam<bool>("file_ignore_empty_lines"));
+  csv_reader.setComment(getParam<std::string>("file_comment"));
+  csv_reader.read();
+
+  // times
+  _times = csv_reader.getData(getParam<std::string>("t"));
+
+  // coords
+  const auto & x = csv_reader.getData(getParam<std::string>("x"));
+  const auto & y = csv_reader.getData(getParam<std::string>("y"));
+  const auto & z = csv_reader.getData(getParam<std::string>("z"));
+  setCoords(x, y, z);
+
+  validate();
+}
diff --git a/src/spatiotemporalpaths/FunctionSpatioTemporalPath.C b/src/spatiotemporalpaths/FunctionSpatioTemporalPath.C
new file mode 100644
index 00000000..cb83ad9e
--- /dev/null
+++ b/src/spatiotemporalpaths/FunctionSpatioTemporalPath.C
@@ -0,0 +1,39 @@
+/****************************************************************************/
+/*                        DO NOT MODIFY THIS HEADER                         */
+/*                                                                          */
+/* MALAMUTE: MOOSE Application Library for Advanced Manufacturing UTilitiEs */
+/*                                                                          */
+/*           Copyright 2021 - 2023, Battelle Energy Alliance, LLC           */
+/*                           ALL RIGHTS RESERVED                            */
+/****************************************************************************/
+
+#include "FunctionSpatioTemporalPath.h"
+#include "Function.h"
+
+registerMooseObject("MalamuteApp", FunctionSpatioTemporalPath);
+
+InputParameters
+FunctionSpatioTemporalPath::validParams()
+{
+  auto params = SpatioTemporalPath::validParams();
+  params.addClassDescription(
+      "A spatiotemporal path whose coordinates are specified using MOOSE functions.");
+  params.addParam<FunctionName>("x", "The function for the x-coordinate.");
+  params.addParam<FunctionName>("y", "The function for the y-coordinate.");
+  params.addParam<FunctionName>("z", "The function for the z-coordinate.");
+  return params;
+}
+
+FunctionSpatioTemporalPath::FunctionSpatioTemporalPath(const InputParameters & params)
+  : SpatioTemporalPath(params),
+    _x(isParamValid("x") ? &getFunction("x") : nullptr),
+    _y(isParamValid("y") ? &getFunction("y") : nullptr),
+    _z(isParamValid("z") ? &getFunction("z") : nullptr)
+{
+}
+
+Point
+FunctionSpatioTemporalPath::position(Real t) const
+{
+  return Point(_x ? _x->value(t) : 0.0, _y ? _y->value(t) : 0.0, _z ? _z->value(t) : 0.0);
+}
diff --git a/src/spatiotemporalpaths/PiecewiseLinearSpatioTemporalPath.C b/src/spatiotemporalpaths/PiecewiseLinearSpatioTemporalPath.C
new file mode 100644
index 00000000..69cc44a5
--- /dev/null
+++ b/src/spatiotemporalpaths/PiecewiseLinearSpatioTemporalPath.C
@@ -0,0 +1,35 @@
+/****************************************************************************/
+/*                        DO NOT MODIFY THIS HEADER                         */
+/*                                                                          */
+/* MALAMUTE: MOOSE Application Library for Advanced Manufacturing UTilitiEs */
+/*                                                                          */
+/*           Copyright 2021 - 2023, Battelle Energy Alliance, LLC           */
+/*                           ALL RIGHTS RESERVED                            */
+/****************************************************************************/
+
+#include "PiecewiseLinearSpatioTemporalPath.h"
+
+registerMooseObject("MalamuteApp", PiecewiseLinearSpatioTemporalPath);
+
+InputParameters
+PiecewiseLinearSpatioTemporalPath::validParams()
+{
+  InputParameters params = PiecewiseLinearSpatioTemporalPathBase::validParams();
+  params.addClassDescription(
+      "A piecewise linear spatiotemporal path from discrete times and vertices.");
+  params.addRequiredParam<std::vector<Real>>("t", "Times associated with the path vertices");
+  params.addParam<std::vector<Real>>("x", {}, "x-coordinates of the path vertices");
+  params.addParam<std::vector<Real>>("y", {}, "y-coordinates of the path vertices");
+  params.addParam<std::vector<Real>>("z", {}, "z-coordinates of the path vertices");
+  return params;
+}
+
+PiecewiseLinearSpatioTemporalPath::PiecewiseLinearSpatioTemporalPath(const InputParameters & params)
+  : PiecewiseLinearSpatioTemporalPathBase(params)
+{
+  _times = getParam<std::vector<Real>>("t");
+  setCoords(getParam<std::vector<Real>>("x"),
+            getParam<std::vector<Real>>("y"),
+            getParam<std::vector<Real>>("z"));
+  validate();
+}
diff --git a/src/spatiotemporalpaths/PiecewiseLinearSpatioTemporalPathBase.C b/src/spatiotemporalpaths/PiecewiseLinearSpatioTemporalPathBase.C
new file mode 100644
index 00000000..50447dec
--- /dev/null
+++ b/src/spatiotemporalpaths/PiecewiseLinearSpatioTemporalPathBase.C
@@ -0,0 +1,155 @@
+/****************************************************************************/
+/*                        DO NOT MODIFY THIS HEADER                         */
+/*                                                                          */
+/* MALAMUTE: MOOSE Application Library for Advanced Manufacturing UTilitiEs */
+/*                                                                          */
+/*           Copyright 2021 - 2023, Battelle Energy Alliance, LLC           */
+/*                           ALL RIGHTS RESERVED                            */
+/****************************************************************************/
+
+#include "PiecewiseLinearSpatioTemporalPathBase.h"
+
+InputParameters
+PiecewiseLinearSpatioTemporalPathBase::validParams()
+{
+  auto params = SpatioTemporalPath::validParams();
+  params.addParam<Real>("time_tolerance", 5e-12, "Tolerance of query time");
+  MooseEnum outside_behavior("CONSTANT EXTRAPOLATION EXCEPTION", "CONSTANT");
+  params.addParam<MooseEnum>(
+      "outside",
+      outside_behavior,
+      "The method to use when extrapolating the path position outside its temporal support. "
+      "CONSTANT: Return the closest path point; EXTRAPOLATION: Linear extrapolation; EXCEPTION: "
+      "Raise an exception when trying to extrapolate.");
+  return params;
+}
+
+PiecewiseLinearSpatioTemporalPathBase::PiecewiseLinearSpatioTemporalPathBase(
+    const InputParameters & params)
+  : SpatioTemporalPath(params),
+    _t_tol(getParam<Real>("time_tolerance")),
+    _outside(getParam<MooseEnum>("outside"))
+{
+}
+
+Point
+PiecewiseLinearSpatioTemporalPathBase::position(Real t) const
+{
+  if (t < (_times[0] - _t_tol) || t > (_times.back() + _t_tol))
+  {
+    if (_outside == "EXCEPTION")
+      mooseException("A spatiotemporal path is queried outside its temporal support.");
+    else if (_outside == "CONSTANT")
+    {
+      if (t < _times[0])
+        return _coords[0];
+      if (t > _times.back())
+        return _coords.back();
+    }
+    else if (_outside == "EXTRAPOLATION")
+    {
+      // no-op
+    }
+    else
+      paramError("extrapolation_method", "Unsupported extrapolation method.");
+  }
+
+  auto [i1, i2] = getIntervalIndices(t);
+
+  // Linearly interpolate the coordinates
+  auto p1 = _coords[i1];
+  auto p2 = _coords[i2];
+  auto fraction = (t - _times[i1]) / (_times[i2] - _times[i1]);
+  auto p = p1 + fraction * (p2 - p1);
+
+  if (std::isnan(p(0)) || std::isnan(p(1)) || std::isnan(p(2)))
+    mooseException("Encountered NaN when querying a spatiotemporal path.");
+
+  return p;
+}
+
+void
+PiecewiseLinearSpatioTemporalPathBase::setCoords(const std::vector<Real> & x,
+                                                 const std::vector<Real> & y,
+                                                 const std::vector<Real> & z)
+{
+  // Size check
+  auto sz = std::max({x.size(), y.size(), z.size()});
+  if (sz == 0 || (!x.empty() && x.size() != sz) || (!x.empty() && x.size() != sz) ||
+      (!x.empty() && x.size() != sz))
+    mooseError(
+        "Error while constructing a spatio-temporal path: At lease one of the x-, y-, and z- "
+        "coordinates must be non-empty, and the non-zero sizes must match. x-coordinates have "
+        "size ",
+        x.size(),
+        ", y-coordinates have size ",
+        y.size(),
+        ", z-coordinates have size ",
+        z.size(),
+        ".");
+
+  // Assigne coordinates
+  _coords.resize(sz);
+  for (auto i : make_range(sz))
+  {
+    _coords[i](0) = x.empty() ? 0.0 : x[i];
+    _coords[i](1) = y.empty() ? 0.0 : y[i];
+    _coords[i](2) = z.empty() ? 0.0 : z[i];
+  }
+}
+
+void
+PiecewiseLinearSpatioTemporalPathBase::validate() const
+{
+  // Time and coords must have same size
+  if (_times.size() != _coords.size())
+    mooseError("Error while constructing a spatio-temporal path: The time series and the "
+               "coordinates must have the same size. The time series have size ",
+               _times.size(),
+               ", and the coordinates have size ",
+               _coords.size(),
+               ".");
+
+  // There must be at least one line segment
+  if (_times.size() < 2)
+    mooseError("Error while constructing a spatio-temporal path: There must be at least two "
+               "vertices on the path, but only ",
+               _times.size(),
+               " is provided.");
+
+  // Time must be non-decreasing
+  for (auto i : index_range(_times))
+    if (i > 0 && _times[i] < _times[i - 1])
+      mooseError("Error while constructing a spatio-temporal path: The time series must be "
+                 "non-decreasing. The time at index ",
+                 i - 1,
+                 " is ",
+                 _times[i - 1],
+                 ", and the time at index ",
+                 i,
+                 " is ",
+                 _times[i],
+                 ".");
+}
+
+std::pair<unsigned int, unsigned int>
+PiecewiseLinearSpatioTemporalPathBase::getIntervalIndices(Real t) const
+{
+  if (t < _times[0])
+    return {0, 1};
+
+  for (auto i : index_range(_times))
+    if (t >= _times[i])
+    {
+      if (i == _times.size() - 1)
+        return {_times.size() - 2, _times.size() - 1};
+      else
+        return {i, i + 1};
+    }
+
+  mooseError("Failed to query a spatiotemporal path with time ",
+             t,
+             ". If you believe the query is valid, try increasing the time_tolerance as query may "
+             "fail due to machine precision.");
+  return {0, 0};
+}
diff --git a/src/spatiotemporalpaths/SpatioTemporalPath.C b/src/spatiotemporalpaths/SpatioTemporalPath.C
new file mode 100644
index 00000000..c1634742
--- /dev/null
+++ b/src/spatiotemporalpaths/SpatioTemporalPath.C
@@ -0,0 +1,217 @@
+/****************************************************************************/
+/*                        DO NOT MODIFY THIS HEADER                         */
+/*                                                                          */
+/* MALAMUTE: MOOSE Application Library for Advanced Manufacturing UTilitiEs */
+/*                                                                          */
+/*           Copyright 2021 - 2023, Battelle Energy Alliance, LLC           */
+/*                           ALL RIGHTS RESERVED                            */
+/****************************************************************************/
+
+#include "SpatioTemporalPath.h"
+
+InputParameters
+SpatioTemporalPath::validParams()
+{
+  auto params = UserObject::validParams();
+  params.addParam<Real>("FD_abs_eps",
+                        1e-6,
+                        "The absolute epsilon to use when using finite-differencing to "
+                        "approximate the path's velocity and direction.");
+  params.addParam<Real>("FD_rel_eps",
+                        1e-6,
+                        "The relative epsilon to use when using finite-differencing to "
+                        "approximate the path's velocity and direction.");
+  params.addParam<bool>(
+      "verbose", false, "Set to true to print additional information about the path.");
+  params.addParam<Real>(
+      "update_interval",
+      0,
+      "Time interval betweem path front updates. This parameter controls how often the path front "
+      "is updated. The default value (0) is equivalent to live update, i.e., at every time step "
+      "the path position, velocity, and direction are recomputed.");
+
+  params.addParam<bool>(
+      "smoothing",
+      false,
+      "If the path is not smooth, the velocity and direction may experience abrupt change at "
+      "vertices. Set this option to true to enable smoothing by averaging the direction and "
+      "velocity over a time window specified FD_smoothing_time_window.");
+  params.addParam<Real>("smoothing_time_window",
+                        0,
+                        "The window size used to smooth direction and velocity. See FD_smoothing "
+                        "for explanation about smoothing.");
+  params.addParam<unsigned int>(
+      "smoothing_points", 20, "The number of sampling points used in smoothing.");
+
+  // Only execute at INITIAL and TIMESTEP_BEGIN -- that's when the current time changes
+  ExecFlagEnum execute_options = MooseUtils::getDefaultExecFlagEnum();
+  execute_options = {EXEC_INITIAL, EXEC_TIMESTEP_BEGIN};
+  params.set<ExecFlagEnum>("execute_on") = execute_options;
+  params.suppressParameter<ExecFlagEnum>("execute_on");
+
+  return params;
+}
+
+SpatioTemporalPath::SpatioTemporalPath(const InputParameters & params)
+  : UserObject(params),
+    TransientInterface(this),
+    _verbose(getParam<bool>("verbose")),
+    _abs_epsilon(getParam<Real>("FD_abs_eps")),
+    _rel_epsilon(getParam<Real>("FD_rel_eps")),
+    _interval(getParam<Real>("update_interval")),
+    _last_updated(declareRestartableData<Real>("last_updated")),
+    _smooth(getParam<bool>("smoothing")),
+    _smooth_window(getParam<Real>("smoothing_time_window")),
+    _smooth_points(getParam<unsigned int>("smoothing_points")),
+    _current_position(std::numeric_limits<Real>::quiet_NaN(),
+                      std::numeric_limits<Real>::quiet_NaN(),
+                      std::numeric_limits<Real>::quiet_NaN()),
+    _current_velocity(std::numeric_limits<Real>::quiet_NaN(),
+                      std::numeric_limits<Real>::quiet_NaN(),
+                      std::numeric_limits<Real>::quiet_NaN()),
+    _current_direction(std::numeric_limits<Real>::quiet_NaN(),
+                       std::numeric_limits<Real>::quiet_NaN(),
+                       std::numeric_limits<Real>::quiet_NaN()),
+    _previous_position(declareRestartableData<Point>("previous_position")),
+    _previous_velocity(declareRestartableData<RealVectorValue>("previous_velocity")),
+    _previous_direction(declareRestartableData<RealVectorValue>("previous_direction"))
+{
+}
+
+void
+SpatioTemporalPath::initialSetup()
+{
+  update();
+}
+
+void
+SpatioTemporalPath::timestepSetup()
+{
+  if (_t > _last_updated + _interval)
+    update();
+}
+
+void
+SpatioTemporalPath::meshChanged()
+{
+  timestepSetup();
+}
+
+void
+SpatioTemporalPath::update()
+{
+  _previous_position = _current_position;
+  _previous_velocity = _current_velocity;
+  _previous_direction = _current_direction;
+
+  _current_position = position(_t);
+  _current_velocity = _smooth ? smoothVelocity(_t) : velocity(_t);
+  _current_direction = _smooth ? smoothDirection(_t) : direction(_t);
+
+  _last_updated = _t;
+
+  if (_verbose)
+  {
+    _console << "Spatio-temporal path: " << name() << std::endl;
+    _console << "      position: " << position() << std::endl;
+    _console << "      velocity: " << velocity() << std::endl;
+    _console << "     direction: " << direction() << std::endl;
+  }
+}
+
+RealVectorValue
+SpatioTemporalPath::velocity(Real t) const
+{
+  auto dt = _rel_epsilon * std::abs(t);
+  if (dt < _abs_epsilon)
+    dt = _abs_epsilon;
+
+  return (position(t + dt) - position(t)) / dt;
+}
+
+RealVectorValue
+SpatioTemporalPath::smoothVelocity(Real t) const
+{
+  auto dt = _smooth_window / (_smooth_points - 1);
+  RealVectorValue vel;
+  for (auto i : make_range(_smooth_points))
+    vel += velocity(t - _smooth_window / 2 + i * dt);
+  return vel / _smooth_points;
+}
+
+RealVectorValue
+SpatioTemporalPath::direction(Real t) const
+{
+  auto dt = _rel_epsilon * std::abs(t);
+  if (dt < _abs_epsilon)
+    dt = _abs_epsilon;
+
+  auto dist = position(t + dt) - position(t);
+
+  return dist.norm() == 0.0 ? RealVectorValue(0, 0, 0) : dist.unit();
+}
+
+RealVectorValue
+SpatioTemporalPath::smoothDirection(Real t) const
+{
+  auto dt = _smooth_window / (_smooth_points - 1);
+
+  // Averaging directions is equivalent to finding the dominant singular vector
+  DenseMatrix<Real> dirs;
+  dirs.resize(_smooth_points, 3);
+  dirs.zero();
+
+  // Fill out all directions (to be averaged)
+  for (auto i : make_range(_smooth_points))
+  {
+    auto dir = direction(t - _smooth_window / 2 + i * dt);
+    dirs(i, 0) = dir(0);
+    dirs(i, 1) = dir(1);
+    dirs(i, 2) = dir(2);
+  };
+
+  // SVD
+  DenseVector<Real> sigma;
+  DenseMatrix<Real> U, VT;
+  dirs.svd(sigma, U, VT);
+
+  return RealVectorValue(VT(0, 0), VT(0, 1), VT(0, 2));
+}
+
+Real
+SpatioTemporalPath::tangentialDistance(Real t, const Point & p) const
+{
+  auto p0 = position(t);
+  auto d = _smooth ? smoothDirection(t) : direction(t);
+  return (p - p0) * d;
+}
+
+Real
+SpatioTemporalPath::tangentialDistance(const Point & p) const
+{
+  auto p0 = position();
+  auto d = direction();
+  return (p - p0) * d;
+}
+
+Real
+SpatioTemporalPath::normalDistance(Real t, const Point & p) const
+{
+  auto p0 = position(t);
+  auto d = _smooth ? smoothDirection(t) : direction(t);
+  auto dp = p - p0;
+  auto dpt = (p - p0) * d;
+  auto dpn = dp - dpt * d;
+  return dpn.norm();
+}
+
+Real
+SpatioTemporalPath::normalDistance(const Point & p) const
+{
+  auto p0 = position();
+  auto d = direction();
+  auto dp = p - p0;
+  auto dpt = (p - p0) * d;
+  auto dpn = dp - dpt * d;
+  return dpn.norm();
+}
diff --git a/src/userobjects/SpatioTemporalPathElementSubdomainModifier.C b/src/userobjects/SpatioTemporalPathElementSubdomainModifier.C
new file mode 100644
index 00000000..49d57358
--- /dev/null
+++ b/src/userobjects/SpatioTemporalPathElementSubdomainModifier.C
@@ -0,0 +1,46 @@
+/****************************************************************************/
+/*                        DO NOT MODIFY THIS HEADER                         */
+/*                                                                          */
+/* MALAMUTE: MOOSE Application Library for Advanced Manufacturing UTilitiEs */
+/*                                                                          */
+/*           Copyright 2021 - 2023, Battelle Energy Alliance, LLC           */
+/*                           ALL RIGHTS RESERVED                            */
+/****************************************************************************/
+
+#include "SpatioTemporalPathElementSubdomainModifier.h"
+
+registerMooseObject("MalamuteApp", SpatioTemporalPathElementSubdomainModifier);
+
+InputParameters
+SpatioTemporalPathElementSubdomainModifier::validParams()
+{
+  InputParameters params = ElementSubdomainModifier::validParams();
+  params.addClassDescription("Modify subdomain of elements when the element is within the "
+                             "neighborhood of the path's current position.");
+  params.addRequiredParam<SubdomainName>(
+      "target_subdomain", "The subdomain name/ID to set when the path goes through an element");
+  params.addRequiredParam<std::string>("path", "The name of the spatio-temporal path object.");
+  params.addRequiredParam<Real>("radius",
+                                "The element subdomain is changed to the target subdomain if its "
+                                "centroid is within the radius of the current path front.");
+  return params;
+}
+
+SpatioTemporalPathElementSubdomainModifier::SpatioTemporalPathElementSubdomainModifier(
+    const InputParameters & params)
+  : ElementSubdomainModifier(params),
+    SpatioTemporalPathInterface(this),
+    _path(getSpatioTemporalPath("path")),
+    _subdomain_id(_mesh.getSubdomainID(getParam<SubdomainName>("target_subdomain"))),
+    _r(getParam<Real>("radius"))
+{
+}
+
+SubdomainID
+SpatioTemporalPathElementSubdomainModifier::computeSubdomainID()
+{
+  if ((_current_elem->centroid() - _path.position()).norm_sq() < _r * _r)
+    return _subdomain_id;
+
+  return _current_elem->subdomain_id();
+}
diff --git a/test/tests/spatiotemporal_path/csv.i b/test/tests/spatiotemporal_path/csv.i
new file mode 100644
index 00000000..cd85b0db
--- /dev/null
+++ b/test/tests/spatiotemporal_path/csv.i
@@ -0,0 +1,44 @@
+[Mesh]
+  [gmg]
+    type = GeneratedMeshGenerator
+    dim = 2
+    nx = 20
+    ny = 20
+  []
+  use_displaced_mesh = false
+[]
+
+[Problem]
+  solve = false
+[]
+
+[SpatioTemporalPaths]
+  [path]
+    type = CSVPiecewiseLinearSpatioTemporalPath
+    file = 'gold/path.csv'
+    verbose = true
+  []
+[]
+
+[Materials]
+  [heat_source]
+    type = ADMovingEllipsoidalHeatSource
+    path = path
+    power = 1
+    efficiency = 1
+    scale = 1
+    a = 0.4
+    b = 0.2
+    outputs = exodus
+  []
+[]
+
+[Executioner]
+  type = Transient
+  dt = 0.5
+  end_time = 10
+[]
+
+[Outputs]
+  exodus = true
+[]
diff --git a/test/tests/spatiotemporal_path/esm.i b/test/tests/spatiotemporal_path/esm.i
new file mode 100644
index 00000000..bc076080
--- /dev/null
+++ b/test/tests/spatiotemporal_path/esm.i
@@ -0,0 +1,77 @@
+[Mesh]
+  [gmg]
+    type = GeneratedMeshGenerator
+    dim = 2
+    nx = 20
+    ny = 20
+  []
+  [solid]
+    type = SubdomainBoundingBoxGenerator
+    input = gmg
+    block_id = 0
+    block_name = solid
+    bottom_left = '0 0 0'
+    top_right = '1 1 0'
+  []
+  [liquid]
+    type = SubdomainBoundingBoxGenerator
+    input = solid
+    block_id = 1
+    block_name = liquid
+    bottom_left = '0 0 0'
+    top_right = '0.05 0.05 0'
+  []
+  use_displaced_mesh = false
+[]
+
+[Problem]
+  solve = false
+[]
+
+[AuxVariables]
+  [u]
+  []
+[]
+
+[Functions]
+  [path_x]
+    type = PiecewiseLinear
+    x = '3   4   5   6   7   8'
+    y = '0 0.1 0.2 0.3 0.4 0.5'
+  []
+  [path_y]
+    type = PiecewiseLinear
+    x = '3   4   5   6   7   8'
+    y = '0 0.1 0.2 0.3 0.4 0.5'
+  []
+[]
+
+[SpatioTemporalPaths]
+  [path]
+    type = FunctionSpatioTemporalPath
+    x = path_x
+    y = path_y
+
+    verbose = true
+  []
+[]
+
+[UserObjects]
+  [esm]
+    type = SpatioTemporalPathElementSubdomainModifier
+    path = 'path'
+    radius = 0.2
+    target_subdomain = 'liquid'
+    execute_on = 'INITIAL TIMESTEP_END'
+  []
+[]
+
+[Executioner]
+  type = Transient
+  dt = 1
+  end_time = 8
+[]
+
+[Outputs]
+  exodus = true
+[]
diff --git a/test/tests/spatiotemporal_path/function.i b/test/tests/spatiotemporal_path/function.i
new file mode 100644
index 00000000..1923508c
--- /dev/null
+++ b/test/tests/spatiotemporal_path/function.i
@@ -0,0 +1,58 @@
+[Mesh]
+  [gmg]
+    type = GeneratedMeshGenerator
+    dim = 2
+    nx = 20
+    ny = 20
+  []
+  use_displaced_mesh = false
+[]
+
+[Problem]
+  solve = false
+[]
+
+[Functions]
+  [path_x]
+    type = PiecewiseLinear
+    x = '3   4   5   6   7   8'
+    y = '0 0.1 0.2 0.3 0.4 0.5'
+  []
+  [path_y]
+    type = PiecewiseLinear
+    x = '3   4   5   6   7   8'
+    y = '0 0.1 0.2 0.3 0.4 0.5'
+  []
+[]
+
+[SpatioTemporalPaths]
+  [path]
+    type = FunctionSpatioTemporalPath
+    x = path_x
+    y = path_y
+    verbose = true
+  []
+[]
+
+[Materials]
+  [heat_source]
+    type = ADMovingEllipsoidalHeatSource
+    path = path
+    power = 1
+    efficiency = 1
+    scale = 1
+    a = 0.4
+    b = 0.2
+    outputs = exodus
+  []
+[]
+
+[Executioner]
+  type = Transient
+  dt = 0.5
+  end_time = 10
+[]
+
+[Outputs]
+  exodus = true
+[]
diff --git a/test/tests/spatiotemporal_path/gold/csv_out.e b/test/tests/spatiotemporal_path/gold/csv_out.e
new file mode 100644
index 00000000..04e0c9df
Binary files /dev/null and b/test/tests/spatiotemporal_path/gold/csv_out.e differ
diff --git a/test/tests/spatiotemporal_path/gold/esm_out.e b/test/tests/spatiotemporal_path/gold/esm_out.e
new file mode 100644
index 00000000..f071d603
Binary files /dev/null and b/test/tests/spatiotemporal_path/gold/esm_out.e differ
diff --git a/test/tests/spatiotemporal_path/gold/esm_out.e-s002 b/test/tests/spatiotemporal_path/gold/esm_out.e-s002
new file mode 100644
index 00000000..89f19bc4
Binary files /dev/null and b/test/tests/spatiotemporal_path/gold/esm_out.e-s002 differ
diff --git a/test/tests/spatiotemporal_path/gold/esm_out.e-s003 b/test/tests/spatiotemporal_path/gold/esm_out.e-s003
new file mode 100644
index 00000000..e3c352d5
Binary files /dev/null and b/test/tests/spatiotemporal_path/gold/esm_out.e-s003 differ
diff --git a/test/tests/spatiotemporal_path/gold/esm_out.e-s004 b/test/tests/spatiotemporal_path/gold/esm_out.e-s004
new file mode 100644
index 00000000..3f2ea8e1
Binary files /dev/null and b/test/tests/spatiotemporal_path/gold/esm_out.e-s004 differ
diff --git a/test/tests/spatiotemporal_path/gold/esm_out.e-s005 b/test/tests/spatiotemporal_path/gold/esm_out.e-s005
new file mode 100644
index 00000000..cc2c3bee
Binary files /dev/null and b/test/tests/spatiotemporal_path/gold/esm_out.e-s005 differ
diff --git a/test/tests/spatiotemporal_path/gold/esm_out.e-s006 b/test/tests/spatiotemporal_path/gold/esm_out.e-s006
new file mode 100644
index 00000000..7cfb1c59
Binary files /dev/null and b/test/tests/spatiotemporal_path/gold/esm_out.e-s006 differ
diff --git a/test/tests/spatiotemporal_path/gold/esm_out.e-s007 b/test/tests/spatiotemporal_path/gold/esm_out.e-s007
new file mode 100644
index 00000000..1a4cccaa
Binary files /dev/null and b/test/tests/spatiotemporal_path/gold/esm_out.e-s007 differ
diff --git a/test/tests/spatiotemporal_path/gold/esm_out.e-s008 b/test/tests/spatiotemporal_path/gold/esm_out.e-s008
new file mode 100644
index 00000000..81eb3740
Binary files /dev/null and b/test/tests/spatiotemporal_path/gold/esm_out.e-s008 differ
diff --git a/test/tests/spatiotemporal_path/gold/esm_out.e-s009 b/test/tests/spatiotemporal_path/gold/esm_out.e-s009
new file mode 100644
index 00000000..7e7f64a2
Binary files /dev/null and b/test/tests/spatiotemporal_path/gold/esm_out.e-s009 differ
diff --git a/test/tests/spatiotemporal_path/gold/function_out.e b/test/tests/spatiotemporal_path/gold/function_out.e
new file mode 100644
index 00000000..da7e5a5e
Binary files /dev/null and b/test/tests/spatiotemporal_path/gold/function_out.e differ
diff --git a/test/tests/spatiotemporal_path/gold/manual_out.e b/test/tests/spatiotemporal_path/gold/manual_out.e
new file mode 100644
index 00000000..bd8cf769
Binary files /dev/null and b/test/tests/spatiotemporal_path/gold/manual_out.e differ
diff --git a/test/tests/spatiotemporal_path/gold/path.csv b/test/tests/spatiotemporal_path/gold/path.csv
new file mode 100644
index 00000000..265e9e3f
--- /dev/null
+++ b/test/tests/spatiotemporal_path/gold/path.csv
@@ -0,0 +1,7 @@
+t,x,y,z
+3,0,0,0
+4,0.1,0.1,0
+5,0.2,0.2,0
+6,0.3,0.3,0
+7,0.4,0.4,0
+8,0.5,0.5,0
diff --git a/test/tests/spatiotemporal_path/manual.i b/test/tests/spatiotemporal_path/manual.i
new file mode 100644
index 00000000..849804eb
--- /dev/null
+++ b/test/tests/spatiotemporal_path/manual.i
@@ -0,0 +1,46 @@
+[Mesh]
+  [gmg]
+    type = GeneratedMeshGenerator
+    dim = 2
+    nx = 20
+    ny = 20
+  []
+  use_displaced_mesh = false
+[]
+
+[Problem]
+  solve = false
+[]
+
+[SpatioTemporalPaths]
+  [path]
+    type = PiecewiseLinearSpatioTemporalPath
+    t = '3   4   5   6   7   8'
+    x = '0 0.1 0.2 0.3 0.4 0.5'
+    y = '0 0.1 0.2 0.3 0.4 0.5'
+    verbose = true
+  []
+[]
+
+[Materials]
+  [heat_source]
+    type = ADMovingEllipsoidalHeatSource
+    path = path
+    power = 1
+    efficiency = 1
+    scale = 1
+    a = 0.4
+    b = 0.2
+    outputs = exodus
+  []
+[]
+
+[Executioner]
+  type = Transient
+  dt = 0.5
+  end_time = 10
+[]
+
+[Outputs]
+  exodus = true
+[]
diff --git a/test/tests/spatiotemporal_path/tests b/test/tests/spatiotemporal_path/tests
new file mode 100644
index 00000000..3f079204
--- /dev/null
+++ b/test/tests/spatiotemporal_path/tests
@@ -0,0 +1,35 @@
+[Tests]
+  issues = '#116'
+  [manual]
+    type = 'Exodiff'
+    input = 'manual.i'
+    exodiff = 'manual_out.e'
+    design = 'ADMovingEllipsoidalHeatSource.md PiecewiseLinearSpatioTemporalPath.md'
+    requirement = 'The system shall be able to use a manually defined path to evolve the heat source.'
+  []
+  [csv]
+    type = 'Exodiff'
+    input = 'csv.i'
+    exodiff = 'csv_out.e'
+    design = 'ADMovingEllipsoidalHeatSource.md CSVPiecewiseLinearSpatioTemporalPath.md'
+    requirement = 'The system shall be able to use a CSV-defined path to evolve the heat source.'
+  []
+  [function]
+    type = 'Exodiff'
+    input = 'function.i'
+    exodiff = 'function_out.e'
+    design = 'ADMovingEllipsoidalHeatSource.md FunctionSpatioTemporalPath.md'
+    requirement = 'The system shall be able to use a function-defined path to evolve the heat source.'
+  []
+  [esm]
+    type = 'Exodiff'
+    input = 'esm.i'
+    exodiff = "esm_out.e
+               esm_out.e-s002 esm_out.e-s003 esm_out.e-s004
+               esm_out.e-s005 esm_out.e-s006 esm_out.e-s007
+               esm_out.e-s008 esm_out.e-s009"
+    design = 'SpatioTemporalPathElementSubdomainModifier.md'
+    mesh_mode = 'REPLICATED'
+    requirement = 'The system shall be able to use a SpatioTemporalPath to modify element subdomains.'
+  []
+[]