diff --git a/src/finn/analysis/verify_custom_nodes.py b/src/finn/analysis/verify_custom_nodes.py
new file mode 100644
index 0000000000000000000000000000000000000000..73d7ae590ac69226516ee1bf308ca6c2fbb41ce1
--- /dev/null
+++ b/src/finn/analysis/verify_custom_nodes.py
@@ -0,0 +1,16 @@
+import finn.custom_op.registry as registry
+
+
+def verify_nodes(model):
+    """Checks if custom ops in graph are correctly built, with all attributes
+    and inputs. Returns {node op_type : info_messages}
+    *info_messages is list of strings about the result of the verification"""
+
+    verification_dict = {}
+    for node in model.graph.node:
+        if node.domain == "finn":
+            op_type = node.op_type
+            inst = registry.custom_op[op_type](node)
+            verification_dict[op_type] = inst.verify_node()
+
+    return verification_dict
diff --git a/src/finn/custom_op/__init__.py b/src/finn/custom_op/__init__.py
index 519d48e97075c6e6be57d187720269ac08312028..797916cf20ce9c61cc349df41383b687ed0c101b 100644
--- a/src/finn/custom_op/__init__.py
+++ b/src/finn/custom_op/__init__.py
@@ -78,3 +78,10 @@ class CustomOp(ABC):
         """Execute this CustomOp instance, given the execution context and
         ONNX graph."""
         pass
+
+    @abstractmethod
+    def verify_node(self):
+        """Verifies that all attributes the node needs are there and
+        that particular attributes are set correctly. Also checks if
+        the number of inputs is equal to the expected number"""
+        pass
diff --git a/src/finn/custom_op/fpgadataflow/__init__.py b/src/finn/custom_op/fpgadataflow/__init__.py
index 0160c5f02381b1af55182f168a0df7b202635266..6a37519d372687fbd21fea2c326f55531b6f1eea 100644
--- a/src/finn/custom_op/fpgadataflow/__init__.py
+++ b/src/finn/custom_op/fpgadataflow/__init__.py
@@ -40,7 +40,11 @@ class HLSCustomOp(CustomOp):
         self.code_gen_dict = {}
 
     def get_nodeattr_types(self):
-        return {"code_gen_dir": ("s", False, ""), "executable_path": ("s", False, "")}
+        return {
+            "backend": ("s", True, "fpgadataflow"),
+            "code_gen_dir": ("s", False, ""),
+            "executable_path": ("s", False, ""),
+        }
 
     def code_generation(self, model):
         node = self.onnx_node
