diff --git a/synapse/cores/common.py b/synapse/cores/common.py
index 69634180c..47202a198 100644
--- a/synapse/cores/common.py
+++ b/synapse/cores/common.py
@@ -2208,7 +2208,7 @@ class Cortex(EventBus, DataModel, Runtime, Configable, s_ingest.IngestApi):
             valu = inprops.get(name)
 
             prop = form + ':' + name
-            if not self._okSetProp(prop):
+            if not self.isSetPropOk(prop):
                 inprops.pop(name, None)
                 continue
 
@@ -2328,8 +2328,27 @@ class Cortex(EventBus, DataModel, Runtime, Configable, s_ingest.IngestApi):
 
         return node
 
-    def _okSetProp(self, prop):
-        # check for enforcement and validity of a full prop name
+    def isSetPropOk(self, prop):
+        '''
+        Check for enforcement and validity of a full prop name.
+
+        This can be used to determine if a property name may be set on a node,
+        given the data models currently loaded in a Cortex.
+
+        Args:
+            prop (str): Full property name to check.
+
+        Examples:
+            Check if a value is valid before calling a function.::
+
+                prop = 'foo:bar:baz'
+                if core.isSetPropOk(prop):
+                    doSomething(...)
+
+        Returns:
+            bool: True if the property can be set on the node; False if it cannot be set.
+        '''
+        #
         if not self.enforce:
             return True
 
@@ -2339,10 +2358,17 @@ class Cortex(EventBus, DataModel, Runtime, Configable, s_ingest.IngestApi):
         '''
         Set ( with de-duplication ) the given tufo props.
 
-        Example:
+        Args:
+            tufo ((str, dict)): The tufo to set properties on.
+            **props:  Properties to set on the tufo.
 
-            tufo = core.setTufoProps(tufo, woot='hehe', blah=10)
+        Examples:
+            ::
+
+                tufo = core.setTufoProps(tufo, woot='hehe', blah=10)
 
+        Returns:
+            ((str, dict)): The source tufo, with any updated properties.
         '''
         reqiden(tufo)
         # add tufo form prefix to props
@@ -2398,7 +2424,7 @@ class Cortex(EventBus, DataModel, Runtime, Configable, s_ingest.IngestApi):
         form = tufo[1].get('tufo:form')
         prop = form + ':' + prop
 
-        if not self._okSetProp(prop):
+        if not self.isSetPropOk(prop):
             return tufo
 
         return self._incTufoProp(tufo, prop, incval=incval)
diff --git a/synapse/lib/storm.py b/synapse/lib/storm.py
index 1254253f7..376aee35f 100644
--- a/synapse/lib/storm.py
+++ b/synapse/lib/storm.py
@@ -4,6 +4,7 @@ import re
 import time
 import fnmatch
 import logging
+import collections
 
 import synapse.common as s_common
 import synapse.compat as s_compat
@@ -1121,12 +1122,58 @@ class Runtime(Configable):
         # TODO: use edits here for requested delete
 
     def _stormOperSetProp(self, query, oper):
+        # Coverage of this function is affected by the following issue:
+        # https://bitbucket.org/ned/coveragepy/issues/198/continue-marked-as-not-covered
         args = oper[1].get('args')
         props = dict(oper[1].get('kwlist'))
 
         core = self.getStormCore()
 
-        [core.setTufoProps(node, **props) for node in query.data()]
+        formnodes = collections.defaultdict(list)
+        formprops = collections.defaultdict(dict)
+
+        for node in query.data():
+            formnodes[node[1].get('tufo:form')].append(node)
+
+        forms = tuple(formnodes.keys())
+
+        for prop, valu in props.items():
+
+            if prop.startswith(':'):
+                valid = False
+                _prop = prop[1:]
+                # Check against every lifted form, since we may have a relative prop
+                # Which is valid against
+                for form in forms:
+                    _fprop = form + prop
+                    if core.isSetPropOk(_fprop):
+                        formprops[form][_prop] = valu
+                        valid = True
+                if not valid:
+                    mesg = 'Relative prop is not valid on any lifted forms.'
+                    raise s_common.BadSyntaxError(name=prop, mesg=mesg)
+                continue  # pragma: no cover
+
+            if prop.startswith(forms):
+                valid = False
+                for form in forms:
+                    if prop.startswith(form + ':') and core.isSetPropOk(prop):
+                        _prop = prop[len(form) + 1:]
+                        formprops[form][_prop] = valu
+                        valid = True
+                        break
+                if not valid:
+                    mesg = 'Full prop is not valid on any lifted forms.'
+                    raise s_common.BadSyntaxError(name=prop, mesg=mesg)
+                continue  # pragma: no cover
+
+            mesg = 'setprop operator requires props to start with relative or full prop names.'
+            raise s_common.BadSyntaxError(name=prop, mesg=mesg)
+
+        for form, nodes in formnodes.items():
+            props = formprops.get(form)
+            if props:
+                [core.setTufoProps(node, **props) for node in nodes]
 
     def _iterPropTags(self, props, tags):
         for prop in props:
