diff --git a/README.rst b/README.rst
index 98caee18457157cd43cd53c4c9d17c5af08db2ec..cc9a2d3396da09ade14de1b25a7d076891053cbe 100644
--- a/README.rst
+++ b/README.rst
@@ -214,6 +214,53 @@ To configure parameter processors add the following snippet to your Ansible vari
           - 'customer_package.lti_processors:team_and_cohort'
           - 'example_package.lti_processors:extra_lti_params'
 
+Dynamic LTI Custom Parameters
+=============================
+
+This XBlock gives us the capability to attach static and dynamic custom parameters in the custom parameters field,
+in the case we need to declare a dynamic custom parameter we must set the value of the parameter as a templated parameter
+wrapped with the tags '${' and '}' just like the following example:
+
+.. code:: python
+
+    ["static_param=static_value", "dynamic_custom_param=${templated_param_value}"]
+
+Defining a dynamic LTI Custom Parameter Processor
+-------------------------------------------------
+
+The custom parameter processor is a function that expects an XBlock instance, and returns a ``string`` which should be the resolved value.
+Exceptions must be handled by the processor itself.
+
+.. code:: python
+
+    def get_course_name(xblock):
+        try:
+            course = CourseOverview.objects.get(id=xblock.course.id)
+        except CourseOverview.DoesNotExist:
+            log.error('Course does not exist.')
+            return ''
+
+        return course.display_name
+
+Note. The processor function must return a ``string`` object.
+
+Configuring the LTI Dynamic Custom Parameters Settings
+------------------------------------------------------
+
+The setting LTI_CUSTOM_PARAM_TEMPLATES must be set in order to map the template value for the dynamic custom parameter
+as the following example:
+
+.. code:: python
+
+    LTI_CUSTOM_PARAM_TEMPLATES = {
+        'templated_param_value': 'customer_package.module:func',
+    }
+
+* 'templated_param_value': custom parameter template name.
+* 'customer_package.module:func': custom parameter processor path and function name.
+
+
+
 LTI Advantage Features
 ======================
 
