code: def cached_workflow_property(func=None, attr=None, setter=True):
      
      def wrapper(func):
          _attr = attr or "_workflow_cached_" + func.__name__
  
          @functools.wraps(func)
          def getter(self):
              wf = self.as_workflow()
              if not hasattr(wf, _attr):
                  setattr(wf, _attr, func(wf))
              return getattr(wf, _attr)
  
          _setter = None
          if setter:
              def _setter(self, value):
                  wf = self.as_workflow()
                  setattr(wf, _attr, value)
  
              _setter.__name__ = func.__name__
  
          return property(fget=getter, fset=_setter)
  
      return wrapper if not func else wrapper(func)
docs: Decorator to declare an attribute that is stored only on a workflow and also cached for
      subsequent calls. Therefore, the decorated method is expected to (lazily) provide the value to
      cache. The resulting value is stored as ``_workflow_cached_<func.__name__>`` on the workflow,
      which can be overwritten by setting the *attr* argument. By default, a setter is provded to
      overwrite the cache value. Set *setter* to *False* to disable this feature. Example:
  
      .. code-block:: python
  
          class MyTask(Workflow):
  
              @cached_workflow_property
              def common_data(self):
                  # this method is always called with *self* is the *workflow*
                  return some_demanding_computation()
  
              @cached_workflow_property(attr="my_own_property", setter=False)
              def common_data2(self):
                  return some_other_computation()