diff --git a/synapse/lib/syntax.py b/synapse/lib/syntax.py
index b0f8c2f49..e12e62122 100644
--- a/synapse/lib/syntax.py
+++ b/synapse/lib/syntax.py
@@ -611,7 +611,7 @@ def parse(text, off=0):
 
                 valu, off = parse_macro_valu(text, off + 1)
                 if prop[0] == ':':
-                    kwargs = {prop[1:]: valu}
+                    kwargs = {prop: valu}
                     ret.append(oper('setprop', **kwargs))
                     continue
 

setprop() should take both full props and rel props
```
setprop(foo:bar:baz=10)
- or -
setprop(:baz=10)
```
rather than
```
setprop(baz=10)
```
diff --git a/synapse/tests/test_lib_storm.py b/synapse/tests/test_lib_storm.py
index e4236c6b4..1cede3be2 100644
--- a/synapse/tests/test_lib_storm.py
+++ b/synapse/tests/test_lib_storm.py
@@ -44,13 +44,62 @@ class StormTest(SynTest):
         with s_cortex.openurl('ram:///') as core:
             core.setConfOpt('enforce', 1)
 
+            # relative key/val syntax, explicitly relative vals
+            node = core.formTufoByProp('inet:netuser', 'vertex.link/pennywise')
+            node = core.formTufoByProp('inet:netuser', 'vertex.link/visi')
+            node = core.eval('inet:netuser=vertex.link/pennywise setprop(:realname="Robert Gray")')[0]
+
+            self.eq(node[1].get('inet:netuser'), 'vertex.link/pennywise')
+            self.eq(node[1].get('inet:netuser:realname'), 'robert gray')
+
+            # Full prop val syntax
+            node = core.eval('inet:netuser=vertex.link/pennywise setprop(inet:netuser:signup="1970-01-01")')[0]
+            self.eq(node[1].get('inet:netuser'), 'vertex.link/pennywise')
+            self.eq(node[1].get('inet:netuser:signup'), 0)
+
+            # Combined syntax using both relative props and full props together
+            cmd = 'inet:netuser=vertex.link/pennywise setprop(:seen:min="2000", :seen:max="2017", ' \
+                  'inet:netuser:email=pennywise@vertex.link, inet:netuser:signup:ipv4="127.0.0.1")'
+            node = core.eval(cmd)[0]
+            self.nn(node[1].get('inet:netuser:seen:min'))
+            self.nn(node[1].get('inet:netuser:seen:max'))
+            self.nn(node[1].get('inet:netuser:signup:ipv4'))
+            self.eq(node[1].get('inet:netuser:email'), 'pennywise@vertex.link')
+
+            # old / bad syntax fails
+            # kwlist key/val syntax is no longer valid in setprop()
             node = core.formTufoByProp('inet:fqdn', 'vertex.link')
-
-            node = core.eval('inet:fqdn=vertex.link setprop(created="2016-05-05",updated="2017/05/05")')[0]
-
-            self.eq(node[1].get('inet:fqdn'), 'vertex.link')
-            self.eq(node[1].get('inet:fqdn:created'), 1462406400000)
-            self.eq(node[1].get('inet:fqdn:updated'), 1493942400000)
+            bad_cmd = 'inet:fqdn=vertex.link setprop(created="2016-05-05",updated="2017/05/05")'
+            self.raises(BadSyntaxError, core.eval, bad_cmd)
+            # a full prop which isn't valid for the node is bad
+            bad_cmd = 'inet:fqdn=vertex.link setprop(inet:fqdn:typocreated="2016-05-05")'
+            self.raises(BadSyntaxError, core.eval, bad_cmd)
+            # a rel prop which isn't valid for the node is bad
+            bad_cmd = 'inet:fqdn=vertex.link setprop(:typocreated="2016-05-05")'
+            self.raises(BadSyntaxError, core.eval, bad_cmd)
+
+            # test  possible form confusion
+            modl = {
+                'types': (
+                    ('foo:bar', {'subof': 'str'}),
+                    ('foo:barbaz', {'subof': 'str'})
+                ),
+                'forms': (
+                    ('foo:bar', {'ptype': 'str'}, [
+                        ('blah', {'ptype': 'str'})
+                    ]),
+                    ('foo:barbaz', {'ptype': 'str'}, [
+                        ('blah', {'ptype': 'str'})
+                    ]),
+                )
+            }
+            core.addDataModel('form_confusion', modl)
+            node = core.formTufoByProp('foo:bar', 'hehe')
+            core.addTufoTag(node, 'confusion')
+            node = core.formTufoByProp('foo:barbaz', 'haha')
+            core.addTufoTag(node, 'confusion')
+            node = core.eval('''#confusion setprop(foo:barbaz:blah=duck) +foo:barbaz''')[0]
+            self.eq(node[1].get('foo:barbaz:blah'), 'duck')
 
     def test_storm_filt_regex(self):
 