@@ -320,6 +367,11 @@ Changelog
 
 Please See the [releases tab](https://github.com/edx/xblock-lti-consumer/releases) for the complete changelog.
 
+3.2.0 - 2022-01-18
+-------------------
+
+* Dynamic custom parameters support with the help of template parameter processors.
+
 3.1.2 - 2021-11-12
 -------------------
 
diff --git a/lti_consumer/__init__.py b/lti_consumer/__init__.py
index dc833d9ddcc8acf5cf6a8404c9a2cb3efeb7792c..0ddc23dac6db7e92c29851b3cf718e24dfd14ef5 100644
--- a/lti_consumer/__init__.py
+++ b/lti_consumer/__init__.py
@@ -4,4 +4,4 @@ Runtime will load the XBlock class from here.
 from .lti_xblock import LtiConsumerXBlock
 from .apps import LTIConsumerApp
 
-__version__ = '3.1.2'
+__version__ = '3.2.0'
diff --git a/lti_consumer/lti_xblock.py b/lti_consumer/lti_xblock.py
index 6d6cd54e9009d28a408a6a2a7e6c35562a70037f..4ca545f949eb4022db038b450ac27e65898a03d3 100644
--- a/lti_consumer/lti_xblock.py
+++ b/lti_consumer/lti_xblock.py
@@ -81,7 +81,7 @@ from .lti_1p3.exceptions import (
 )
 from .lti_1p3.constants import LTI_1P3_CONTEXT_TYPE
 from .outcomes import OutcomeService
-from .utils import _
+from .utils import _, resolve_custom_parameter_template
 
 
 log = logging.getLogger(__name__)
@@ -100,6 +100,7 @@ ROLE_MAP = {
     'staff': 'Administrator',
     'instructor': 'Instructor',
 }
+CUSTOM_PARAMETER_TEMPLATE_TAGS = ('${', '}')
 
 
 def parse_handler_suffix(suffix):
@@ -819,6 +820,10 @@ class LtiConsumerXBlock(StudioEditableXBlockMixin, XBlock):
                 if param_name not in LTI_PARAMETERS:
                     param_name = 'custom_' + param_name
 
+                if (param_value.startswith(CUSTOM_PARAMETER_TEMPLATE_TAGS[0]) and
+                        param_value.endswith(CUSTOM_PARAMETER_TEMPLATE_TAGS[1])):
+                    param_value = resolve_custom_parameter_template(self, param_value)
+
                 custom_parameters[param_name] = param_value
 
         custom_parameters['custom_component_display_name'] = str(self.display_name)
diff --git a/lti_consumer/tests/unit/test_lti_xblock.py b/lti_consumer/tests/unit/test_lti_xblock.py
index 1a0eeae5b5403055f04f5aaa34ea010a71277846..d9bc4bf93e5b1f08c073e0388832586707fa4d90 100644
--- a/lti_consumer/tests/unit/test_lti_xblock.py
+++ b/lti_consumer/tests/unit/test_lti_xblock.py
@@ -3,12 +3,14 @@ Unit tests for LtiConsumerXBlock
 """
 
 import json
+import logging
 import urllib.parse
 from datetime import timedelta
 from unittest.mock import Mock, NonCallableMock, PropertyMock, patch
 
 import ddt
 from Cryptodome.PublicKey import RSA
+from django.conf import settings as dj_settings
 from django.test.testcases import TestCase
 from django.utils import timezone
 from jwkest.jwk import RSAKey
@@ -19,6 +21,7 @@ from lti_consumer.lti_1p3.tests.utils import create_jwt
 from lti_consumer.lti_xblock import LtiConsumerXBlock, parse_handler_suffix
 from lti_consumer.tests.unit import test_utils
 from lti_consumer.tests.unit.test_utils import FAKE_USER_ID, make_request, make_xblock
+from lti_consumer.utils import resolve_custom_parameter_template
 
 HTML_PROBLEM_PROGRESS = '<div class="problem-progress">'
 HTML_ERROR_MESSAGE = '<h3 class="error_message">'
@@ -301,6 +304,30 @@ class TestProperties(TestLtiConsumerXBlock):
 
         self.assertEqual(params, expected_params)
 
+    @patch('lti_consumer.lti_xblock.resolve_custom_parameter_template')
+    def test_templated_custom_parameters(self, mock_resolve_custom_parameter_template):
+        """
+        Test `prefixed_custom_parameters` when a custom parameter with templated value has been provided.
+        """
+        now = timezone.now()
+        one_day = timedelta(days=1)
+        self.xblock.due = now
+        self.xblock.graceperiod = one_day
+        self.xblock.custom_parameters = ['dynamic_param_1=${template_value}', 'param_2=false']
+        mock_resolve_custom_parameter_template.return_value = 'resolved_template_value'
+        expected_params = {
+            'custom_component_display_name': self.xblock.display_name,
+            'custom_component_due_date': now.strftime('%Y-%m-%d %H:%M:%S'),
+            'custom_component_graceperiod': str(one_day.total_seconds()),
+            'custom_dynamic_param_1': 'resolved_template_value',
+            'custom_param_2': 'false',
+        }
+
+        params = self.xblock.prefixed_custom_parameters
+
+        self.assertEqual(params, expected_params)
+        mock_resolve_custom_parameter_template.assert_called_once_with(self.xblock, '${template_value}')
+
     def test_invalid_custom_parameter(self):
         """
         Test `prefixed_custom_parameters` when a custom parameter has been configured with the wrong format
@@ -1532,3 +1559,77 @@ class TestLti1p3AccessTokenEndpoint(TestLtiConsumerXBlock):
 
         response = self.xblock.lti_1p3_access_token(request)
         self.assertEqual(response.status_code, 200)
+
+
+@patch('lti_consumer.utils.log')
+@patch('lti_consumer.utils.import_module')
+class TestDynamicCustomParametersResolver(TestLtiConsumerXBlock):
+    """
+    Unit tests for lti_xblock utils resolve_custom_parameter_template method.
+    """
+
+    def setUp(self):
+        super().setUp()
+
+        self.logger = logging.getLogger()
+        dj_settings.LTI_CUSTOM_PARAM_TEMPLATES = {
+            'templated_param_value': 'customer_package.module:func',
+        }
+        self.mock_processor_module = Mock(func=Mock())
+
+    def test_successful_resolve_custom_parameter_template(self, mock_import_module, *_):
+        """
+        Test a successful module import and execution. The template value to be resolved
+        should be replaced by the processor.
+        """
+
+        custom_parameter_template_value = '${templated_param_value}'
+        expected_resolved_value = 'resolved_value'
+        mock_import_module.return_value = self.mock_processor_module
+        self.mock_processor_module.func.return_value = expected_resolved_value
+
+        resolved_value = resolve_custom_parameter_template(self.xblock, custom_parameter_template_value)
+
+        mock_import_module.assert_called_once()
+        self.assertEqual(resolved_value, expected_resolved_value)
+
+    def test_resolve_custom_parameter_template_with_invalid_data_type_returned(self, mock_import_module, mock_log):
+        """
+        Test a successful module import and execution. The value returned by the processor should be a string object.
+        Otherwise, it should log an error.
+        """
+
+        custom_parameter_template_value = '${templated_param_value}'
+        mock_import_module.return_value = self.mock_processor_module
+        self.mock_processor_module.func.return_value = 1
+
+        resolved_value = resolve_custom_parameter_template(self.xblock, custom_parameter_template_value)
+
+        self.assertEqual(resolved_value, custom_parameter_template_value)
+        assert mock_log.error.called
+
+    def test_resolve_custom_parameter_template_with_invalid_module(self, mock_import_module, mock_log):
+        """
+        Test a failed import with an undefined module. This should log an error.
+        """
+        mock_import_module.side_effect = ModuleNotFoundError
+        custom_parameter_template_value = '${not_defined_parameter_template}'
+
+        resolved_value = resolve_custom_parameter_template(self.xblock, custom_parameter_template_value)
+
+        self.assertEqual(resolved_value, custom_parameter_template_value)
+        assert mock_log.error.called
+
+    def test_lti_custom_param_templates_not_configured(self, mock_import_module, mock_log):
+        """
+        Test the feature with LTI_CUSTOM_PARAM_TEMPLATES setting attribute not configured.
+        """
+        custom_parameter_template_value = '${templated_param_value}'
+
+        dj_settings.__delattr__('LTI_CUSTOM_PARAM_TEMPLATES')
+
+        resolved_value = resolve_custom_parameter_template(self.xblock, custom_parameter_template_value)
+
+        self.assertEqual(resolved_value, custom_parameter_template_value)
+        assert mock_log.error.called
+        mock_import_module.asser_not_called()
diff --git a/lti_consumer/utils.py b/lti_consumer/utils.py
index e81f177a31e48402c10c85447f80cac59d5a3491..52a63559dd4a508a0b65389200c99befc0224035 100644
--- a/lti_consumer/utils.py
+++ b/lti_consumer/utils.py
@@ -1,8 +1,13 @@
 """
 Utility functions for LTI Consumer block
 """
+import logging
+from importlib import import_module
+
 from django.conf import settings
 
+log = logging.getLogger(__name__)
+
 
 def _(text):
     """
@@ -113,3 +118,37 @@ def get_lti_nrps_context_membership_url(lti_config_id):
         lms_base=get_lms_base(),
         lti_config_id=str(lti_config_id),
     )
+
+
+def resolve_custom_parameter_template(xblock, template):
+    """
+    Return the value processed according to the template processor.
+    The template processor must return a string object.
+
+    :param xblock: LTI consumer xblock.
+    :param template: processor key.
+    """
+    try:
+        module_name, func_name = settings.LTI_CUSTOM_PARAM_TEMPLATES.get(
+            template[2:len(template) - 1],
+            ':',
+        ).split(':', 1)
+        template_value = getattr(
+            import_module(module_name),
+            func_name,
+        )(xblock)
+
+        if not isinstance(template_value, str):
+            log.error('The \'%s\' processor must return a string object.', func_name)
+            return template
+    except ValueError:
+        log.error(
+            'Error while processing \'%s\' value. Reason: The template processor definition must be wrong.',
+            template,
+        )
+        return template
+    except (AttributeError, ModuleNotFoundError) as ex:
+        log.error('Error while processing \'%s\' value. Reason: %s', template, str(ex))
+        return template
+
+    return template_value