diff --git a/src/finn/custom_op/fpgadataflow/streamingfclayer_batch.py b/src/finn/custom_op/fpgadataflow/streamingfclayer_batch.py
index ce5a0fb3a18eb16952f98362ffffa0e0adf7e3e4..975da666f0ba728f542b06865aaa2c66c5f07c07 100644
--- a/src/finn/custom_op/fpgadataflow/streamingfclayer_batch.py
+++ b/src/finn/custom_op/fpgadataflow/streamingfclayer_batch.py
@@ -14,6 +14,9 @@ class StreamingFCLayer_Batch(HLSCustomOp):
 
     def get_nodeattr_types(self):
         my_attrs = {
+            # "backend": ("s", True, "fpgadataflow"),
+            # "code_gen_dir": ("s", True, ""),
+            # "executable_path": ("s", True, ""),
             "PE": ("i", True, 0),
             "SIMD": ("i", True, 0),
             "MW": ("i", True, 0),
@@ -57,6 +60,90 @@ class StreamingFCLayer_Batch(HLSCustomOp):
     def infer_node_datatype(self, model):
         pass
 
+    def verify_node(self):
+        info_messages = []
+
+        # verify number of attributes
+        num_of_attr = 14
+        if len(self.onnx_node.attribute) == num_of_attr:
+            info_messages.append("The number of attributes is correct")
+        else:
+            info_messages.append(
+                """The number of attributes is incorrect,
+            {} should have {} attributes""".format(
+                    self.onnx_node.op_type, num_of_attr
+                )
+            )
+
+        # verify that "domain" is set to "finn"
+        domain_value = self.onnx_node.domain
+        if domain_value == "finn":
+            info_messages.append("Attribute domain is set correctly")
+        else:
+            info_messages.append('Attribute domain should be set to "finn"')
+
+        # verify that "backend" is set to "fpgadataflow"
+        backend_value = self.get_nodeattr("backend")
+        if backend_value == "fpgadataflow":
+            info_messages.append("Attribute backend is set correctly")
+        else:
+            info_messages.append('Attribute backend should be set to "fpgadataflow"')
+
+        # verify that all necessary attributes exist
+        try:
+            self.get_nodeattr("code_gen_dir")
+            self.get_nodeattr("executable_path")
+            self.get_nodeattr("resType")
+            self.get_nodeattr("MW")
+            self.get_nodeattr("MH")
+            self.get_nodeattr("SIMD")
+            self.get_nodeattr("PE")
+            self.get_nodeattr("inputDataType")
+            self.get_nodeattr("weightDataType")
+            self.get_nodeattr("outputDataType")
+            self.get_nodeattr("ActVal")
+            self.get_nodeattr("binaryXnorMode")
+            self.get_nodeattr("noActivation")
+            info_messages.append("All necessary attributes exist")
+        except Exception:
+            info_messages.append(
+                """The necessary attributes do not exist.
+                StreamingFCLayer_Batch needs the following attributes:
+                code_gen_dir, executable_path, resType, MW, MH, SIMD, PE,
+                inputDataType, weightDataType, outputDataType, ActVal,
+                binaryXnorMode, noActivation"""
+            )
+
+        # verify the number of inputs depending on noActivation value
+        # check noActivation value to determine the number of inputs
+        no_act = self.get_nodeattr("noActivation")
+
+        if no_act == 1:
+            if len(self.onnx_node.input) == 2:
+                info_messages.append("The number of inputs is correct")
+            else:
+                info_messages.append(
+                    """StreamingFCLayer_Batch needs in no
+                            activation mode 2 inputs (data input and weights)"""
+                )
+        elif no_act == 0:
+            if len(self.onnx_node.input) == 3:
+                info_messages.append("The number of inputs is correct")
+            else:
+                info_messages.append(
+                    """StreamingFCLayer_Batch needs 3 inputs
+                            (data input and weights and threshold values)"""
+                )
+        else:
+            info_messages.append(
+                """noActivation attribute contains {} should
+                be 0 or 1""".format(
+                    no_act
+                )
+            )
+
+        return info_messages
+
     def get_input_datatype(self):
         return DataType[self.get_nodeattr("inputDataType")]
 
diff --git a/src/finn/custom_op/fpgadataflow/streamingmaxpool_batch.py b/src/finn/custom_op/fpgadataflow/streamingmaxpool_batch.py
index 7be3b6cbc74713662713bd422ce79610c804a0e1..92f499b6771efe4455e7259a8eb62ab9c636cb1f 100644
--- a/src/finn/custom_op/fpgadataflow/streamingmaxpool_batch.py
+++ b/src/finn/custom_op/fpgadataflow/streamingmaxpool_batch.py
@@ -4,6 +4,9 @@ from finn.custom_op.fpgadataflow import HLSCustomOp
 class StreamingMaxPool_Batch(HLSCustomOp):
     def get_nodeattr_types(self):
         my_attrs = {
+            # "backend": ("s", True, "fpgadataflow"),
+            # "code_gen_dir": ("s", True, ""),
+            # "executable_path": ("s", True, ""),
             "ImgDim": ("i", True, 0),
             "PoolDim": ("i", True, 0),
             "NumChannels": ("i", True, 0),
@@ -17,6 +20,58 @@ class StreamingMaxPool_Batch(HLSCustomOp):
     def infer_node_datatype(self, model):
         pass
 
+    def verify_node(self):
+        info_messages = []
+
+        # verify number of attributes
+        num_of_attr = 6
+        if len(self.onnx_node.attribute) == num_of_attr:
+            info_messages.append("The number of attributes is correct")
+        else:
+            info_messages.append(
+                """The number of attributes is incorrect,
+            {} should have {} attributes""".format(
+                    self.onnx_node.op_type, num_of_attr
+                )
+            )
+
+        # verify that "domain" is set to "finn"
+        domain_value = self.onnx_node.domain
+        if domain_value == "finn":
+            info_messages.append("Attribute domain is set correctly")
+        else:
+            info_messages.append('Attribute domain should be set to "finn"')
+
+        # verify that "backend" is set to "fpgadataflow"
+        backend_value = self.get_nodeattr("backend")
+        if backend_value == "fpgadataflow":
+            info_messages.append("Attribute backend is set correctly")
+        else:
+            info_messages.append('Attribute backend should be set to "fpgadataflow"')
+
+        # verify that all necessary attributes exist
+        try:
+            self.get_nodeattr("code_gen_dir")
+            self.get_nodeattr("executable_path")
+            self.get_nodeattr("ImgDim")
+            self.get_nodeattr("PoolDim")
+            self.get_nodeattr("NumChannels")
+            info_messages.append("All necessary attributes exist")
+        except Exception:
+            info_messages.append(
+                """The necessary attributes do not exist.
+                StreamingMaxPool_Batch  needs the following attributes:
+                code_gen_dir, executable_path, ImgDim, PoolDim, NumChannels"""
+            )
+
+        # verify the number of inputs
+        if len(self.onnx_node.input) == 1:
+            info_messages.append("The number of inputs is correct")
+        else:
+            info_messages.append("""StreamingMaxPool_Batch needs 1 data input""")
+
+        return info_messages
+
     def global_includes(self):
         self.code_gen_dict["$GLOBALS$"] = ['#include "maxpool.h"']
 
diff --git a/src/finn/custom_op/multithreshold.py b/src/finn/custom_op/multithreshold.py
index 3cff87b3fa850a7bb35c39d028d359d3513cc80e..52cf2504b174b06df1ba0aa0bdac112fee872b91 100644
--- a/src/finn/custom_op/multithreshold.py
+++ b/src/finn/custom_op/multithreshold.py
@@ -85,3 +85,49 @@ class MultiThreshold(CustomOp):
         output = multithreshold(v, thresholds, out_scale, out_bias)
         # setting context according to output
         context[node.output[0]] = output
+
+    def verify_node(self):
+        info_messages = []
+
+        # verify number of attributes
+        num_of_attr = 3
+        if len(self.onnx_node.attribute) == num_of_attr:
+            info_messages.append("The number of attributes is correct")
+        else:
+            info_messages.append(
+                """The number of attributes is incorrect,
+            {} should have {} attributes""".format(
+                    self.onnx_node.op_type, num_of_attr
+                )
+            )
+
+        # verify that "domain" is set to "finn"
+        domain_value = self.onnx_node.domain
+        if domain_value == "finn":
+            info_messages.append("Attribute domain is set correctly")
+        else:
+            info_messages.append('Attribute domain should be set to "finn"')
+
+        # verify that all necessary attributes exist
+        try:
+            self.get_nodeattr("out_scale")
+            self.get_nodeattr("out_bias")
+            self.get_nodeattr("out_dtype")
+            info_messages.append("All necessary attributes exist")
+        except Exception:
+            info_messages.append(
+                """The necessary attributes do not exist.
+                MultiThreshold needs the following attributes:
+                out_scale, out_bias, out_dtype"""
+            )
+
+        # verify the number of inputs
+        if len(self.onnx_node.input) == 2:
+            info_messages.append("The number of inputs is correct")
+        else:
+            info_messages.append(
+                """MultiThreshold needs 2 inputs
+                    (data input and threshold values)"""
+            )
+
+        return info_messages
diff --git a/src/finn/custom_op/xnorpopcount.py b/src/finn/custom_op/xnorpopcount.py
index 8af7429a5d54d675e62b527cae6e2fdf55b09536..15ec57a002148a4e9213d28fe4f68c5a78837ab6 100644
--- a/src/finn/custom_op/xnorpopcount.py
+++ b/src/finn/custom_op/xnorpopcount.py
@@ -57,3 +57,36 @@ class XnorPopcountMatMul(CustomOp):
         output = xnorpopcountmatmul(inp0, inp1)
         # set context according to output name
         context[node.output[0]] = output
+
+    def verify_node(self):
+        info_messages = []
+
+        # verify number of attributes
+        num_of_attr = 0
+        if len(self.onnx_node.attribute) == num_of_attr:
+            info_messages.append("The number of attributes is correct")
+        else:
+            info_messages.append(
+                """The number of attributes is incorrect,
+            {} should have {} attributes""".format(
+                    self.onnx_node.op_type, num_of_attr
+                )
+            )
+
+        # verify that "domain" is set to "finn"
+        domain_value = self.onnx_node.domain
+        if domain_value == "finn":
+            info_messages.append("Attribute domain is set correctly")
+        else:
+            info_messages.append('Attribute domain should be set to "finn"')
+
+        # verify that all necessary attributes exist
+        info_messages.append("XnorPopcountMatMul should not have any attributes")
+
+        # verify the number of inputs
+        if len(self.onnx_node.input) == 2:
+            info_messages.append("The number of inputs is correct")
+        else:
+            info_messages.append("XnorPopcountMatMul needs 2 data inputs")
+
+        return info_messages
diff --git a/tests/test_verify_custom_nodes.py b/tests/test_verify_custom_nodes.py
new file mode 100644
index 0000000000000000000000000000000000000000..9146f9d30853e6c7a47436c100166a4fa9127e87
--- /dev/null
+++ b/tests/test_verify_custom_nodes.py
@@ -0,0 +1,156 @@
+from onnx import TensorProto, helper
+
+from finn.analysis.verify_custom_nodes import verify_nodes
+from finn.core.modelwrapper import ModelWrapper
+
+
+def check_two_dict_for_equality(dict1, dict2):
+    for key in dict1:
+        assert key in dict2, "Key: {} is not in both dictionaries".format(key)
+        assert (
+            dict1[key] == dict2[key]
+        ), """Values for key {} are not the same
+        in both dictionaries""".format(
+            key
+        )
+
+    return True
+
+
+def test_verify_custom_nodes():
+    inp = helper.make_tensor_value_info("inp", TensorProto.FLOAT, [1, 13, 64])
+    outp = helper.make_tensor_value_info("outp", TensorProto.FLOAT, [1, 1, 64])
+
+    # MultiThreshold
+    m_node = helper.make_node(
+        "MultiThreshold",
+        ["xnor_out", "threshs"],
+        ["outp"],
+        domain="finn",
+        out_scale=2.0,
+        out_bias=-1.0,
+        out_dtype="",
+    )
+
+    # XnorPopcountMatMul
+    xnor_node = helper.make_node(
+        "XnorPopcountMatMul",
+        ["fclayer_out0", "fclayer_out1"],
+        ["xnor_out"],
+        domain="finn",
+    )
+
+    # StreamingMaxPool_Batch
+    MaxPool_batch_node = helper.make_node(
+        "StreamingMaxPool_Batch",
+        ["inp"],
+        ["max_out"],
+        domain="finn",
+        backend="fpgadataflow",
+        code_gen_dir="",
+        executable_path="",
+        ImgDim=4,
+        PoolDim=2,
+        NumChannels=2,
+    )
+
+    # StreamingFCLayer_Batch - no activation
+    FCLayer0_node = helper.make_node(
+        "StreamingFCLayer_Batch",
+        ["max_out", "weights"],
+        ["fclayer_out0"],
+        domain="finn",
+        backend="fpgadataflow",
+        code_gen_dir="",
+        executable_path="",
+        resType="ap_resource_lut()",
+        MW=8,
+        MH=8,
+        SIMD=4,
+        PE=4,
+        inputDataType="<FINN DataType>",
+        weightDataType="<FINN DataType>",
+        outputDataType="<FINN DataType>",
+        ActVal=0,
+        binaryXnorMode=1,
+        noActivation=1,
+    )
+
+    # StreamingFCLayer_Batch - with activation
+    FCLayer1_node = helper.make_node(
+        "StreamingFCLayer_Batch",
+        ["fclayer_out0", "weights", "threshs"],
+        ["fclayer_out1"],
+        domain="finn",
+        backend="fpgadataflow",
+        code_gen_dir="",
+        executable_path="",
+        resType="ap_resource_lut()",
+        MW=8,
+        MH=8,
+        SIMD=4,
+        PE=4,
+        inputDataType="<FINN DataType>",
+        weightDataType="<FINN DataType>",
+        outputDataType="<FINN DataType>",
+        ActVal=0,
+        binaryXnorMode=1,
+        noActivation=0,
+    )
+
+    graph = helper.make_graph(
+        nodes=[MaxPool_batch_node, FCLayer0_node, FCLayer1_node, xnor_node, m_node],
+        name="custom_op_graph",
+        inputs=[inp],
+        outputs=[outp],
+        value_info=[
+            helper.make_tensor_value_info("max_out", TensorProto.FLOAT, [1, 13, 64]),
+            helper.make_tensor_value_info("weights", TensorProto.FLOAT, [64, 32, 416]),
+            helper.make_tensor_value_info("threshs", TensorProto.FLOAT, [32, 32, 16]),
+            helper.make_tensor_value_info("xnor_out", TensorProto.FLOAT, [1, 32, 32]),
+            helper.make_tensor_value_info(
+                "fclayer_out0", TensorProto.FLOAT, [1, 32, 32]
+            ),
+            helper.make_tensor_value_info(
+                "fclayer_out1", TensorProto.FLOAT, [32, 64, 512]
+            ),
+        ],
+    )
+    model = helper.make_model(graph, producer_name="custom-op-model")
+    model = ModelWrapper(model)
+
+    produced = model.analysis(verify_nodes)
+
+    expected = {
+        "StreamingMaxPool_Batch": [
+            "The number of attributes is correct",
+            "Attribute domain is set correctly",
+            "Attribute backend is set correctly",
+            "All necessary attributes exist",
+            "The number of inputs is correct",
+        ],
+        "StreamingFCLayer_Batch": [
+            "The number of attributes is correct",
+            "Attribute domain is set correctly",
+            "Attribute backend is set correctly",
+            "All necessary attributes exist",
+            "The number of inputs is correct",
+        ],
+        "XnorPopcountMatMul": [
+            "The number of attributes is correct",
+            "Attribute domain is set correctly",
+            "XnorPopcountMatMul should not have any attributes",
+            "The number of inputs is correct",
+        ],
+        "MultiThreshold": [
+            "The number of attributes is correct",
+            "Attribute domain is set correctly",
+            "All necessary attributes exist",
+            "The number of inputs is correct",
+        ],
+    }
+
+    assert check_two_dict_for_equality(
+        produced, expected
+    ), """The produced output of
+    the verification analysis pass is not equal to the expected one"""