diff --git a/pype/ftrack/events/action_sync_hier_attrs.py b/pype/ftrack/events/action_sync_hier_attrs.py
new file mode 100644
index 0000000000..7fa024edf4
--- /dev/null
+++ b/pype/ftrack/events/action_sync_hier_attrs.py
@@ -0,0 +1,274 @@
+import os
+import sys
+import json
+import argparse
+import logging
+import collections
+
+from pypeapp import config
+from pype.vendor import ftrack_api
+from pype.ftrack import BaseAction, lib
+from avalon.tools.libraryloader.io_nonsingleton import DbConnector
+from bson.objectid import ObjectId
+
+
+class SyncHierarchicalAttrs(BaseAction):
+
+ db_con = DbConnector()
+ ca_mongoid = lib.get_ca_mongoid()
+
+ #: Action identifier.
+ identifier = 'sync.hierarchical.attrs'
+ #: Action label.
+ label = 'Sync HierAttrs'
+ #: Action description.
+ description = 'Synchronize hierarchical attributes'
+ #: Icon
+ icon = '{}/ftrack/action_icons/SyncHierarchicalAttrs.svg'.format(
+ os.environ.get(
+ 'PYPE_STATICS_SERVER',
+ 'http://localhost:{}'.format(
+ config.get_presets().get('services', {}).get(
+ 'statics_server', {}
+ ).get('default_port', 8021)
+ )
+ )
+ )
+
+ def register(self):
+ self.session.event_hub.subscribe(
+ 'topic=ftrack.action.discover',
+ self._discover
+ )
+
+ self.session.event_hub.subscribe(
+ 'topic=ftrack.action.launch and data.actionIdentifier={}'.format(
+ self.identifier
+ ),
+ self._launch
+ )
+
+ def discover(self, session, entities, event):
+ ''' Validation '''
+ role_check = False
+ discover = False
+ role_list = ['Pypeclub', 'Administrator', 'Project Manager']
+ user = session.query(
+ 'User where id is "{}"'.format(event['source']['user']['id'])
+ ).one()
+
+ for role in user['user_security_roles']:
+ if role['security_role']['name'] in role_list:
+ role_check = True
+ break
+ print(self.icon)
+ if role_check is True:
+ for entity in entities:
+ context_type = entity.get('context_type', '').lower()
+ if (
+ context_type in ('show', 'task') and
+ entity.entity_type.lower() != 'task'
+ ):
+ discover = True
+ break
+
+ return discover
+
+ def launch(self, session, entities, event):
+ user = session.query(
+ 'User where id is "{}"'.format(event['source']['user']['id'])
+ ).one()
+
+ job = session.create('Job', {
+ 'user': user,
+ 'status': 'running',
+ 'data': json.dumps({
+ 'description': 'Sync Hierachical attributes'
+ })
+ })
+ session.commit()
+
+ try:
+ # Collect hierarchical attrs
+ custom_attributes = {}
+ all_avalon_attr = session.query(
+ 'CustomAttributeGroup where name is "avalon"'
+ ).one()
+ for cust_attr in all_avalon_attr['custom_attribute_configurations']:
+ if 'avalon_' in cust_attr['key']:
+ continue
+
+ if not cust_attr['is_hierarchical']:
+ continue
+
+ if cust_attr['default']:
+ self.log.warning((
+ 'Custom attribute "{}" has set default value.'
+ ' This attribute can\'t be synchronized'
+ ).format(cust_attr['label']))
+ continue
+
+ custom_attributes[cust_attr['key']] = cust_attr
+
+ if not custom_attributes:
+ msg = 'No hierarchical attributes to sync.'
+ self.log.debug(msg)
+ return {
+ 'success': True,
+ 'message': msg
+ }
+
+ entity = entities[0]
+ if entity.entity_type.lower() == 'project':
+ project_name = entity['full_name']
+ else:
+ project_name = entity['project']['full_name']
+
+ self.db_con.install()
+ self.db_con.Session['AVALON_PROJECT'] = project_name
+
+ for entity in entities:
+ for key in custom_attributes:
+ # check if entity has that attribute
+ if key not in entity['custom_attributes']:
+ self.log.debug(
+ 'Hierachical attribute "{}" not found on "{}"'.format(
+ key, entity.get('name', entity)
+ )
+ )
+ continue
+
+ value = self.get_hierarchical_value(key, entity)
+ if value is None:
+ self.log.warning(
+ 'Hierarchical attribute "{}" not set on "{}"'.format(
+ key, entity.get('name', entity)
+ )
+ )
+ continue
+
+ self.update_hierarchical_attribute(entity, key, value)
+
+ except Exception:
+ self.log.error(
+ 'Action "{}" failed'.format(self.label),
+ exc_info=True
+ )
+
+ finally:
+ self.db_con.uninstall()
+
+ if job['status'] in ('queued', 'running'):
+ job['status'] = 'failed'
+ session.commit()
+
+ return True
+
+ def get_hierarchical_value(self, key, entity):
+ value = entity['custom_attributes'][key]
+ if (
+ value is not None or
+ entity.entity_type.lower() == 'project'
+ ):
+ return value
+
+ return self.get_hierarchical_value(key, entity['parent'])
+
+ def update_hierarchical_attribute(self, entity, key, value):
+ if (
+ entity['context_type'].lower() not in ('show', 'task') or
+ entity.entity_type.lower() == 'task'
+ ):
+ return
+ # collect entity's custom attributes
+ custom_attributes = entity.get('custom_attributes')
+ if not custom_attributes:
+ return
+
+ mongoid = custom_attributes.get(self.ca_mongoid)
+ if not mongoid:
+ self.log.debug('Entity "{}" is not synchronized to avalon.'.format(
+ entity.get('name', entity)
+ ))
+ return
+
+ try:
+ mongoid = ObjectId(mongoid)
+ except Exception:
+ self.log.warning('Entity "{}" has stored invalid MongoID.'.format(
+ entity.get('name', entity)
+ ))
+ return
+ # Find entity in Mongo DB
+ mongo_entity = self.db_con.find_one({'_id': mongoid})
+ if not mongo_entity:
+ self.log.warning(
+ 'Entity "{}" is not synchronized to avalon.'.format(
+ entity.get('name', entity)
+ )
+ )
+ return
+
+ # Change value if entity has set it's own
+ entity_value = custom_attributes[key]
+ if entity_value is not None:
+ value = entity_value
+
+ data = mongo_entity.get('data') or {}
+
+ data[key] = value
+ self.db_con.update_many(
+ {'_id': mongoid},
+ {'$set': {'data': data}}
+ )
+
+ for child in entity.get('children', []):
+ self.update_hierarchical_attribute(child, key, value)
+
+
+def register(session, **kw):
+ '''Register plugin. Called when used as an plugin.'''
+
+ if not isinstance(session, ftrack_api.session.Session):
+ return
+
+ SyncHierarchicalAttrs(session).register()
+
+
+def main(arguments=None):
+ '''Set up logging and register action.'''
+ if arguments is None:
+ arguments = []
+
+ parser = argparse.ArgumentParser()
+ # Allow setting of logging level from arguments.
+ loggingLevels = {}
+ for level in (
+ logging.NOTSET, logging.DEBUG, logging.INFO, logging.WARNING,
+ logging.ERROR, logging.CRITICAL
+ ):
+ loggingLevels[logging.getLevelName(level).lower()] = level
+
+ parser.add_argument(
+ '-v', '--verbosity',
+ help='Set the logging output verbosity.',
+ choices=loggingLevels.keys(),
+ default='info'
+ )
+ namespace = parser.parse_args(arguments)
+
+ # Set up basic logging
+ logging.basicConfig(level=loggingLevels[namespace.verbosity])
+
+ session = ftrack_api.Session()
+ register(session)
+
+ # Wait for events
+ logging.info(
+ 'Registered actions and listening for events. Use Ctrl-C to abort.'
+ )
+ session.event_hub.wait()
+
+
+if __name__ == '__main__':
+ raise SystemExit(main(sys.argv[1:]))
diff --git a/res/ftrack/action_icons/SyncHierarchicalAttrs.svg b/res/ftrack/action_icons/SyncHierarchicalAttrs.svg
index 0c59189168..8b7953299f 100644
--- a/res/ftrack/action_icons/SyncHierarchicalAttrs.svg
+++ b/res/ftrack/action_icons/SyncHierarchicalAttrs.svg
@@ -1 +1,9 @@
-
\ No newline at end of file
+
+