pip install -e .
parse_log_pytest
tornado>=3.2.2 cryptography>=1.7.2 pyOpenSSL>=16.2.0 msgpack-python>=0.4.2 xxhash>=1.0.1 lmdb>=0.92
pytest
apt-get update apt-get install -y gcc
3.6
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
attrs==22.2.0
certifi==2021.5.30
cffi @ file:///tmp/build/80754af9/cffi_1625814693874/work
cryptography @ file:///tmp/build/80754af9/cryptography_1635366128178/work
importlib-metadata==4.8.3
iniconfig==1.1.1
lmdb==1.6.2
msgpack @ file:///tmp/build/80754af9/msgpack-python_1612287171716/work
msgpack-python==0.5.6
packaging==21.3
pluggy==1.0.0
py==1.11.0
pycparser @ file:///tmp/build/80754af9/pycparser_1636541352034/work
pyOpenSSL @ file:///opt/conda/conda-bld/pyopenssl_1643788558760/work
pyparsing==3.1.4
pytest==7.0.1
-e git+https://github.com/vertexproject/synapse.git@2cf04d94d938409b246e89d1aecefe8ed0feb577#egg=synapse
tomli==1.2.3
tornado @ file:///tmp/build/80754af9/tornado_1606942266872/work
typing_extensions==4.1.1
xxhash==3.2.0
zipp==3.6.0

name: synapse
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2021.5.30=py36h06a4308_0
  - cffi=1.14.6=py36h400218f_0
  - cryptography=35.0.0=py36hd23ed53_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.3=he6710b0_2
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=1.1.1w=h7f8727e_0
  - pip=21.2.2=py36h06a4308_0
  - pycparser=2.21=pyhd3eb1b0_0
  - pyopenssl=22.0.0=pyhd3eb1b0_0
  - python=3.6.13=h12debd9_1
  - readline=8.2=h5eee18b_0
  - setuptools=58.0.4=py36h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tornado=6.1=py36h27cfd23_0
  - wheel=0.37.1=pyhd3eb1b0_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - attrs==22.2.0
      - importlib-metadata==4.8.3
      - iniconfig==1.1.1
      - lmdb==1.6.2
      - msgpack-python==0.5.6
      - packaging==21.3
      - pluggy==1.0.0
      - py==1.11.0
      - pyparsing==3.1.4
      - pytest==7.0.1
      - tomli==1.2.3
      - typing-extensions==4.1.1
      - xxhash==3.2.0
      - zipp==3.6.0
prefix: /opt/conda/envs/synapse

synapse/tests/test_lib_storm.py::StormTest::test_storm_setprop
synapse/tests/test_lib_storm.py::StormTest::test_storm_addnode synapse/tests/test_lib_storm.py::StormTest::test_storm_addtag synapse/tests/test_lib_storm.py::StormTest::test_storm_alltag synapse/tests/test_lib_storm.py::StormTest::test_storm_cmpr_norm synapse/tests/test_lib_storm.py::StormTest::test_storm_delnode synapse/tests/test_lib_storm.py::StormTest::test_storm_delnode_caching synapse/tests/test_lib_storm.py::StormTest::test_storm_deltag synapse/tests/test_lib_storm.py::StormTest::test_storm_edit_end synapse/tests/test_lib_storm.py::StormTest::test_storm_editmode synapse/tests/test_lib_storm.py::StormTest::test_storm_filt_regex synapse/tests/test_lib_storm.py::StormTest::test_storm_lift synapse/tests/test_lib_storm.py::StormTest::test_storm_lifts_by synapse/tests/test_lib_storm.py::StormTest::test_storm_pivot synapse/tests/test_lib_storm.py::StormTest::test_storm_refs synapse/tests/test_lib_storm.py::StormTest::test_storm_show_help synapse/tests/test_lib_storm.py::StormTest::test_storm_tag_fromtag synapse/tests/test_lib_storm.py::StormTest::test_storm_tag_glob synapse/tests/test_lib_storm.py::StormTest::test_storm_tag_ival synapse/tests/test_lib_storm.py::StormTest::test_storm_tag_jointag synapse/tests/test_lib_storm.py::StormTest::test_storm_tag_query synapse/tests/test_lib_storm.py::StormTest::test_storm_tag_totag synapse/tests/test_lib_storm.py::LimitTest::test_limit_behavior synapse/tests/test_lib_storm.py::LimitTest::test_limit_behavior_negatives synapse/tests/test_lib_storm.py::LimitTest::test_limit_default
synapse/lib/syntax.py synapse/cores/common.py synapse/lib/storm.py
synapse/lib/syntax.py synapse/cores/common.py synapse/lib/storm.py