diff --git a/CHANGELOG.md b/CHANGELOG.md
index 51cb0dac8185c29662825119f2eb9335e7fa68a9..82ebe848712c08861b848009e0556ecae5369e0c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -36,6 +36,7 @@ release.
 ## Unreleased
 
 ### Added
+- `create_csm` now dispatches to `_from_isd` and `_from_state` to test whether the sensor model can be instantiated from either and ISD or a state file.
 - `generate_image_coordinate` to `csm.py`. This provides a similar interface to `generate_ground_coordinate` and abstracts away the `csmapi` from the user.
 - A surface class (moved from AutoCNet; credit @jessemapel) with support for Ellipsoid DEMs and basic support for raster DEMs readable by the plio.io.io_gdal.GeoDataset. Support is basic because it uses a single pixel intersection and not an interpolated elevation like ISIS does.
 - A check to `generate_ground_point` when a GeoDataset is used to raise a `ValueError` if the algorithm intersects a no data value in the passed DEM. This ensures that valid heights are used in the intersection computation. Fixes [#120](https://github.com/DOI-USGS/knoten/issues/120)
diff --git a/knoten/csm.py b/knoten/csm.py
index da04ae9596101ad51ba9f5e30dabf51cf4fbfc43..9912283d00736e9d645dcc9e2ef22a37d0a1ccb9 100644
--- a/knoten/csm.py
+++ b/knoten/csm.py
@@ -74,22 +74,26 @@ def create_camera(label, url='http://pfeffer.wr.usgs.gov/api/1.0/pds/'):
         model = plugin.constructModelFromISD(isd, model_name)
         return model
 
-def create_csm(image, verbose=False):
-    """
-    Given an image file, create a Community Sensor Model.
+def _from_state(state, verbose):
+    with open(state, 'r') as stream:
+        model_name = stream.readline().rstrip()
+        state = json.load(stream)
+    state = json.dumps(state)
 
-    Parameters
-    ----------
-    image : str
-            The image filename to create a CSM for
-    verbose : bool
-              Print information about which plugins and models were attempted
+    plugins = csmapi.Plugin.getList()
+    for plugin in plugins:
+        if verbose:
+            print(f'Trying plugin {plugin.getPluginName()}')
+        if plugin.canModelBeConstructedFromState(model_name, state):
+            camera_warnings = csmapi.WarningList()
+            camera = plugin.constructModelFromState(state, camera_warnings)
+            if verbose:
+                for warning in camera_warnings:
+                    print(f'Warning in function {warning.getFunction()}: "{warning.getMessage()}"')
+                print('Success!')
+            return plugin.constructModelFromState(state)
 
-    Returns
-    -------
-    model : object
-            A CSM sensor model (or None if no associated model is available.)
-    """
+def _from_isd(image, verbose):
     isd = csmapi.Isd(image)
     plugins = csmapi.Plugin.getList()
     for plugin in plugins:
@@ -113,6 +117,34 @@ def create_csm(image, verbose=False):
                 for warning in warnings:
                     print(f'Warning in function {warning.getFunction()}: "{warning.getMessage()}"')
                 print('Failed!')
+    raise TypeError('NoneType is not a sensor model.')
+
+def create_csm(image, verbose=False):
+    """
+    Given an image file, create a Community Sensor Model.
+
+    Parameters
+    ----------
+    image : str
+            The image filename to create a CSM for
+    verbose : bool
+              Print information about which plugins and models were attempted
+
+    Returns
+    -------
+    model : object
+            A CSM sensor model (or None if no associated model is available.)
+    """
+    try:
+        return _from_isd(image, verbose=verbose)
+    except:
+        if verbose:
+            print('Unable to instantiate CSM from ISD')
+    try:
+        return _from_state(image, verbose=verbose)
+    except:
+        if verbose:
+            print('Unable to instantiate CSM from state file.')
 
 @singledispatch
 def generate_ground_point(dem, image_pt, camera):
@@ -551,3 +583,4 @@ def triangulate_ground_pt(cameras, image_pts):
         M[2] += look[2] * look - look_squared * unit_z
         b += np.dot(pos, look) * look - look_squared * pos
     return tuple(np.dot(np.linalg.inv(M), b))
+