You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@syncope.apache.org by il...@apache.org on 2022/10/10 12:16:02 UTC

[syncope] branch master updated: [SYNCOPE-1697] Command and Macro introduced (#378)

This is an automated email from the ASF dual-hosted git repository.

ilgrosso pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/syncope.git


The following commit(s) were added to refs/heads/master by this push:
     new 782cb9c147 [SYNCOPE-1697] Command and Macro introduced (#378)
782cb9c147 is described below

commit 782cb9c1479bc00a57cb9ef14757b930af4011ff
Author: Francesco Chicchiriccò <il...@users.noreply.github.com>
AuthorDate: Mon Oct 10 14:15:57 2022 +0200

    [SYNCOPE-1697] Command and Macro introduced (#378)
---
 .../authprofiles/AuthProfileDirectoryPanel.java    | 344 +++++++++++----------
 .../AuthProfileItemDirectoryPanel.java             |   8 +-
 .../clientapps/ClientAppDirectoryPanel.java        |  14 +-
 ...irectoryPanelAdditionalActionLinksProvider.java |  46 ++-
 .../console/panels/ResourceDirectoryPanel.java     |  19 +-
 .../console/status/AnyStatusDirectoryPanel.java    |  87 +++---
 .../client/console/status/AnyStatusModal.java      |   7 +-
 .../status/ResourceStatusDirectoryPanel.java       |  47 ++-
 .../client/console/status/ResourceStatusModal.java |   9 +-
 .../syncope/client/console/status/StatusModal.java |   9 +-
 .../client/console/topology/TabularTopology.java   |  28 +-
 .../console/topology/TopologyTogglePanel.java      |  36 +--
 .../console/topology/TopologyTogglePanel.html      |   3 -
 .../topology/TopologyTogglePanel.properties        |   2 -
 .../topology/TopologyTogglePanel_fr_CA.properties  |  17 +-
 .../topology/TopologyTogglePanel_it.properties     |   2 -
 .../topology/TopologyTogglePanel_ja.properties     |   2 -
 .../topology/TopologyTogglePanel_pt_BR.properties  |   2 -
 .../topology/TopologyTogglePanel_ru.properties     |   2 -
 .../client/console/audit/AuditHistoryDetails.java  |  38 +--
 .../client/console/audit/AuditHistoryModal.java    |   2 +-
 .../client/console/commons/IdRepoConstants.java    |   2 +
 .../commons/IdRepoImplementationInfoProvider.java  |   6 +-
 .../KeywordSearchEvent.java}                       |  27 +-
 .../commons/LinkedAccountPlainAttrProperty.java    |  14 +-
 .../console/notifications/NotificationTasks.java   |   5 +-
 .../syncope/client/console/pages/BasePage.java     |  89 +++---
 .../pages/{Reports.java => Engagements.java}       |  48 ++-
 .../syncope/client/console/pages/Reports.java      |   5 +-
 .../console/panels/AccessTokenDirectoryPanel.java  |   7 +-
 .../client/console/panels/AjaxDataTablePanel.java  |   7 +-
 .../console/panels/CommandDirectoryPanel.java      | 190 ++++++++++++
 .../{SchemasPanel.java => CommandsPanel.java}      |  42 +--
 .../console/panels/DashboardAccessTokensPanel.java |   8 +-
 .../client/console/panels/ExecMessageModal.java    |   1 -
 .../console/panels/ImplementationModalPanel.java   |   4 +-
 .../client/console/panels/ModalDirectoryPanel.java |   2 +-
 .../client/console/panels/SchemaTypePanel.java     |  67 ++--
 .../client/console/panels/SchemasPanel.java        |   4 +-
 .../console/policies/PolicyRuleDirectoryPanel.java |  21 +-
 .../console/policies/PolicyRuleWizardBuilder.java  |   5 +-
 .../console/reports/ReportDirectoryPanel.java      |  58 ++--
 .../console/reports/ReportExecutionDetails.java    |  19 +-
 .../console/reports/ReportletDirectoryPanel.java   |  43 +--
 .../console/reports/ReportletWizardBuilder.java    |  69 +++--
 .../client/console/rest/AccessTokenRestClient.java |   1 -
 .../client/console/rest/CommandRestClient.java     |  50 +++
 .../client/console/rest/TaskRestClient.java        |  47 ++-
 .../client/console/tasks/AnyPropagationTasks.java  |   4 +-
 .../tasks/CommandComposeDirectoryPanel.java        | 234 ++++++++++++++
 .../console/tasks/CommandComposeWizardBuilder.java | 161 ++++++++++
 .../{ExecMessage.java => CommandWrapper.java}      |  36 ++-
 .../syncope/client/console/tasks/ExecMessage.java  |   9 +
 .../console/tasks/ExecutionsDirectoryPanel.java    |  28 +-
 .../console/tasks/MacroTaskDirectoryPanel.java     |  88 ++++++
 .../tasks/NotificationTaskDirectoryPanel.java      |   7 +-
 .../tasks/PropagationTaskDirectoryPanel.java       |  10 +-
 .../client/console/tasks/PropagationTasks.java     |  20 +-
 .../tasks/ProvisioningTaskDirectoryPanel.java      |  88 +-----
 .../console/tasks/PullTaskDirectoryPanel.java      |   4 +-
 .../syncope/client/console/tasks/PullTasks.java    |  10 +-
 .../console/tasks/PushTaskDirectoryPanel.java      |   4 +-
 .../syncope/client/console/tasks/PushTasks.java    |  12 +-
 .../console/tasks/SchedTaskDirectoryPanel.java     | 200 +++++-------
 .../console/tasks/SchedTaskWizardBuilder.java      |  67 +++-
 .../syncope/client/console/tasks/SchedTasks.java   |  76 -----
 .../client/console/tasks/TaskDirectoryPanel.java   |  40 ++-
 .../client/console/tasks/TaskExecutionDetails.java |   8 +-
 .../markup/html/form/ActionLinksTogglePanel.java   |   6 +
 .../wicket/markup/html/form/ConfirmBehavior.java   |   8 +
 .../syncope/client/console/widgets/JobWidget.java  |  51 +--
 .../console/wizards/CommandWizardBuilder.java      |  66 ++++
 .../client/console/wizards/WizardMgtPanel.java     |  12 +-
 .../META-INF/resources/css/syncopeConsole.scss     |   9 +-
 .../console/SyncopeWebApplication.properties       |   2 +
 .../console/SyncopeWebApplication_fr_CA.properties |   1 +
 .../console/SyncopeWebApplication_it.properties    |   2 +
 .../console/SyncopeWebApplication_ja.properties    |   2 +
 .../console/SyncopeWebApplication_pt_BR.properties |   2 +
 .../console/SyncopeWebApplication_ru.properties    |   2 +
 ...yRecipientsProvider.groovy => MyCommand.groovy} |  15 +-
 .../console/implementations/MyLogicActions.groovy  |   2 +-
 .../implementations/MyRecipientsProvider.groovy    |   4 +-
 .../syncope/client/console/pages/BasePage.html     |   1 +
 .../syncope/client/console/pages/Engagements.html  |  47 +++
 .../{Reports.properties => Engagements.properties} |   8 +-
 ...rts.properties => Engagements_fr_CA.properties} |   8 +-
 ..._pt_BR.properties => Engagements_it.properties} |   8 +-
 ...eports.properties => Engagements_ja.properties} |   8 +-
 ..._BR.properties => Engagements_pt_BR.properties} |   8 +-
 ...eports.properties => Engagements_ru.properties} |   8 +-
 .../client/console/pages/Reports.properties        |   2 +-
 .../client/console/pages/Reports_it.properties     |   2 +-
 .../client/console/pages/Reports_ja.properties     |   2 +-
 .../client/console/pages/Reports_pt_BR.properties  |   2 +-
 .../client/console/pages/Reports_ru.properties     |   3 +-
 .../client/console/panels/CommandsPanel.html       |  36 +++
 .../CommandComposeWizardBuilder$CommandArgs.html   |  23 ++
 .../tasks/CommandComposeWizardBuilder$Profile.html |  26 ++
 ...CommandComposeWizardBuilder$Profile.properties} |   4 +-
 ...dComposeWizardBuilder$Profile_fr_CA.properties} |   4 +-
 ...mandComposeWizardBuilder$Profile_it.properties} |   4 +-
 ...mandComposeWizardBuilder$Profile_ja.properties} |   4 +-
 ...dComposeWizardBuilder$Profile_pt_BR.properties} |   4 +-
 ...mandComposeWizardBuilder$Profile_ru.properties} |   4 +-
 .../MacroTaskDirectoryPanel.properties}            |   6 +-
 .../MacroTaskDirectoryPanel_fr_CA.properties}      |   6 +-
 .../MacroTaskDirectoryPanel_it.properties}         |   6 +-
 .../MacroTaskDirectoryPanel_ja.properties}         |   6 +-
 .../MacroTaskDirectoryPanel_pt_BR.properties}      |   6 +-
 .../MacroTaskDirectoryPanel_ru.properties}         |   6 +-
 .../tasks/SchedTaskDirectoryPanel.properties       |   1 +
 .../tasks/SchedTaskDirectoryPanel_fr_CA.properties |  11 +-
 .../tasks/SchedTaskDirectoryPanel_it.properties    |   1 +
 .../tasks/SchedTaskDirectoryPanel_ja.properties    |   1 +
 .../tasks/SchedTaskDirectoryPanel_pt_BR.properties |   1 +
 .../tasks/SchedTaskDirectoryPanel_ru.properties    |   1 +
 .../tasks/SchedTaskWizardBuilder$Profile.html      |   6 +
 ...SchedTaskWizardBuilder$Profile_fr_CA.properties |  20 +-
 .../wizards/CommandWizardBuilder$CommandArgs.html  |  23 ++
 .../syncope/common/lib/command/CommandArgs.java    |  18 +-
 .../syncope/common/lib/command/CommandOutput.java  |  66 ++++
 .../syncope/common/lib/command/CommandTO.java      |  77 +++++
 .../apache/syncope/common/lib/to/MacroTaskTO.java  | 113 +++++++
 .../apache/syncope/common/lib/to/SchedTaskTO.java  |   4 +-
 .../common/lib/types/ClientExceptionType.java      |   3 +-
 .../common/lib/types/IdRepoEntitlement.java        |   2 +
 .../common/lib/types/IdRepoImplementationType.java |   5 +-
 .../apache/syncope/common/lib/types/TaskType.java  |   6 +-
 .../common/rest/api/beans/CommandQuery.java        |  55 ++++
 .../common/rest/api/service/CommandService.java    |  80 +++++
 .../common/rest/api/service/JAXRSService.java      |   2 +
 .../syncope/core/logic/AbstractAnyLogic.java       |   2 +-
 .../apache/syncope/core/logic/AnyObjectLogic.java  |   2 +-
 .../apache/syncope/core/logic/CommandLogic.java    | 130 ++++++++
 .../org/apache/syncope/core/logic/GroupLogic.java  |   2 +-
 .../syncope/core/logic/IdRepoLogicContext.java     |  24 +-
 .../syncope/core/logic/ImplementationLogic.java    |   4 +
 .../org/apache/syncope/core/logic/RealmLogic.java  |  41 ++-
 .../org/apache/syncope/core/logic/TaskLogic.java   | 114 ++++++-
 .../org/apache/syncope/core/logic/UserLogic.java   |   2 +-
 .../apache/syncope/core/logic/api/Command.java}    |  18 +-
 .../syncope/core/logic}/api/LogicActions.java      |   2 +-
 .../init/ClassPathScanImplementationLookup.java    |   7 +-
 .../core/logic/job/MacroRunJobDelegate.java        |  92 ++++++
 .../core/rest/cxf/IdRepoRESTCXFContext.java        |   9 +
 .../core/rest/cxf/service/CommandServiceImpl.java  |  56 ++++
 .../persistence/api/dao/ImplementationDAO.java     |   2 +
 .../syncope/core/persistence/api/dao/TaskDAO.java  |   6 +
 .../core/persistence/api/entity/EntityFactory.java |   4 -
 .../task/MacroTask.java}                           |  24 +-
 .../persistence/api/entity/task/TaskUtils.java     |  10 +
 .../entity/anyobject/JPAJSONAnyObjectListener.java |   7 +-
 .../jpa/entity/group/JPAJSONGroupListener.java     |   7 +-
 .../entity/user/JPAJSONLinkedAccountListener.java  |   7 +-
 .../jpa/entity/user/JPAJSONUserListener.java       |   7 +-
 .../resources/domains/jpa-json/MasterContent.xml   |   3 +
 .../src/test/resources/domains/MasterContent.xml   |   4 +
 .../core/persistence/jpa/PersistenceContext.java   |  26 +-
 .../jpa/dao/DefaultPullCorrelationRule.java        |   5 +-
 .../persistence/jpa/dao/JPAImplementationDAO.java  |  20 +-
 .../core/persistence/jpa/dao/JPARealmDAO.java      |  23 +-
 .../core/persistence/jpa/dao/JPATaskDAO.java       | 237 +++++++-------
 .../core/persistence/jpa/dao/JPATaskExecDAO.java   |  86 +-----
 .../persistence/jpa/entity/JPAConnInstance.java    |   8 +-
 .../persistence/jpa/entity/JPAEntityFactory.java   |  48 +--
 .../jpa/entity/JPAExternalResource.java            |  16 +-
 .../persistence/jpa/entity/JPANotification.java    |  11 +-
 .../core/persistence/jpa/entity/JPARole.java       |   6 +-
 .../jpa/entity/am/AbstractClientApp.java           |   6 +-
 .../persistence/jpa/entity/am/JPAAttrRepo.java     |   9 +-
 .../persistence/jpa/entity/am/JPAAuthModule.java   |   9 +-
 .../persistence/jpa/entity/am/JPAAuthProfile.java  |  36 ++-
 .../jpa/entity/am/JPAOIDCRPClientApp.java          |  23 +-
 .../jpa/entity/am/JPASAML2SPClientApp.java         |  37 +--
 .../jpa/entity/am/JPAWAConfigEntry.java            |   8 +-
 .../persistence/jpa/entity/task/JPAMacroTask.java  | 178 +++++++++++
 .../jpa/entity/task/JPAMacroTaskExec.java          |  49 +++
 .../jpa/entity/task/JPANotificationTask.java       |   7 +-
 .../persistence/jpa/entity/task/JPASchedTask.java  |  20 +-
 .../persistence/jpa/entity/task/JPATaskUtils.java  | 216 ++++++++++++-
 .../jpa/entity/task/JPATaskUtilsFactory.java       |  15 +-
 .../core/persistence/jpa/entity/user/JPAUser.java  |  54 ++--
 .../src/main/resources/domains/MasterContent.xml   |   3 +
 .../persistence/jpa/inner/ImplementationTest.java  |   4 +-
 .../core/persistence/jpa/inner/TaskExecTest.java   |   6 +-
 .../core/persistence/jpa/inner/TaskTest.java       |  22 ++
 .../core/persistence/jpa/outer/TaskTest.java       |  10 +-
 .../src/test/resources/domains/MasterContent.xml   |   4 +
 .../provisioning/api/serialization/POJOHelper.java |  12 +
 .../core/provisioning/api/utils/RealmUtils.java    |  21 +-
 .../provisioning/java/ProvisioningContext.java     |   8 +-
 .../java/data/ImplementationDataBinderImpl.java    | 101 ++----
 .../java/data/ResourceDataBinderImpl.java          |   4 +-
 .../provisioning/java/data/TaskDataBinderImpl.java | 145 ++++++---
 .../java/job/AbstractSchedTaskJobDelegate.java     |   6 +-
 .../provisioning/java/job/DefaultJobManager.java   |  11 +-
 .../core/provisioning/java/job/TaskJob.java        |   5 +-
 .../DefaultNotificationJobDelegate.java            |  10 +-
 .../AbstractPropagationTaskExecutor.java           |  11 +-
 .../PriorityPropagationTaskExecutor.java           |   3 -
 .../pushpull/AbstractProvisioningJobDelegate.java  |  66 ++--
 .../pushpull/DefaultRealmPullResultHandler.java    |  38 ++-
 .../implementation/ImplementationManager.java      |  24 ++
 .../security/DelegatedAdministrationException.java |   2 +-
 .../fit/core/reference/ITImplementationLookup.java |   7 +
 .../fit/core/reference/TestAccountRuleConf.java    |   5 -
 .../syncope/fit/core/reference/TestCommand.java    |  69 +++++
 .../fit/core/reference/TestCommandArgs.java        |  65 ++++
 .../fit/core/reference/TestPasswordRuleConf.java   |   5 -
 .../org/apache/syncope/fit/AbstractITCase.java     |   4 +
 .../syncope/fit/console/AjaxBrowseITCase.java      |   4 +
 .../syncope/fit/console/EngagementsITCase.java     |  57 ++++
 .../apache/syncope/fit/console/PoliciesITCase.java |   8 +-
 .../apache/syncope/fit/console/ReportsITCase.java  |   4 +-
 .../apache/syncope/fit/console/TopologyITCase.java |  37 +--
 .../org/apache/syncope/fit/core/CommandITCase.java |  99 ++++++
 .../apache/syncope/fit/core/DelegationITCase.java  |  12 +-
 .../org/apache/syncope/fit/core/MacroITCase.java   | 160 ++++++++++
 .../org/apache/syncope/fit/core/RealmITCase.java   |   4 +-
 .../org/apache/syncope/fit/core/RoleITCase.java    |   2 +-
 .../test/resources/DoubleValueLogicActions.groovy  |   3 +-
 .../src/test/resources/GroovyCommand.groovy        |  22 +-
 .../src/test/resources/TestPullRule.groovy         |   1 -
 .../src/test/resources/TestPushRule.groovy         |   1 -
 pom.xml                                            |   6 +-
 src/main/asciidoc/images/consoleDashboard.png      | Bin 62451 -> 63518 bytes
 src/main/asciidoc/images/engagements.png           | Bin 0 -> 40213 bytes
 .../reference-guide/concepts/notifications.adoc    |   2 +-
 .../asciidoc/reference-guide/concepts/realms.adoc  |   4 +-
 .../asciidoc/reference-guide/concepts/tasks.adoc   |  41 ++-
 .../configuration/highavailability.adoc            |   4 +-
 .../reference-guide/usage/adminconsole.adoc        |   8 +
 .../reference-guide/usage/customization.adoc       |   8 +-
 234 files changed, 4535 insertions(+), 1965 deletions(-)

diff --git a/client/am/console/src/main/java/org/apache/syncope/client/console/authprofiles/AuthProfileDirectoryPanel.java b/client/am/console/src/main/java/org/apache/syncope/client/console/authprofiles/AuthProfileDirectoryPanel.java
index 474957f0e3..1066f1268c 100644
--- a/client/am/console/src/main/java/org/apache/syncope/client/console/authprofiles/AuthProfileDirectoryPanel.java
+++ b/client/am/console/src/main/java/org/apache/syncope/client/console/authprofiles/AuthProfileDirectoryPanel.java
@@ -64,7 +64,7 @@ import org.apache.wicket.model.StringResourceModel;
 public class AuthProfileDirectoryPanel
         extends DirectoryPanel<AuthProfileTO, AuthProfileTO, AuthProfileProvider, AuthProfileRestClient> {
 
-    private static final long serialVersionUID = 1L;
+    private static final long serialVersionUID = 2018518567549153364L;
 
     private final BaseModal<AuthProfileTO> authProfileModal;
 
@@ -121,6 +121,8 @@ public class AuthProfileDirectoryPanel
 
         columns.add(new BooleanConditionColumn<>(new StringResourceModel("impersonationAccounts")) {
 
+            private static final long serialVersionUID = -8236820422411536323L;
+
             @Override
             protected boolean isCondition(final IModel<AuthProfileTO> rowModel) {
                 return !rowModel.getObject().getImpersonationAccounts().isEmpty();
@@ -128,6 +130,8 @@ public class AuthProfileDirectoryPanel
         });
         columns.add(new BooleanConditionColumn<>(new StringResourceModel("googleMfaAuthTokens")) {
 
+            private static final long serialVersionUID = -8236820422411536323L;
+
             @Override
             protected boolean isCondition(final IModel<AuthProfileTO> rowModel) {
                 return !rowModel.getObject().getGoogleMfaAuthTokens().isEmpty();
@@ -135,6 +139,8 @@ public class AuthProfileDirectoryPanel
         });
         columns.add(new BooleanConditionColumn<>(new StringResourceModel("googleMfaAuthAccounts")) {
 
+            private static final long serialVersionUID = -8236820422411536323L;
+
             @Override
             protected boolean isCondition(final IModel<AuthProfileTO> rowModel) {
                 return !rowModel.getObject().getGoogleMfaAuthAccounts().isEmpty();
@@ -142,6 +148,8 @@ public class AuthProfileDirectoryPanel
         });
         columns.add(new BooleanConditionColumn<>(new StringResourceModel("u2fRegisteredDevices")) {
 
+            private static final long serialVersionUID = -8236820422411536323L;
+
             @Override
             protected boolean isCondition(final IModel<AuthProfileTO> rowModel) {
                 return !rowModel.getObject().getU2FRegisteredDevices().isEmpty();
@@ -149,6 +157,8 @@ public class AuthProfileDirectoryPanel
         });
         columns.add(new BooleanConditionColumn<>(new StringResourceModel("webAuthnAccount")) {
 
+            private static final long serialVersionUID = -8236820422411536323L;
+
             @Override
             protected boolean isCondition(final IModel<AuthProfileTO> rowModel) {
                 return !rowModel.getObject().getWebAuthnDeviceCredentials().isEmpty();
@@ -170,38 +180,40 @@ public class AuthProfileDirectoryPanel
             public void onClick(final AjaxRequestTarget target, final AuthProfileTO ignore) {
                 model.setObject(AuthProfileRestClient.read(model.getObject().getKey()));
                 target.add(authProfileModal.setContent(new ModalDirectoryPanel<>(
-                    authProfileModal,
-                    new AuthProfileItemDirectoryPanel<ImpersonationAccount>(
-                        "panel", authProfileModal, model.getObject(), pageRef) {
-
-                        @Override
-                        protected List<ImpersonationAccount> getItems() {
-                            return model.getObject().getImpersonationAccounts();
-                        }
-
-                        @Override
-                        protected ImpersonationAccount defaultItem() {
-                            return new ImpersonationAccount();
-                        }
-
-                        @Override
-                        protected String sortProperty() {
-                            return "impersonated";
-                        }
-
-                        @Override
-                        protected String paginatorRowsKey() {
-                            return AMConstants.PREF_AUTHPROFILE_IMPERSONATED_PAGINATOR_ROWS;
-                        }
-
-                        @Override
-                        protected List<IColumn<ImpersonationAccount, String>> getColumns() {
-                            List<IColumn<ImpersonationAccount, String>> columns = new ArrayList<>();
-                            columns.add(new PropertyColumn<>(new ResourceModel("impersonated"),
+                        authProfileModal,
+                        new AuthProfileItemDirectoryPanel<ImpersonationAccount>(
+                                "panel", authProfileModal, model.getObject(), pageRef) {
+
+                    private static final long serialVersionUID = -5380664539000792237L;
+
+                    @Override
+                    protected List<ImpersonationAccount> getItems() {
+                        return model.getObject().getImpersonationAccounts();
+                    }
+
+                    @Override
+                    protected ImpersonationAccount defaultItem() {
+                        return new ImpersonationAccount();
+                    }
+
+                    @Override
+                    protected String sortProperty() {
+                        return "impersonated";
+                    }
+
+                    @Override
+                    protected String paginatorRowsKey() {
+                        return AMConstants.PREF_AUTHPROFILE_IMPERSONATED_PAGINATOR_ROWS;
+                    }
+
+                    @Override
+                    protected List<IColumn<ImpersonationAccount, String>> getColumns() {
+                        List<IColumn<ImpersonationAccount, String>> columns = new ArrayList<>();
+                        columns.add(new PropertyColumn<>(new ResourceModel("impersonated"),
                                 "impersonated", "impersonated"));
-                            return columns;
-                        }
-                    }, pageRef)));
+                        return columns;
+                    }
+                }, pageRef)));
                 authProfileModal.header(new Model<>(getString("impersonationAccounts", model)));
                 authProfileModal.show(true);
             }
@@ -215,40 +227,42 @@ public class AuthProfileDirectoryPanel
             public void onClick(final AjaxRequestTarget target, final AuthProfileTO ignore) {
                 model.setObject(AuthProfileRestClient.read(model.getObject().getKey()));
                 target.add(authProfileModal.setContent(new ModalDirectoryPanel<>(
-                    authProfileModal,
-                    new AuthProfileItemDirectoryPanel<GoogleMfaAuthToken>(
-                        "panel", authProfileModal, model.getObject(), pageRef) {
-
-                        @Override
-                        protected List<GoogleMfaAuthToken> getItems() {
-                            return model.getObject().getGoogleMfaAuthTokens();
-                        }
-
-                        @Override
-                        protected GoogleMfaAuthToken defaultItem() {
-                            return new GoogleMfaAuthToken();
-                        }
-
-                        @Override
-                        protected String sortProperty() {
-                            return "issueDate";
-                        }
-
-                        @Override
-                        protected String paginatorRowsKey() {
-                            return AMConstants.PREF_AUTHPROFILE_GOOGLEMFAAUTHTOKENS_PAGINATOR_ROWS;
-                        }
-
-                        @Override
-                        protected List<IColumn<GoogleMfaAuthToken, String>> getColumns() {
-                            List<IColumn<GoogleMfaAuthToken, String>> columns = new ArrayList<>();
-                            columns.add(new DatePropertyColumn<>(
+                        authProfileModal,
+                        new AuthProfileItemDirectoryPanel<GoogleMfaAuthToken>(
+                                "panel", authProfileModal, model.getObject(), pageRef) {
+
+                    private static final long serialVersionUID = 7332357430197837993L;
+
+                    @Override
+                    protected List<GoogleMfaAuthToken> getItems() {
+                        return model.getObject().getGoogleMfaAuthTokens();
+                    }
+
+                    @Override
+                    protected GoogleMfaAuthToken defaultItem() {
+                        return new GoogleMfaAuthToken();
+                    }
+
+                    @Override
+                    protected String sortProperty() {
+                        return "issueDate";
+                    }
+
+                    @Override
+                    protected String paginatorRowsKey() {
+                        return AMConstants.PREF_AUTHPROFILE_GOOGLEMFAAUTHTOKENS_PAGINATOR_ROWS;
+                    }
+
+                    @Override
+                    protected List<IColumn<GoogleMfaAuthToken, String>> getColumns() {
+                        List<IColumn<GoogleMfaAuthToken, String>> columns = new ArrayList<>();
+                        columns.add(new DatePropertyColumn<>(
                                 new ResourceModel("issueDate"), "issueDate", "issueDate"));
-                            columns.add(new PropertyColumn<>(
+                        columns.add(new PropertyColumn<>(
                                 new ResourceModel("otp"), "otp", "otp"));
-                            return columns;
-                        }
-                    }, pageRef)));
+                        return columns;
+                    }
+                }, pageRef)));
                 authProfileModal.header(new Model<>(getString("googleMfaAuthTokens", model)));
                 authProfileModal.show(true);
             }
@@ -262,40 +276,42 @@ public class AuthProfileDirectoryPanel
             public void onClick(final AjaxRequestTarget target, final AuthProfileTO ignore) {
                 model.setObject(AuthProfileRestClient.read(model.getObject().getKey()));
                 target.add(authProfileModal.setContent(new ModalDirectoryPanel<>(
-                    authProfileModal,
-                    new AuthProfileItemDirectoryPanel<GoogleMfaAuthAccount>(
-                        "panel", authProfileModal, model.getObject(), pageRef) {
-
-                        @Override
-                        protected List<GoogleMfaAuthAccount> getItems() {
-                            return model.getObject().getGoogleMfaAuthAccounts();
-                        }
-
-                        @Override
-                        protected GoogleMfaAuthAccount defaultItem() {
-                            return new GoogleMfaAuthAccount();
-                        }
-
-                        @Override
-                        protected String sortProperty() {
-                            return "id";
-                        }
-
-                        @Override
-                        protected String paginatorRowsKey() {
-                            return AMConstants.PREF_AUTHPROFILE_GOOGLEMFAAUTHACCOUNTS_PAGINATOR_ROWS;
-                        }
-
-                        @Override
-                        protected List<IColumn<GoogleMfaAuthAccount, String>> getColumns() {
-                            List<IColumn<GoogleMfaAuthAccount, String>> columns = new ArrayList<>();
-                            columns.add(new PropertyColumn<>(new ResourceModel("id"), "id", "id"));
-                            columns.add(new DatePropertyColumn<>(
+                        authProfileModal,
+                        new AuthProfileItemDirectoryPanel<GoogleMfaAuthAccount>(
+                                "panel", authProfileModal, model.getObject(), pageRef) {
+
+                    private static final long serialVersionUID = -670769282358547044L;
+
+                    @Override
+                    protected List<GoogleMfaAuthAccount> getItems() {
+                        return model.getObject().getGoogleMfaAuthAccounts();
+                    }
+
+                    @Override
+                    protected GoogleMfaAuthAccount defaultItem() {
+                        return new GoogleMfaAuthAccount();
+                    }
+
+                    @Override
+                    protected String sortProperty() {
+                        return "id";
+                    }
+
+                    @Override
+                    protected String paginatorRowsKey() {
+                        return AMConstants.PREF_AUTHPROFILE_GOOGLEMFAAUTHACCOUNTS_PAGINATOR_ROWS;
+                    }
+
+                    @Override
+                    protected List<IColumn<GoogleMfaAuthAccount, String>> getColumns() {
+                        List<IColumn<GoogleMfaAuthAccount, String>> columns = new ArrayList<>();
+                        columns.add(new PropertyColumn<>(new ResourceModel("id"), "id", "id"));
+                        columns.add(new DatePropertyColumn<>(
                                 new ResourceModel("registrationDate"), "registrationDate", "registrationDate"));
-                            columns.add(new PropertyColumn<>(new ResourceModel("name"), "name", "name"));
-                            return columns;
-                        }
-                    }, pageRef)));
+                        columns.add(new PropertyColumn<>(new ResourceModel("name"), "name", "name"));
+                        return columns;
+                    }
+                }, pageRef)));
                 authProfileModal.header(new Model<>(getString("googleMfaAuthAccounts", model)));
                 authProfileModal.show(true);
             }
@@ -309,40 +325,42 @@ public class AuthProfileDirectoryPanel
             public void onClick(final AjaxRequestTarget target, final AuthProfileTO ignore) {
                 model.setObject(AuthProfileRestClient.read(model.getObject().getKey()));
                 target.add(authProfileModal.setContent(new ModalDirectoryPanel<>(
-                    authProfileModal,
-                    new AuthProfileItemDirectoryPanel<U2FDevice>(
-                        "panel", authProfileModal, model.getObject(), pageRef) {
-
-                        @Override
-                        protected List<U2FDevice> getItems() {
-                            return model.getObject().getU2FRegisteredDevices();
-                        }
-
-                        @Override
-                        protected U2FDevice defaultItem() {
-                            return new U2FDevice();
-                        }
-
-                        @Override
-                        protected String sortProperty() {
-                            return "id";
-                        }
-
-                        @Override
-                        protected String paginatorRowsKey() {
-                            return AMConstants.PREF_AUTHPROFILE_U2FDEVICES_PAGINATOR_ROWS;
-                        }
-
-                        @Override
-                        protected List<IColumn<U2FDevice, String>> getColumns() {
-                            List<IColumn<U2FDevice, String>> columns = new ArrayList<>();
-                            columns.add(new PropertyColumn<>(new ResourceModel("id"), "id", "id"));
-                            columns.add(new DatePropertyColumn<>(
+                        authProfileModal,
+                        new AuthProfileItemDirectoryPanel<U2FDevice>(
+                                "panel", authProfileModal, model.getObject(), pageRef) {
+
+                    private static final long serialVersionUID = 5788448799796630011L;
+
+                    @Override
+                    protected List<U2FDevice> getItems() {
+                        return model.getObject().getU2FRegisteredDevices();
+                    }
+
+                    @Override
+                    protected U2FDevice defaultItem() {
+                        return new U2FDevice();
+                    }
+
+                    @Override
+                    protected String sortProperty() {
+                        return "id";
+                    }
+
+                    @Override
+                    protected String paginatorRowsKey() {
+                        return AMConstants.PREF_AUTHPROFILE_U2FDEVICES_PAGINATOR_ROWS;
+                    }
+
+                    @Override
+                    protected List<IColumn<U2FDevice, String>> getColumns() {
+                        List<IColumn<U2FDevice, String>> columns = new ArrayList<>();
+                        columns.add(new PropertyColumn<>(new ResourceModel("id"), "id", "id"));
+                        columns.add(new DatePropertyColumn<>(
                                 new ResourceModel("issueDate"), "issueDate", "issueDate"));
-                            columns.add(new PropertyColumn<>(new ResourceModel("record"), "record", "record"));
-                            return columns;
-                        }
-                    }, pageRef)));
+                        columns.add(new PropertyColumn<>(new ResourceModel("record"), "record", "record"));
+                        return columns;
+                    }
+                }, pageRef)));
                 authProfileModal.header(new Model<>(getString("u2fRegisteredDevices", model)));
                 authProfileModal.show(true);
             }
@@ -356,40 +374,42 @@ public class AuthProfileDirectoryPanel
             public void onClick(final AjaxRequestTarget target, final AuthProfileTO ignore) {
                 model.setObject(AuthProfileRestClient.read(model.getObject().getKey()));
                 target.add(authProfileModal.setContent(new ModalDirectoryPanel<>(
-                    authProfileModal,
-                    new AuthProfileItemDirectoryPanel<WebAuthnDeviceCredential>(
-                        "panel", authProfileModal, model.getObject(), pageRef) {
-
-                        @Override
-                        protected List<WebAuthnDeviceCredential> getItems() {
-                            return model.getObject().getWebAuthnDeviceCredentials();
-                        }
-
-                        @Override
-                        protected WebAuthnDeviceCredential defaultItem() {
-                            return new WebAuthnDeviceCredential();
-                        }
-
-                        @Override
-                        protected String sortProperty() {
-                            return "identifier";
-                        }
-
-                        @Override
-                        protected String paginatorRowsKey() {
-                            return AMConstants.PREF_AUTHPROFILE_WEBAUTHNDEVICECREDENTIALS_PAGINATOR_ROWS;
-                        }
-
-                        @Override
-                        protected List<IColumn<WebAuthnDeviceCredential, String>> getColumns() {
-                            List<IColumn<WebAuthnDeviceCredential, String>> columns = new ArrayList<>();
-                            columns.add(new PropertyColumn<>(
+                        authProfileModal,
+                        new AuthProfileItemDirectoryPanel<WebAuthnDeviceCredential>(
+                                "panel", authProfileModal, model.getObject(), pageRef) {
+
+                    private static final long serialVersionUID = 6820212423488933184L;
+
+                    @Override
+                    protected List<WebAuthnDeviceCredential> getItems() {
+                        return model.getObject().getWebAuthnDeviceCredentials();
+                    }
+
+                    @Override
+                    protected WebAuthnDeviceCredential defaultItem() {
+                        return new WebAuthnDeviceCredential();
+                    }
+
+                    @Override
+                    protected String sortProperty() {
+                        return "identifier";
+                    }
+
+                    @Override
+                    protected String paginatorRowsKey() {
+                        return AMConstants.PREF_AUTHPROFILE_WEBAUTHNDEVICECREDENTIALS_PAGINATOR_ROWS;
+                    }
+
+                    @Override
+                    protected List<IColumn<WebAuthnDeviceCredential, String>> getColumns() {
+                        List<IColumn<WebAuthnDeviceCredential, String>> columns = new ArrayList<>();
+                        columns.add(new PropertyColumn<>(
                                 new ResourceModel("identifier"), "identifier", "identifier"));
-                            columns.add(new PropertyColumn<>(
+                        columns.add(new PropertyColumn<>(
                                 new ResourceModel("json"), "json", "json"));
-                            return columns;
-                        }
-                    }, pageRef)));
+                        return columns;
+                    }
+                }, pageRef)));
                 authProfileModal.header(new Model<>(getString("webAuthnDeviceCredentials", model)));
                 authProfileModal.show(true);
             }
diff --git a/client/am/console/src/main/java/org/apache/syncope/client/console/authprofiles/AuthProfileItemDirectoryPanel.java b/client/am/console/src/main/java/org/apache/syncope/client/console/authprofiles/AuthProfileItemDirectoryPanel.java
index 5543a00097..a125278ef7 100644
--- a/client/am/console/src/main/java/org/apache/syncope/client/console/authprofiles/AuthProfileItemDirectoryPanel.java
+++ b/client/am/console/src/main/java/org/apache/syncope/client/console/authprofiles/AuthProfileItemDirectoryPanel.java
@@ -120,7 +120,7 @@ public abstract class AuthProfileItemDirectoryPanel<I extends BaseBean>
             @Override
             public void onClick(final AjaxRequestTarget target, final I ignore) {
                 send(AuthProfileItemDirectoryPanel.this, Broadcast.EXACT,
-                    new AjaxWizard.EditItemActionEvent<>(model.getObject(), target));
+                        new AjaxWizard.EditItemActionEvent<>(model.getObject(), target));
             }
         }, ActionLink.ActionType.EDIT, AMEntitlement.AUTH_PROFILE_UPDATE);
 
@@ -178,11 +178,11 @@ public abstract class AuthProfileItemDirectoryPanel<I extends BaseBean>
         }
     }
 
-    private class AuthProfileItemWizardBuilder extends AuthProfileWizardBuilder<I> {
+    protected class AuthProfileItemWizardBuilder extends AuthProfileWizardBuilder<I> {
 
-        private static final long serialVersionUID = 1L;
+        private static final long serialVersionUID = -7174537333960225216L;
 
-        AuthProfileItemWizardBuilder(final PageReference pageRef) {
+        protected AuthProfileItemWizardBuilder(final PageReference pageRef) {
             super(defaultItem(), new StepModel<>(), pageRef);
         }
 
diff --git a/client/am/console/src/main/java/org/apache/syncope/client/console/clientapps/ClientAppDirectoryPanel.java b/client/am/console/src/main/java/org/apache/syncope/client/console/clientapps/ClientAppDirectoryPanel.java
index 9cdc7e7a30..68ea786219 100644
--- a/client/am/console/src/main/java/org/apache/syncope/client/console/clientapps/ClientAppDirectoryPanel.java
+++ b/client/am/console/src/main/java/org/apache/syncope/client/console/clientapps/ClientAppDirectoryPanel.java
@@ -57,7 +57,7 @@ import org.apache.wicket.model.StringResourceModel;
 public abstract class ClientAppDirectoryPanel<T extends ClientAppTO>
         extends DirectoryPanel<T, T, DirectoryDataProvider<T>, ClientAppRestClient> {
 
-    private static final long serialVersionUID = 1L;
+    private static final long serialVersionUID = 4100100988730985059L;
 
     private final ClientAppType type;
 
@@ -123,8 +123,8 @@ public abstract class ClientAppDirectoryPanel<T extends ClientAppTO>
             @Override
             public void onClick(final AjaxRequestTarget target, final ClientAppTO ignore) {
                 send(ClientAppDirectoryPanel.this, Broadcast.EXACT,
-                    new AjaxWizard.EditItemActionEvent<>(
-                        ClientAppRestClient.read(type, model.getObject().getKey()), target));
+                        new AjaxWizard.EditItemActionEvent<>(
+                                ClientAppRestClient.read(type, model.getObject().getKey()), target));
             }
         }, ActionLink.ActionType.EDIT, AMEntitlement.CLIENTAPP_UPDATE);
 
@@ -136,9 +136,9 @@ public abstract class ClientAppDirectoryPanel<T extends ClientAppTO>
             public void onClick(final AjaxRequestTarget target, final ClientAppTO ignore) {
                 model.setObject(ClientAppRestClient.read(type, model.getObject().getKey()));
                 target.add(propertiesModal.setContent(new ModalDirectoryPanel<>(
-                    propertiesModal,
-                    new ClientAppPropertiesDirectoryPanel<>("panel", propertiesModal, type, model, pageRef),
-                    pageRef)));
+                        propertiesModal,
+                        new ClientAppPropertiesDirectoryPanel<>("panel", propertiesModal, type, model, pageRef),
+                        pageRef)));
                 propertiesModal.header(new Model<>(getString("properties.title", model)));
                 propertiesModal.show(true);
             }
@@ -154,7 +154,7 @@ public abstract class ClientAppDirectoryPanel<T extends ClientAppTO>
                 clone.setKey(null);
                 clone.setClientAppId(null);
                 send(ClientAppDirectoryPanel.this, Broadcast.EXACT,
-                    new AjaxWizard.EditItemActionEvent<>(clone, target));
+                        new AjaxWizard.EditItemActionEvent<>(clone, target));
             }
         }, ActionLink.ActionType.CLONE, AMEntitlement.CLIENTAPP_CREATE);
 
diff --git a/client/idm/console/src/main/java/org/apache/syncope/client/console/commons/IdMAnyDirectoryPanelAdditionalActionLinksProvider.java b/client/idm/console/src/main/java/org/apache/syncope/client/console/commons/IdMAnyDirectoryPanelAdditionalActionLinksProvider.java
index 9ae35163f9..6a17806fe3 100644
--- a/client/idm/console/src/main/java/org/apache/syncope/client/console/commons/IdMAnyDirectoryPanelAdditionalActionLinksProvider.java
+++ b/client/idm/console/src/main/java/org/apache/syncope/client/console/commons/IdMAnyDirectoryPanelAdditionalActionLinksProvider.java
@@ -68,15 +68,14 @@ public class IdMAnyDirectoryPanelAdditionalActionLinksProvider
             @Override
             public void onClick(final AjaxRequestTarget target, final UserTO ignore) {
                 IModel<AnyWrapper<UserTO>> formModel = new CompoundPropertyModel<>(
-                    new AnyWrapper<>(model.getObject()));
+                        new AnyWrapper<>(model.getObject()));
                 modal.setFormModel(formModel);
 
                 target.add(modal.setContent(new AnyStatusModal<>(
-                    modal,
-                    pageRef,
-                    formModel.getObject().getInnerObject(),
-                    "resource",
-                    true)));
+                        pageRef,
+                        formModel.getObject().getInnerObject(),
+                        "resource",
+                        true)));
 
                 modal.header(new Model<>(header));
 
@@ -96,15 +95,14 @@ public class IdMAnyDirectoryPanelAdditionalActionLinksProvider
             public void onClick(final AjaxRequestTarget target, final UserTO ignore) {
                 model.setObject(new UserRestClient().read(model.getObject().getKey()));
                 IModel<AnyWrapper<UserTO>> formModel = new CompoundPropertyModel<>(
-                    new AnyWrapper<>(model.getObject()));
+                        new AnyWrapper<>(model.getObject()));
                 modal.setFormModel(formModel);
 
                 target.add(modal.setContent(new AnyStatusModal<>(
-                    modal,
-                    pageRef,
-                    formModel.getObject().getInnerObject(),
-                    "resource",
-                    false)));
+                        pageRef,
+                        formModel.getObject().getInnerObject(),
+                        "resource",
+                        false)));
 
                 modal.header(new Model<>(header));
 
@@ -146,7 +144,7 @@ public class IdMAnyDirectoryPanelAdditionalActionLinksProvider
             public void onClick(final AjaxRequestTarget target, final UserTO ignore) {
                 model.setObject(new UserRestClient().read(model.getObject().getKey()));
                 MergeLinkedAccountsWizardBuilder builder =
-                    new MergeLinkedAccountsWizardBuilder(model, pageRef, parentPanel, modal);
+                        new MergeLinkedAccountsWizardBuilder(model, pageRef, parentPanel, modal);
                 builder.setEventSink(builder);
                 target.add(modal.setContent(builder.build(BaseModal.CONTENT_ID, AjaxWizard.Mode.CREATE)));
                 modal.header(new StringResourceModel("mergeLinkedAccounts.title", model));
@@ -179,15 +177,14 @@ public class IdMAnyDirectoryPanelAdditionalActionLinksProvider
             @Override
             public void onClick(final AjaxRequestTarget target, final GroupTO ignore) {
                 IModel<AnyWrapper<GroupTO>> formModel = new CompoundPropertyModel<>(
-                    new AnyWrapper<>(modelObject));
+                        new AnyWrapper<>(modelObject));
                 modal.setFormModel(formModel);
 
                 target.add(modal.setContent(new AnyStatusModal<>(
-                    modal,
-                    pageRef,
-                    formModel.getObject().getInnerObject(),
-                    "resource",
-                    false)));
+                        pageRef,
+                        formModel.getObject().getInnerObject(),
+                        "resource",
+                        false)));
 
                 modal.header(new Model<>(header));
 
@@ -222,15 +219,14 @@ public class IdMAnyDirectoryPanelAdditionalActionLinksProvider
             @Override
             public void onClick(final AjaxRequestTarget target, final AnyObjectTO ignore) {
                 final IModel<AnyWrapper<AnyObjectTO>> formModel = new CompoundPropertyModel<>(
-                    new AnyWrapper<>(modelObject));
+                        new AnyWrapper<>(modelObject));
                 modal.setFormModel(formModel);
 
                 target.add(modal.setContent(new AnyStatusModal<>(
-                    modal,
-                    pageRef,
-                    formModel.getObject().getInnerObject(),
-                    "resource",
-                    false)));
+                        pageRef,
+                        formModel.getObject().getInnerObject(),
+                        "resource",
+                        false)));
 
                 modal.header(new Model<>(header));
 
diff --git a/client/idm/console/src/main/java/org/apache/syncope/client/console/panels/ResourceDirectoryPanel.java b/client/idm/console/src/main/java/org/apache/syncope/client/console/panels/ResourceDirectoryPanel.java
index 81cd889938..6432ce3122 100644
--- a/client/idm/console/src/main/java/org/apache/syncope/client/console/panels/ResourceDirectoryPanel.java
+++ b/client/idm/console/src/main/java/org/apache/syncope/client/console/panels/ResourceDirectoryPanel.java
@@ -257,10 +257,10 @@ public class ResourceDirectoryPanel extends
 
             @Override
             public void onClick(final AjaxRequestTarget target, final Serializable ignore) {
-                target.add(schedTaskModal.setContent(new PullTasks(schedTaskModal, pageRef,
-                        ((ResourceTO) model.getObject()).getKey())));
-                schedTaskModal.header(new Model<>(MessageFormat.format(getString("task.pull.list"),
-                        ((ResourceTO) model.getObject()).getKey())));
+                target.add(schedTaskModal.setContent(new PullTasks(
+                        schedTaskModal, ((ResourceTO) model.getObject()).getKey(), pageRef)));
+                schedTaskModal.header(new Model<>(
+                        MessageFormat.format(getString("task.pull.list"), ((ResourceTO) model.getObject()).getKey())));
                 schedTaskModal.show(true);
             }
         }, ActionLink.ActionType.PULL_TASKS, IdRepoEntitlement.TASK_LIST);
@@ -271,10 +271,10 @@ public class ResourceDirectoryPanel extends
 
             @Override
             public void onClick(final AjaxRequestTarget target, final Serializable ignore) {
-                target.add(schedTaskModal.setContent(new PushTasks(schedTaskModal, pageRef,
-                        ((ResourceTO) model.getObject()).getKey())));
-                schedTaskModal.header(new Model<>(MessageFormat.format(getString("task.push.list"),
-                        ((ResourceTO) model.getObject()).getKey())));
+                target.add(schedTaskModal.setContent(new PushTasks(
+                        schedTaskModal, ((ResourceTO) model.getObject()).getKey(), pageRef)));
+                schedTaskModal.header(new Model<>(
+                        MessageFormat.format(getString("task.push.list"), ((ResourceTO) model.getObject()).getKey())));
                 schedTaskModal.show(true);
             }
         }, ActionLink.ActionType.PUSH_TASKS, IdRepoEntitlement.TASK_LIST);
@@ -286,8 +286,7 @@ public class ResourceDirectoryPanel extends
             @Override
             public void onClick(final AjaxRequestTarget target, final Serializable ignore) {
                 ResourceTO modelObject = ResourceRestClient.read(((ResourceTO) model.getObject()).getKey());
-                target.add(propTaskModal.setContent(
-                        new ResourceStatusModal(propTaskModal, pageRef, modelObject)));
+                target.add(propTaskModal.setContent(new ResourceStatusModal(pageRef, modelObject)));
                 propTaskModal.header(new Model<>(MessageFormat.format(getString("resource.reconciliation"),
                         ((ResourceTO) model.getObject()).getKey())));
                 propTaskModal.show(true);
diff --git a/client/idm/console/src/main/java/org/apache/syncope/client/console/status/AnyStatusDirectoryPanel.java b/client/idm/console/src/main/java/org/apache/syncope/client/console/status/AnyStatusDirectoryPanel.java
index 7c45f5c99b..0b8007c45b 100644
--- a/client/idm/console/src/main/java/org/apache/syncope/client/console/status/AnyStatusDirectoryPanel.java
+++ b/client/idm/console/src/main/java/org/apache/syncope/client/console/status/AnyStatusDirectoryPanel.java
@@ -34,7 +34,6 @@ import org.apache.syncope.client.console.rest.AnyObjectRestClient;
 import org.apache.syncope.client.console.rest.GroupRestClient;
 import org.apache.syncope.client.console.rest.ResourceRestClient;
 import org.apache.syncope.client.console.rest.UserRestClient;
-import org.apache.syncope.client.console.wicket.markup.html.bootstrap.dialog.BaseModal;
 import org.apache.syncope.client.console.wicket.markup.html.form.ActionLink;
 import org.apache.syncope.client.console.wicket.markup.html.form.ActionsPanel;
 import org.apache.syncope.client.ui.commons.Constants;
@@ -73,8 +72,6 @@ public class AnyStatusDirectoryPanel
 
     private static final long serialVersionUID = -9148734710505211261L;
 
-    private final BaseModal<?> baseModal;
-
     private final MultilevelPanel multiLevelPanelRef;
 
     private final AnyTO anyTO;
@@ -86,7 +83,6 @@ public class AnyStatusDirectoryPanel
     private final List<String> resources;
 
     public AnyStatusDirectoryPanel(
-            final BaseModal<?> baseModal,
             final MultilevelPanel multiLevelPanelRef,
             final PageReference pageRef,
             final AnyTO anyTO,
@@ -94,7 +90,6 @@ public class AnyStatusDirectoryPanel
             final boolean statusOnly) {
 
         super(MultilevelPanel.FIRST_LEVEL_ID, pageRef);
-        this.baseModal = baseModal;
         this.multiLevelPanelRef = multiLevelPanelRef;
         this.statusOnly = statusOnly;
         this.anyTO = anyTO;
@@ -120,23 +115,23 @@ public class AnyStatusDirectoryPanel
 
     @Override
     protected void resultTableCustomChanges(final AjaxDataTablePanel.Builder<StatusBean, String> resultTableBuilder) {
-        resultTableBuilder.setMultiLevelPanel(baseModal, multiLevelPanelRef);
+        resultTableBuilder.setMultiLevelPanel(multiLevelPanelRef);
     }
 
     @Override
     protected List<IColumn<StatusBean, String>> getColumns() {
-        final List<IColumn<StatusBean, String>> columns = new ArrayList<>();
+        List<IColumn<StatusBean, String>> columns = new ArrayList<>();
 
         columns.add(new AbstractColumn<>(
-            new StringResourceModel("resource", this), "resource") {
+                new StringResourceModel("resource", this), "resource") {
 
             private static final long serialVersionUID = 2054811145491901166L;
 
             @Override
             public void populateItem(
-                final Item<ICellPopulator<StatusBean>> cellItem,
-                final String componentId,
-                final IModel<StatusBean> model) {
+                    final Item<ICellPopulator<StatusBean>> cellItem,
+                    final String componentId,
+                    final IModel<StatusBean> model) {
 
                 cellItem.add(new Label(componentId, model.getObject().getResource()) {
 
@@ -145,7 +140,7 @@ public class AnyStatusDirectoryPanel
                     @Override
                     protected void onComponentTag(final ComponentTag tag) {
                         if (anyTO.getResources().contains(model.getObject().getResource())
-                            || Constants.SYNCOPE.equalsIgnoreCase(model.getObject().getResource())) {
+                                || Constants.SYNCOPE.equalsIgnoreCase(model.getObject().getResource())) {
 
                             super.onComponentTag(tag);
                         } else {
@@ -166,9 +161,9 @@ public class AnyStatusDirectoryPanel
 
                 @Override
                 public void populateItem(
-                    final Item<ICellPopulator<StatusBean>> cellItem,
-                    final String componentId,
-                    final IModel<StatusBean> model) {
+                        final Item<ICellPopulator<StatusBean>> cellItem,
+                        final String componentId,
+                        final IModel<StatusBean> model) {
 
                     if (model.getObject().isLinked()) {
                         cellItem.add(StatusUtils.getStatusImage(componentId, model.getObject().getStatus()));
@@ -194,8 +189,8 @@ public class AnyStatusDirectoryPanel
                 @Override
                 public void onClick(final AjaxRequestTarget target, final StatusBean bean) {
                     multiLevelPanelRef.next(bean.getResource(),
-                        new ReconStatusPanel(bean.getResource(), anyTO.getType(), anyTO.getKey()),
-                        target);
+                            new ReconStatusPanel(bean.getResource(), anyTO.getType(), anyTO.getKey()),
+                            target);
                     target.add(multiLevelPanelRef);
                     AnyStatusDirectoryPanel.this.getTogglePanel().close(target);
                 }
@@ -210,15 +205,15 @@ public class AnyStatusDirectoryPanel
                 @Override
                 public void onClick(final AjaxRequestTarget target, final StatusBean bean) {
                     multiLevelPanelRef.next("PUSH " + bean.getResource(),
-                        new ReconTaskPanel(
-                            bean.getResource(),
-                            new PushTaskTO(),
-                            anyTO.getType(),
-                            anyTO.getKey(),
-                            true,
-                            multiLevelPanelRef,
-                            pageRef),
-                        target);
+                            new ReconTaskPanel(
+                                    bean.getResource(),
+                                    new PushTaskTO(),
+                                    anyTO.getType(),
+                                    anyTO.getKey(),
+                                    true,
+                                    multiLevelPanelRef,
+                                    pageRef),
+                            target);
                     target.add(multiLevelPanelRef);
                     AnyStatusDirectoryPanel.this.getTogglePanel().close(target);
                 }
@@ -231,15 +226,15 @@ public class AnyStatusDirectoryPanel
                 @Override
                 public void onClick(final AjaxRequestTarget target, final StatusBean bean) {
                     multiLevelPanelRef.next("PULL " + bean.getResource(),
-                        new ReconTaskPanel(
-                            bean.getResource(),
-                            new PullTaskTO(),
-                            anyTO.getType(),
-                            anyTO.getKey(),
-                            true,
-                            multiLevelPanelRef,
-                            pageRef),
-                        target);
+                            new ReconTaskPanel(
+                                    bean.getResource(),
+                                    new PullTaskTO(),
+                                    anyTO.getType(),
+                                    anyTO.getKey(),
+                                    true,
+                                    multiLevelPanelRef,
+                                    pageRef),
+                            target);
                     target.add(multiLevelPanelRef);
                     AnyStatusDirectoryPanel.this.getTogglePanel().close(target);
                 }
@@ -256,17 +251,17 @@ public class AnyStatusDirectoryPanel
 
                 panel.add(new ActionLink<>() {
 
-                              private static final long serialVersionUID = 5168094747477174155L;
-
-                              @Override
-                              public void onClick(final AjaxRequestTarget target, final StatusBean bean) {
-                                  multiLevelPanelRef.next("ACCOUNTS",
-                                      new LinkedAccountsStatusModalPanel(Model.of(UserTO.class.cast(anyTO)), pageRef),
-                                      target);
-                                  target.add(multiLevelPanelRef);
-                                  AnyStatusDirectoryPanel.this.getTogglePanel().close(target);
-                              }
-                          }, ActionLink.ActionType.MANAGE_ACCOUNTS,
+                    private static final long serialVersionUID = 5168094747477174155L;
+
+                    @Override
+                    public void onClick(final AjaxRequestTarget target, final StatusBean bean) {
+                        multiLevelPanelRef.next("ACCOUNTS",
+                                new LinkedAccountsStatusModalPanel(Model.of(UserTO.class.cast(anyTO)), pageRef),
+                                target);
+                        target.add(multiLevelPanelRef);
+                        AnyStatusDirectoryPanel.this.getTogglePanel().close(target);
+                    }
+                }, ActionLink.ActionType.MANAGE_ACCOUNTS,
                         String.format("%s,%s,%s", IdRepoEntitlement.USER_READ, IdRepoEntitlement.USER_UPDATE,
                                 IdMEntitlement.RESOURCE_GET_CONNOBJECT));
             }
diff --git a/client/idm/console/src/main/java/org/apache/syncope/client/console/status/AnyStatusModal.java b/client/idm/console/src/main/java/org/apache/syncope/client/console/status/AnyStatusModal.java
index 6bb17a63b1..166996da5b 100644
--- a/client/idm/console/src/main/java/org/apache/syncope/client/console/status/AnyStatusModal.java
+++ b/client/idm/console/src/main/java/org/apache/syncope/client/console/status/AnyStatusModal.java
@@ -21,7 +21,6 @@ package org.apache.syncope.client.console.status;
 import org.apache.syncope.client.console.panels.DirectoryPanel;
 import org.apache.syncope.client.console.panels.MultilevelPanel;
 import org.apache.syncope.client.console.rest.AbstractAnyRestClient;
-import org.apache.syncope.client.console.wicket.markup.html.bootstrap.dialog.BaseModal;
 import org.apache.syncope.client.ui.commons.DirectoryDataProvider;
 import org.apache.syncope.client.ui.commons.status.StatusBean;
 import org.apache.syncope.common.lib.to.AnyTO;
@@ -32,25 +31,23 @@ public class AnyStatusModal<T extends AnyTO> extends StatusModal<T> {
     private static final long serialVersionUID = 1066124171682570080L;
 
     public AnyStatusModal(
-            final BaseModal<?> baseModal,
             final PageReference pageReference,
             final T anyTO,
             final String itemKeyFieldName,
             final boolean statusOnly) {
 
-        super(baseModal, pageReference, anyTO, itemKeyFieldName, statusOnly);
+        super(pageReference, anyTO, itemKeyFieldName, statusOnly);
     }
 
     @Override
     protected DirectoryPanel<
         StatusBean, StatusBean, DirectoryDataProvider<StatusBean>, AbstractAnyRestClient<?>> getStatusDirectoryPanel(
             final MultilevelPanel mlp,
-            final BaseModal<?> baseModal,
             final PageReference pageReference,
             final T entity,
             final String itemKeyFieldName,
             final boolean statusOnly) {
 
-        return new AnyStatusDirectoryPanel(baseModal, mlp, pageReference, entity, itemKeyFieldName, statusOnly);
+        return new AnyStatusDirectoryPanel(mlp, pageReference, entity, itemKeyFieldName, statusOnly);
     }
 }
diff --git a/client/idm/console/src/main/java/org/apache/syncope/client/console/status/ResourceStatusDirectoryPanel.java b/client/idm/console/src/main/java/org/apache/syncope/client/console/status/ResourceStatusDirectoryPanel.java
index 3bb36d2b48..bff2bbba54 100644
--- a/client/idm/console/src/main/java/org/apache/syncope/client/console/status/ResourceStatusDirectoryPanel.java
+++ b/client/idm/console/src/main/java/org/apache/syncope/client/console/status/ResourceStatusDirectoryPanel.java
@@ -33,7 +33,6 @@ import org.apache.syncope.client.console.rest.AbstractAnyRestClient;
 import org.apache.syncope.client.console.rest.AnyObjectRestClient;
 import org.apache.syncope.client.console.rest.GroupRestClient;
 import org.apache.syncope.client.console.rest.UserRestClient;
-import org.apache.syncope.client.console.wicket.markup.html.bootstrap.dialog.BaseModal;
 import org.apache.syncope.client.console.wicket.markup.html.form.ActionLink;
 import org.apache.syncope.client.console.wicket.markup.html.form.ActionsPanel;
 import org.apache.syncope.client.lib.SyncopeClient;
@@ -65,8 +64,6 @@ public class ResourceStatusDirectoryPanel
 
     private static final long serialVersionUID = -9148734710505211261L;
 
-    private final BaseModal<?> baseModal;
-
     private final MultilevelPanel multiLevelPanelRef;
 
     private String type;
@@ -74,14 +71,12 @@ public class ResourceStatusDirectoryPanel
     private final ResourceTO resource;
 
     public ResourceStatusDirectoryPanel(
-            final BaseModal<?> baseModal,
             final MultilevelPanel multiLevelPanelRef,
             final PageReference pageRef,
             final String type,
             final ResourceTO resource) {
 
         super(MultilevelPanel.FIRST_LEVEL_ID, pageRef);
-        this.baseModal = baseModal;
         this.multiLevelPanelRef = multiLevelPanelRef;
         this.type = type;
         this.resource = resource;
@@ -92,7 +87,7 @@ public class ResourceStatusDirectoryPanel
 
     @Override
     protected void resultTableCustomChanges(final AjaxDataTablePanel.Builder<StatusBean, String> resultTableBuilder) {
-        resultTableBuilder.setMultiLevelPanel(baseModal, multiLevelPanelRef);
+        resultTableBuilder.setMultiLevelPanel(multiLevelPanelRef);
     }
 
     @Override
@@ -121,8 +116,8 @@ public class ResourceStatusDirectoryPanel
             @Override
             public void onClick(final AjaxRequestTarget target, final StatusBean bean) {
                 multiLevelPanelRef.next(bean.getResource(),
-                    new ReconStatusPanel(bean.getResource(), type, bean.getKey()),
-                    target);
+                        new ReconStatusPanel(bean.getResource(), type, bean.getKey()),
+                        target);
                 target.add(multiLevelPanelRef);
                 getTogglePanel().close(target);
             }
@@ -140,15 +135,15 @@ public class ResourceStatusDirectoryPanel
             @Override
             public void onClick(final AjaxRequestTarget target, final StatusBean bean) {
                 multiLevelPanelRef.next("PUSH " + bean.getResource(),
-                    new ReconTaskPanel(
-                        bean.getResource(),
-                        new PushTaskTO(),
-                        type,
-                        bean.getKey(),
-                        true,
-                        multiLevelPanelRef,
-                        pageRef),
-                    target);
+                        new ReconTaskPanel(
+                                bean.getResource(),
+                                new PushTaskTO(),
+                                type,
+                                bean.getKey(),
+                                true,
+                                multiLevelPanelRef,
+                                pageRef),
+                        target);
                 target.add(multiLevelPanelRef);
                 getTogglePanel().close(target);
             }
@@ -166,15 +161,15 @@ public class ResourceStatusDirectoryPanel
             @Override
             public void onClick(final AjaxRequestTarget target, final StatusBean bean) {
                 multiLevelPanelRef.next("PULL " + bean.getResource(),
-                    new ReconTaskPanel(
-                        bean.getResource(),
-                        new PullTaskTO(),
-                        type,
-                        bean.getKey(),
-                        true,
-                        multiLevelPanelRef,
-                        pageRef),
-                    target);
+                        new ReconTaskPanel(
+                                bean.getResource(),
+                                new PullTaskTO(),
+                                type,
+                                bean.getKey(),
+                                true,
+                                multiLevelPanelRef,
+                                pageRef),
+                        target);
                 target.add(multiLevelPanelRef);
                 getTogglePanel().close(target);
             }
diff --git a/client/idm/console/src/main/java/org/apache/syncope/client/console/status/ResourceStatusModal.java b/client/idm/console/src/main/java/org/apache/syncope/client/console/status/ResourceStatusModal.java
index 0399dc1f30..3ab581b962 100644
--- a/client/idm/console/src/main/java/org/apache/syncope/client/console/status/ResourceStatusModal.java
+++ b/client/idm/console/src/main/java/org/apache/syncope/client/console/status/ResourceStatusModal.java
@@ -24,7 +24,6 @@ import org.apache.syncope.client.console.panels.DirectoryPanel;
 import org.apache.syncope.client.console.panels.MultilevelPanel;
 import org.apache.syncope.client.console.rest.AbstractAnyRestClient;
 import org.apache.syncope.client.console.rest.AnyTypeRestClient;
-import org.apache.syncope.client.console.wicket.markup.html.bootstrap.dialog.BaseModal;
 import org.apache.syncope.client.ui.commons.Constants;
 import org.apache.syncope.client.ui.commons.DirectoryDataProvider;
 import org.apache.syncope.client.ui.commons.markup.html.form.AjaxDropDownChoicePanel;
@@ -43,11 +42,10 @@ public class ResourceStatusModal extends StatusModal<ResourceTO> {
     private Model<String> typeModel = new Model<>();
 
     public ResourceStatusModal(
-            final BaseModal<?> baseModal,
-            final PageReference pageReference,
+            final PageReference pageRef,
             final ResourceTO resource) {
 
-        super(baseModal, pageReference, resource, null, false);
+        super(pageRef, resource, null, false);
 
         List<String> availableAnyTypes = resource.getProvisions().stream().
                 map(Provision::getAnyType).
@@ -76,12 +74,11 @@ public class ResourceStatusModal extends StatusModal<ResourceTO> {
     protected DirectoryPanel<
         StatusBean, StatusBean, DirectoryDataProvider<StatusBean>, AbstractAnyRestClient<?>> getStatusDirectoryPanel(
             final MultilevelPanel mlp,
-            final BaseModal<?> baseModal,
             final PageReference pageReference,
             final ResourceTO entity,
             final String itemKeyFieldName,
             final boolean statusOnly) {
 
-        return new ResourceStatusDirectoryPanel(baseModal, mlp, pageReference, null, entity);
+        return new ResourceStatusDirectoryPanel(mlp, pageReference, null, entity);
     }
 }
diff --git a/client/idm/console/src/main/java/org/apache/syncope/client/console/status/StatusModal.java b/client/idm/console/src/main/java/org/apache/syncope/client/console/status/StatusModal.java
index 7aaf0794d3..a723ffaea3 100644
--- a/client/idm/console/src/main/java/org/apache/syncope/client/console/status/StatusModal.java
+++ b/client/idm/console/src/main/java/org/apache/syncope/client/console/status/StatusModal.java
@@ -36,7 +36,6 @@ public abstract class StatusModal<T extends EntityTO> extends Panel implements M
     protected final DirectoryPanel<StatusBean, StatusBean, ?, ?> directoryPanel;
 
     public StatusModal(
-            final BaseModal<?> baseModal,
             final PageReference pageReference,
             final T entity,
             final String itemKeyFieldName,
@@ -44,17 +43,15 @@ public abstract class StatusModal<T extends EntityTO> extends Panel implements M
 
         super(BaseModal.CONTENT_ID);
 
-        final MultilevelPanel mlp = new MultilevelPanel("status");
+        MultilevelPanel mlp = new MultilevelPanel("status");
         mlp.setOutputMarkupId(true);
-        this.directoryPanel = getStatusDirectoryPanel(
-                mlp, baseModal, pageReference, entity, itemKeyFieldName, statusOnly);
-        add(mlp.setFirstLevel(this.directoryPanel));
+        directoryPanel = getStatusDirectoryPanel(mlp, pageReference, entity, itemKeyFieldName, statusOnly);
+        add(mlp.setFirstLevel(directoryPanel));
     }
 
     protected abstract DirectoryPanel<
         StatusBean, StatusBean, DirectoryDataProvider<StatusBean>, AbstractAnyRestClient<?>> getStatusDirectoryPanel(
             MultilevelPanel mlp,
-            BaseModal<?> baseModal,
             PageReference pageReference,
             T entity,
             String itemKeyFieldName,
diff --git a/client/idm/console/src/main/java/org/apache/syncope/client/console/topology/TabularTopology.java b/client/idm/console/src/main/java/org/apache/syncope/client/console/topology/TabularTopology.java
index 946e5d445f..02ae460c80 100644
--- a/client/idm/console/src/main/java/org/apache/syncope/client/console/topology/TabularTopology.java
+++ b/client/idm/console/src/main/java/org/apache/syncope/client/console/topology/TabularTopology.java
@@ -18,9 +18,7 @@
  */
 package org.apache.syncope.client.console.topology;
 
-import de.agilecoders.wicket.core.markup.html.bootstrap.dialog.Modal;
 import de.agilecoders.wicket.core.markup.html.bootstrap.tabs.AjaxBootstrapTabbedPanel;
-import java.io.Serializable;
 import java.util.ArrayList;
 import java.util.List;
 import org.apache.syncope.client.console.annotations.IdMPage;
@@ -28,9 +26,6 @@ import org.apache.syncope.client.console.pages.BasePage;
 import org.apache.syncope.client.console.pages.Connectors;
 import org.apache.syncope.client.console.pages.Resources;
 import org.apache.syncope.client.console.panels.ConnidLocations;
-import org.apache.syncope.client.console.tasks.SchedTasks;
-import org.apache.syncope.client.console.wicket.markup.html.bootstrap.dialog.BaseModal;
-import org.apache.syncope.client.ui.commons.Constants;
 import org.apache.syncope.common.lib.types.IdMEntitlement;
 import org.apache.wicket.extensions.markup.html.tabs.AbstractTab;
 import org.apache.wicket.extensions.markup.html.tabs.ITab;
@@ -54,28 +49,7 @@ public class TabularTopology extends BasePage {
     }
 
     protected List<ITab> buildTabList() {
-        final List<ITab> tabs = new ArrayList<>();
-
-        tabs.add(new AbstractTab(new Model<>("CustomTasks")) {
-
-            private static final long serialVersionUID = -6815067322125799251L;
-
-            @Override
-            public Panel getPanel(final String panelId) {
-                BaseModal<Serializable> schedTaskModal = new BaseModal<>(Constants.OUTER) {
-
-                    private static final long serialVersionUID = -1673561782333149836L;
-
-                    @Override
-                    protected void onConfigure() {
-                        super.onConfigure();
-                        setFooterVisible(false);
-                    }
-                };
-                schedTaskModal.size(Modal.Size.Large);
-                return new SchedTasks(schedTaskModal, getPageReference(), true, panelId);
-            }
-        });
+        List<ITab> tabs = new ArrayList<>();
 
         tabs.add(new AbstractTab(new Model<>("Resources")) {
 
diff --git a/client/idm/console/src/main/java/org/apache/syncope/client/console/topology/TopologyTogglePanel.java b/client/idm/console/src/main/java/org/apache/syncope/client/console/topology/TopologyTogglePanel.java
index a45072f82c..aeb8ec3e64 100644
--- a/client/idm/console/src/main/java/org/apache/syncope/client/console/topology/TopologyTogglePanel.java
+++ b/client/idm/console/src/main/java/org/apache/syncope/client/console/topology/TopologyTogglePanel.java
@@ -33,7 +33,6 @@ import org.apache.syncope.client.console.status.ResourceStatusModal;
 import org.apache.syncope.client.console.tasks.PropagationTasks;
 import org.apache.syncope.client.console.tasks.PullTasks;
 import org.apache.syncope.client.console.tasks.PushTasks;
-import org.apache.syncope.client.console.tasks.SchedTasks;
 import org.apache.syncope.client.console.wicket.markup.html.bootstrap.dialog.BaseModal;
 import org.apache.syncope.client.console.wicket.markup.html.form.IndicatingOnConfirmAjaxLink;
 import org.apache.syncope.client.console.wizards.resources.ConnectorWizardBuilder;
@@ -62,7 +61,6 @@ import org.apache.wicket.markup.html.panel.Fragment;
 import org.apache.wicket.model.CompoundPropertyModel;
 import org.apache.wicket.model.IModel;
 import org.apache.wicket.model.Model;
-import org.apache.wicket.model.ResourceModel;
 import org.apache.wicket.model.StringResourceModel;
 
 public class TopologyTogglePanel extends TogglePanel<Serializable> {
@@ -190,25 +188,6 @@ public class TopologyTogglePanel extends TogglePanel<Serializable> {
         fragment.add(reload);
         MetaDataRoleAuthorizationStrategy.authorize(reload, RENDER, IdMEntitlement.CONNECTOR_RELOAD);
 
-        AjaxLink<String> tasks = new IndicatingAjaxLink<>("tasks") {
-
-            private static final long serialVersionUID = 3776750333491622263L;
-
-            @Override
-            public void onClick(final AjaxRequestTarget target) {
-                target.add(schedTaskModal.setContent(new SchedTasks(schedTaskModal, pageRef)));
-                schedTaskModal.header(new ResourceModel("task.custom.list"));
-                schedTaskModal.show(true);
-            }
-
-            @Override
-            public String getAjaxIndicatorMarkupId() {
-                return Constants.VEIL_INDICATOR_MARKUP_ID;
-            }
-        };
-        fragment.add(tasks);
-        MetaDataRoleAuthorizationStrategy.authorize(tasks, RENDER, IdRepoEntitlement.TASK_LIST);
-
         return fragment;
     }
 
@@ -313,8 +292,8 @@ public class TopologyTogglePanel extends TogglePanel<Serializable> {
                         build(BaseModal.CONTENT_ID,
                                 SyncopeConsoleSession.get().
                                         owns(IdMEntitlement.CONNECTOR_UPDATE, connInstance.getAdminRealm())
-                                        ? AjaxWizard.Mode.EDIT
-                                        : AjaxWizard.Mode.READONLY)));
+                                ? AjaxWizard.Mode.EDIT
+                                : AjaxWizard.Mode.READONLY)));
 
                 modal.header(
                         new Model<>(MessageFormat.format(getString("connector.edit"), connInstance.getDisplayName())));
@@ -419,8 +398,8 @@ public class TopologyTogglePanel extends TogglePanel<Serializable> {
                         build(BaseModal.CONTENT_ID,
                                 SyncopeConsoleSession.get().
                                         owns(IdMEntitlement.RESOURCE_UPDATE, connInstance.getAdminRealm())
-                                        ? AjaxWizard.Mode.EDIT
-                                        : AjaxWizard.Mode.READONLY)));
+                                ? AjaxWizard.Mode.EDIT
+                                : AjaxWizard.Mode.READONLY)));
 
                 modal.header(new Model<>(MessageFormat.format(getString("resource.edit"), node.getKey())));
                 modal.show(true);
@@ -441,8 +420,7 @@ public class TopologyTogglePanel extends TogglePanel<Serializable> {
             @Override
             public void onClick(final AjaxRequestTarget target) {
                 ResourceTO modelObject = ResourceRestClient.read(node.getKey());
-                target.add(propTaskModal.setContent(
-                        new ResourceStatusModal(propTaskModal, pageRef, modelObject)));
+                target.add(propTaskModal.setContent(new ResourceStatusModal(pageRef, modelObject)));
                 propTaskModal.header(
                         new Model<>(MessageFormat.format(getString("resource.reconciliation"), node.getKey())));
                 propTaskModal.show(true);
@@ -540,7 +518,7 @@ public class TopologyTogglePanel extends TogglePanel<Serializable> {
 
             @Override
             public void onClick(final AjaxRequestTarget target) {
-                target.add(schedTaskModal.setContent(new PullTasks(schedTaskModal, pageRef, node.getKey())));
+                target.add(schedTaskModal.setContent(new PullTasks(schedTaskModal, node.getKey(), pageRef)));
                 schedTaskModal.header(new Model<>(MessageFormat.format(getString("task.pull.list"), node.getKey())));
                 schedTaskModal.show(true);
             }
@@ -559,7 +537,7 @@ public class TopologyTogglePanel extends TogglePanel<Serializable> {
 
             @Override
             public void onClick(final AjaxRequestTarget target) {
-                target.add(schedTaskModal.setContent(new PushTasks(schedTaskModal, pageRef, node.getKey())));
+                target.add(schedTaskModal.setContent(new PushTasks(schedTaskModal, node.getKey(), pageRef)));
                 schedTaskModal.header(new Model<>(MessageFormat.format(getString("task.push.list"), node.getKey())));
                 schedTaskModal.show(true);
             }
diff --git a/client/idm/console/src/main/resources/org/apache/syncope/client/console/topology/TopologyTogglePanel.html b/client/idm/console/src/main/resources/org/apache/syncope/client/console/topology/TopologyTogglePanel.html
index 8794ecb5d9..64be1d7e3b 100644
--- a/client/idm/console/src/main/resources/org/apache/syncope/client/console/topology/TopologyTogglePanel.html
+++ b/client/idm/console/src/main/resources/org/apache/syncope/client/console/topology/TopologyTogglePanel.html
@@ -94,9 +94,6 @@ under the License.
         <wicket:enclosure child="reload">
           <li><a href="#" wicket:id="reload"><i class="fa fa-sync"></i><wicket:message key="connectors.reload"/></a></li>
         </wicket:enclosure>
-        <wicket:enclosure child="tasks">
-          <li><a href="#" wicket:id="tasks"><i class="fa fa-tasks"></i><wicket:message key="task.custom.list"/></a></li>
-        </wicket:enclosure>
       </ul>
     </wicket:fragment>
 
diff --git a/client/idm/console/src/main/resources/org/apache/syncope/client/console/topology/TopologyTogglePanel.properties b/client/idm/console/src/main/resources/org/apache/syncope/client/console/topology/TopologyTogglePanel.properties
index 22cce2d2ec..6ade698bec 100644
--- a/client/idm/console/src/main/resources/org/apache/syncope/client/console/topology/TopologyTogglePanel.properties
+++ b/client/idm/console/src/main/resources/org/apache/syncope/client/console/topology/TopologyTogglePanel.properties
@@ -31,8 +31,6 @@ resource.menu.provision=Edit provision rules
 resource.menu.explore=Explore resource
 resource.menu.history=Configuration history
 resource.menu.clone=Clone resource
-
-task.custom.list=Custom tasks
 task.propagation.list=Propagation tasks {0}
 task.pull.list=Pull tasks {0}
 task.push.list=Push tasks {0}
diff --git a/client/idm/console/src/main/resources/org/apache/syncope/client/console/topology/TopologyTogglePanel_fr_CA.properties b/client/idm/console/src/main/resources/org/apache/syncope/client/console/topology/TopologyTogglePanel_fr_CA.properties
index 838f3b30f6..fde8651859 100644
--- a/client/idm/console/src/main/resources/org/apache/syncope/client/console/topology/TopologyTogglePanel_fr_CA.properties
+++ b/client/idm/console/src/main/resources/org/apache/syncope/client/console/topology/TopologyTogglePanel_fr_CA.properties
@@ -26,18 +26,17 @@ resource.clone=Cloner la ressource {0}
 resource.menu.add=Ajouter une nouvelle ressource
 resource.menu.remove=Supprimer une ressource
 resource.menu.edit=Modifier la ressource
-resource.menu.provision=Modifier les r�gles de provision
+resource.menu.provision=Modifier les r\u00e8gles de provision
 resource.menu.explore=Explorer la ressource
 resource.menu.history=Historique de configuration
 resource.menu.clone=Cloner la ressource
-task.custom.list=T�ches personnalis�es
-task.propagation.list=T�ches de propagation {0}
-task.pull.list=T�ches de traction {0}
-task.push.list=T�ches de pouss�e {0}
+task.propagation.list=T\u00e2ches de propagation {0}
+task.pull.list=T\u00e2ches de traction {0}
+task.push.list=T\u00e2ches de pouss\u00e9e {0}
 resource.explore.list=Explorer ${key}
 connectors.reload=Recharger tous les connecteurs
-resource.reconciliation=R�conciliation {0}
+resource.reconciliation=R\u00e9conciliation {0}
 resource.menu.reconciliation=Rapprochement
-resource.menu.push.list=T�ches de pouss�e
-resource.menu.pull.list=T�ches d'extraction
-resource.menu.propagation.list=T�ches de propagation
+resource.menu.push.list=T\u00e2ches de pouss\u00e9e
+resource.menu.pull.list=T\u00e2ches d'extraction
+resource.menu.propagation.list=T\u00e2ches de propagation
diff --git a/client/idm/console/src/main/resources/org/apache/syncope/client/console/topology/TopologyTogglePanel_it.properties b/client/idm/console/src/main/resources/org/apache/syncope/client/console/topology/TopologyTogglePanel_it.properties
index ec33424648..618ad09663 100644
--- a/client/idm/console/src/main/resources/org/apache/syncope/client/console/topology/TopologyTogglePanel_it.properties
+++ b/client/idm/console/src/main/resources/org/apache/syncope/client/console/topology/TopologyTogglePanel_it.properties
@@ -31,8 +31,6 @@ resource.menu.provision=Modifica regole di provisioning
 resource.menu.explore=Esplora risorsa
 resource.menu.history=Storico delle configurazioni
 resource.menu.clone=Duplica risorsa
-
-task.custom.list=Task personalizzati
 task.propagation.list=Task di propagazione {0}
 task.pull.list=Pull task {0}
 task.push.list=Push task {0}
diff --git a/client/idm/console/src/main/resources/org/apache/syncope/client/console/topology/TopologyTogglePanel_ja.properties b/client/idm/console/src/main/resources/org/apache/syncope/client/console/topology/TopologyTogglePanel_ja.properties
index 0b70bf0e62..befad7fc37 100644
--- a/client/idm/console/src/main/resources/org/apache/syncope/client/console/topology/TopologyTogglePanel_ja.properties
+++ b/client/idm/console/src/main/resources/org/apache/syncope/client/console/topology/TopologyTogglePanel_ja.properties
@@ -31,8 +31,6 @@ resource.menu.provision=\u30d7\u30ed\u30d3\u30b8\u30e7\u30f3\u30eb\u30fc\u30eb\u
 resource.menu.explore=\u30ea\u30bd\u30fc\u30b9\u3092\u63a2\u7d22
 resource.menu.history=\u8a2d\u5b9a\u5c65\u6b74
 resource.menu.clone=\u30ea\u30bd\u30fc\u30b9\u3092\u8907\u88fd
-
-task.custom.list=\u30ab\u30b9\u30bf\u30e0\u30bf\u30b9\u30af
 task.propagation.list=\u4f1d\u64ad\u30bf\u30b9\u30af {0}
 task.pull.list=\u30d7\u30eb\u30bf\u30b9\u30af {0}
 task.push.list=\u30d7\u30c3\u30b7\u30e5\u30bf\u30b9\u30af {0}
diff --git a/client/idm/console/src/main/resources/org/apache/syncope/client/console/topology/TopologyTogglePanel_pt_BR.properties b/client/idm/console/src/main/resources/org/apache/syncope/client/console/topology/TopologyTogglePanel_pt_BR.properties
index ee13fed7d8..7bf53cd4e6 100644
--- a/client/idm/console/src/main/resources/org/apache/syncope/client/console/topology/TopologyTogglePanel_pt_BR.properties
+++ b/client/idm/console/src/main/resources/org/apache/syncope/client/console/topology/TopologyTogglePanel_pt_BR.properties
@@ -31,8 +31,6 @@ resource.menu.provision=Alterar regras de provision
 resource.menu.explore=Explorar recurso
 resource.menu.history=Hist\u00f3rico de configura\u00e7\u00e3o
 resource.menu.clone=Clone recurso
-
-task.custom.list=Custom tasks
 task.propagation.list=Propagation tasks {0}
 task.pull.list=Pull tasks {0}
 task.push.list=Push tasks {0}
diff --git a/client/idm/console/src/main/resources/org/apache/syncope/client/console/topology/TopologyTogglePanel_ru.properties b/client/idm/console/src/main/resources/org/apache/syncope/client/console/topology/TopologyTogglePanel_ru.properties
index fc2b246ec0..b017f0ecf3 100644
--- a/client/idm/console/src/main/resources/org/apache/syncope/client/console/topology/TopologyTogglePanel_ru.properties
+++ b/client/idm/console/src/main/resources/org/apache/syncope/client/console/topology/TopologyTogglePanel_ru.properties
@@ -32,8 +32,6 @@ resource.menu.provision=\u0418\u0437\u043c\u0435\u043d\u0438\u0442\u044c \u043f\
 resource.menu.explore=\u041f\u0440\u043e\u0441\u043c\u043e\u0442\u0440 \u0440\u0435\u0441\u0443\u0440\u0441\u0430
 resource.menu.history=\u0418\u0441\u0442\u043e\u0440\u0438\u044f \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438
 resource.menu.clone=\u0414\u0443\u0431\u043b\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0440\u0435\u0441\u0443\u0440\u0441
-
-task.custom.list=\u041f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0435 \u0437\u0430\u0434\u0430\u0447\u0438
 task.propagation.list=\u0417\u0430\u0434\u0430\u0447\u0438 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0439 {0}
 task.pull.list=\u0417\u0430\u0434\u0430\u0447\u0438 \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0434\u0430\u043d\u043d\u044b\u0445 {0}
 task.push.list=\u0417\u0430\u0434\u0430\u0447\u0438 \u043f\u0435\u0440\u0435\u0434\u0430\u0447\u0438 \u0434\u0430\u043d\u043d\u044b\u0445 {0}
diff --git a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/audit/AuditHistoryDetails.java b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/audit/AuditHistoryDetails.java
index 713727c8f2..cf538151ac 100644
--- a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/audit/AuditHistoryDetails.java
+++ b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/audit/AuditHistoryDetails.java
@@ -74,6 +74,7 @@ public abstract class AuditHistoryDetails<T extends Serializable> extends Panel
     private static final SortParam<String> REST_SORT = new SortParam<>("event_date", false);
 
     private EntityTO currentEntity;
+
     private AuditElements.EventCategoryType type;
 
     private String category;
@@ -90,7 +91,7 @@ public abstract class AuditHistoryDetails<T extends Serializable> extends Panel
 
     private AjaxDropDownChoicePanel<AuditEntry> afterVersionsPanel;
 
-    private AjaxLink<Void> restore;
+    private final AjaxLink<Void> restore;
 
     private static class SortingNodeFactory extends JsonNodeFactory {
 
@@ -156,6 +157,7 @@ public abstract class AuditHistoryDetails<T extends Serializable> extends Panel
             registerModule(new SimpleModule().addSerializer(new SortedSetJsonSerializer(cast(Set.class)))).
             registerModule(new JavaTimeModule());
 
+    @SuppressWarnings("unchecked")
     public AuditHistoryDetails(
             final String id,
             final EntityTO currentEntity,
@@ -187,10 +189,10 @@ public abstract class AuditHistoryDetails<T extends Serializable> extends Panel
 
             @Override
             public AuditEntry getObject(final String id, final IModel<? extends List<? extends AuditEntry>> choices) {
-                return choices.getObject().stream()
-                        .filter(c -> StringUtils.isNotBlank(id)
-                                && Long.valueOf(id) == c.getDate().toInstant().toEpochMilli()).findFirst()
-                        .orElse(null);
+                return choices.getObject().stream().
+                        filter(c -> StringUtils.isNotBlank(id)
+                        && Long.parseLong(id) == c.getDate().toInstant().toEpochMilli()).
+                        findFirst().orElse(null);
             }
         };
         // add also select to choose with which version compare
@@ -213,10 +215,10 @@ public abstract class AuditHistoryDetails<T extends Serializable> extends Panel
                 AuditHistoryDetails.this.addOrReplace(new JsonDiffPanel(toJSON(beforeEntry, reference),
                         toJSON(afterEntry, reference)));
                 // change after audit entries in order to match only the ones newer than the current after one
-                afterVersionsPanel.setChoices(auditEntries.stream().filter(ae ->
-                                ae.getDate().isAfter(beforeEntry.getDate())
-                                        || ae.getDate().isEqual(beforeEntry.getDate()))
-                        .collect(Collectors.toList()));
+                afterVersionsPanel.setChoices(auditEntries.stream().
+                        filter(ae -> ae.getDate().isAfter(beforeEntry.getDate())
+                        || ae.getDate().isEqual(beforeEntry.getDate())).
+                        collect(Collectors.toList()));
                 // set the new after entry
                 afterVersionsPanel.setModelObject(afterEntry);
                 target.add(AuditHistoryDetails.this);
@@ -232,13 +234,13 @@ public abstract class AuditHistoryDetails<T extends Serializable> extends Panel
 
             @Override
             protected void onEvent(final AjaxRequestTarget target) {
-                AuditHistoryDetails.this.addOrReplace(
-                        new JsonDiffPanel(toJSON(beforeVersionsPanel.getModelObject() == null
+                AuditHistoryDetails.this.addOrReplace(new JsonDiffPanel(
+                        toJSON(beforeVersionsPanel.getModelObject() == null
                                 ? latestAuditEntry
                                 : beforeVersionsPanel.getModelObject(), reference),
-                                toJSON(afterVersionsPanel.getModelObject() == null
-                                        ? after
-                                        : buildAfterAuditEntry(afterVersionsPanel.getModelObject()), reference)));
+                        toJSON(afterVersionsPanel.getModelObject() == null
+                                ? after
+                                : buildAfterAuditEntry(afterVersionsPanel.getModelObject()), reference)));
                 target.add(AuditHistoryDetails.this);
             }
         });
@@ -268,7 +270,7 @@ public abstract class AuditHistoryDetails<T extends Serializable> extends Panel
         };
         MetaDataRoleAuthorizationStrategy.authorize(restore, ENABLE, auditRestoreEntitlement);
         add(restore);
-        
+
         initDiff();
     }
 
@@ -294,9 +296,9 @@ public abstract class AuditHistoryDetails<T extends Serializable> extends Panel
         addOrReplace(new JsonDiffPanel(toJSON(latestAuditEntry, reference), toJSON(after, reference)));
 
         beforeVersionsPanel.setChoices(auditEntries);
-        afterVersionsPanel.setChoices(auditEntries.stream().filter(ae ->
-                ae.getDate().isAfter(after.getDate()) || ae.getDate().isEqual(after.getDate())).collect(
-                Collectors.toList()));
+        afterVersionsPanel.setChoices(auditEntries.stream().
+                filter(ae -> ae.getDate().isAfter(after.getDate()) || ae.getDate().isEqual(after.getDate())).
+                collect(Collectors.toList()));
 
         beforeVersionsPanel.setModelObject(latestAuditEntry);
         afterVersionsPanel.setModelObject(after);
diff --git a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/audit/AuditHistoryModal.java b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/audit/AuditHistoryModal.java
index ccbf968ddf..5f6c181315 100644
--- a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/audit/AuditHistoryModal.java
+++ b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/audit/AuditHistoryModal.java
@@ -37,7 +37,7 @@ public abstract class AuditHistoryModal<T extends EntityTO> extends Panel implem
 
         super(BaseModal.CONTENT_ID);
 
-        add(new AuditHistoryDetails(
+        add(new AuditHistoryDetails<>(
                 "history",
                 entity,
                 type,
diff --git a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/commons/IdRepoConstants.java b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/commons/IdRepoConstants.java
index ff5b475e59..6af40b7ff1 100644
--- a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/commons/IdRepoConstants.java
+++ b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/commons/IdRepoConstants.java
@@ -66,6 +66,8 @@ public final class IdRepoConstants {
 
     public static final String PREF_ACCESS_TOKEN_PAGINATOR_ROWS = "accessToken.paginator.rows";
 
+    public static final String PREF_COMMAND_PAGINATOR_ROWS = "command.paginator.rows";
+
     public static final String PREF_AUDIT_HISTORY_PAGINATOR_ROWS = "audit.history.paginator.rows";
 
     public static final String PREF_NOTIFICATION_PAGINATOR_ROWS = "notification.paginator.rows";
diff --git a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/commons/IdRepoImplementationInfoProvider.java b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/commons/IdRepoImplementationInfoProvider.java
index dd12dffa52..121c9fd2c2 100644
--- a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/commons/IdRepoImplementationInfoProvider.java
+++ b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/commons/IdRepoImplementationInfoProvider.java
@@ -123,6 +123,10 @@ public class IdRepoImplementationInfoProvider implements ImplementationInfoProvi
                 templateClassName = "MyItemTransformer";
                 break;
 
+            case IdRepoImplementationType.COMMAND:
+                templateClassName = "MyCommand";
+                break;
+
             default:
         }
 
@@ -163,7 +167,7 @@ public class IdRepoImplementationInfoProvider implements ImplementationInfoProvi
             @Override
             protected List<String> load() {
                 return ImplementationRestClient.list(IdRepoImplementationType.TASKJOB_DELEGATE).stream().
-                    map(ImplementationTO::getKey).sorted().collect(Collectors.toList());
+                        map(ImplementationTO::getKey).sorted().collect(Collectors.toList());
             }
         };
     }
diff --git a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/panels/ModalDirectoryPanel.java b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/commons/KeywordSearchEvent.java
similarity index 58%
copy from client/idrepo/console/src/main/java/org/apache/syncope/client/console/panels/ModalDirectoryPanel.java
copy to client/idrepo/console/src/main/java/org/apache/syncope/client/console/commons/KeywordSearchEvent.java
index 6d524c537c..e35627bd83 100644
--- a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/panels/ModalDirectoryPanel.java
+++ b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/commons/KeywordSearchEvent.java
@@ -16,20 +16,29 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.syncope.client.console.panels;
+package org.apache.syncope.client.console.commons;
 
 import java.io.Serializable;
-import org.apache.syncope.client.console.wicket.markup.html.bootstrap.dialog.BaseModal;
-import org.apache.wicket.PageReference;
+import org.apache.wicket.ajax.AjaxRequestTarget;
 
-public class ModalDirectoryPanel<T extends Serializable> extends AbstractModalPanel<T> {
+public class KeywordSearchEvent implements Serializable {
 
-    private static final long serialVersionUID = 1L;
+    private static final long serialVersionUID = 203866008113326777L;
 
-    public ModalDirectoryPanel(
-            final BaseModal<T> modal, final DirectoryPanel<?, ?, ?, ?> directoryPanel, final PageReference pageRef) {
+    private final AjaxRequestTarget target;
 
-        super(modal, pageRef);
-        add(directoryPanel);
+    private final String keyword;
+
+    public KeywordSearchEvent(final AjaxRequestTarget target, final String keyword) {
+        this.target = target;
+        this.keyword = keyword;
+    }
+
+    public AjaxRequestTarget getTarget() {
+        return target;
+    }
+
+    public String getKeyword() {
+        return keyword;
     }
 }
diff --git a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/commons/LinkedAccountPlainAttrProperty.java b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/commons/LinkedAccountPlainAttrProperty.java
index 3f0468a929..6ce77c97fc 100644
--- a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/commons/LinkedAccountPlainAttrProperty.java
+++ b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/commons/LinkedAccountPlainAttrProperty.java
@@ -18,20 +18,15 @@
  */
 package org.apache.syncope.client.console.commons;
 
-import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
+import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
 import java.io.Serializable;
 import java.util.ArrayList;
 import java.util.List;
-import javax.xml.bind.annotation.XmlElement;
-import javax.xml.bind.annotation.XmlElementWrapper;
-import javax.xml.bind.annotation.XmlRootElement;
-import javax.xml.bind.annotation.XmlType;
 import org.apache.commons.lang3.ObjectUtils;
 import org.apache.commons.lang3.builder.EqualsBuilder;
 import org.apache.commons.lang3.builder.HashCodeBuilder;
 
-@XmlRootElement
-@XmlType
 public class LinkedAccountPlainAttrProperty implements Serializable, Comparable<LinkedAccountPlainAttrProperty> {
 
     private static final long serialVersionUID = -5309050337675968950L;
@@ -50,9 +45,8 @@ public class LinkedAccountPlainAttrProperty implements Serializable, Comparable<
         this.schema = schema;
     }
 
-    @XmlElementWrapper(name = "values")
-    @XmlElement(name = "value")
-    @JsonProperty("values")
+    @JacksonXmlElementWrapper(localName = "values")
+    @JacksonXmlProperty(localName = "value")
     public List<String> getValues() {
         return values;
     }
diff --git a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/notifications/NotificationTasks.java b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/notifications/NotificationTasks.java
index 82e9369420..0af41223fa 100644
--- a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/notifications/NotificationTasks.java
+++ b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/notifications/NotificationTasks.java
@@ -68,10 +68,10 @@ public class NotificationTasks extends Panel implements ModalPanel {
             private static final long serialVersionUID = -2195387360323687302L;
 
             @Override
-            protected void viewTask(final NotificationTaskTO taskTO, final AjaxRequestTarget target) {
+            protected void viewTaskExecs(final NotificationTaskTO taskTO, final AjaxRequestTarget target) {
                 mlp.next(
                         new StringResourceModel("task.view", this, new Model<>(Pair.of(null, taskTO))).getObject(),
-                        new TaskExecutionDetails<>(null, taskTO, pageReference), target);
+                        new TaskExecutionDetails<>(taskTO, pageReference), target);
             }
 
             @Override
@@ -82,7 +82,6 @@ public class NotificationTasks extends Panel implements ModalPanel {
                         new StringResourceModel("content", this).setParameters(format.name()).getObject(),
                         new NotificationMailBodyDetails(content), target);
             }
-
         });
     }
 }
diff --git a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/pages/BasePage.java b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/pages/BasePage.java
index e29dbe7bef..b60d9c7aa5 100644
--- a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/pages/BasePage.java
+++ b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/pages/BasePage.java
@@ -122,12 +122,12 @@ public class BasePage extends BaseWebPage {
             public void onClick() {
                 try {
                     HttpResourceStream stream = new HttpResourceStream(
-                        new ResponseHolder(SyncopeRestClient.exportInternalStorageContent()));
+                            new ResponseHolder(SyncopeRestClient.exportInternalStorageContent()));
 
                     ResourceStreamRequestHandler rsrh = new ResourceStreamRequestHandler(stream);
                     rsrh.setFileName(stream.getFilename() == null
-                        ? SyncopeConsoleSession.get().getDomain() + "Content.xml"
-                        : stream.getFilename());
+                            ? SyncopeConsoleSession.get().getDomain() + "Content.xml"
+                            : stream.getFilename());
                     rsrh.setContentDisposition(ContentDisposition.ATTACHMENT);
                     rsrh.setCacheDuration(Duration.ZERO);
 
@@ -153,6 +153,13 @@ public class BasePage extends BaseWebPage {
 
         liContainer.add(link);
 
+        liContainer = new WebMarkupContainer(getLIContainerId("engagements"));
+        body.add(liContainer);
+        link = BookmarkablePageLinkBuilder.build("engagements", Engagements.class);
+        MetaDataRoleAuthorizationStrategy.authorize(link, WebPage.RENDER,
+                String.format("%s,%s", IdRepoEntitlement.TASK_LIST, IdRepoEntitlement.IMPLEMENTATION_LIST));
+        liContainer.add(link);
+
         liContainer = new WebMarkupContainer(getLIContainerId("reports"));
         body.add(liContainer);
         link = BookmarkablePageLinkBuilder.build("reports", Reports.class);
@@ -160,8 +167,7 @@ public class BasePage extends BaseWebPage {
         liContainer.add(link);
 
         List<Class<? extends BasePage>> idmPageClasses = SyncopeWebApplication.get().getLookup().getIdMPageClasses();
-        ListView<Class<? extends BasePage>> idmPages = new ListView<>(
-            "idmPages", idmPageClasses) {
+        ListView<Class<? extends BasePage>> idmPages = new ListView<>("idmPages", idmPageClasses) {
 
             private static final long serialVersionUID = 4949588177564901031L;
 
@@ -200,8 +206,7 @@ public class BasePage extends BaseWebPage {
         body.add(idmPages);
 
         List<Class<? extends BasePage>> amPageClasses = SyncopeWebApplication.get().getLookup().getAMPageClasses();
-        ListView<Class<? extends BasePage>> amPages = new ListView<>(
-            "amPages", amPageClasses) {
+        ListView<Class<? extends BasePage>> amPages = new ListView<>("amPages", amPageClasses) {
 
             private static final long serialVersionUID = -9112553137618363167L;
 
@@ -337,6 +342,8 @@ public class BasePage extends BaseWebPage {
         delegationsContainer.add(new Label("delegationsHeader", new ResourceModel("delegations")));
         delegationsContainer.add(new ListView<>("delegations", SyncopeConsoleSession.get().getDelegations()) {
 
+            private static final long serialVersionUID = -9112553137618363167L;
+
             @Override
             protected void populateItem(final ListItem<String> item) {
                 item.add(new DelegationSelectionPanel("delegation", item.getModelObject()));
@@ -345,6 +352,8 @@ public class BasePage extends BaseWebPage {
 
         body.add(new IndicatingOnConfirmAjaxLink<String>("endDelegation", "confirmDelegation", true) {
 
+            private static final long serialVersionUID = 1632838687547839512L;
+
             @Override
             public void onClick(final AjaxRequestTarget target) {
                 SyncopeConsoleSession.get().setDelegatedBy(null);
@@ -467,7 +476,7 @@ public class BasePage extends BaseWebPage {
         List<Class<? extends ExtAlertWidget<?>>> extAlertWidgetClasses =
                 SyncopeWebApplication.get().getLookup().getExtAlertWidgetClasses();
         ListView<Class<? extends ExtAlertWidget<?>>> extAlertWidgets = new ListView<>(
-            "extAlertWidgets", extAlertWidgetClasses) {
+                "extAlertWidgets", extAlertWidgetClasses) {
 
             private static final long serialVersionUID = -9112553137618363167L;
 
@@ -475,7 +484,7 @@ public class BasePage extends BaseWebPage {
             protected void populateItem(final ListItem<Class<? extends ExtAlertWidget<?>>> item) {
                 try {
                     Constructor<? extends ExtAlertWidget<?>> constructor =
-                        item.getModelObject().getDeclaredConstructor(String.class, PageReference.class);
+                            item.getModelObject().getDeclaredConstructor(String.class, PageReference.class);
                     ExtAlertWidget<?> widget = constructor.newInstance("extAlertWidget", getPageReference());
 
                     SyncopeConsoleSession.get().setAttribute(widget.getClass().getName(), widget);
@@ -497,46 +506,46 @@ public class BasePage extends BaseWebPage {
         body.add(extensionsLI);
 
         ListView<Class<? extends BaseExtPage>> extPages =
-            new ListView<>("extPages", extPageClasses) {
+                new ListView<>("extPages", extPageClasses) {
 
-                private static final long serialVersionUID = 4949588177564901031L;
+            private static final long serialVersionUID = 4949588177564901031L;
 
-                @Override
-                protected void populateItem(final ListItem<Class<? extends BaseExtPage>> item) {
-                    WebMarkupContainer containingLI = new WebMarkupContainer("extPageLI");
-                    item.add(containingLI);
+            @Override
+            protected void populateItem(final ListItem<Class<? extends BaseExtPage>> item) {
+                WebMarkupContainer containingLI = new WebMarkupContainer("extPageLI");
+                item.add(containingLI);
 
-                    ExtPage ann = item.getModelObject().getAnnotation(ExtPage.class);
+                ExtPage ann = item.getModelObject().getAnnotation(ExtPage.class);
 
-                    BookmarkablePageLink<Page> link = new BookmarkablePageLink<>("extPage", item.getModelObject());
-                    link.add(new Label("extPageLabel", ann.label()));
-                    if (StringUtils.isNotBlank(ann.listEntitlement())) {
-                        MetaDataRoleAuthorizationStrategy.authorize(link, WebPage.RENDER, ann.listEntitlement());
-                    }
-                    if (item.getModelObject().equals(BasePage.this.getClass())) {
-                        link.add(new Behavior() {
+                BookmarkablePageLink<Page> link = new BookmarkablePageLink<>("extPage", item.getModelObject());
+                link.add(new Label("extPageLabel", ann.label()));
+                if (StringUtils.isNotBlank(ann.listEntitlement())) {
+                    MetaDataRoleAuthorizationStrategy.authorize(link, WebPage.RENDER, ann.listEntitlement());
+                }
+                if (item.getModelObject().equals(BasePage.this.getClass())) {
+                    link.add(new Behavior() {
 
-                            private static final long serialVersionUID = 1469628524240283489L;
+                        private static final long serialVersionUID = 1469628524240283489L;
 
-                            @Override
-                            public void renderHead(final Component component, final IHeaderResponse response) {
-                                response.render(OnDomReadyHeaderItem.forScript(
+                        @Override
+                        public void renderHead(final Component component, final IHeaderResponse response) {
+                            response.render(OnDomReadyHeaderItem.forScript(
                                     "$('#extensionsLink').addClass('active')"));
-                            }
-
-                            @Override
-                            public void onComponentTag(final Component component, final ComponentTag tag) {
-                                tag.append("class", "active", " ");
-                            }
-                        });
-                    }
-                    containingLI.add(link);
+                        }
 
-                    Label extPageIcon = new Label("extPageIcon");
-                    extPageIcon.add(new AttributeModifier("class", "nav-icon " + ann.icon()));
-                    link.add(extPageIcon);
+                        @Override
+                        public void onComponentTag(final Component component, final ComponentTag tag) {
+                            tag.append("class", "active", " ");
+                        }
+                    });
                 }
-            };
+                containingLI.add(link);
+
+                Label extPageIcon = new Label("extPageIcon");
+                extPageIcon.add(new AttributeModifier("class", "nav-icon " + ann.icon()));
+                link.add(extPageIcon);
+            }
+        };
         extPages.setRenderBodyOnly(true);
         extPages.setOutputMarkupId(true);
         extensionsLI.add(extPages);
diff --git a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/pages/Reports.java b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/pages/Engagements.java
similarity index 60%
copy from client/idrepo/console/src/main/java/org/apache/syncope/client/console/pages/Reports.java
copy to client/idrepo/console/src/main/java/org/apache/syncope/client/console/pages/Engagements.java
index 46763162a2..249e5fc139 100644
--- a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/pages/Reports.java
+++ b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/pages/Engagements.java
@@ -21,12 +21,15 @@ package org.apache.syncope.client.console.pages;
 import de.agilecoders.wicket.core.markup.html.bootstrap.tabs.AjaxBootstrapTabbedPanel;
 import java.util.ArrayList;
 import java.util.List;
+import org.apache.commons.lang3.tuple.Pair;
 import org.apache.syncope.client.console.BookmarkablePageLinkBuilder;
+import org.apache.syncope.client.console.panels.CommandsPanel;
 import org.apache.syncope.client.console.panels.MultilevelPanel;
-import org.apache.syncope.client.console.reports.ReportDirectoryPanel;
-import org.apache.syncope.client.console.reports.ReportExecutionDetails;
-import org.apache.syncope.client.console.reports.ReportTemplateDirectoryPanel;
-import org.apache.syncope.common.lib.to.ReportTO;
+import org.apache.syncope.client.console.tasks.MacroTaskDirectoryPanel;
+import org.apache.syncope.client.console.tasks.SchedTaskDirectoryPanel;
+import org.apache.syncope.client.console.tasks.TaskExecutionDetails;
+import org.apache.syncope.common.lib.to.SchedTaskTO;
+import org.apache.syncope.common.lib.types.TaskType;
 import org.apache.wicket.ajax.AjaxRequestTarget;
 import org.apache.wicket.extensions.markup.html.tabs.AbstractTab;
 import org.apache.wicket.extensions.markup.html.tabs.ITab;
@@ -37,41 +40,43 @@ import org.apache.wicket.model.ResourceModel;
 import org.apache.wicket.model.StringResourceModel;
 import org.apache.wicket.request.mapper.parameter.PageParameters;
 
-public class Reports extends BasePage {
+public class Engagements extends BasePage {
 
     private static final long serialVersionUID = -1100228004207271271L;
 
-    public Reports(final PageParameters parameters) {
+    public Engagements(final PageParameters parameters) {
         super(parameters);
 
         body.add(BookmarkablePageLinkBuilder.build("dashboard", "dashboardBr", Dashboard.class));
 
         WebMarkupContainer content = new WebMarkupContainer("content");
         content.setOutputMarkupId(true);
-        content.setMarkupId("reports");
+        content.setMarkupId("engagements");
         content.add(new AjaxBootstrapTabbedPanel<>("tabbedPanel", buildTabList()));
         body.add(content);
     }
 
     private List<ITab> buildTabList() {
-        final List<ITab> tabs = new ArrayList<>();
+        List<ITab> tabs = new ArrayList<>();
 
-        tabs.add(new AbstractTab(new ResourceModel("reports")) {
+        tabs.add(new AbstractTab(new ResourceModel("schedTasks")) {
 
             private static final long serialVersionUID = -6815067322125799251L;
 
             @Override
             public Panel getPanel(final String panelId) {
                 MultilevelPanel mlp = new MultilevelPanel(panelId);
-                mlp.setFirstLevel(new ReportDirectoryPanel(getPageReference()) {
+                mlp.setFirstLevel(new SchedTaskDirectoryPanel<>(MultilevelPanel.FIRST_LEVEL_ID,
+                        null, null, TaskType.SCHEDULED, new SchedTaskTO(), getPageReference(), true) {
 
                     private static final long serialVersionUID = -2195387360323687302L;
 
                     @Override
-                    protected void viewReport(final ReportTO reportTO, final AjaxRequestTarget target) {
+                    protected void viewTaskExecs(final SchedTaskTO taskTO, final AjaxRequestTarget target) {
                         mlp.next(
-                                new StringResourceModel("report.view", this, new Model<>(reportTO)).getObject(),
-                                new ReportExecutionDetails(reportTO, getPageReference()),
+                                new StringResourceModel(
+                                        "task.view", this, new Model<>(Pair.of(null, taskTO))).getObject(),
+                                new TaskExecutionDetails<>(taskTO, pageRef),
                                 target);
                     }
                 });
@@ -79,15 +84,28 @@ public class Reports extends BasePage {
             }
         });
 
-        tabs.add(new AbstractTab(new ResourceModel("report.templates")) {
+        tabs.add(new AbstractTab(new ResourceModel("commands")) {
 
             private static final long serialVersionUID = -6815067322125799251L;
 
             @Override
             public Panel getPanel(final String panelId) {
-                return new ReportTemplateDirectoryPanel(panelId, getPageReference());
+                return new CommandsPanel(panelId, getPageReference());
             }
         });
+
+        tabs.add(new AbstractTab(new ResourceModel("macroTasks")) {
+
+            private static final long serialVersionUID = 5211692813425391144L;
+
+            @Override
+            public Panel getPanel(final String panelId) {
+                MultilevelPanel mlp = new MultilevelPanel(panelId);
+                mlp.setFirstLevel(new MacroTaskDirectoryPanel(mlp, getPageReference()));
+                return mlp;
+            }
+        });
+
         return tabs;
     }
 }
diff --git a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/pages/Reports.java b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/pages/Reports.java
index 46763162a2..d886d61a2d 100644
--- a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/pages/Reports.java
+++ b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/pages/Reports.java
@@ -54,7 +54,7 @@ public class Reports extends BasePage {
     }
 
     private List<ITab> buildTabList() {
-        final List<ITab> tabs = new ArrayList<>();
+        List<ITab> tabs = new ArrayList<>();
 
         tabs.add(new AbstractTab(new ResourceModel("reports")) {
 
@@ -68,7 +68,7 @@ public class Reports extends BasePage {
                     private static final long serialVersionUID = -2195387360323687302L;
 
                     @Override
-                    protected void viewReport(final ReportTO reportTO, final AjaxRequestTarget target) {
+                    protected void viewReportExecs(final ReportTO reportTO, final AjaxRequestTarget target) {
                         mlp.next(
                                 new StringResourceModel("report.view", this, new Model<>(reportTO)).getObject(),
                                 new ReportExecutionDetails(reportTO, getPageReference()),
@@ -88,6 +88,7 @@ public class Reports extends BasePage {
                 return new ReportTemplateDirectoryPanel(panelId, getPageReference());
             }
         });
+
         return tabs;
     }
 }
diff --git a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/panels/AccessTokenDirectoryPanel.java b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/panels/AccessTokenDirectoryPanel.java
index 22a7bd7ff7..2164a5fce9 100644
--- a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/panels/AccessTokenDirectoryPanel.java
+++ b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/panels/AccessTokenDirectoryPanel.java
@@ -82,6 +82,7 @@ public class AccessTokenDirectoryPanel
     @Override
     protected List<IColumn<AccessTokenTO, String>> getColumns() {
         List<IColumn<AccessTokenTO, String>> columns = new ArrayList<>();
+
         columns.add(new KeyPropertyColumn<>(
                 new StringResourceModel(Constants.KEY_FIELD_NAME, this),
                 Constants.KEY_FIELD_NAME,
@@ -118,7 +119,7 @@ public class AccessTokenDirectoryPanel
 
     @Override
     public ActionsPanel<AccessTokenTO> getActions(final IModel<AccessTokenTO> model) {
-        final ActionsPanel<AccessTokenTO> panel = super.getActions(model);
+        ActionsPanel<AccessTokenTO> panel = super.getActions(model);
 
         panel.add(new ActionLink<>() {
 
@@ -146,7 +147,7 @@ public class AccessTokenDirectoryPanel
         return List.of();
     }
 
-    public abstract static class Builder
+    public static class Builder
             extends DirectoryPanel.Builder<AccessTokenTO, AccessTokenTO, AccessTokenRestClient> {
 
         private static final long serialVersionUID = 5088962796986706805L;
@@ -165,8 +166,6 @@ public class AccessTokenDirectoryPanel
 
         private static final long serialVersionUID = 6267494272884913376L;
 
-        private final AccessTokenRestClient restClient = new AccessTokenRestClient();
-
         public AccessTokenDataProvider(final int paginatorRows) {
             super(paginatorRows);
 
diff --git a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/panels/AjaxDataTablePanel.java b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/panels/AjaxDataTablePanel.java
index 2f9c72c581..343710b916 100644
--- a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/panels/AjaxDataTablePanel.java
+++ b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/panels/AjaxDataTablePanel.java
@@ -80,8 +80,6 @@ public final class AjaxDataTablePanel<T extends Serializable, S> extends DataTab
 
         private MultilevelPanel multiLevelPanel;
 
-        private BaseModal<?> baseModal;
-
         public Builder(final ISortableDataProvider<T, S> provider, final PageReference pageRef) {
             this.dataProvider = provider;
             this.pageRef = pageRef;
@@ -152,9 +150,8 @@ public final class AjaxDataTablePanel<T extends Serializable, S> extends DataTab
             return checkBoxEnabled && batchExecutor != null && !batches.isEmpty();
         }
 
-        public void setMultiLevelPanel(final BaseModal<?> baseModal, final MultilevelPanel multiLevelPanel) {
+        public void setMultiLevelPanel(final MultilevelPanel multiLevelPanel) {
             this.multiLevelPanel = multiLevelPanel;
-            this.baseModal = baseModal;
         }
 
         protected ActionsPanel<T> getActions(final IModel<T> model) {
@@ -213,7 +210,7 @@ public final class AjaxDataTablePanel<T extends Serializable, S> extends DataTab
         }
 
         dataTable = new AjaxFallbackDataTable<>(
-            "dataTable", builder.columns, builder.dataProvider, builder.rowsPerPage, builder.container) {
+                "dataTable", builder.columns, builder.dataProvider, builder.rowsPerPage, builder.container) {
 
             private static final long serialVersionUID = -7370603907251344224L;
 
diff --git a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/panels/CommandDirectoryPanel.java b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/panels/CommandDirectoryPanel.java
new file mode 100644
index 0000000000..85322a8423
--- /dev/null
+++ b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/panels/CommandDirectoryPanel.java
@@ -0,0 +1,190 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.client.console.panels;
+
+import de.agilecoders.wicket.core.markup.html.bootstrap.dialog.Modal;
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.syncope.client.console.commons.IdRepoConstants;
+import org.apache.syncope.client.console.commons.KeywordSearchEvent;
+import org.apache.syncope.client.console.panels.CommandDirectoryPanel.CommandDataProvider;
+import org.apache.syncope.client.console.rest.CommandRestClient;
+import org.apache.syncope.client.console.tasks.ExecMessage;
+import org.apache.syncope.client.console.wicket.markup.html.form.ActionLink;
+import org.apache.syncope.client.console.wicket.markup.html.form.ActionsPanel;
+import org.apache.syncope.client.console.wizards.CommandWizardBuilder;
+import org.apache.syncope.client.ui.commons.Constants;
+import org.apache.syncope.client.ui.commons.DirectoryDataProvider;
+import org.apache.syncope.client.ui.commons.wizards.AjaxWizard;
+import org.apache.syncope.common.lib.command.CommandTO;
+import org.apache.syncope.common.lib.types.IdRepoEntitlement;
+import org.apache.wicket.PageReference;
+import org.apache.wicket.ajax.AjaxRequestTarget;
+import org.apache.wicket.event.Broadcast;
+import org.apache.wicket.event.IEvent;
+import org.apache.wicket.extensions.markup.html.repeater.data.grid.ICellPopulator;
+import org.apache.wicket.extensions.markup.html.repeater.data.table.AbstractColumn;
+import org.apache.wicket.extensions.markup.html.repeater.data.table.IColumn;
+import org.apache.wicket.extensions.markup.html.repeater.data.table.PropertyColumn;
+import org.apache.wicket.markup.html.basic.Label;
+import org.apache.wicket.markup.html.panel.Panel;
+import org.apache.wicket.markup.repeater.Item;
+import org.apache.wicket.model.CompoundPropertyModel;
+import org.apache.wicket.model.IModel;
+import org.apache.wicket.model.ResourceModel;
+
+public class CommandDirectoryPanel
+        extends DirectoryPanel<CommandTO, CommandTO, CommandDataProvider, CommandRestClient> {
+
+    private static final long serialVersionUID = -8723262033772725592L;
+
+    private String keyword;
+
+    public CommandDirectoryPanel(final String id, final PageReference pageRef) {
+        super(id, pageRef);
+        disableCheckBoxes();
+
+        modal.size(Modal.Size.Large);
+
+        modal.setWindowClosedCallback(target -> {
+            updateResultTable(target);
+            modal.show(false);
+        });
+
+        addNewItemPanelBuilder(new CommandWizardBuilder(new CommandTO(), pageRef), false);
+
+        setShowResultPanel(true);
+
+        initResultTable();
+    }
+
+    @Override
+    protected CommandDataProvider dataProvider() {
+        return new CommandDataProvider(rows);
+    }
+
+    @Override
+    protected String paginatorRowsKey() {
+        return IdRepoConstants.PREF_COMMAND_PAGINATOR_ROWS;
+    }
+
+    @Override
+    protected List<IColumn<CommandTO, String>> getColumns() {
+        List<IColumn<CommandTO, String>> columns = new ArrayList<>();
+
+        columns.add(new PropertyColumn<>(
+                new ResourceModel(Constants.KEY_FIELD_NAME), Constants.KEY_FIELD_NAME, Constants.KEY_FIELD_NAME));
+
+        columns.add(new AbstractColumn<>(new ResourceModel("arguments"), "arguments") {
+
+            private static final long serialVersionUID = -4008579357070833846L;
+
+            @Override
+            public void populateItem(
+                    final Item<ICellPopulator<CommandTO>> cellItem,
+                    final String componentId,
+                    final IModel<CommandTO> rowModel) {
+
+                cellItem.add(new Label(componentId, rowModel.getObject().getArgs().getClass().getName()));
+            }
+        });
+
+        return columns;
+    }
+
+    @Override
+    protected ActionsPanel<CommandTO> getActions(final IModel<CommandTO> model) {
+        ActionsPanel<CommandTO> panel = super.getActions(model);
+
+        panel.add(new ActionLink<>() {
+
+            private static final long serialVersionUID = -3722207913631435501L;
+
+            @Override
+            public void onClick(final AjaxRequestTarget target, final CommandTO ignore) {
+                send(CommandDirectoryPanel.this, Broadcast.EXACT,
+                        new AjaxWizard.EditItemActionEvent<>(model.getObject(), target));
+            }
+        }, ActionLink.ActionType.EXECUTE, IdRepoEntitlement.COMMAND_RUN);
+
+        return panel;
+    }
+
+    @Override
+    protected Collection<ActionLink.ActionType> getBatches() {
+        return List.of();
+    }
+
+    @Override
+    protected Panel customResultBody(final String panelId, final CommandTO item, final Serializable result) {
+        return new ExecMessage(panelId, (String) result);
+    }
+
+    @Override
+    public void onEvent(final IEvent<?> event) {
+        if (event.getPayload() instanceof KeywordSearchEvent) {
+            KeywordSearchEvent payload = KeywordSearchEvent.class.cast(event.getPayload());
+
+            keyword = payload.getKeyword();
+            if (StringUtils.isNotBlank(keyword)) {
+                if (!StringUtils.startsWith(keyword, "*")) {
+                    keyword = "*" + keyword;
+                }
+                if (!StringUtils.endsWith(keyword, "*")) {
+                    keyword += "*";
+                }
+            }
+
+            updateResultTable(payload.getTarget());
+        } else {
+            super.onEvent(event);
+        }
+    }
+
+    protected final class CommandDataProvider extends DirectoryDataProvider<CommandTO> {
+
+        private static final long serialVersionUID = 6267494272884913376L;
+
+        public CommandDataProvider(final int paginatorRows) {
+            super(paginatorRows);
+        }
+
+        @Override
+        public Iterator<CommandTO> iterator(final long first, final long count) {
+            int page = ((int) first / paginatorRows);
+            return CommandRestClient.search(
+                    (page < 0 ? 0 : page) + 1, paginatorRows, keyword).
+                    iterator();
+        }
+
+        @Override
+        public long size() {
+            return CommandRestClient.count(keyword);
+        }
+
+        @Override
+        public IModel<CommandTO> model(final CommandTO object) {
+            return new CompoundPropertyModel<>(object);
+        }
+    }
+}
diff --git a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/panels/SchemasPanel.java b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/panels/CommandsPanel.java
similarity index 60%
copy from client/idrepo/console/src/main/java/org/apache/syncope/client/console/panels/SchemasPanel.java
copy to client/idrepo/console/src/main/java/org/apache/syncope/client/console/panels/CommandsPanel.java
index 4cfcb573e2..f3e1f94eed 100644
--- a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/panels/SchemasPanel.java
+++ b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/panels/CommandsPanel.java
@@ -18,34 +18,25 @@
  */
 package org.apache.syncope.client.console.panels;
 
-import java.util.ArrayList;
-import java.util.List;
 import org.apache.commons.lang3.StringUtils;
+import org.apache.syncope.client.console.commons.KeywordSearchEvent;
 import org.apache.syncope.client.ui.commons.markup.html.form.AjaxTextFieldPanel;
-import org.apache.syncope.client.ui.commons.wicket.markup.html.bootstrap.tabs.Accordion;
-import org.apache.syncope.common.lib.types.SchemaType;
 import org.apache.wicket.PageReference;
 import org.apache.wicket.ajax.AjaxRequestTarget;
 import org.apache.wicket.ajax.markup.html.form.AjaxButton;
 import org.apache.wicket.event.Broadcast;
-import org.apache.wicket.extensions.markup.html.tabs.AbstractTab;
-import org.apache.wicket.extensions.markup.html.tabs.ITab;
 import org.apache.wicket.markup.html.WebMarkupContainer;
 import org.apache.wicket.markup.html.form.Form;
 import org.apache.wicket.markup.html.panel.Panel;
 import org.apache.wicket.model.Model;
 
-public class SchemasPanel extends Panel {
+public class CommandsPanel extends Panel {
 
-    private static final long serialVersionUID = -1140213992451232279L;
+    private static final long serialVersionUID = -4716856239434102405L;
 
-    private final PageReference pageRef;
-
-    public SchemasPanel(final String id, final PageReference pageRef) {
+    public CommandsPanel(final String id, final PageReference pageRef) {
         super(id);
 
-        this.pageRef = pageRef;
-
         Model<String> keywordModel = new Model<>(StringUtils.EMPTY);
 
         WebMarkupContainer searchBoxContainer = new WebMarkupContainer("searchBox");
@@ -63,34 +54,13 @@ public class SchemasPanel extends Panel {
 
             @Override
             protected void onSubmit(final AjaxRequestTarget target) {
-                send(SchemasPanel.this, Broadcast.DEPTH, 
-                        new SchemaTypePanel.SchemaSearchEvent(target, keywordModel.getObject()));
+                send(CommandsPanel.this, Broadcast.DEPTH, new KeywordSearchEvent(target, keywordModel.getObject()));
             }
         };
         search.setOutputMarkupId(true);
         form.add(search);
         form.setDefaultButton(search);
 
-        Accordion accordion = new Accordion("accordionPanel", buildTabList());
-        accordion.setOutputMarkupId(true);
-        add(accordion);
-    }
-
-    private List<ITab> buildTabList() {
-        List<ITab> tabs = new ArrayList<>();
-
-        for (final SchemaType schemaType : SchemaType.values()) {
-            tabs.add(new AbstractTab(new Model<>(schemaType.name())) {
-
-                private static final long serialVersionUID = 1037272333056449378L;
-
-                @Override
-                public Panel getPanel(final String panelId) {
-                    return new SchemaTypePanel(panelId, schemaType, pageRef);
-                }
-            });
-        }
-
-        return tabs;
+        add(new CommandDirectoryPanel("commands", pageRef).setOutputMarkupId(true));
     }
 }
diff --git a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/panels/DashboardAccessTokensPanel.java b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/panels/DashboardAccessTokensPanel.java
index d9dcc89f9e..9cecc65985 100644
--- a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/panels/DashboardAccessTokensPanel.java
+++ b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/panels/DashboardAccessTokensPanel.java
@@ -33,14 +33,10 @@ public class DashboardAccessTokensPanel extends Panel {
     public DashboardAccessTokensPanel(final String id, final PageReference pageRef) {
         super(id);
 
-        WizardMgtPanel<AccessTokenTO> accessTokens = new AccessTokenDirectoryPanel.Builder(pageRef) {
-
-            private static final long serialVersionUID = -5960765294082359003L;
-
-        }.disableCheckBoxes().build("accessTokens");
+        WizardMgtPanel<AccessTokenTO> accessTokens = new AccessTokenDirectoryPanel.Builder(pageRef).
+                disableCheckBoxes().build("accessTokens");
         MetaDataRoleAuthorizationStrategy.authorize(
                 accessTokens, Component.RENDER, IdRepoEntitlement.ACCESS_TOKEN_LIST);
         add(accessTokens);
     }
-
 }
diff --git a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/panels/ExecMessageModal.java b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/panels/ExecMessageModal.java
index 0b776699d9..589ae3e254 100644
--- a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/panels/ExecMessageModal.java
+++ b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/panels/ExecMessageModal.java
@@ -35,5 +35,4 @@ public class ExecMessageModal extends Panel implements ModalPanel {
         super(id);
         add(new Label("executionMessage", executionMessage).setOutputMarkupId(true));
     }
-
 }
diff --git a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/panels/ImplementationModalPanel.java b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/panels/ImplementationModalPanel.java
index 1e618537fa..bcd0f29284 100644
--- a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/panels/ImplementationModalPanel.java
+++ b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/panels/ImplementationModalPanel.java
@@ -95,8 +95,8 @@ public class ImplementationModalPanel extends AbstractModalPanel<ImplementationT
         if (viewMode == ViewMode.JSON_BODY && StringUtils.isNotBlank(implementation.getBody())) {
             try {
                 JsonNode node = MAPPER.readTree(implementation.getBody());
-                if (node.has("@class")) {
-                    jsonClass.setModelObject(node.get("@class").asText());
+                if (node.has("_class")) {
+                    jsonClass.setModelObject(node.get("_class").asText());
                 }
             } catch (IOException e) {
                 LOG.error("Could not parse as JSON payload: {}", implementation.getBody(), e);
diff --git a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/panels/ModalDirectoryPanel.java b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/panels/ModalDirectoryPanel.java
index 6d524c537c..bddd1bf790 100644
--- a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/panels/ModalDirectoryPanel.java
+++ b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/panels/ModalDirectoryPanel.java
@@ -24,7 +24,7 @@ import org.apache.wicket.PageReference;
 
 public class ModalDirectoryPanel<T extends Serializable> extends AbstractModalPanel<T> {
 
-    private static final long serialVersionUID = 1L;
+    private static final long serialVersionUID = -2882969833580638049L;
 
     public ModalDirectoryPanel(
             final BaseModal<T> modal, final DirectoryPanel<?, ?, ?, ?> directoryPanel, final PageReference pageRef) {
diff --git a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/panels/SchemaTypePanel.java b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/panels/SchemaTypePanel.java
index f087da2afa..4e839eef26 100644
--- a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/panels/SchemaTypePanel.java
+++ b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/panels/SchemaTypePanel.java
@@ -18,7 +18,6 @@
  */
 package org.apache.syncope.client.console.panels;
 
-import java.io.Serializable;
 import java.lang.reflect.Field;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -28,6 +27,7 @@ import java.util.Map;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.syncope.client.console.SyncopeConsoleSession;
 import org.apache.syncope.client.console.commons.IdRepoConstants;
+import org.apache.syncope.client.console.commons.KeywordSearchEvent;
 import org.apache.syncope.client.console.commons.SortableDataProviderComparator;
 import org.apache.syncope.client.console.pages.BasePage;
 import org.apache.syncope.client.console.panels.SchemaTypePanel.SchemaProvider;
@@ -184,6 +184,27 @@ public class SchemaTypePanel extends TypesDirectoryPanel<SchemaTO, SchemaProvide
         return panel;
     }
 
+    @Override
+    public void onEvent(final IEvent<?> event) {
+        if (event.getPayload() instanceof KeywordSearchEvent) {
+            KeywordSearchEvent payload = KeywordSearchEvent.class.cast(event.getPayload());
+
+            keyword = payload.getKeyword();
+            if (StringUtils.isNotBlank(keyword)) {
+                if (!StringUtils.startsWith(keyword, "*")) {
+                    keyword = "*" + keyword;
+                }
+                if (!StringUtils.endsWith(keyword, "*")) {
+                    keyword += "*";
+                }
+            }
+
+            updateResultTable(payload.getTarget());
+        } else {
+            super.onEvent(event);
+        }
+    }
+
     protected final class SchemaProvider extends DirectoryDataProvider<SchemaTO> {
 
         private static final long serialVersionUID = -185944053385660794L;
@@ -218,48 +239,4 @@ public class SchemaTypePanel extends TypesDirectoryPanel<SchemaTO, SchemaProvide
             return new CompoundPropertyModel<>(object);
         }
     }
-
-    @Override
-    public void onEvent(final IEvent<?> event) {
-        if (event.getPayload() instanceof SchemaSearchEvent) {
-            SchemaSearchEvent payload = SchemaSearchEvent.class.cast(event.getPayload());
-            AjaxRequestTarget target = payload.getTarget();
-
-            keyword = payload.getKeyword();
-            if (StringUtils.isNotBlank(keyword)) {
-                if (!StringUtils.startsWith(keyword, "*")) {
-                    keyword = "*" + keyword;
-                }
-                if (!StringUtils.endsWith(keyword, "*")) {
-                    keyword += "*";
-                }
-            }
-
-            updateResultTable(target);
-        } else {
-            super.onEvent(event);
-        }
-    }
-
-    public static class SchemaSearchEvent implements Serializable {
-
-        private static final long serialVersionUID = -282052400565266028L;
-
-        private final AjaxRequestTarget target;
-
-        private final String keyword;
-
-        SchemaSearchEvent(final AjaxRequestTarget target, final String keyword) {
-            this.target = target;
-            this.keyword = keyword;
-        }
-
-        public AjaxRequestTarget getTarget() {
-            return target;
-        }
-
-        public String getKeyword() {
-            return keyword;
-        }
-    }
 }
diff --git a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/panels/SchemasPanel.java b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/panels/SchemasPanel.java
index 4cfcb573e2..1bbd5229b8 100644
--- a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/panels/SchemasPanel.java
+++ b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/panels/SchemasPanel.java
@@ -21,6 +21,7 @@ package org.apache.syncope.client.console.panels;
 import java.util.ArrayList;
 import java.util.List;
 import org.apache.commons.lang3.StringUtils;
+import org.apache.syncope.client.console.commons.KeywordSearchEvent;
 import org.apache.syncope.client.ui.commons.markup.html.form.AjaxTextFieldPanel;
 import org.apache.syncope.client.ui.commons.wicket.markup.html.bootstrap.tabs.Accordion;
 import org.apache.syncope.common.lib.types.SchemaType;
@@ -63,8 +64,7 @@ public class SchemasPanel extends Panel {
 
             @Override
             protected void onSubmit(final AjaxRequestTarget target) {
-                send(SchemasPanel.this, Broadcast.DEPTH, 
-                        new SchemaTypePanel.SchemaSearchEvent(target, keywordModel.getObject()));
+                send(SchemasPanel.this, Broadcast.DEPTH, new KeywordSearchEvent(target, keywordModel.getObject()));
             }
         };
         search.setOutputMarkupId(true);
diff --git a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/policies/PolicyRuleDirectoryPanel.java b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/policies/PolicyRuleDirectoryPanel.java
index 48904d3bbc..0692cb4a71 100644
--- a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/policies/PolicyRuleDirectoryPanel.java
+++ b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/policies/PolicyRuleDirectoryPanel.java
@@ -108,21 +108,21 @@ public class PolicyRuleDirectoryPanel<T extends PolicyTO> extends DirectoryPanel
 
     @Override
     protected List<IColumn<PolicyRuleWrapper, String>> getColumns() {
-        final List<IColumn<PolicyRuleWrapper, String>> columns = new ArrayList<>();
+        List<IColumn<PolicyRuleWrapper, String>> columns = new ArrayList<>();
 
         columns.add(new PropertyColumn<>(
                 new StringResourceModel("rule", this), "implementationKey", "implementationKey"));
 
         columns.add(new AbstractColumn<>(
-            new StringResourceModel("configuration", this)) {
+                new StringResourceModel("configuration", this)) {
 
             private static final long serialVersionUID = -4008579357070833846L;
 
             @Override
             public void populateItem(
-                final Item<ICellPopulator<PolicyRuleWrapper>> cellItem,
-                final String componentId,
-                final IModel<PolicyRuleWrapper> rowModel) {
+                    final Item<ICellPopulator<PolicyRuleWrapper>> cellItem,
+                    final String componentId,
+                    final IModel<PolicyRuleWrapper> rowModel) {
 
                 if (rowModel.getObject().getConf() == null) {
                     cellItem.add(new Label(componentId, ""));
@@ -131,6 +131,7 @@ public class PolicyRuleDirectoryPanel<T extends PolicyTO> extends DirectoryPanel
                 }
             }
         });
+
         return columns;
     }
 
@@ -150,10 +151,11 @@ public class PolicyRuleDirectoryPanel<T extends PolicyTO> extends DirectoryPanel
                     ((BaseWebPage) pageRef.getPage()).getNotificationPanel().refresh(target);
                 } else {
                     send(PolicyRuleDirectoryPanel.this, Broadcast.EXACT,
-                        new AjaxWizard.EditItemActionEvent<>(model.getObject(), target));
+                            new AjaxWizard.EditItemActionEvent<>(model.getObject(), target));
                 }
             }
         }, ActionLink.ActionType.EDIT, IdRepoEntitlement.POLICY_UPDATE);
+
         panel.add(new ActionLink<>() {
 
             private static final long serialVersionUID = -3722207913631435501L;
@@ -183,7 +185,7 @@ public class PolicyRuleDirectoryPanel<T extends PolicyTO> extends DirectoryPanel
 
     @Override
     public ActionsPanel<Serializable> getHeader(final String componentId) {
-        final ActionsPanel<Serializable> panel = new ActionsPanel<>(componentId, null);
+        ActionsPanel<Serializable> panel = new ActionsPanel<>(componentId, null);
 
         panel.add(new ActionLink<>() {
 
@@ -196,6 +198,7 @@ public class PolicyRuleDirectoryPanel<T extends PolicyTO> extends DirectoryPanel
                 }
             }
         }, ActionLink.ActionType.RELOAD, IdRepoEntitlement.POLICY_LIST).hideLabel();
+
         return panel;
     }
 
@@ -251,7 +254,7 @@ public class PolicyRuleDirectoryPanel<T extends PolicyTO> extends DirectoryPanel
 
         @Override
         public Iterator<PolicyRuleWrapper> iterator(final long first, final long count) {
-            final T actual = PolicyRestClient.read(type, policy);
+            T actual = PolicyRestClient.read(type, policy);
 
             List<PolicyRuleWrapper> rules = actual instanceof ComposablePolicy
                     ? getPolicyRuleWrappers((ComposablePolicy) actual)
@@ -263,7 +266,7 @@ public class PolicyRuleDirectoryPanel<T extends PolicyTO> extends DirectoryPanel
 
         @Override
         public long size() {
-            final T actual = PolicyRestClient.read(type, policy);
+            T actual = PolicyRestClient.read(type, policy);
             return actual instanceof ComposablePolicy
                     ? getPolicyRuleWrappers((ComposablePolicy) actual).size()
                     : 0;
diff --git a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/policies/PolicyRuleWizardBuilder.java b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/policies/PolicyRuleWizardBuilder.java
index 4a031a5638..b4fa3e11c5 100644
--- a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/policies/PolicyRuleWizardBuilder.java
+++ b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/policies/PolicyRuleWizardBuilder.java
@@ -118,7 +118,7 @@ public class PolicyRuleWizardBuilder extends BaseAjaxWizardBuilder<PolicyRuleWra
         public Profile(final PolicyRuleWrapper rule) {
             this.rule = rule;
 
-            final AjaxDropDownChoicePanel<String> conf = new AjaxDropDownChoicePanel<>(
+            AjaxDropDownChoicePanel<String> conf = new AjaxDropDownChoicePanel<>(
                     "rule", "rule", new PropertyModel<>(rule, "implementationKey"));
 
             List<String> choices;
@@ -151,8 +151,7 @@ public class PolicyRuleWizardBuilder extends BaseAjaxWizardBuilder<PolicyRuleWra
                     rule.setImplementationEngine(impl.getEngine());
                     if (impl.getEngine() == ImplementationEngine.JAVA) {
                         try {
-                            RuleConf ruleConf = MAPPER.readValue(impl.getBody(), RuleConf.class);
-                            rule.setConf(ruleConf);
+                            rule.setConf(MAPPER.readValue(impl.getBody(), RuleConf.class));
                         } catch (Exception e) {
                             LOG.error("During deserialization", e);
                         }
diff --git a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/reports/ReportDirectoryPanel.java b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/reports/ReportDirectoryPanel.java
index 1c3375ddd2..4bd232851d 100644
--- a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/reports/ReportDirectoryPanel.java
+++ b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/reports/ReportDirectoryPanel.java
@@ -79,9 +79,9 @@ public abstract class ReportDirectoryPanel
 
     protected ReportDirectoryPanel(final PageReference pageRef) {
         super(MultilevelPanel.FIRST_LEVEL_ID, pageRef, true);
-        this.restClient = new ReportRestClient();
+        restClient = new ReportRestClient();
 
-        this.addNewItemPanelBuilder(new ReportWizardBuilder(new ReportTO(), pageRef), true);
+        addNewItemPanelBuilder(new ReportWizardBuilder(new ReportTO(), pageRef), true);
         MetaDataRoleAuthorizationStrategy.authorize(addAjaxLink, RENDER, IdRepoEntitlement.REPORT_CREATE);
 
         modal.size(Modal.Size.Large);
@@ -104,7 +104,7 @@ public abstract class ReportDirectoryPanel
 
     @Override
     protected List<IColumn<ReportTO, String>> getColumns() {
-        final List<IColumn<ReportTO, String>> columns = new ArrayList<>();
+        List<IColumn<ReportTO, String>> columns = new ArrayList<>();
 
         columns.add(new KeyPropertyColumn<>(
                 new StringResourceModel(Constants.KEY_FIELD_NAME, this), Constants.KEY_FIELD_NAME));
@@ -135,18 +135,18 @@ public abstract class ReportDirectoryPanel
 
             @Override
             public void populateItem(
-                final Item<ICellPopulator<ReportTO>> cellItem,
-                final String componentId,
-                final IModel<ReportTO> rowModel) {
+                    final Item<ICellPopulator<ReportTO>> cellItem,
+                    final String componentId,
+                    final IModel<ReportTO> rowModel) {
 
                 Component panel;
                 try {
                     JobTO jobTO = ReportRestClient.getJob(rowModel.getObject().getKey());
                     panel = new JobActionPanel(componentId, jobTO, false, ReportDirectoryPanel.this);
                     MetaDataRoleAuthorizationStrategy.authorize(panel, WebPage.ENABLE,
-                        String.format("%s,%s",
-                            IdRepoEntitlement.REPORT_EXECUTE,
-                            IdRepoEntitlement.REPORT_UPDATE));
+                            String.format("%s,%s",
+                                    IdRepoEntitlement.REPORT_EXECUTE,
+                                    IdRepoEntitlement.REPORT_UPDATE));
                 } catch (Exception e) {
                     LOG.error("Could not get job for report {}", rowModel.getObject().getKey(), e);
                     panel = new Label(componentId, Model.of());
@@ -156,7 +156,7 @@ public abstract class ReportDirectoryPanel
 
             @Override
             public String getCssClass() {
-                return "col-xs-1";
+                return "running-col";
             }
         });
 
@@ -175,7 +175,17 @@ public abstract class ReportDirectoryPanel
 
     @Override
     public ActionsPanel<ReportTO> getActions(final IModel<ReportTO> model) {
-        final ActionsPanel<ReportTO> panel = super.getActions(model);
+        ActionsPanel<ReportTO> panel = super.getActions(model);
+
+        panel.add(new ActionLink<>() {
+
+            private static final long serialVersionUID = -3722207913631435501L;
+
+            @Override
+            public void onClick(final AjaxRequestTarget target, final ReportTO ignore) {
+                viewReportExecs(model.getObject(), target);
+            }
+        }, ActionLink.ActionType.VIEW_EXECUTIONS, IdRepoEntitlement.REPORT_READ);
 
         panel.add(new ActionLink<>() {
 
@@ -184,8 +194,8 @@ public abstract class ReportDirectoryPanel
             @Override
             public void onClick(final AjaxRequestTarget target, final ReportTO ignore) {
                 send(ReportDirectoryPanel.this, Broadcast.EXACT,
-                    new AjaxWizard.EditItemActionEvent<>(
-                        ReportRestClient.read(model.getObject().getKey()), target));
+                        new AjaxWizard.EditItemActionEvent<>(
+                                ReportRestClient.read(model.getObject().getKey()), target));
             }
         }, ActionLink.ActionType.EDIT, IdRepoEntitlement.REPORT_UPDATE);
 
@@ -198,7 +208,7 @@ public abstract class ReportDirectoryPanel
                 final ReportTO clone = SerializationUtils.clone(model.getObject());
                 clone.setKey(null);
                 send(ReportDirectoryPanel.this, Broadcast.EXACT,
-                    new AjaxWizard.EditItemActionEvent<>(clone, target));
+                        new AjaxWizard.EditItemActionEvent<>(clone, target));
             }
         }, ActionLink.ActionType.CLONE, IdRepoEntitlement.REPORT_CREATE);
 
@@ -208,25 +218,14 @@ public abstract class ReportDirectoryPanel
 
             @Override
             public void onClick(final AjaxRequestTarget target, final ReportTO ignore) {
-                target.add(modal.setContent(new ReportletDirectoryPanel(
-                    modal, model.getObject().getKey(), pageRef)));
+                target.add(modal.setContent(new ReportletDirectoryPanel(modal, model.getObject().getKey(), pageRef)));
 
                 modal.header(new StringResourceModel(
-                    "reportlet.conf", ReportDirectoryPanel.this, Model.of(model.getObject())));
+                        "reportlet.conf", ReportDirectoryPanel.this, Model.of(model.getObject())));
                 modal.show(true);
             }
         }, ActionLink.ActionType.COMPOSE, IdRepoEntitlement.REPORT_UPDATE);
 
-        panel.add(new ActionLink<>() {
-
-            private static final long serialVersionUID = -3722207913631435501L;
-
-            @Override
-            public void onClick(final AjaxRequestTarget target, final ReportTO ignore) {
-                viewReport(model.getObject(), target);
-            }
-        }, ActionLink.ActionType.VIEW_EXECUTIONS, IdRepoEntitlement.REPORT_READ);
-
         panel.add(new ActionLink<>() {
 
             private static final long serialVersionUID = -3722207913631435501L;
@@ -234,7 +233,7 @@ public abstract class ReportDirectoryPanel
             @Override
             public void onClick(final AjaxRequestTarget target, final ReportTO ignore) {
                 startAt.setExecutionDetail(
-                    model.getObject().getKey(), model.getObject().getName(), target);
+                        model.getObject().getKey(), model.getObject().getName(), target);
                 startAt.toggle(target, true);
             }
         }, ActionLink.ActionType.EXECUTE, IdRepoEntitlement.REPORT_EXECUTE);
@@ -257,6 +256,7 @@ public abstract class ReportDirectoryPanel
                 ((BasePage) pageRef.getPage()).getNotificationPanel().refresh(target);
             }
         }, ActionLink.ActionType.DELETE, IdRepoEntitlement.REPORT_DELETE, true);
+
         return panel;
     }
 
@@ -278,7 +278,7 @@ public abstract class ReportDirectoryPanel
         return IdRepoConstants.PREF_REPORT_PAGINATOR_ROWS;
     }
 
-    protected abstract void viewReport(ReportTO reportTO, AjaxRequestTarget target);
+    protected abstract void viewReportExecs(ReportTO reportTO, AjaxRequestTarget target);
 
     protected static class ReportDataProvider extends DirectoryDataProvider<ReportTO> {
 
diff --git a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/reports/ReportExecutionDetails.java b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/reports/ReportExecutionDetails.java
index fb7246743e..66cad1a013 100644
--- a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/reports/ReportExecutionDetails.java
+++ b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/reports/ReportExecutionDetails.java
@@ -44,7 +44,7 @@ public class ReportExecutionDetails extends MultilevelPanel.SecondLevel {
     public ReportExecutionDetails(final ReportTO reportTO, final PageReference pageRef) {
         super();
 
-        final MultilevelPanel mlp = new MultilevelPanel("executions");
+        MultilevelPanel mlp = new MultilevelPanel("executions");
         add(mlp);
 
         mlp.setFirstLevel(new ReportExecutionDirectoryPanel(mlp, reportTO.getKey(), new ReportRestClient(), pageRef));
@@ -54,8 +54,6 @@ public class ReportExecutionDetails extends MultilevelPanel.SecondLevel {
 
         private static final long serialVersionUID = 5691719817252887541L;
 
-        private final MultilevelPanel mlp;
-
         private final AjaxDownloadBehavior downloadBehavior;
 
         ReportExecutionDirectoryPanel(
@@ -63,8 +61,8 @@ public class ReportExecutionDetails extends MultilevelPanel.SecondLevel {
                 final String key,
                 final ExecutionRestClient executionRestClient,
                 final PageReference pageRef) {
+
             super(multiLevelPanelRef, key, executionRestClient, pageRef);
-            this.mlp = multiLevelPanelRef;
 
             this.downloadBehavior = new AjaxDownloadBehavior();
             this.add(downloadBehavior);
@@ -75,7 +73,8 @@ public class ReportExecutionDetails extends MultilevelPanel.SecondLevel {
                 final String title,
                 final MultilevelPanel.SecondLevel slevel,
                 final AjaxRequestTarget target) {
-            mlp.next(title, slevel, target);
+
+            multiLevelPanelRef.next(title, slevel, target);
         }
 
         @Override
@@ -87,7 +86,7 @@ public class ReportExecutionDetails extends MultilevelPanel.SecondLevel {
                 @Override
                 public void onClick(final AjaxRequestTarget target, final ExecTO ignore) {
                     downloadBehavior.setResponse(new ResponseHolder(ReportRestClient.exportExecutionResult(
-                        model.getObject().getKey(), ReportExecExportFormat.CSV)));
+                            model.getObject().getKey(), ReportExecExportFormat.CSV)));
                     downloadBehavior.initiate(target);
                 }
             }, ActionLink.ActionType.EXPORT_CSV, IdRepoEntitlement.REPORT_READ);
@@ -99,7 +98,7 @@ public class ReportExecutionDetails extends MultilevelPanel.SecondLevel {
                 @Override
                 public void onClick(final AjaxRequestTarget target, final ExecTO ignore) {
                     downloadBehavior.setResponse(new ResponseHolder(ReportRestClient.exportExecutionResult(
-                        model.getObject().getKey(), ReportExecExportFormat.HTML)));
+                            model.getObject().getKey(), ReportExecExportFormat.HTML)));
                     downloadBehavior.initiate(target);
                 }
             }, ActionLink.ActionType.EXPORT_HTML, IdRepoEntitlement.REPORT_READ);
@@ -111,7 +110,7 @@ public class ReportExecutionDetails extends MultilevelPanel.SecondLevel {
                 @Override
                 public void onClick(final AjaxRequestTarget target, final ExecTO ignore) {
                     downloadBehavior.setResponse(new ResponseHolder(ReportRestClient.exportExecutionResult(
-                        model.getObject().getKey(), ReportExecExportFormat.PDF)));
+                            model.getObject().getKey(), ReportExecExportFormat.PDF)));
                     downloadBehavior.initiate(target);
                 }
             }, ActionLink.ActionType.EXPORT_PDF, IdRepoEntitlement.REPORT_READ);
@@ -123,7 +122,7 @@ public class ReportExecutionDetails extends MultilevelPanel.SecondLevel {
                 @Override
                 public void onClick(final AjaxRequestTarget target, final ExecTO ignore) {
                     downloadBehavior.setResponse(new ResponseHolder(ReportRestClient.exportExecutionResult(
-                        model.getObject().getKey(), ReportExecExportFormat.RTF)));
+                            model.getObject().getKey(), ReportExecExportFormat.RTF)));
                     downloadBehavior.initiate(target);
                 }
             }, ActionLink.ActionType.EXPORT_RTF, IdRepoEntitlement.REPORT_READ);
@@ -135,7 +134,7 @@ public class ReportExecutionDetails extends MultilevelPanel.SecondLevel {
                 @Override
                 public void onClick(final AjaxRequestTarget target, final ExecTO ignore) {
                     downloadBehavior.setResponse(new ResponseHolder(ReportRestClient.exportExecutionResult(
-                        model.getObject().getKey(), ReportExecExportFormat.XML)));
+                            model.getObject().getKey(), ReportExecExportFormat.XML)));
                     downloadBehavior.initiate(target);
                 }
             }, ActionLink.ActionType.EXPORT_XML, IdRepoEntitlement.REPORT_READ);
diff --git a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/reports/ReportletDirectoryPanel.java b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/reports/ReportletDirectoryPanel.java
index 9bd23ea6f4..921061bc7e 100644
--- a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/reports/ReportletDirectoryPanel.java
+++ b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/reports/ReportletDirectoryPanel.java
@@ -78,6 +78,7 @@ public class ReportletDirectoryPanel extends DirectoryPanel<
 
     public ReportletDirectoryPanel(
             final BaseModal<ReportTO> baseModal, final String report, final PageReference pageRef) {
+
         super(BaseModal.CONTENT_ID, pageRef, false);
 
         disableCheckBoxes();
@@ -88,8 +89,7 @@ public class ReportletDirectoryPanel extends DirectoryPanel<
 
         enableUtilityButton();
 
-        this.addNewItemPanelBuilder(
-                new ReportletWizardBuilder(report, new ReportletWrapper(true), pageRef), true);
+        addNewItemPanelBuilder(new ReportletWizardBuilder(report, new ReportletWrapper(true), pageRef), true);
 
         MetaDataRoleAuthorizationStrategy.authorize(addAjaxLink, RENDER, IdRepoEntitlement.REPORT_UPDATE);
         initResultTable();
@@ -103,15 +103,15 @@ public class ReportletDirectoryPanel extends DirectoryPanel<
                 new StringResourceModel("reportlet", this), "implementationKey", "implementationKey"));
 
         columns.add(new AbstractColumn<>(
-            new StringResourceModel("configuration", this)) {
+                new StringResourceModel("configuration", this)) {
 
             private static final long serialVersionUID = -4008579357070833846L;
 
             @Override
             public void populateItem(
-                final Item<ICellPopulator<ReportletWrapper>> cellItem,
-                final String componentId,
-                final IModel<ReportletWrapper> rowModel) {
+                    final Item<ICellPopulator<ReportletWrapper>> cellItem,
+                    final String componentId,
+                    final IModel<ReportletWrapper> rowModel) {
 
                 if (rowModel.getObject().getConf() == null) {
                     cellItem.add(new Label(componentId, ""));
@@ -139,17 +139,18 @@ public class ReportletDirectoryPanel extends DirectoryPanel<
                     SyncopeConsoleSession.get().info(getString("noConf"));
                 } else {
                     send(ReportletDirectoryPanel.this, Broadcast.EXACT,
-                        new AjaxWizard.EditItemActionEvent<>(model.getObject(), target));
+                            new AjaxWizard.EditItemActionEvent<>(model.getObject(), target));
                 }
             }
         }, ActionLink.ActionType.EDIT, IdRepoEntitlement.REPORT_UPDATE);
+
         panel.add(new ActionLink<>() {
 
             private static final long serialVersionUID = -3722207913631435501L;
 
             @Override
             public void onClick(final AjaxRequestTarget target, final ReportletWrapper ignore) {
-                final ReportletConf reportlet = model.getObject().getConf();
+                ReportletConf reportlet = model.getObject().getConf();
                 try {
                     ReportTO actual = ReportRestClient.read(report);
                     actual.getReportlets().remove(model.getObject().getImplementationKey());
@@ -170,7 +171,7 @@ public class ReportletDirectoryPanel extends DirectoryPanel<
 
     @Override
     public ActionsPanel<Serializable> getHeader(final String componentId) {
-        final ActionsPanel<Serializable> panel = new ActionsPanel<>(componentId, null);
+        ActionsPanel<Serializable> panel = new ActionsPanel<>(componentId, null);
 
         panel.add(new ActionLink<>() {
 
@@ -182,7 +183,7 @@ public class ReportletDirectoryPanel extends DirectoryPanel<
                     customActionOnFinishCallback(target);
                 }
             }
-        }, ActionLink.ActionType.RELOAD, IdRepoEntitlement.TASK_LIST).hideLabel();
+        }, ActionLink.ActionType.RELOAD, IdRepoEntitlement.REPORT_READ).hideLabel();
         return panel;
     }
 
@@ -201,6 +202,16 @@ public class ReportletDirectoryPanel extends DirectoryPanel<
         return IdRepoConstants.PREF_REPORTLET_PAGINATOR_ROWS;
     }
 
+    @Override
+    public void onEvent(final IEvent<?> event) {
+        super.onEvent(event);
+        if (event.getPayload() instanceof ExitEvent) {
+            AjaxRequestTarget target = ExitEvent.class.cast(event.getPayload()).getTarget();
+            baseModal.show(false);
+            baseModal.close(target);
+        }
+    }
+
     protected class ReportDataProvider extends DirectoryDataProvider<ReportletWrapper> {
 
         private static final long serialVersionUID = 4725679400450513556L;
@@ -247,7 +258,7 @@ public class ReportletDirectoryPanel extends DirectoryPanel<
 
         @Override
         public long size() {
-            final ReportTO actual = ReportRestClient.read(report);
+            ReportTO actual = ReportRestClient.read(report);
             return getReportletWrappers(actual).size();
         }
 
@@ -256,14 +267,4 @@ public class ReportletDirectoryPanel extends DirectoryPanel<
             return new CompoundPropertyModel<>(object);
         }
     }
-
-    @Override
-    public void onEvent(final IEvent<?> event) {
-        super.onEvent(event);
-        if (event.getPayload() instanceof ExitEvent) {
-            AjaxRequestTarget target = ExitEvent.class.cast(event.getPayload()).getTarget();
-            baseModal.show(false);
-            baseModal.close(target);
-        }
-    }
 }
diff --git a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/reports/ReportletWizardBuilder.java b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/reports/ReportletWizardBuilder.java
index 74f7be7664..0339e88557 100644
--- a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/reports/ReportletWizardBuilder.java
+++ b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/reports/ReportletWizardBuilder.java
@@ -20,6 +20,7 @@ package org.apache.syncope.client.console.reports;
 
 import com.fasterxml.jackson.databind.json.JsonMapper;
 import java.io.Serializable;
+import java.util.List;
 import java.util.stream.Collectors;
 import org.apache.syncope.client.console.panels.BeanPanel;
 import org.apache.syncope.client.console.panels.search.SearchUtils;
@@ -50,6 +51,16 @@ public class ReportletWizardBuilder extends BaseAjaxWizardBuilder<ReportletWrapp
 
     private static final JsonMapper MAPPER = JsonMapper.builder().findAndAddModules().build();
 
+    private final LoadableDetachableModel<List<ImplementationTO>> reportlets = new LoadableDetachableModel<>() {
+
+        private static final long serialVersionUID = 4659376149825914247L;
+
+        @Override
+        protected List<ImplementationTO> load() {
+            return ImplementationRestClient.list(IdRepoImplementationType.REPORTLET);
+        }
+    };
+
     private final String report;
 
     public ReportletWizardBuilder(final String report, final ReportletWrapper reportlet, final PageReference pageRef) {
@@ -63,14 +74,17 @@ public class ReportletWizardBuilder extends BaseAjaxWizardBuilder<ReportletWrapp
             BeanWrapper confWrapper = PropertyAccessorFactory.forBeanPropertyAccess(modelObject.getConf());
             modelObject.getSCondWrapper().forEach((fieldName, pair) -> confWrapper.setPropertyValue(
                     fieldName, SearchUtils.buildFIQL(pair.getRight(), pair.getLeft())));
-            ImplementationTO reportlet = ImplementationRestClient.read(
-                    IdRepoImplementationType.REPORTLET, modelObject.getImplementationKey());
-            try {
-                reportlet.setBody(MAPPER.writeValueAsString(modelObject.getConf()));
-                ImplementationRestClient.update(reportlet);
-            } catch (Exception e) {
-                throw new WicketRuntimeException(e);
-            }
+            reportlets.getObject().stream().
+                    filter(r -> r.getKey().equals(modelObject.getImplementationKey())).
+                    findFirst().
+                    ifPresent(reportlet -> {
+                        try {
+                            reportlet.setBody(MAPPER.writeValueAsString(modelObject.getConf()));
+                            ImplementationRestClient.update(reportlet);
+                        } catch (Exception e) {
+                            throw new WicketRuntimeException(e);
+                        }
+                    });
         }
 
         ReportTO reportTO = ReportRestClient.read(report);
@@ -89,15 +103,19 @@ public class ReportletWizardBuilder extends BaseAjaxWizardBuilder<ReportletWrapp
         return wizardModel;
     }
 
-    public static class Profile extends WizardStep {
+    public class Profile extends WizardStep {
 
         private static final long serialVersionUID = -3043839139187792810L;
 
+        private final ReportletWrapper reportlet;
+
         public Profile(final ReportletWrapper reportlet) {
+            this.reportlet = reportlet;
+
             AjaxDropDownChoicePanel<String> conf = new AjaxDropDownChoicePanel<>(
                     "reportlet", getString("reportlet"), new PropertyModel<>(reportlet, "implementationKey"));
 
-            conf.setChoices(ImplementationRestClient.list(IdRepoImplementationType.REPORTLET).stream().
+            conf.setChoices(reportlets.getObject().stream().
                     map(ImplementationTO::getKey).sorted().collect(Collectors.toList()));
             conf.addRequiredLabel();
             conf.setNullValid(false);
@@ -108,21 +126,30 @@ public class ReportletWizardBuilder extends BaseAjaxWizardBuilder<ReportletWrapp
 
                 @Override
                 protected void onEvent(final AjaxRequestTarget target) {
-                    ImplementationTO impl = ImplementationRestClient.read(
-                            IdRepoImplementationType.REPORTLET, conf.getModelObject());
-                    reportlet.setImplementationEngine(impl.getEngine());
-                    if (impl.getEngine() == ImplementationEngine.JAVA) {
-                        try {
-                            ReportletConf conf = MAPPER.readValue(impl.getBody(), ReportletConf.class);
-                            reportlet.setConf(conf);
-                        } catch (Exception e) {
-                            LOG.error("During deserialization", e);
-                        }
-                    }
+                    reportlets.getObject().stream().
+                            filter(r -> r.getKey().equals(conf.getModelObject())).
+                            findFirst().
+                            ifPresent(impl -> {
+                                reportlet.setImplementationEngine(impl.getEngine());
+                                if (impl.getEngine() == ImplementationEngine.JAVA) {
+                                    try {
+                                        reportlet.setConf(MAPPER.readValue(impl.getBody(), ReportletConf.class));
+                                    } catch (Exception e) {
+                                        LOG.error("During deserialization", e);
+                                    }
+                                }
+                            });
                 }
             });
             add(conf);
         }
+
+        @Override
+        public void applyState() {
+            if (reportlet.getImplementationEngine() == ImplementationEngine.GROOVY) {
+                getWizardModel().finish();
+            }
+        }
     }
 
     public class Configuration extends WizardStep {
diff --git a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/rest/AccessTokenRestClient.java b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/rest/AccessTokenRestClient.java
index ed95b43653..df311a4f3c 100644
--- a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/rest/AccessTokenRestClient.java
+++ b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/rest/AccessTokenRestClient.java
@@ -46,5 +46,4 @@ public class AccessTokenRestClient extends BaseRestClient {
                 new AccessTokenQuery.Builder().page(page).size(size).orderBy(toOrderBy(sort)).build()).
                 getResult();
     }
-
 }
diff --git a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/rest/CommandRestClient.java b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/rest/CommandRestClient.java
new file mode 100644
index 0000000000..d11aa166be
--- /dev/null
+++ b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/rest/CommandRestClient.java
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.client.console.rest;
+
+import java.util.List;
+import org.apache.syncope.common.lib.command.CommandOutput;
+import org.apache.syncope.common.lib.command.CommandTO;
+import org.apache.syncope.common.rest.api.beans.CommandQuery;
+import org.apache.syncope.common.rest.api.service.CommandService;
+
+public class CommandRestClient extends BaseRestClient {
+
+    private static final long serialVersionUID = -3582864276979370967L;
+
+    public static int count(final String keyword) {
+        return getService(CommandService.class).
+                search(new CommandQuery.Builder().page(1).size(0).keyword(keyword).build()).
+                getTotalCount();
+    }
+
+    public static List<CommandTO> search(final int page, final int size, final String keyword) {
+        return getService(CommandService.class).
+                search(new CommandQuery.Builder().page(page).size(size).keyword(keyword).build()).
+                getResult();
+    }
+
+    public static CommandTO read(final String key) {
+        return getService(CommandService.class).read(key);
+    }
+
+    public static CommandOutput run(final CommandTO command) {
+        return getService(CommandService.class).run(command);
+    }
+}
diff --git a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/rest/TaskRestClient.java b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/rest/TaskRestClient.java
index 4719f33f4c..604954da7e 100644
--- a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/rest/TaskRestClient.java
+++ b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/rest/TaskRestClient.java
@@ -32,8 +32,6 @@ import org.apache.syncope.common.lib.to.JobTO;
 import org.apache.syncope.common.lib.to.NotificationTaskTO;
 import org.apache.syncope.common.lib.to.PagedResult;
 import org.apache.syncope.common.lib.to.PropagationTaskTO;
-import org.apache.syncope.common.lib.to.PullTaskTO;
-import org.apache.syncope.common.lib.to.PushTaskTO;
 import org.apache.syncope.common.lib.to.SchedTaskTO;
 import org.apache.syncope.common.lib.to.TaskTO;
 import org.apache.syncope.common.lib.types.AnyTypeKind;
@@ -144,26 +142,33 @@ public class TaskRestClient extends BaseRestClient implements ExecutionRestClien
         return list.getResult();
     }
 
-    @SuppressWarnings("unchecked")
     public static <T extends TaskTO> List<T> list(
-            final Class<T> reference, final int page, final int size, final SortParam<String> sort) {
-
-        return (List<T>) getService(TaskService.class).
-                search(new TaskQuery.Builder(getTaskType(reference)).page(page).size(size).
-                        orderBy(toOrderBy(sort)).build()).getResult();
+            final TaskType taskType, final int page, final int size, final SortParam<String> sort) {
+
+        return getService(TaskService.class).<T>search(
+                new TaskQuery.Builder(taskType).
+                        page(page).
+                        size(size).
+                        orderBy(toOrderBy(sort)).
+                        build()).
+                getResult();
     }
 
-    @SuppressWarnings("unchecked")
     public static <T extends TaskTO> List<T> list(
             final String resource,
-            final Class<T> reference,
+            final TaskType taskType,
             final int page,
             final int size,
             final SortParam<String> sort) {
 
-        return (List<T>) getService(TaskService.class).
-                search(new TaskQuery.Builder(getTaskType(reference)).page(page).size(size).resource(resource).
-                        orderBy(toOrderBy(sort)).build()).getResult();
+        return getService(TaskService.class).<T>search(
+                new TaskQuery.Builder(taskType).
+                        page(page).
+                        size(size).
+                        resource(resource).
+                        orderBy(toOrderBy(sort)).
+                        build()).
+                getResult();
     }
 
     @Override
@@ -175,22 +180,6 @@ public class TaskRestClient extends BaseRestClient implements ExecutionRestClien
                         orderBy(toOrderBy(sort)).build()).getResult();
     }
 
-    private static TaskType getTaskType(final Class<?> reference) {
-        TaskType result = null;
-        if (PropagationTaskTO.class.equals(reference)) {
-            result = TaskType.PROPAGATION;
-        } else if (NotificationTaskTO.class.equals(reference)) {
-            result = TaskType.NOTIFICATION;
-        } else if (SchedTaskTO.class.equals(reference)) {
-            result = TaskType.SCHEDULED;
-        } else if (PullTaskTO.class.equals(reference)) {
-            result = TaskType.PULL;
-        } else if (PushTaskTO.class.equals(reference)) {
-            result = TaskType.PUSH;
-        }
-        return result;
-    }
-
     public static PropagationTaskTO readPropagationTask(final String taskKey) {
         return getService(TaskService.class).read(TaskType.PROPAGATION, taskKey, false);
     }
diff --git a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/tasks/AnyPropagationTasks.java b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/tasks/AnyPropagationTasks.java
index 7caf80cca9..de56660737 100644
--- a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/tasks/AnyPropagationTasks.java
+++ b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/tasks/AnyPropagationTasks.java
@@ -47,10 +47,10 @@ public class AnyPropagationTasks extends AbstractPropagationTasks {
             private static final long serialVersionUID = -2195387360323687302L;
 
             @Override
-            protected void viewTask(final PropagationTaskTO taskTO, final AjaxRequestTarget target) {
+            protected void viewTaskExecs(final PropagationTaskTO taskTO, final AjaxRequestTarget target) {
                 mlp.next(
                         new StringResourceModel("task.view", this, new Model<>(Pair.of(null, taskTO))).getObject(),
-                        new TaskExecutionDetails<>(baseModal, taskTO, pageRef),
+                        new TaskExecutionDetails<>(taskTO, pageRef),
                         target);
             }
 
diff --git a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/tasks/CommandComposeDirectoryPanel.java b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/tasks/CommandComposeDirectoryPanel.java
new file mode 100644
index 0000000000..82dc29bb3c
--- /dev/null
+++ b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/tasks/CommandComposeDirectoryPanel.java
@@ -0,0 +1,234 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.client.console.tasks;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+import org.apache.syncope.client.console.SyncopeConsoleSession;
+import org.apache.syncope.client.console.commons.IdRepoConstants;
+import org.apache.syncope.client.console.panels.DirectoryPanel;
+import org.apache.syncope.client.console.rest.CommandRestClient;
+import org.apache.syncope.client.console.rest.TaskRestClient;
+import org.apache.syncope.client.console.wicket.markup.html.bootstrap.dialog.BaseModal;
+import org.apache.syncope.client.console.wicket.markup.html.form.ActionLink;
+import org.apache.syncope.client.console.wicket.markup.html.form.ActionsPanel;
+import org.apache.syncope.client.ui.commons.Constants;
+import org.apache.syncope.client.ui.commons.DirectoryDataProvider;
+import org.apache.syncope.client.ui.commons.pages.BaseWebPage;
+import org.apache.syncope.client.ui.commons.panels.ModalPanel;
+import org.apache.syncope.client.ui.commons.wizards.AjaxWizard;
+import org.apache.syncope.common.lib.SyncopeClientException;
+import org.apache.syncope.common.lib.command.CommandTO;
+import org.apache.syncope.common.lib.to.MacroTaskTO;
+import org.apache.syncope.common.lib.types.IdRepoEntitlement;
+import org.apache.syncope.common.lib.types.TaskType;
+import org.apache.wicket.PageReference;
+import org.apache.wicket.ajax.AjaxRequestTarget;
+import org.apache.wicket.authroles.authorization.strategies.role.metadata.MetaDataRoleAuthorizationStrategy;
+import org.apache.wicket.event.Broadcast;
+import org.apache.wicket.event.IEvent;
+import org.apache.wicket.extensions.markup.html.repeater.data.grid.ICellPopulator;
+import org.apache.wicket.extensions.markup.html.repeater.data.table.AbstractColumn;
+import org.apache.wicket.extensions.markup.html.repeater.data.table.IColumn;
+import org.apache.wicket.markup.html.basic.Label;
+import org.apache.wicket.markup.repeater.Item;
+import org.apache.wicket.model.CompoundPropertyModel;
+import org.apache.wicket.model.IModel;
+import org.apache.wicket.model.ResourceModel;
+
+public class CommandComposeDirectoryPanel extends DirectoryPanel<
+        CommandWrapper, CommandWrapper, DirectoryDataProvider<CommandWrapper>, CommandRestClient>
+        implements ModalPanel {
+
+    private static final long serialVersionUID = 8899580817658145305L;
+
+    private final BaseModal<MacroTaskTO> baseModal;
+
+    private final String task;
+
+    public CommandComposeDirectoryPanel(
+            final BaseModal<MacroTaskTO> baseModal, final String task, final PageReference pageRef) {
+
+        super(BaseModal.CONTENT_ID, pageRef, false);
+
+        disableCheckBoxes();
+
+        this.baseModal = baseModal;
+        this.task = task;
+
+        enableUtilityButton();
+
+        addNewItemPanelBuilder(new CommandComposeWizardBuilder(task, new CommandWrapper(true), pageRef), true);
+
+        MetaDataRoleAuthorizationStrategy.authorize(addAjaxLink, RENDER, IdRepoEntitlement.TASK_UPDATE);
+        initResultTable();
+    }
+
+    @Override
+    protected List<IColumn<CommandWrapper, String>> getColumns() {
+        List<IColumn<CommandWrapper, String>> columns = new ArrayList<>();
+
+        columns.add(new AbstractColumn<>(new ResourceModel(Constants.KEY_FIELD_NAME), Constants.KEY_FIELD_NAME) {
+
+            private static final long serialVersionUID = -4008579357070833846L;
+
+            @Override
+            public void populateItem(
+                    final Item<ICellPopulator<CommandWrapper>> cellItem,
+                    final String componentId,
+                    final IModel<CommandWrapper> rowModel) {
+
+                cellItem.add(new Label(componentId, rowModel.getObject().getCommand().getKey()));
+            }
+        });
+
+        columns.add(new AbstractColumn<>(new ResourceModel("arguments"), "arguments") {
+
+            private static final long serialVersionUID = -4008579357070833846L;
+
+            @Override
+            public void populateItem(
+                    final Item<ICellPopulator<CommandWrapper>> cellItem,
+                    final String componentId,
+                    final IModel<CommandWrapper> rowModel) {
+
+                cellItem.add(new Label(componentId, rowModel.getObject().getCommand().getArgs().getClass().getName()));
+            }
+        });
+
+        return columns;
+    }
+
+    @Override
+    public ActionsPanel<CommandWrapper> getActions(final IModel<CommandWrapper> model) {
+        ActionsPanel<CommandWrapper> panel = super.getActions(model);
+
+        panel.add(new ActionLink<>() {
+
+            private static final long serialVersionUID = -3722207913631435501L;
+
+            @Override
+            public void onClick(final AjaxRequestTarget target, final CommandWrapper ignore) {
+                CommandComposeDirectoryPanel.this.getTogglePanel().close(target);
+                send(CommandComposeDirectoryPanel.this, Broadcast.EXACT,
+                        new AjaxWizard.EditItemActionEvent<>(model.getObject(), target));
+            }
+        }, ActionLink.ActionType.EDIT, IdRepoEntitlement.TASK_UPDATE);
+
+        panel.add(new ActionLink<>() {
+
+            private static final long serialVersionUID = -3722207913631435501L;
+
+            @Override
+            public void onClick(final AjaxRequestTarget target, final CommandWrapper ignore) {
+                try {
+                    MacroTaskTO actual = TaskRestClient.readTask(TaskType.MACRO, task);
+                    actual.getCommands().remove(model.getObject().getCommand());
+                    TaskRestClient.update(TaskType.MACRO, actual);
+
+                    SyncopeConsoleSession.get().success(getString(Constants.OPERATION_SUCCEEDED));
+                    customActionOnFinishCallback(target);
+                } catch (SyncopeClientException e) {
+                    LOG.error("While deleting {}", model.getObject(), e);
+                    SyncopeConsoleSession.get().onException(e);
+                }
+                ((BaseWebPage) pageRef.getPage()).getNotificationPanel().refresh(target);
+            }
+        }, ActionLink.ActionType.DELETE, IdRepoEntitlement.TASK_UPDATE);
+
+        return panel;
+    }
+
+    @Override
+    public ActionsPanel<Serializable> getHeader(final String componentId) {
+        ActionsPanel<Serializable> panel = new ActionsPanel<>(componentId, null);
+
+        panel.add(new ActionLink<>() {
+
+            private static final long serialVersionUID = -7978723352517770644L;
+
+            @Override
+            public void onClick(final AjaxRequestTarget target, final Serializable ignore) {
+                if (target != null) {
+                    customActionOnFinishCallback(target);
+                }
+            }
+        }, ActionLink.ActionType.RELOAD, IdRepoEntitlement.TASK_READ).hideLabel();
+        return panel;
+    }
+
+    @Override
+    protected Collection<ActionLink.ActionType> getBatches() {
+        return List.of();
+    }
+
+    @Override
+    protected CommandComposeDataProvider dataProvider() {
+        return new CommandComposeDataProvider(rows);
+    }
+
+    @Override
+    protected String paginatorRowsKey() {
+        return IdRepoConstants.PREF_COMMAND_PAGINATOR_ROWS;
+    }
+
+    @Override
+    public void onEvent(final IEvent<?> event) {
+        super.onEvent(event);
+        if (event.getPayload() instanceof ExitEvent) {
+            AjaxRequestTarget target = ExitEvent.class.cast(event.getPayload()).getTarget();
+            baseModal.show(false);
+            baseModal.close(target);
+        }
+    }
+
+    protected class CommandComposeDataProvider extends DirectoryDataProvider<CommandWrapper> {
+
+        private static final long serialVersionUID = 4725679400450513556L;
+
+        public CommandComposeDataProvider(final int paginatorRows) {
+            super(paginatorRows);
+        }
+
+        @Override
+        public Iterator<CommandWrapper> iterator(final long first, final long count) {
+            MacroTaskTO actual = TaskRestClient.readTask(TaskType.MACRO, task);
+
+            List<CommandTO> commands = actual.getCommands();
+
+            return commands.subList((int) first, (int) (first + count)).stream().
+                    map(command -> new CommandWrapper(false).setCommand(command)).
+                    iterator();
+        }
+
+        @Override
+        public long size() {
+            MacroTaskTO actual = TaskRestClient.readTask(TaskType.MACRO, task);
+            return actual.getCommands().size();
+        }
+
+        @Override
+        public IModel<CommandWrapper> model(final CommandWrapper object) {
+            return new CompoundPropertyModel<>(object);
+        }
+    }
+}
diff --git a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/tasks/CommandComposeWizardBuilder.java b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/tasks/CommandComposeWizardBuilder.java
new file mode 100644
index 0000000000..93a24e40f9
--- /dev/null
+++ b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/tasks/CommandComposeWizardBuilder.java
@@ -0,0 +1,161 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.client.console.tasks;
+
+import java.io.Serializable;
+import java.util.Iterator;
+import java.util.List;
+import org.apache.syncope.client.console.panels.BeanPanel;
+import org.apache.syncope.client.console.rest.CommandRestClient;
+import org.apache.syncope.client.console.rest.ImplementationRestClient;
+import org.apache.syncope.client.console.rest.TaskRestClient;
+import org.apache.syncope.client.console.wizards.BaseAjaxWizardBuilder;
+import org.apache.syncope.client.ui.commons.Constants;
+import org.apache.syncope.client.ui.commons.ajax.form.IndicatorAjaxFormComponentUpdatingBehavior;
+import org.apache.syncope.common.lib.command.CommandTO;
+import org.apache.syncope.common.lib.to.ImplementationTO;
+import org.apache.syncope.common.lib.to.MacroTaskTO;
+import org.apache.syncope.common.lib.types.IdRepoImplementationType;
+import org.apache.syncope.common.lib.types.ImplementationEngine;
+import org.apache.syncope.common.lib.types.TaskType;
+import org.apache.wicket.PageReference;
+import org.apache.wicket.ajax.AjaxRequestTarget;
+import org.apache.wicket.extensions.ajax.markup.html.autocomplete.AutoCompleteSettings;
+import org.apache.wicket.extensions.ajax.markup.html.autocomplete.AutoCompleteTextField;
+import org.apache.wicket.extensions.wizard.WizardModel;
+import org.apache.wicket.extensions.wizard.WizardStep;
+import org.apache.wicket.model.LoadableDetachableModel;
+import org.apache.wicket.model.PropertyModel;
+
+public class CommandComposeWizardBuilder extends BaseAjaxWizardBuilder<CommandWrapper> {
+
+    private static final long serialVersionUID = -2300926041782845851L;
+
+    private final LoadableDetachableModel<List<ImplementationTO>> commands = new LoadableDetachableModel<>() {
+
+        private static final long serialVersionUID = 4659376149825914247L;
+
+        @Override
+        protected List<ImplementationTO> load() {
+            return ImplementationRestClient.list(IdRepoImplementationType.COMMAND);
+        }
+    };
+
+    private final String task;
+
+    public CommandComposeWizardBuilder(
+            final String task, final CommandWrapper defaultItem, final PageReference pageRef) {
+
+        super(defaultItem, pageRef);
+        this.task = task;
+    }
+
+    @Override
+    protected Serializable onApplyInternal(final CommandWrapper modelObject) {
+        MacroTaskTO taskTO = TaskRestClient.readTask(TaskType.MACRO, task);
+        if (modelObject.isNew()) {
+            taskTO.getCommands().add(modelObject.getCommand());
+        } else {
+            taskTO.getCommands().stream().
+                    filter(cmd -> cmd.getKey().equals(modelObject.getCommand().getKey())).
+                    findFirst().
+                    ifPresent(cmd -> cmd.setArgs(modelObject.getCommand().getArgs()));
+        }
+
+        TaskRestClient.update(TaskType.MACRO, taskTO);
+        return modelObject;
+    }
+
+    @Override
+    protected WizardModel buildModelSteps(final CommandWrapper modelObject, final WizardModel wizardModel) {
+        wizardModel.add(new Profile(modelObject));
+        wizardModel.add(new CommandArgs(modelObject));
+        return wizardModel;
+    }
+
+    public class Profile extends WizardStep {
+
+        private static final long serialVersionUID = -3043839139187792810L;
+
+        private final CommandWrapper command;
+
+        public Profile(final CommandWrapper command) {
+            this.command = command;
+            MacroTaskTO taskTO = TaskRestClient.readTask(TaskType.MACRO, task);
+
+            AutoCompleteSettings settings = new AutoCompleteSettings();
+            settings.setShowCompleteListOnFocusGain(false);
+            settings.setShowListOnEmptyInput(false);
+
+            AutoCompleteTextField<String> args = new AutoCompleteTextField<>(
+                    "command", new PropertyModel<>(command, "command.key"), settings) {
+
+                private static final long serialVersionUID = -6556576139048844857L;
+
+                @Override
+                protected Iterator<String> getChoices(final String input) {
+                    return commands.getObject().stream().
+                            map(ImplementationTO::getKey).
+                            filter(cmd -> cmd.contains(input)
+                            && taskTO.getCommands().stream().noneMatch(c -> c.getKey().equals(cmd))).
+                            sorted().iterator();
+                }
+            };
+            args.setRequired(true);
+            args.setEnabled(command.isNew());
+            args.add(new IndicatorAjaxFormComponentUpdatingBehavior(Constants.ON_CHANGE) {
+
+                private static final long serialVersionUID = -6139318907146065915L;
+
+                @Override
+                protected void onUpdate(final AjaxRequestTarget target) {
+                    CommandTO cmd = CommandRestClient.read(command.getCommand().getKey());
+                    command.getCommand().setArgs(cmd.getArgs());
+                }
+            });
+            add(args);
+        }
+
+        @Override
+        public void applyState() {
+            commands.getObject().stream().
+                    filter(cmd -> cmd.getKey().equals(command.getCommand().getKey())
+                    && cmd.getEngine() == ImplementationEngine.GROOVY).
+                    findFirst().ifPresent(cmd -> getWizardModel().finish());
+        }
+    }
+
+    public class CommandArgs extends WizardStep {
+
+        private static final long serialVersionUID = -785981096328637758L;
+
+        public CommandArgs(final CommandWrapper command) {
+            LoadableDetachableModel<Serializable> bean = new LoadableDetachableModel<>() {
+
+                private static final long serialVersionUID = 2092144708018739371L;
+
+                @Override
+                protected Serializable load() {
+                    return command.getCommand().getArgs();
+                }
+            };
+            add(new BeanPanel<>("bean", bean, pageRef).setRenderBodyOnly(true));
+        }
+    }
+}
diff --git a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/tasks/ExecMessage.java b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/tasks/CommandWrapper.java
similarity index 54%
copy from client/idrepo/console/src/main/java/org/apache/syncope/client/console/tasks/ExecMessage.java
copy to client/idrepo/console/src/main/java/org/apache/syncope/client/console/tasks/CommandWrapper.java
index 293e051987..d8dab2d5c6 100644
--- a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/tasks/ExecMessage.java
+++ b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/tasks/CommandWrapper.java
@@ -18,16 +18,36 @@
  */
 package org.apache.syncope.client.console.tasks;
 
-import org.apache.syncope.client.console.panels.MultilevelPanel;
-import org.apache.wicket.markup.html.basic.Label;
-import org.apache.wicket.model.Model;
+import java.io.Serializable;
+import org.apache.syncope.common.lib.command.CommandTO;
 
-public class ExecMessage extends MultilevelPanel.SecondLevel {
+public class CommandWrapper implements Serializable {
 
-    private static final long serialVersionUID = 3163146190501510888L;
+    private static final long serialVersionUID = -2423427579112218652L;
 
-    public ExecMessage(final String message) {
-        super();
-        add(new Label("message", Model.of(message)).setOutputMarkupId(true));
+    private final boolean isNew;
+
+    private CommandTO command;
+
+    public CommandWrapper(final boolean isNew) {
+        this.isNew = isNew;
+    }
+
+    public boolean isNew() {
+        return isNew;
+    }
+
+    public CommandTO getCommand() {
+        return command;
+    }
+
+    public CommandWrapper setCommand(final CommandTO command) {
+        this.command = command;
+        return this;
+    }
+
+    @Override
+    public String toString() {
+        return "CommandWrapper{" + "isNew=" + isNew + ", command=" + command + '}';
     }
 }
diff --git a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/tasks/ExecMessage.java b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/tasks/ExecMessage.java
index 293e051987..f09179d056 100644
--- a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/tasks/ExecMessage.java
+++ b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/tasks/ExecMessage.java
@@ -28,6 +28,15 @@ public class ExecMessage extends MultilevelPanel.SecondLevel {
 
     public ExecMessage(final String message) {
         super();
+        init(message);
+    }
+
+    public ExecMessage(final String id, final String message) {
+        super(id);
+        init(message);
+    }
+
+    protected void init(final String message) {
         add(new Label("message", Model.of(message)).setOutputMarkupId(true));
     }
 }
diff --git a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/tasks/ExecutionsDirectoryPanel.java b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/tasks/ExecutionsDirectoryPanel.java
index a4873d15a3..658eabcddc 100644
--- a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/tasks/ExecutionsDirectoryPanel.java
+++ b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/tasks/ExecutionsDirectoryPanel.java
@@ -33,7 +33,6 @@ import org.apache.syncope.client.console.rest.ExecutionRestClient;
 import org.apache.syncope.client.console.tasks.ExecutionsDirectoryPanel.ExecProvider;
 import org.apache.syncope.client.console.wicket.extensions.markup.html.repeater.data.table.DatePropertyColumn;
 import org.apache.syncope.client.console.wicket.extensions.markup.html.repeater.data.table.KeyPropertyColumn;
-import org.apache.syncope.client.console.wicket.markup.html.bootstrap.dialog.BaseModal;
 import org.apache.syncope.client.console.wicket.markup.html.form.ActionLink;
 import org.apache.syncope.client.console.wicket.markup.html.form.ActionsPanel;
 import org.apache.syncope.client.ui.commons.Constants;
@@ -54,29 +53,18 @@ public abstract class ExecutionsDirectoryPanel
 
     private static final long serialVersionUID = 2039393934721149162L;
 
-    private final BaseModal<?> baseModal;
+    protected final MultilevelPanel multiLevelPanelRef;
 
-    private final MultilevelPanel multiLevelPanelRef;
-
-    private final String key;
+    protected final String key;
 
     public ExecutionsDirectoryPanel(
             final MultilevelPanel multiLevelPanelRef,
             final String key,
             final ExecutionRestClient executionRestClient,
             final PageReference pageRef) {
-        this(null, multiLevelPanelRef, key, executionRestClient, pageRef);
-    }
 
-    public ExecutionsDirectoryPanel(
-            final BaseModal<?> baseModal,
-            final MultilevelPanel multiLevelPanelRef,
-            final String key,
-            final ExecutionRestClient executionRestClient,
-            final PageReference pageRef) {
         super(MultilevelPanel.FIRST_LEVEL_ID, pageRef, false);
 
-        this.baseModal = baseModal;
         this.multiLevelPanelRef = multiLevelPanelRef;
         restClient = executionRestClient;
         setOutputMarkupId(true);
@@ -86,14 +74,14 @@ public abstract class ExecutionsDirectoryPanel
 
     @Override
     protected void resultTableCustomChanges(final AjaxDataTablePanel.Builder<ExecTO, String> resultTableBuilder) {
-        resultTableBuilder.setMultiLevelPanel(baseModal, multiLevelPanelRef);
+        resultTableBuilder.setMultiLevelPanel(multiLevelPanelRef);
     }
 
     protected abstract void next(String title, SecondLevel secondLevel, AjaxRequestTarget target);
 
     @Override
     protected List<IColumn<ExecTO, String>> getColumns() {
-        final List<IColumn<ExecTO, String>> columns = new ArrayList<>();
+        List<IColumn<ExecTO, String>> columns = new ArrayList<>();
 
         columns.add(new KeyPropertyColumn<>(
                 new StringResourceModel(Constants.KEY_FIELD_NAME, this),
@@ -112,8 +100,7 @@ public abstract class ExecutionsDirectoryPanel
 
     @Override
     public ActionsPanel<ExecTO> getActions(final IModel<ExecTO> model) {
-        final ActionsPanel<ExecTO> panel = super.getActions(model);
-        final ExecTO taskExecutionTO = model.getObject();
+        ActionsPanel<ExecTO> panel = super.getActions(model);
 
         panel.add(new ActionLink<>() {
 
@@ -123,9 +110,10 @@ public abstract class ExecutionsDirectoryPanel
             public void onClick(final AjaxRequestTarget target, final ExecTO ignore) {
                 ExecutionsDirectoryPanel.this.getTogglePanel().close(target);
                 next(new StringResourceModel("execution.view", ExecutionsDirectoryPanel.this, model).
-                    getObject(), new ExecMessage(model.getObject().getMessage()), target);
+                        getObject(), new ExecMessage(model.getObject().getMessage()), target);
             }
         }, ActionLink.ActionType.VIEW, IdRepoEntitlement.TASK_READ);
+
         panel.add(new ActionLink<>() {
 
             private static final long serialVersionUID = -3722207913631435501L;
@@ -134,7 +122,7 @@ public abstract class ExecutionsDirectoryPanel
             public void onClick(final AjaxRequestTarget target, final ExecTO ignore) {
                 ExecutionsDirectoryPanel.this.getTogglePanel().close(target);
                 try {
-                    restClient.deleteExecution(taskExecutionTO.getKey());
+                    restClient.deleteExecution(model.getObject().getKey());
                     SyncopeConsoleSession.get().success(getString(Constants.OPERATION_SUCCEEDED));
                     target.add(container);
                 } catch (SyncopeClientException e) {
diff --git a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/tasks/MacroTaskDirectoryPanel.java b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/tasks/MacroTaskDirectoryPanel.java
new file mode 100644
index 0000000000..92d2e094cc
--- /dev/null
+++ b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/tasks/MacroTaskDirectoryPanel.java
@@ -0,0 +1,88 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.client.console.tasks;
+
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.commons.lang3.tuple.Pair;
+import org.apache.syncope.client.console.panels.MultilevelPanel;
+import org.apache.syncope.client.console.wicket.extensions.markup.html.repeater.data.table.BooleanPropertyColumn;
+import org.apache.syncope.client.console.wicket.markup.html.form.ActionLink;
+import org.apache.syncope.client.console.wicket.markup.html.form.ActionsPanel;
+import org.apache.syncope.common.lib.to.MacroTaskTO;
+import org.apache.syncope.common.lib.types.IdRepoEntitlement;
+import org.apache.syncope.common.lib.types.TaskType;
+import org.apache.wicket.PageReference;
+import org.apache.wicket.ajax.AjaxRequestTarget;
+import org.apache.wicket.extensions.markup.html.repeater.data.table.IColumn;
+import org.apache.wicket.model.IModel;
+import org.apache.wicket.model.Model;
+import org.apache.wicket.model.ResourceModel;
+import org.apache.wicket.model.StringResourceModel;
+
+public class MacroTaskDirectoryPanel extends SchedTaskDirectoryPanel<MacroTaskTO> {
+
+    private static final long serialVersionUID = -6247673131495530094L;
+
+    public MacroTaskDirectoryPanel(final MultilevelPanel mlp, final PageReference pageRef) {
+        super(MultilevelPanel.FIRST_LEVEL_ID, null, mlp, TaskType.MACRO, new MacroTaskTO(), pageRef, true);
+    }
+
+    @Override
+    protected List<IColumn<MacroTaskTO, String>> getFieldColumns() {
+        List<IColumn<MacroTaskTO, String>> columns = new ArrayList<>();
+
+        columns.addAll(getHeadingFieldColumns());
+
+        columns.add(new BooleanPropertyColumn<>(
+                new ResourceModel("continueOnError"), "continueOnError", "continueOnError"));
+        columns.add(new BooleanPropertyColumn<>(
+                new ResourceModel("saveExecs"), "saveExecs", "saveExecs"));
+
+        columns.addAll(getTrailingFieldColumns());
+
+        return columns;
+    }
+
+    @Override
+    protected void viewTaskExecs(final MacroTaskTO taskTO, final AjaxRequestTarget target) {
+        multiLevelPanelRef.next(
+                new StringResourceModel("task.view", this, new Model<>(Pair.of(null, taskTO))).getObject(),
+                new TaskExecutionDetails<>(taskTO, pageRef),
+                target);
+    }
+
+    @Override
+    protected void addFurtherActions(final ActionsPanel<MacroTaskTO> panel, final IModel<MacroTaskTO> model) {
+        panel.add(new ActionLink<>() {
+
+            private static final long serialVersionUID = -3722207913631435501L;
+
+            @Override
+            public void onClick(final AjaxRequestTarget target, final MacroTaskTO ignore) {
+                target.add(modal.setContent(
+                        new CommandComposeDirectoryPanel(modal, model.getObject().getKey(), pageRef)));
+
+                modal.header(new StringResourceModel(
+                        "command.conf", MacroTaskDirectoryPanel.this, Model.of(model.getObject())));
+                modal.show(true);
+            }
+        }, ActionLink.ActionType.COMPOSE, IdRepoEntitlement.TASK_UPDATE);
+    }
+}
diff --git a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/tasks/NotificationTaskDirectoryPanel.java b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/tasks/NotificationTaskDirectoryPanel.java
index 32506d3839..5e2da31309 100644
--- a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/tasks/NotificationTaskDirectoryPanel.java
+++ b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/tasks/NotificationTaskDirectoryPanel.java
@@ -67,7 +67,7 @@ public abstract class NotificationTaskDirectoryPanel
             final MultilevelPanel multiLevelPanelRef,
             final PageReference pageRef) {
 
-        super(null, multiLevelPanelRef, pageRef);
+        super(null, multiLevelPanelRef, pageRef, false);
         this.notification = notification;
         this.anyTypeKind = anyTypeKind;
         this.entityKey = entityKey;
@@ -78,7 +78,7 @@ public abstract class NotificationTaskDirectoryPanel
 
     @Override
     protected List<IColumn<NotificationTaskTO, String>> getColumns() {
-        final List<IColumn<NotificationTaskTO, String>> columns = new ArrayList<>();
+        List<IColumn<NotificationTaskTO, String>> columns = new ArrayList<>();
 
         columns.add(new KeyPropertyColumn<>(
                 new StringResourceModel(Constants.KEY_FIELD_NAME, this), Constants.KEY_FIELD_NAME));
@@ -100,6 +100,7 @@ public abstract class NotificationTaskDirectoryPanel
 
         columns.add(new PropertyColumn<>(
                 new StringResourceModel("latestExecStatus", this), "latestExecStatus", "latestExecStatus"));
+
         return columns;
     }
 
@@ -114,7 +115,7 @@ public abstract class NotificationTaskDirectoryPanel
 
             @Override
             public void onClick(final AjaxRequestTarget target, final NotificationTaskTO modelObject) {
-                viewTask(taskTO, target);
+                viewTaskExecs(taskTO, target);
             }
         }, ActionLink.ActionType.VIEW, IdRepoEntitlement.TASK_READ);
         panel.add(new ActionLink<>() {
diff --git a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/tasks/PropagationTaskDirectoryPanel.java b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/tasks/PropagationTaskDirectoryPanel.java
index 7d398f1d68..579903b1d3 100644
--- a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/tasks/PropagationTaskDirectoryPanel.java
+++ b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/tasks/PropagationTaskDirectoryPanel.java
@@ -61,14 +61,15 @@ public abstract class PropagationTaskDirectoryPanel
             final MultilevelPanel multiLevelPanelRef,
             final String resource,
             final PageReference pageRef) {
-        super(baseModal, multiLevelPanelRef, pageRef);
+
+        super(baseModal, multiLevelPanelRef, pageRef, false);
         this.resource = resource;
         initResultTable();
     }
 
     @Override
     protected List<IColumn<PropagationTaskTO, String>> getColumns() {
-        final List<IColumn<PropagationTaskTO, String>> columns = new ArrayList<>();
+        List<IColumn<PropagationTaskTO, String>> columns = new ArrayList<>();
 
         columns.add(new KeyPropertyColumn<>(
                 new StringResourceModel(Constants.KEY_FIELD_NAME, this), Constants.KEY_FIELD_NAME));
@@ -81,7 +82,7 @@ public abstract class PropagationTaskDirectoryPanel
                     new StringResourceModel("resource", this), "resource", "resource"));
         } else {
             columns.add(new PropertyColumn<>(
-                new StringResourceModel("anyTypeKind", this), "anyTypeKind", "anyTypeKind") {
+                    new StringResourceModel("anyTypeKind", this), "anyTypeKind", "anyTypeKind") {
 
                 private static final long serialVersionUID = 3344577098912281394L;
 
@@ -110,6 +111,7 @@ public abstract class PropagationTaskDirectoryPanel
 
         columns.add(new PropertyColumn<>(
                 new StringResourceModel("latestExecStatus", this), "latestExecStatus", "latestExecStatus"));
+
         return columns;
     }
 
@@ -125,7 +127,7 @@ public abstract class PropagationTaskDirectoryPanel
             @Override
             public void onClick(final AjaxRequestTarget target, final PropagationTaskTO modelObject) {
                 PropagationTaskDirectoryPanel.this.getTogglePanel().close(target);
-                viewTask(taskTO, target);
+                viewTaskExecs(taskTO, target);
             }
         }, ActionLink.ActionType.VIEW_EXECUTIONS, IdRepoEntitlement.TASK_READ);
 
diff --git a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/tasks/PropagationTasks.java b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/tasks/PropagationTasks.java
index b6d7b9f430..b4beae3674 100644
--- a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/tasks/PropagationTasks.java
+++ b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/tasks/PropagationTasks.java
@@ -39,29 +39,29 @@ public class PropagationTasks extends AbstractPropagationTasks {
 
         super(BaseModal.CONTENT_ID);
 
-        final MultilevelPanel tasks = new MultilevelPanel("tasks");
-        
-        tasks.setFirstLevel(new PropagationTaskDirectoryPanel(baseModal, tasks, resource, pageRef) {
+        MultilevelPanel mlp = new MultilevelPanel("tasks");
+        add(mlp);
+
+        mlp.setFirstLevel(new PropagationTaskDirectoryPanel(baseModal, mlp, resource, pageRef) {
 
             private static final long serialVersionUID = -2195387360323687302L;
 
             @Override
-            protected void viewTask(final PropagationTaskTO taskTO, final AjaxRequestTarget target) {
-                tasks.next(
+            protected void viewTaskExecs(final PropagationTaskTO taskTO, final AjaxRequestTarget target) {
+                mlp.next(
                         new StringResourceModel("task.view", this, new Model<>(Pair.of(null, taskTO))).getObject(),
-                        new TaskExecutionDetails<>(baseModal, taskTO, pageRef),
+                        new TaskExecutionDetails<>(taskTO, pageRef),
                         target);
             }
 
             @Override
             protected void viewTaskDetails(final PropagationTaskTO taskTO, final AjaxRequestTarget target) {
-                tasks.next(
-                        new StringResourceModel("task.view.details", this, new Model<>(Pair.of(null, taskTO))).
-                                getObject(),
+                mlp.next(
+                        new StringResourceModel(
+                                "task.view.details", this, new Model<>(Pair.of(null, taskTO))).getObject(),
                         new PropagationDataView(taskTO),
                         target);
             }
         });
-        add(tasks);
     }
 }
diff --git a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/tasks/ProvisioningTaskDirectoryPanel.java b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/tasks/ProvisioningTaskDirectoryPanel.java
index 2fd8dc16f4..33efcc8f46 100644
--- a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/tasks/ProvisioningTaskDirectoryPanel.java
+++ b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/tasks/ProvisioningTaskDirectoryPanel.java
@@ -25,32 +25,17 @@ import java.util.List;
 import org.apache.syncope.client.console.panels.MultilevelPanel;
 import org.apache.syncope.client.console.rest.TaskRestClient;
 import org.apache.syncope.client.console.wicket.ajax.IndicatorAjaxTimerBehavior;
-import org.apache.syncope.client.console.wicket.extensions.markup.html.repeater.data.table.BooleanPropertyColumn;
-import org.apache.syncope.client.console.wicket.extensions.markup.html.repeater.data.table.DatePropertyColumn;
-import org.apache.syncope.client.console.wicket.extensions.markup.html.repeater.data.table.KeyPropertyColumn;
 import org.apache.syncope.client.console.wicket.markup.html.bootstrap.dialog.BaseModal;
 import org.apache.syncope.client.console.widgets.JobActionPanel;
-import org.apache.syncope.client.ui.commons.Constants;
-import org.apache.syncope.common.lib.to.JobTO;
 import org.apache.syncope.common.lib.to.ProvisioningTaskTO;
 import org.apache.syncope.common.lib.to.PullTaskTO;
 import org.apache.syncope.common.lib.to.PushTaskTO;
-import org.apache.syncope.common.lib.types.IdRepoEntitlement;
 import org.apache.syncope.common.lib.types.TaskType;
-import org.apache.wicket.Component;
 import org.apache.wicket.PageReference;
 import org.apache.wicket.ajax.AjaxRequestTarget;
-import org.apache.wicket.authroles.authorization.strategies.role.metadata.MetaDataRoleAuthorizationStrategy;
 import org.apache.wicket.event.IEvent;
-import org.apache.wicket.extensions.markup.html.repeater.data.grid.ICellPopulator;
-import org.apache.wicket.extensions.markup.html.repeater.data.table.AbstractColumn;
 import org.apache.wicket.extensions.markup.html.repeater.data.table.IColumn;
 import org.apache.wicket.extensions.markup.html.repeater.data.table.PropertyColumn;
-import org.apache.wicket.markup.html.WebPage;
-import org.apache.wicket.markup.html.basic.Label;
-import org.apache.wicket.markup.repeater.Item;
-import org.apache.wicket.model.IModel;
-import org.apache.wicket.model.Model;
 import org.apache.wicket.model.StringResourceModel;
 
 /**
@@ -69,16 +54,17 @@ public abstract class ProvisioningTaskDirectoryPanel<T extends ProvisioningTaskT
             final BaseModal<?> baseModal,
             final MultilevelPanel multiLevelPanelRef,
             final TaskType taskType,
-            final Class<T> reference,
+            final T newTaskTO,
             final String resource,
             final PageReference pageRef) {
 
-        super(baseModal, multiLevelPanelRef, taskType, reference, pageRef);
+        super(MultilevelPanel.FIRST_LEVEL_ID, baseModal, multiLevelPanelRef, taskType, newTaskTO, pageRef, false);
         this.resource = resource;
 
         this.schedTaskTO.setResource(resource);
 
         // super in order to call the parent implementation
+        enableUtilityButton();
         super.initResultTable();
 
         container.add(new IndicatorAjaxTimerBehavior(java.time.Duration.of(10, ChronoUnit.SECONDS)) {
@@ -102,66 +88,17 @@ public abstract class ProvisioningTaskDirectoryPanel<T extends ProvisioningTaskT
     protected List<IColumn<T, String>> getFieldColumns() {
         List<IColumn<T, String>> columns = new ArrayList<>();
 
-        columns.add(new KeyPropertyColumn<>(
-                new StringResourceModel(Constants.KEY_FIELD_NAME, this), Constants.KEY_FIELD_NAME));
+        columns.addAll(getHeadingFieldColumns());
 
-        columns.add(new PropertyColumn<>(
-                new StringResourceModel(Constants.NAME_FIELD_NAME, this),
-                Constants.NAME_FIELD_NAME, Constants.NAME_FIELD_NAME));
-
-        columns.add(new PropertyColumn<>(
-                new StringResourceModel(Constants.DESCRIPTION_FIELD_NAME, this),
-                Constants.DESCRIPTION_FIELD_NAME, Constants.DESCRIPTION_FIELD_NAME));
-
-        if (reference == PullTaskTO.class) {
+        if (schedTaskTO instanceof PullTaskTO) {
             columns.add(new PropertyColumn<>(
                     new StringResourceModel("destinationRealm", this), "destinationRealm", "destinationRealm"));
-        } else if (reference == PushTaskTO.class) {
+        } else if (schedTaskTO instanceof PushTaskTO) {
             columns.add(new PropertyColumn<>(
                     new StringResourceModel("sourceRealm", this), "sourceRealm", "sourceRealm"));
         }
 
-        columns.add(new DatePropertyColumn<>(
-                new StringResourceModel("lastExec", this), null, "lastExec"));
-
-        columns.add(new DatePropertyColumn<>(
-                new StringResourceModel("nextExec", this), null, "nextExec"));
-
-        columns.add(new PropertyColumn<>(
-                new StringResourceModel("latestExecStatus", this), "latestExecStatus", "latestExecStatus"));
-
-        columns.add(new BooleanPropertyColumn<>(
-                new StringResourceModel("active", this), "active", "active"));
-
-        columns.add(new AbstractColumn<>(new Model<>(""), "running") {
-
-            private static final long serialVersionUID = -4008579357070833846L;
-
-            @Override
-            public void populateItem(
-                final Item<ICellPopulator<T>> cellItem,
-                final String componentId,
-                final IModel<T> rowModel) {
-
-                Component panel;
-                try {
-                    JobTO jobTO = TaskRestClient.getJob(rowModel.getObject().getKey());
-                    panel = new JobActionPanel(componentId, jobTO, false, ProvisioningTaskDirectoryPanel.this);
-                    MetaDataRoleAuthorizationStrategy.authorize(
-                        panel, WebPage.ENABLE,
-                        String.format("%s,%s", IdRepoEntitlement.TASK_EXECUTE, IdRepoEntitlement.TASK_UPDATE));
-                } catch (Exception e) {
-                    LOG.error("Could not get job for task {}", rowModel.getObject().getKey(), e);
-                    panel = new Label(componentId, Model.of());
-                }
-                cellItem.add(panel);
-            }
-
-            @Override
-            public String getCssClass() {
-                return "col-xs-1";
-            }
-        });
+        columns.addAll(getTrailingFieldColumns());
 
         return columns;
     }
@@ -180,11 +117,8 @@ public abstract class ProvisioningTaskDirectoryPanel<T extends ProvisioningTaskT
 
         private static final long serialVersionUID = 4725679400450513556L;
 
-        private final Class<T> reference;
-
-        public ProvisioningTasksProvider(final Class<T> reference, final TaskType id, final int paginatorRows) {
-            super(reference, id, paginatorRows);
-            this.reference = reference;
+        public ProvisioningTasksProvider(final TaskType taskType, final int paginatorRows) {
+            super(taskType, paginatorRows);
         }
 
         @Override
@@ -195,8 +129,8 @@ public abstract class ProvisioningTaskDirectoryPanel<T extends ProvisioningTaskT
         @Override
         public Iterator<T> iterator(final long first, final long count) {
             int page = ((int) first / paginatorRows);
-            return TaskRestClient.list(
-                    resource, reference, (page < 0 ? 0 : page) + 1, paginatorRows, getSort()).
+            return TaskRestClient.<T>list(
+                    resource, taskType, (page < 0 ? 0 : page) + 1, paginatorRows, getSort()).
                     iterator();
         }
     }
diff --git a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/tasks/PullTaskDirectoryPanel.java b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/tasks/PullTaskDirectoryPanel.java
index b496930640..b61580c222 100644
--- a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/tasks/PullTaskDirectoryPanel.java
+++ b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/tasks/PullTaskDirectoryPanel.java
@@ -40,7 +40,7 @@ public abstract class PullTaskDirectoryPanel extends ProvisioningTaskDirectoryPa
             final String resource,
             final PageReference pageRef) {
 
-        super(baseModal, multiLevelPanelRef, TaskType.PULL, PullTaskTO.class, resource, pageRef);
+        super(baseModal, multiLevelPanelRef, TaskType.PULL, new PullTaskTO(), resource, pageRef);
     }
 
     @Override
@@ -50,7 +50,7 @@ public abstract class PullTaskDirectoryPanel extends ProvisioningTaskDirectoryPa
 
     @Override
     protected ProvisioningTasksProvider<PullTaskTO> dataProvider() {
-        return new ProvisioningTasksProvider<>(reference, TaskType.PULL, rows);
+        return new ProvisioningTasksProvider<>(TaskType.PULL, rows);
     }
 
     @Override
diff --git a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/tasks/PullTasks.java b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/tasks/PullTasks.java
index ca8bf343a8..6faf4181ca 100644
--- a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/tasks/PullTasks.java
+++ b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/tasks/PullTasks.java
@@ -33,10 +33,11 @@ public class PullTasks extends AbstractTasks {
     private static final long serialVersionUID = -4013796607157549641L;
 
     public <T extends AnyTO> PullTasks(
-            final BaseModal<?> baseModal, final PageReference pageRef, final String resource) {
+            final BaseModal<?> baseModal, final String resource, final PageReference pageRef) {
+
         super(BaseModal.CONTENT_ID);
 
-        final MultilevelPanel mlp = new MultilevelPanel("tasks");
+        MultilevelPanel mlp = new MultilevelPanel("tasks");
         add(mlp);
 
         mlp.setFirstLevel(new PullTaskDirectoryPanel(baseModal, mlp, resource, pageRef) {
@@ -44,10 +45,11 @@ public class PullTasks extends AbstractTasks {
             private static final long serialVersionUID = -2195387360323687302L;
 
             @Override
-            protected void viewTask(final PullTaskTO taskTO, final AjaxRequestTarget target) {
+            protected void viewTaskExecs(final PullTaskTO taskTO, final AjaxRequestTarget target) {
                 mlp.next(
                         new StringResourceModel("task.view", this, new Model<>(Pair.of(null, taskTO))).getObject(),
-                        new TaskExecutionDetails<>(baseModal, taskTO, pageRef), target);
+                        new TaskExecutionDetails<>(taskTO, pageRef),
+                        target);
             }
         });
     }
diff --git a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/tasks/PushTaskDirectoryPanel.java b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/tasks/PushTaskDirectoryPanel.java
index 94c5e23b85..0a6eab078e 100644
--- a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/tasks/PushTaskDirectoryPanel.java
+++ b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/tasks/PushTaskDirectoryPanel.java
@@ -37,7 +37,7 @@ public abstract class PushTaskDirectoryPanel extends ProvisioningTaskDirectoryPa
             final MultilevelPanel multiLevelPanelRef,
             final String resource,
             final PageReference pageRef) {
-        super(baseModal, multiLevelPanelRef, TaskType.PUSH, PushTaskTO.class, resource, pageRef);
+        super(baseModal, multiLevelPanelRef, TaskType.PUSH, new PushTaskTO(), resource, pageRef);
     }
 
     @Override
@@ -47,6 +47,6 @@ public abstract class PushTaskDirectoryPanel extends ProvisioningTaskDirectoryPa
 
     @Override
     protected ProvisioningTasksProvider<PushTaskTO> dataProvider() {
-        return new ProvisioningTasksProvider<>(reference, TaskType.PUSH, rows);
+        return new ProvisioningTasksProvider<>(TaskType.PUSH, rows);
     }
 }
diff --git a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/tasks/PushTasks.java b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/tasks/PushTasks.java
index 56dea3a05c..47828c52ed 100644
--- a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/tasks/PushTasks.java
+++ b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/tasks/PushTasks.java
@@ -33,21 +33,23 @@ public class PushTasks extends AbstractTasks {
     private static final long serialVersionUID = -4013796607157549641L;
 
     public <T extends AnyTO> PushTasks(
-            final BaseModal<?> baseModal, final PageReference pageReference, final String resource) {
+            final BaseModal<?> baseModal, final String resource, final PageReference pageRef) {
+
         super(BaseModal.CONTENT_ID);
 
-        final MultilevelPanel mlp = new MultilevelPanel("tasks");
+        MultilevelPanel mlp = new MultilevelPanel("tasks");
         add(mlp);
 
-        mlp.setFirstLevel(new PushTaskDirectoryPanel(baseModal, mlp, resource, pageReference) {
+        mlp.setFirstLevel(new PushTaskDirectoryPanel(baseModal, mlp, resource, pageRef) {
 
             private static final long serialVersionUID = -2195387360323687302L;
 
             @Override
-            protected void viewTask(final PushTaskTO taskTO, final AjaxRequestTarget target) {
+            protected void viewTaskExecs(final PushTaskTO taskTO, final AjaxRequestTarget target) {
                 mlp.next(
                         new StringResourceModel("task.view", this, new Model<>(Pair.of(null, taskTO))).getObject(),
-                        new TaskExecutionDetails<>(baseModal, taskTO, pageReference), target);
+                        new TaskExecutionDetails<>(taskTO, pageRef), 
+                        target);
             }
         });
     }
diff --git a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/tasks/SchedTaskDirectoryPanel.java b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/tasks/SchedTaskDirectoryPanel.java
index 3aa524bf1f..7a0e48abba 100644
--- a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/tasks/SchedTaskDirectoryPanel.java
+++ b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/tasks/SchedTaskDirectoryPanel.java
@@ -18,6 +18,7 @@
  */
 package org.apache.syncope.client.console.tasks;
 
+import de.agilecoders.wicket.core.markup.html.bootstrap.dialog.Modal;
 import java.io.Serializable;
 import java.time.Duration;
 import java.time.temporal.ChronoUnit;
@@ -79,95 +80,33 @@ public abstract class SchedTaskDirectoryPanel<T extends SchedTaskTO>
 
     private static final long serialVersionUID = 4984337552918213290L;
 
-    protected TaskType taskType;
+    protected final TaskType taskType;
 
-    protected final Class<T> reference;
+    protected final T schedTaskTO;
 
-    protected T schedTaskTO;
-
-    private final TaskStartAtTogglePanel startAt;
+    protected final TaskStartAtTogglePanel startAt;
 
     protected final TemplatesTogglePanel templates;
 
     protected SchedTaskDirectoryPanel(
+            final String id,
             final BaseModal<?> baseModal,
             final MultilevelPanel multiLevelPanelRef,
             final TaskType taskType,
-            final Class<T> reference,
-            final PageReference pageRef) {
-
-        super(baseModal, multiLevelPanelRef, pageRef);
-        this.taskType = taskType;
-        this.reference = reference;
-
-        try {
-            schedTaskTO = reference.getDeclaredConstructor().newInstance();
-        } catch (Exception e) {
-            LOG.error("Failure instantiating task", e);
-        }
-
-        this.addNewItemPanelBuilder(new SchedTaskWizardBuilder<>(taskType, schedTaskTO, pageRef), true);
-
-        MetaDataRoleAuthorizationStrategy.authorize(addAjaxLink, RENDER, IdRepoEntitlement.TASK_CREATE);
-
-        enableUtilityButton();
-        setFooterVisibility(false);
-
-        initResultTable();
-
-        container.add(new IndicatorAjaxTimerBehavior(Duration.of(10, ChronoUnit.SECONDS)) {
-
-            private static final long serialVersionUID = -4661303265651934868L;
-
-            @Override
-            protected void onTimer(final AjaxRequestTarget target) {
-                container.modelChanged();
-                target.add(container);
-            }
-        });
-
-        startAt = new TaskStartAtTogglePanel(container, pageRef);
-        addInnerObject(startAt);
-
-        templates = new TemplatesTogglePanel(getActualId(), this, pageRef) {
-
-            private static final long serialVersionUID = -8765794727538618705L;
-
-            @Override
-            protected Serializable onApplyInternal(
-                    final TemplatableTO targetObject, final String type, final AnyTO anyTO) {
-
-                targetObject.getTemplates().put(type, anyTO);
-                TaskRestClient.update(taskType, SchedTaskTO.class.cast(targetObject));
-                return targetObject;
-            }
-        };
-        addInnerObject(templates);
-    }
-
-    protected SchedTaskDirectoryPanel(
-            final BaseModal<?> baseModal,
-            final MultilevelPanel multiLevelPanelRef,
-            final TaskType taskType,
-            final Class<T> reference,
+            final T newTaskTO,
             final PageReference pageRef,
             final boolean wizardInModal) {
 
-        super(baseModal, multiLevelPanelRef, pageRef, wizardInModal);
+        super(id, baseModal, multiLevelPanelRef, pageRef, wizardInModal);
         this.taskType = taskType;
-        this.reference = reference;
+        this.schedTaskTO = newTaskTO;
 
-        try {
-            schedTaskTO = reference.getDeclaredConstructor().newInstance();
-        } catch (Exception e) {
-            LOG.error("Failure instantiating task", e);
-        }
+        modal.size(Modal.Size.Large);
 
-        this.addNewItemPanelBuilder(new SchedTaskWizardBuilder<>(taskType, schedTaskTO, pageRef), true);
+        addNewItemPanelBuilder(new SchedTaskWizardBuilder<>(taskType, schedTaskTO, pageRef), true);
 
         MetaDataRoleAuthorizationStrategy.authorize(addAjaxLink, RENDER, IdRepoEntitlement.TASK_CREATE);
 
-        enableUtilityButton();
         setFooterVisibility(false);
 
         initResultTable();
@@ -202,7 +141,7 @@ public abstract class SchedTaskDirectoryPanel<T extends SchedTaskTO>
         addInnerObject(templates);
     }
 
-    protected List<IColumn<T, String>> getFieldColumns() {
+    protected List<IColumn<T, String>> getHeadingFieldColumns() {
         List<IColumn<T, String>> columns = new ArrayList<>();
 
         columns.add(new KeyPropertyColumn<>(
@@ -212,30 +151,11 @@ public abstract class SchedTaskDirectoryPanel<T extends SchedTaskTO>
                 new StringResourceModel(Constants.NAME_FIELD_NAME, this),
                 Constants.NAME_FIELD_NAME, Constants.NAME_FIELD_NAME));
 
-        columns.add(new PropertyColumn<>(
-            new StringResourceModel("jobDelegate", this), "jobDelegate", "jobDelegate") {
-
-            private static final long serialVersionUID = -3223917055078733093L;
-
-            @Override
-            public void populateItem(
-                final Item<ICellPopulator<T>> item,
-                final String componentId,
-                final IModel<T> rowModel) {
+        return columns;
+    }
 
-                IModel<?> model = getDataModel(rowModel);
-                if (model != null && model.getObject() instanceof String) {
-                    String value = String.class.cast(model.getObject());
-                    if (value.length() > 20) {
-                        item.add(new Label(componentId, new Model<>("..." + value.substring(value.length() - 17))));
-                    } else {
-                        item.add(new Label(componentId, getDataModel(rowModel)));
-                    }
-                } else {
-                    super.populateItem(item, componentId, rowModel);
-                }
-            }
-        });
+    protected List<IColumn<T, String>> getTrailingFieldColumns() {
+        List<IColumn<T, String>> columns = new ArrayList<>();
 
         columns.add(new DatePropertyColumn<>(
                 new StringResourceModel("lastExec", this), null, "lastExec"));
@@ -255,17 +175,17 @@ public abstract class SchedTaskDirectoryPanel<T extends SchedTaskTO>
 
             @Override
             public void populateItem(
-                final Item<ICellPopulator<T>> cellItem,
-                final String componentId,
-                final IModel<T> rowModel) {
+                    final Item<ICellPopulator<T>> cellItem,
+                    final String componentId,
+                    final IModel<T> rowModel) {
 
                 Component panel;
                 try {
                     JobTO jobTO = TaskRestClient.getJob(rowModel.getObject().getKey());
                     panel = new JobActionPanel(componentId, jobTO, false, SchedTaskDirectoryPanel.this);
                     MetaDataRoleAuthorizationStrategy.authorize(
-                        panel, WebPage.ENABLE,
-                        String.format("%s,%s", IdRepoEntitlement.TASK_EXECUTE, IdRepoEntitlement.TASK_UPDATE));
+                            panel, WebPage.ENABLE,
+                            String.format("%s,%s", IdRepoEntitlement.TASK_EXECUTE, IdRepoEntitlement.TASK_UPDATE));
                 } catch (Exception e) {
                     LOG.error("Could not get job for task {}", rowModel.getObject().getKey(), e);
                     panel = new Label(componentId, Model.of());
@@ -275,25 +195,60 @@ public abstract class SchedTaskDirectoryPanel<T extends SchedTaskTO>
 
             @Override
             public String getCssClass() {
-                return "col-xs-1";
+                return "running-col";
             }
         });
 
         return columns;
     }
 
+    protected List<IColumn<T, String>> getFieldColumns() {
+        List<IColumn<T, String>> columns = new ArrayList<>();
+
+        columns.addAll(getHeadingFieldColumns());
+
+        columns.add(new PropertyColumn<>(new StringResourceModel("jobDelegate", this), "jobDelegate", "jobDelegate") {
+
+            private static final long serialVersionUID = -3223917055078733093L;
+
+            @Override
+            public void populateItem(
+                    final Item<ICellPopulator<T>> item,
+                    final String componentId,
+                    final IModel<T> rowModel) {
+
+                IModel<?> model = getDataModel(rowModel);
+                if (model != null && model.getObject() instanceof String) {
+                    String value = String.class.cast(model.getObject());
+                    if (value.length() > 20) {
+                        item.add(new Label(componentId, new Model<>("..." + value.substring(value.length() - 17))));
+                    } else {
+                        item.add(new Label(componentId, getDataModel(rowModel)));
+                    }
+                } else {
+                    super.populateItem(item, componentId, rowModel);
+                }
+            }
+        });
+
+        columns.addAll(getTrailingFieldColumns());
+
+        return columns;
+    }
+
     @Override
     protected final List<IColumn<T, String>> getColumns() {
-        final List<IColumn<T, String>> columns = new ArrayList<>();
+        List<IColumn<T, String>> columns = new ArrayList<>();
 
         columns.addAll(getFieldColumns());
+
         return columns;
     }
 
     @Override
     public ActionsPanel<T> getActions(final IModel<T> model) {
-        final ActionsPanel<T> panel = super.getActions(model);
-        final T taskTO = model.getObject();
+        ActionsPanel<T> panel = super.getActions(model);
+        T taskTO = model.getObject();
 
         panel.add(new ActionLink<>() {
 
@@ -302,7 +257,7 @@ public abstract class SchedTaskDirectoryPanel<T extends SchedTaskTO>
             @Override
             public void onClick(final AjaxRequestTarget target, final T ignore) {
                 SchedTaskDirectoryPanel.this.getTogglePanel().close(target);
-                viewTask(taskTO, target);
+                viewTaskExecs(taskTO, target);
             }
         }, ActionLink.ActionType.VIEW_EXECUTIONS, IdRepoEntitlement.TASK_READ);
 
@@ -314,12 +269,12 @@ public abstract class SchedTaskDirectoryPanel<T extends SchedTaskTO>
             public void onClick(final AjaxRequestTarget target, final T ignore) {
                 SchedTaskDirectoryPanel.this.getTogglePanel().close(target);
                 send(SchedTaskDirectoryPanel.this, Broadcast.EXACT,
-                    new AjaxWizard.EditItemActionEvent<>(
-                        TaskRestClient.readTask(taskType, model.getObject().getKey()),
-                        target).setResourceModel(
-                        new StringResourceModel("inner.task.edit",
-                            SchedTaskDirectoryPanel.this,
-                            Model.of(Pair.of(ActionLink.ActionType.EDIT, model.getObject())))));
+                        new AjaxWizard.EditItemActionEvent<>(
+                                TaskRestClient.readTask(taskType, model.getObject().getKey()),
+                                target).setResourceModel(
+                                new StringResourceModel("inner.task.edit",
+                                        SchedTaskDirectoryPanel.this,
+                                        Model.of(Pair.of(ActionLink.ActionType.EDIT, model.getObject())))));
             }
         }, ActionLink.ActionType.EDIT, IdRepoEntitlement.TASK_UPDATE);
 
@@ -333,13 +288,15 @@ public abstract class SchedTaskDirectoryPanel<T extends SchedTaskTO>
                 final T clone = SerializationUtils.clone(model.getObject());
                 clone.setKey(null);
                 send(SchedTaskDirectoryPanel.this, Broadcast.EXACT,
-                    new AjaxWizard.EditItemActionEvent<>(clone, target).setResourceModel(
-                        new StringResourceModel("inner.task.clone",
-                            SchedTaskDirectoryPanel.this,
-                            Model.of(Pair.of(ActionLink.ActionType.CLONE, model.getObject())))));
+                        new AjaxWizard.EditItemActionEvent<>(clone, target).setResourceModel(
+                                new StringResourceModel("inner.task.clone",
+                                        SchedTaskDirectoryPanel.this,
+                                        Model.of(Pair.of(ActionLink.ActionType.CLONE, model.getObject())))));
             }
         }, ActionLink.ActionType.CLONE, IdRepoEntitlement.TASK_CREATE);
 
+        addFurtherActions(panel, model);
+
         panel.add(new ActionLink<>() {
 
             private static final long serialVersionUID = -3722207913631435501L;
@@ -352,8 +309,6 @@ public abstract class SchedTaskDirectoryPanel<T extends SchedTaskTO>
             }
         }, ActionLink.ActionType.EXECUTE, IdRepoEntitlement.TASK_EXECUTE);
 
-        addFurtherActions(panel, model);
-
         panel.add(new ActionLink<>() {
 
             private static final long serialVersionUID = -3722207913631435501L;
@@ -395,19 +350,16 @@ public abstract class SchedTaskDirectoryPanel<T extends SchedTaskTO>
 
     @Override
     protected SchedTasksProvider<T> dataProvider() {
-        return new SchedTasksProvider<>(reference, taskType, rows);
+        return new SchedTasksProvider<>(taskType, rows);
     }
 
     protected static class SchedTasksProvider<T extends SchedTaskTO> extends TaskDataProvider<T> {
 
         private static final long serialVersionUID = 4725679400450513556L;
 
-        private final Class<T> reference;
-
-        public SchedTasksProvider(final Class<T> reference, final TaskType taskType, final int paginatorRows) {
+        public SchedTasksProvider(final TaskType taskType, final int paginatorRows) {
             super(paginatorRows, taskType);
             setSort(Constants.NAME_FIELD_NAME, SortOrder.ASCENDING);
-            this.reference = reference;
         }
 
         @Override
@@ -418,8 +370,8 @@ public abstract class SchedTaskDirectoryPanel<T extends SchedTaskTO>
         @Override
         public Iterator<T> iterator(final long first, final long count) {
             int page = ((int) first / paginatorRows);
-            return TaskRestClient.list(
-                    reference, (page < 0 ? 0 : page) + 1, paginatorRows, getSort()).
+            return TaskRestClient.<T>list(
+                    taskType, (page < 0 ? 0 : page) + 1, paginatorRows, getSort()).
                     iterator();
         }
     }
diff --git a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/tasks/SchedTaskWizardBuilder.java b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/tasks/SchedTaskWizardBuilder.java
index cbf73b6c8d..2c4c0ce16e 100644
--- a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/tasks/SchedTaskWizardBuilder.java
+++ b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/tasks/SchedTaskWizardBuilder.java
@@ -36,6 +36,7 @@ import org.apache.syncope.client.ui.commons.markup.html.form.AjaxDropDownChoiceP
 import org.apache.syncope.client.ui.commons.markup.html.form.AjaxPalettePanel;
 import org.apache.syncope.client.ui.commons.markup.html.form.AjaxTextFieldPanel;
 import org.apache.syncope.common.lib.SyncopeConstants;
+import org.apache.syncope.common.lib.to.MacroTaskTO;
 import org.apache.syncope.common.lib.to.ProvisioningTaskTO;
 import org.apache.syncope.common.lib.to.PullTaskTO;
 import org.apache.syncope.common.lib.to.PushTaskTO;
@@ -136,8 +137,8 @@ public class SchedTaskWizardBuilder<T extends SchedTaskTO> extends BaseAjaxWizar
             description.setEnabled(true);
             add(description);
 
-            AjaxCheckBoxPanel active = new AjaxCheckBoxPanel("active", "active", new PropertyModel<>(taskTO, "active"),
-                    false);
+            AjaxCheckBoxPanel active = new AjaxCheckBoxPanel(
+                    "active", "active", new PropertyModel<>(taskTO, "active"), false);
             add(active);
 
             AjaxDropDownChoicePanel<String> jobDelegate = new AjaxDropDownChoicePanel<>(
@@ -147,6 +148,47 @@ public class SchedTaskWizardBuilder<T extends SchedTaskTO> extends BaseAjaxWizar
             jobDelegate.setEnabled(taskTO.getKey() == null);
             add(jobDelegate);
 
+            AutoCompleteSettings settings = new AutoCompleteSettings();
+            settings.setShowCompleteListOnFocusGain(!isSearchEnabled);
+            settings.setShowListOnEmptyInput(!isSearchEnabled);
+
+            // ------------------------------
+            // Only for macro tasks
+            // ------------------------------            
+            WebMarkupContainer macroTaskSpecifics = new WebMarkupContainer("macroTaskSpecifics");
+            add(macroTaskSpecifics.setRenderBodyOnly(true));
+
+            AjaxSearchFieldPanel realm =
+                    new AjaxSearchFieldPanel("realm", "realm",
+                            new PropertyModel<>(taskTO, "realm"), settings) {
+
+                private static final long serialVersionUID = -6390474600233486704L;
+
+                @Override
+                protected Iterator<String> getChoices(final String input) {
+                    return (RealmsUtils.checkInput(input)
+                            ? searchRealms(input).stream().map(RealmTO::getFullPath).collect(Collectors.toList())
+                            : List.<String>of()).iterator();
+                }
+            };
+
+            if (taskTO instanceof MacroTaskTO) {
+                realm.addRequiredLabel();
+                if (StringUtils.isBlank(MacroTaskTO.class.cast(taskTO).getRealm())) {
+                    // add a default destination realm if missing in the task
+                    realm.setModelObject(SyncopeConstants.ROOT_REALM);
+                }
+            }
+            macroTaskSpecifics.add(realm);
+
+            AjaxCheckBoxPanel continueOnError = new AjaxCheckBoxPanel(
+                    "continueOnError", "continueOnError", new PropertyModel<>(taskTO, "continueOnError"), false);
+            macroTaskSpecifics.add(continueOnError);
+
+            AjaxCheckBoxPanel saveExecs = new AjaxCheckBoxPanel(
+                    "saveExecs", "saveExecs", new PropertyModel<>(taskTO, "saveExecs"), false);
+            macroTaskSpecifics.add(saveExecs);
+
             // ------------------------------
             // Only for pull tasks
             // ------------------------------            
@@ -160,7 +202,7 @@ public class SchedTaskWizardBuilder<T extends SchedTaskTO> extends BaseAjaxWizar
                 pullTaskSpecifics.setEnabled(false).setVisible(false);
             }
 
-            final AjaxDropDownChoicePanel<PullMode> pullMode = new AjaxDropDownChoicePanel<>(
+            AjaxDropDownChoicePanel<PullMode> pullMode = new AjaxDropDownChoicePanel<>(
                     "pullMode", "pullMode", new PropertyModel<>(taskTO, "pullMode"), false);
             pullMode.setChoices(List.of(PullMode.values()));
             if (taskTO instanceof PullTaskTO) {
@@ -169,7 +211,7 @@ public class SchedTaskWizardBuilder<T extends SchedTaskTO> extends BaseAjaxWizar
             pullMode.setNullValid(!(taskTO instanceof PullTaskTO));
             pullTaskSpecifics.add(pullMode);
 
-            final AjaxDropDownChoicePanel<String> reconFilterBuilder = new AjaxDropDownChoicePanel<>(
+            AjaxDropDownChoicePanel<String> reconFilterBuilder = new AjaxDropDownChoicePanel<>(
                     "reconFilterBuilder", "reconFilterBuilder",
                     new PropertyModel<>(taskTO, "reconFilterBuilder"), false);
             reconFilterBuilder.setChoices(reconFilterBuilders.getObject());
@@ -191,11 +233,7 @@ public class SchedTaskWizardBuilder<T extends SchedTaskTO> extends BaseAjaxWizar
                 }
             });
 
-            final AutoCompleteSettings settings = new AutoCompleteSettings();
-            settings.setShowCompleteListOnFocusGain(!isSearchEnabled);
-            settings.setShowListOnEmptyInput(!isSearchEnabled);
-
-            final AjaxSearchFieldPanel destinationRealm =
+            AjaxSearchFieldPanel destinationRealm =
                     new AjaxSearchFieldPanel("destinationRealm", "destinationRealm",
                             new PropertyModel<>(taskTO, "destinationRealm"), settings) {
 
@@ -224,7 +262,7 @@ public class SchedTaskWizardBuilder<T extends SchedTaskTO> extends BaseAjaxWizar
 
             // ------------------------------
             // Only for push tasks
-            // ------------------------------  
+            // ------------------------------
             WebMarkupContainer pushTaskSpecifics = new WebMarkupContainer("pushTaskSpecifics");
             add(pushTaskSpecifics.setRenderBodyOnly(true));
 
@@ -232,8 +270,8 @@ public class SchedTaskWizardBuilder<T extends SchedTaskTO> extends BaseAjaxWizar
                 pushTaskSpecifics.setEnabled(false).setVisible(false);
             }
 
-            final AjaxSearchFieldPanel sourceRealm = new AjaxSearchFieldPanel("sourceRealm", "sourceRealm",
-                    new PropertyModel<>(taskTO, "sourceRealm"), settings) {
+            AjaxSearchFieldPanel sourceRealm = new AjaxSearchFieldPanel(
+                    "sourceRealm", "sourceRealm", new PropertyModel<>(taskTO, "sourceRealm"), settings) {
 
                 private static final long serialVersionUID = -6390474600233486704L;
 
@@ -258,8 +296,13 @@ public class SchedTaskWizardBuilder<T extends SchedTaskTO> extends BaseAjaxWizar
 
             if (taskTO instanceof ProvisioningTaskTO) {
                 jobDelegate.setEnabled(false).setVisible(false);
+                macroTaskSpecifics.setEnabled(false).setVisible(false);
+            } else if (taskTO instanceof MacroTaskTO) {
+                jobDelegate.setEnabled(false).setVisible(false);
+                provisioningTaskSpecifics.setEnabled(false).setVisible(false);
             } else {
                 provisioningTaskSpecifics.setEnabled(false).setVisible(false);
+                macroTaskSpecifics.setEnabled(false).setVisible(false);
             }
 
             AjaxPalettePanel<String> actions = new AjaxPalettePanel.Builder<String>().
diff --git a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/tasks/SchedTasks.java b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/tasks/SchedTasks.java
deleted file mode 100644
index b19b0effaa..0000000000
--- a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/tasks/SchedTasks.java
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package org.apache.syncope.client.console.tasks;
-
-import org.apache.commons.lang3.tuple.Pair;
-import org.apache.syncope.client.console.panels.MultilevelPanel;
-import org.apache.syncope.client.console.wicket.markup.html.bootstrap.dialog.BaseModal;
-import org.apache.syncope.common.lib.to.AnyTO;
-import org.apache.syncope.common.lib.to.SchedTaskTO;
-import org.apache.syncope.common.lib.types.TaskType;
-import org.apache.wicket.PageReference;
-import org.apache.wicket.ajax.AjaxRequestTarget;
-import org.apache.wicket.model.Model;
-import org.apache.wicket.model.StringResourceModel;
-
-public class SchedTasks extends AbstractTasks {
-
-    private static final long serialVersionUID = -4013796607157549641L;
-
-    public <T extends AnyTO> SchedTasks(final BaseModal<?> baseModal, final PageReference pageReference) {
-        super(BaseModal.CONTENT_ID);
-
-        final MultilevelPanel mlp = new MultilevelPanel("tasks");
-        add(mlp);
-
-        mlp.setFirstLevel(new SchedTaskDirectoryPanel<>(
-            baseModal, mlp, TaskType.SCHEDULED, SchedTaskTO.class, pageReference) {
-
-            private static final long serialVersionUID = -2195387360323687302L;
-
-            @Override
-            protected void viewTask(final SchedTaskTO taskTO, final AjaxRequestTarget target) {
-                mlp.next(
-                    new StringResourceModel("task.view", this, new Model<>(Pair.of(null, taskTO))).getObject(),
-                    new TaskExecutionDetails<>(baseModal, taskTO, pageReference), target);
-            }
-        });
-    }
-
-    public <T extends AnyTO> SchedTasks(final BaseModal<?> baseModal, final PageReference pageReference,
-                                        final boolean wizardInModal, final String id) {
-        super(id);
-
-        final MultilevelPanel mlp = new MultilevelPanel("tasks");
-        add(mlp);
-
-        mlp.setFirstLevel(new SchedTaskDirectoryPanel<>(
-                baseModal, mlp, TaskType.SCHEDULED, SchedTaskTO.class, pageReference, wizardInModal) {
-
-            private static final long serialVersionUID = -2195387360323687302L;
-
-            @Override
-            protected void viewTask(final SchedTaskTO taskTO, final AjaxRequestTarget target) {
-                mlp.next(
-                        new StringResourceModel("task.view", this, new Model<>(Pair.of(null, taskTO))).getObject(),
-                        new TaskExecutionDetails<>(baseModal, taskTO, pageReference), target);
-            }
-        });
-    }
-}
diff --git a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/tasks/TaskDirectoryPanel.java b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/tasks/TaskDirectoryPanel.java
index 03cc5d6147..17cd66e533 100644
--- a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/tasks/TaskDirectoryPanel.java
+++ b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/tasks/TaskDirectoryPanel.java
@@ -48,31 +48,29 @@ public abstract class TaskDirectoryPanel<T extends TaskTO>
 
     protected final BaseModal<?> baseModal;
 
-    private final MultilevelPanel multiLevelPanelRef;
+    protected final MultilevelPanel multiLevelPanelRef;
 
     protected TaskDirectoryPanel(
-            final BaseModal<?> baseModal, final MultilevelPanel multiLevelPanelRef, final PageReference pageRef) {
-        super(MultilevelPanel.FIRST_LEVEL_ID, pageRef, false);
-        this.baseModal = baseModal;
-        this.multiLevelPanelRef = multiLevelPanelRef;
-        restClient = new TaskRestClient();
-        setShowResultPanel(false);
+            final BaseModal<?> baseModal,
+            final MultilevelPanel multiLevelPanelRef,
+            final PageReference pageRef,
+            final boolean wizardInModal) {
+
+        this(MultilevelPanel.FIRST_LEVEL_ID, baseModal, multiLevelPanelRef, pageRef, wizardInModal);
     }
 
-    protected TaskDirectoryPanel(
-            final BaseModal<?> baseModal, final MultilevelPanel multiLevelPanelRef, final PageReference pageRef,
-            final boolean wizardInModal) {
-        super(MultilevelPanel.FIRST_LEVEL_ID, pageRef, wizardInModal);
-        this.baseModal = baseModal;
-        this.multiLevelPanelRef = multiLevelPanelRef;
-        restClient = new TaskRestClient();
-        setShowResultPanel(false);
+    protected TaskDirectoryPanel(final String id, final PageReference pageRef) {
+        this(id, null, null, pageRef, false);
     }
 
     protected TaskDirectoryPanel(
-            final BaseModal<?> baseModal, final MultilevelPanel multiLevelPanelRef, final PageReference pageRef,
-            final String id) {
-        super(id, pageRef, false);
+            final String id,
+            final BaseModal<?> baseModal,
+            final MultilevelPanel multiLevelPanelRef,
+            final PageReference pageRef,
+            final boolean wizardInModal) {
+
+        super(id, pageRef, wizardInModal);
         this.baseModal = baseModal;
         this.multiLevelPanelRef = multiLevelPanelRef;
         restClient = new TaskRestClient();
@@ -81,10 +79,10 @@ public abstract class TaskDirectoryPanel<T extends TaskTO>
 
     @Override
     protected void resultTableCustomChanges(final AjaxDataTablePanel.Builder<T, String> resultTableBuilder) {
-        resultTableBuilder.setMultiLevelPanel(baseModal, multiLevelPanelRef);
+        resultTableBuilder.setMultiLevelPanel(multiLevelPanelRef);
     }
 
-    protected abstract void viewTask(T taskTO, AjaxRequestTarget target);
+    protected abstract void viewTaskExecs(T taskTO, AjaxRequestTarget target);
 
     protected abstract static class TasksProvider<T extends TaskTO> extends DirectoryDataProvider<T> {
 
@@ -114,7 +112,7 @@ public abstract class TaskDirectoryPanel<T extends TaskTO>
     public void onEvent(final IEvent<?> event) {
         super.onEvent(event);
         if (event.getPayload() instanceof ExitEvent) {
-            final AjaxRequestTarget target = ExitEvent.class.cast(event.getPayload()).getTarget();
+            AjaxRequestTarget target = ExitEvent.class.cast(event.getPayload()).getTarget();
             baseModal.show(false);
             baseModal.close(target);
         }
diff --git a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/tasks/TaskExecutionDetails.java b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/tasks/TaskExecutionDetails.java
index ef243f3335..1b0cf18c0d 100644
--- a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/tasks/TaskExecutionDetails.java
+++ b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/tasks/TaskExecutionDetails.java
@@ -20,7 +20,6 @@ package org.apache.syncope.client.console.tasks;
 
 import org.apache.syncope.client.console.panels.MultilevelPanel;
 import org.apache.syncope.client.console.rest.TaskRestClient;
-import org.apache.syncope.client.console.wicket.markup.html.bootstrap.dialog.BaseModal;
 import org.apache.syncope.common.lib.to.TaskTO;
 import org.apache.wicket.PageReference;
 import org.apache.wicket.ajax.AjaxRequestTarget;
@@ -34,13 +33,13 @@ public class TaskExecutionDetails<T extends TaskTO> extends MultilevelPanel.Seco
 
     private static final long serialVersionUID = -4110576026663173545L;
 
-    public TaskExecutionDetails(final BaseModal<?> baseModal, final T taskTO, final PageReference pageRef) {
+    public TaskExecutionDetails(final T taskTO, final PageReference pageRef) {
         super();
 
-        final MultilevelPanel mlp = new MultilevelPanel("executions");
+        MultilevelPanel mlp = new MultilevelPanel("executions");
         add(mlp);
 
-        mlp.setFirstLevel(new ExecutionsDirectoryPanel(baseModal, mlp, taskTO.getKey(), new TaskRestClient(), pageRef) {
+        mlp.setFirstLevel(new ExecutionsDirectoryPanel(mlp, taskTO.getKey(), new TaskRestClient(), pageRef) {
 
             private static final long serialVersionUID = 5691719817252887541L;
 
@@ -49,6 +48,7 @@ public class TaskExecutionDetails<T extends TaskTO> extends MultilevelPanel.Seco
                     final String title,
                     final MultilevelPanel.SecondLevel slevel,
                     final AjaxRequestTarget target) {
+
                 mlp.next(title, slevel, target);
             }
         });
diff --git a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wicket/markup/html/form/ActionLinksTogglePanel.java b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wicket/markup/html/form/ActionLinksTogglePanel.java
index b22eb5c37b..caf67cb5ae 100644
--- a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wicket/markup/html/form/ActionLinksTogglePanel.java
+++ b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wicket/markup/html/form/ActionLinksTogglePanel.java
@@ -27,12 +27,14 @@ import org.apache.syncope.client.console.panels.TogglePanel;
 import org.apache.syncope.client.console.panels.ToggleableTarget;
 import org.apache.syncope.client.console.policies.PolicyRuleWrapper;
 import org.apache.syncope.client.console.reports.ReportletWrapper;
+import org.apache.syncope.client.console.tasks.CommandWrapper;
 import org.apache.syncope.client.console.wizards.any.GroupWrapper;
 import org.apache.syncope.client.ui.commons.status.StatusBean;
 import org.apache.syncope.client.ui.commons.wizards.any.AnyWrapper;
 import org.apache.syncope.client.ui.commons.wizards.any.UserWrapper;
 import org.apache.syncope.common.keymaster.client.api.model.Domain;
 import org.apache.syncope.common.lib.Attr;
+import org.apache.syncope.common.lib.command.CommandTO;
 import org.apache.syncope.common.lib.policy.PolicyTO;
 import org.apache.syncope.common.lib.to.AccessTokenTO;
 import org.apache.syncope.common.lib.to.AnyObjectTO;
@@ -104,6 +106,8 @@ public class ActionLinksTogglePanel<T extends Serializable> extends TogglePanel<
             header = ((PolicyRuleWrapper) modelObject).getImplementationKey();
         } else if (modelObject instanceof ReportletWrapper) {
             header = ((ReportletWrapper) modelObject).getImplementationKey();
+        } else if (modelObject instanceof CommandWrapper) {
+            header = ((CommandWrapper) modelObject).getCommand().getKey();
         } else if (modelObject instanceof JobTO) {
             header = ((JobTO) modelObject).getRefKey() == null
                     ? ((JobTO) modelObject).getRefDesc() : ((JobTO) modelObject).getRefKey();
@@ -111,6 +115,8 @@ public class ActionLinksTogglePanel<T extends Serializable> extends TogglePanel<
             header = ((ToggleableTarget) modelObject).getAnyType();
         } else if (modelObject instanceof Domain) {
             header = ((Domain) modelObject).getKey();
+        } else if (modelObject instanceof CommandTO) {
+            header = ((CommandTO) modelObject).getKey();
         } else if (modelObject instanceof NamedEntityTO) {
             header = ((NamedEntityTO) modelObject).getName();
         } else if (modelObject instanceof EntityTO) {
diff --git a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wicket/markup/html/form/ConfirmBehavior.java b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wicket/markup/html/form/ConfirmBehavior.java
index 19deec2e6d..c4f0218a57 100644
--- a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wicket/markup/html/form/ConfirmBehavior.java
+++ b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wicket/markup/html/form/ConfirmBehavior.java
@@ -55,6 +55,14 @@ public class ConfirmBehavior extends Behavior {
                         + "  evt.stopImmediatePropagation();"
                         + "  bootbox.confirm({"
                         + "message:'" + new ResourceModel(msg).getObject() + "', "
+                        + "buttons: {"
+                        + "    confirm: {"
+                        + "        className: 'btn-success'"
+                        + "    },"
+                        + "    cancel: {"
+                        + "        className: 'btn-danger'"
+                        + "    }"
+                        + "},"
                         + "locale: '" + SyncopeConsoleSession.get().getLocale().getLanguage() + "',"
                         + "callback: function(result) {"
                         + "    if (result == true) {"
diff --git a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/widgets/JobWidget.java b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/widgets/JobWidget.java
index 9424c83f41..8bdbfbc660 100644
--- a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/widgets/JobWidget.java
+++ b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/widgets/JobWidget.java
@@ -225,7 +225,7 @@ public class JobWidget extends BaseWidget {
     }
 
     private List<ITab> buildTabList(final PageReference pageRef) {
-        final List<ITab> tabs = new ArrayList<>();
+        List<ITab> tabs = new ArrayList<>();
 
         tabs.add(new AbstractTab(new ResourceModel("available")) {
 
@@ -324,9 +324,9 @@ public class JobWidget extends BaseWidget {
 
                 @Override
                 public void populateItem(
-                    final Item<ICellPopulator<JobTO>> cellItem,
-                    final String componentId,
-                    final IModel<JobTO> rowModel) {
+                        final Item<ICellPopulator<JobTO>> cellItem,
+                        final String componentId,
+                        final IModel<JobTO> rowModel) {
 
                     JobTO jobTO = rowModel.getObject();
                     JobActionPanel panel = new JobActionPanel(componentId, jobTO, true, JobWidget.this);
@@ -335,17 +335,17 @@ public class JobWidget extends BaseWidget {
                     switch (jobTO.getType()) {
                         case TASK:
                             roles = String.format("%s,%s",
-                                IdRepoEntitlement.TASK_EXECUTE, IdRepoEntitlement.TASK_UPDATE);
+                                    IdRepoEntitlement.TASK_EXECUTE, IdRepoEntitlement.TASK_UPDATE);
                             break;
 
                         case REPORT:
                             roles = String.format("%s,%s",
-                                IdRepoEntitlement.REPORT_EXECUTE, IdRepoEntitlement.REPORT_UPDATE);
+                                    IdRepoEntitlement.REPORT_EXECUTE, IdRepoEntitlement.REPORT_UPDATE);
                             break;
 
                         case NOTIFICATION:
                             roles = String.format("%s,%s",
-                                IdRepoEntitlement.NOTIFICATION_EXECUTE, IdRepoEntitlement.NOTIFICATION_UPDATE);
+                                    IdRepoEntitlement.NOTIFICATION_EXECUTE, IdRepoEntitlement.NOTIFICATION_UPDATE);
                             break;
 
                         default:
@@ -358,7 +358,7 @@ public class JobWidget extends BaseWidget {
 
                 @Override
                 public String getCssClass() {
-                    return "col-xs-1";
+                    return "running-col";
                 }
             });
 
@@ -390,9 +390,9 @@ public class JobWidget extends BaseWidget {
                             target.add(jobModal.setContent(rwb.build(BaseModal.CONTENT_ID, AjaxWizard.Mode.EDIT)));
 
                             jobModal.header(new StringResourceModel(
-                                "any.edit",
-                                AvailableJobsPanel.this,
-                                new Model<>(reportTO)));
+                                    "any.edit",
+                                    AvailableJobsPanel.this,
+                                    new Model<>(reportTO)));
 
                             jobModal.show(true);
                             break;
@@ -403,21 +403,21 @@ public class JobWidget extends BaseWidget {
                                 schedTaskTO = TaskRestClient.readTask(TaskType.PULL, jobTO.getRefKey());
                             } catch (Exception e) {
                                 LOG.debug("Failed to read {} as {}, attempting {}",
-                                    jobTO.getRefKey(), TaskType.PULL, TaskType.PUSH, e);
+                                        jobTO.getRefKey(), TaskType.PULL, TaskType.PUSH, e);
                                 schedTaskTO = TaskRestClient.readTask(TaskType.PUSH, jobTO.getRefKey());
                             }
 
                             SchedTaskWizardBuilder<ProvisioningTaskTO> swb =
-                                new SchedTaskWizardBuilder<>(schedTaskTO instanceof PullTaskTO
-                                    ? TaskType.PULL : TaskType.PUSH, schedTaskTO, pageRef);
+                                    new SchedTaskWizardBuilder<>(schedTaskTO instanceof PullTaskTO
+                                            ? TaskType.PULL : TaskType.PUSH, schedTaskTO, pageRef);
                             swb.setEventSink(AvailableJobsPanel.this);
 
                             target.add(jobModal.setContent(swb.build(BaseModal.CONTENT_ID, AjaxWizard.Mode.EDIT)));
 
                             jobModal.header(new StringResourceModel(
-                                "any.edit",
-                                AvailableJobsPanel.this,
-                                new Model<>(schedTaskTO)));
+                                    "any.edit",
+                                    AvailableJobsPanel.this,
+                                    new Model<>(schedTaskTO)));
 
                             jobModal.show(true);
                             break;
@@ -451,14 +451,14 @@ public class JobWidget extends BaseWidget {
                                 final ReportTO reportTO = ReportRestClient.read(jobTO.getRefKey());
 
                                 target.add(AvailableJobsPanel.this.reportModal.setContent(
-                                    new ReportletDirectoryPanel(reportModal, jobTO.getRefKey(), pageRef)));
+                                        new ReportletDirectoryPanel(reportModal, jobTO.getRefKey(), pageRef)));
 
                                 MetaDataRoleAuthorizationStrategy.authorize(
-                                    reportModal.getForm(),
-                                    ENABLE, IdRepoEntitlement.REPORT_UPDATE);
+                                        reportModal.getForm(),
+                                        ENABLE, IdRepoEntitlement.REPORT_UPDATE);
 
                                 reportModal.header(new StringResourceModel(
-                                    "reportlet.conf", AvailableJobsPanel.this, new Model<>(reportTO)));
+                                        "reportlet.conf", AvailableJobsPanel.this, new Model<>(reportTO)));
 
                                 reportModal.show(true);
 
@@ -476,7 +476,7 @@ public class JobWidget extends BaseWidget {
                 @Override
                 protected boolean statusCondition(final JobTO modelObject) {
                     return !(null != jobTO.getType() && (JobType.TASK.equals(jobTO.getType())
-                        || JobType.NOTIFICATION.equals(jobTO.getType())));
+                            || JobType.NOTIFICATION.equals(jobTO.getType())));
                 }
 
             }, ActionType.COMPOSE, IdRepoEntitlement.TASK_UPDATE);
@@ -518,8 +518,8 @@ public class JobWidget extends BaseWidget {
                 @Override
                 protected boolean statusCondition(final JobTO modelObject) {
                     return (null != jobTO.getType()
-                        && !JobType.NOTIFICATION.equals(jobTO.getType())
-                        && (jobTO.isScheduled() && !jobTO.isRunning()));
+                            && !JobType.NOTIFICATION.equals(jobTO.getType())
+                            && (jobTO.isScheduled() && !jobTO.isRunning()));
                 }
             }, ActionLink.ActionType.DELETE, IdRepoEntitlement.TASK_DELETE, true);
 
@@ -625,7 +625,7 @@ public class JobWidget extends BaseWidget {
 
         @Override
         public ActionsPanel<ExecTO> getActions(final IModel<ExecTO> model) {
-            final ActionsPanel<ExecTO> panel = super.getActions(model);
+            ActionsPanel<ExecTO> panel = super.getActions(model);
 
             panel.add(new ActionLink<>() {
 
@@ -639,6 +639,7 @@ public class JobWidget extends BaseWidget {
                     target.add(detailModal);
                 }
             }, ActionLink.ActionType.VIEW, IdRepoEntitlement.TASK_READ);
+
             return panel;
         }
 
diff --git a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wizards/CommandWizardBuilder.java b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wizards/CommandWizardBuilder.java
new file mode 100644
index 0000000000..cd27db62e6
--- /dev/null
+++ b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wizards/CommandWizardBuilder.java
@@ -0,0 +1,66 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.client.console.wizards;
+
+import java.io.Serializable;
+import org.apache.syncope.client.console.panels.BeanPanel;
+import org.apache.syncope.client.console.rest.CommandRestClient;
+import org.apache.syncope.common.lib.command.CommandTO;
+import org.apache.wicket.PageReference;
+import org.apache.wicket.extensions.wizard.WizardModel;
+import org.apache.wicket.extensions.wizard.WizardStep;
+import org.apache.wicket.model.LoadableDetachableModel;
+
+public class CommandWizardBuilder extends BaseAjaxWizardBuilder<CommandTO> {
+
+    private static final long serialVersionUID = 5288806466136582164L;
+
+    public CommandWizardBuilder(final CommandTO defaultItem, final PageReference pageRef) {
+        super(defaultItem, pageRef);
+    }
+
+    @Override
+    protected Serializable onApplyInternal(final CommandTO modelObject) {
+        return CommandRestClient.run(modelObject).getOutput();
+    }
+
+    @Override
+    protected WizardModel buildModelSteps(final CommandTO modelObject, final WizardModel wizardModel) {
+        wizardModel.add(new CommandArgs(modelObject));
+        return wizardModel;
+    }
+
+    public class CommandArgs extends WizardStep {
+
+        private static final long serialVersionUID = -785981096328637758L;
+
+        public CommandArgs(final CommandTO command) {
+            LoadableDetachableModel<Serializable> bean = new LoadableDetachableModel<>() {
+
+                private static final long serialVersionUID = -1096114645494621802L;
+
+                @Override
+                protected Serializable load() {
+                    return command.getArgs();
+                }
+            };
+            add(new BeanPanel<>("bean", bean, pageRef).setRenderBodyOnly(true));
+        }
+    }
+}
diff --git a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wizards/WizardMgtPanel.java b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wizards/WizardMgtPanel.java
index 20b36d2f6c..c79f0bde58 100644
--- a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wizards/WizardMgtPanel.java
+++ b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wizards/WizardMgtPanel.java
@@ -227,13 +227,12 @@ public abstract class WizardMgtPanel<T extends Serializable> extends AbstractWiz
                 target.ifPresent(this::customActionOnCancelCallback);
             } else if (event.getPayload() instanceof AjaxWizard.NewItemFinishEvent) {
                 SyncopeConsoleSession.get().success(getString(Constants.OPERATION_SUCCEEDED));
-                target.ifPresent(ajaxRequestTarget ->
-                    ((BaseWebPage) pageRef.getPage()).getNotificationPanel().refresh(ajaxRequestTarget));
+                target.ifPresent(t -> ((BaseWebPage) pageRef.getPage()).getNotificationPanel().refresh(t));
 
                 if (wizardInModal && showResultPanel) {
                     modal.setContent(new ResultPanel<>(
-                        item,
-                        AjaxWizard.NewItemFinishEvent.class.cast(newItemEvent).getResult()) {
+                            item,
+                            AjaxWizard.NewItemFinishEvent.class.cast(newItemEvent).getResult()) {
 
                         private static final long serialVersionUID = -2630573849050255233L;
 
@@ -243,8 +242,11 @@ public abstract class WizardMgtPanel<T extends Serializable> extends AbstractWiz
                         }
 
                         @Override
-                        protected Panel customResultBody(final String panelId, final T item, 
+                        protected Panel customResultBody(
+                                final String panelId,
+                                final T item,
                                 final Serializable result) {
+
                             return WizardMgtPanel.this.customResultBody(panelId, item, result);
                         }
                     });
diff --git a/client/idrepo/console/src/main/resources/META-INF/resources/css/syncopeConsole.scss b/client/idrepo/console/src/main/resources/META-INF/resources/css/syncopeConsole.scss
index 6d62576586..1deeea07ab 100644
--- a/client/idrepo/console/src/main/resources/META-INF/resources/css/syncopeConsole.scss
+++ b/client/idrepo/console/src/main/resources/META-INF/resources/css/syncopeConsole.scss
@@ -96,9 +96,12 @@ body {
   }
 }
 
+/* JSON diff
+============================================================================= */
+
 .json-diff-header {
   text-align: center;
-  
+
   display: table;
   width: 100%;
   table-layout: fixed;
@@ -109,3 +112,7 @@ body {
   display: table-cell;
   vertical-align: middle;
 }
+
+.running-col {
+  width: 65px;
+}
diff --git a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/SyncopeWebApplication.properties b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/SyncopeWebApplication.properties
index 966af0ddad..e013427500 100644
--- a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/SyncopeWebApplication.properties
+++ b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/SyncopeWebApplication.properties
@@ -80,3 +80,5 @@ domains=Domains
 nomatch=No matches found
 tooLargeFile=File is too large, max upload file size is ${maxUploadSizeB} bytes (${maxUploadSizeMB} MB). 
 confirmDelegation=Do you really want to switch user?
+topology=Topology
+engagements=Engagements
diff --git a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/SyncopeWebApplication_fr_CA.properties b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/SyncopeWebApplication_fr_CA.properties
index de288344fb..90301999a4 100644
--- a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/SyncopeWebApplication_fr_CA.properties
+++ b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/SyncopeWebApplication_fr_CA.properties
@@ -79,3 +79,4 @@ domains=Domaines
 nomatch=Aucune correspondance trouv\u00e9e
 tooLargeFile=Fichier trop volumineux, la taille maximale autoris\u00e9e est de $ {maxUploadSizeB} octets ($ {maxUploadSizeMB} Mo).
 confirmDelegation=Do you really want to switch user?
+engagements=Engagements
diff --git a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/SyncopeWebApplication_it.properties b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/SyncopeWebApplication_it.properties
index 59c56dc705..3200a9be9d 100644
--- a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/SyncopeWebApplication_it.properties
+++ b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/SyncopeWebApplication_it.properties
@@ -80,3 +80,5 @@ domains=Domini
 nomatch=Nessun risultato trovato
 tooLargeFile=File troppo grande, la dimensione massima ammessa \u00e8 ${maxUploadSizeB} bytes (${maxUploadSizeMB} MB). 
 confirmDelegation=Vuoi davvero cambiare utenza?
+topology=Topologia
+engagements=Impegni
diff --git a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/SyncopeWebApplication_ja.properties b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/SyncopeWebApplication_ja.properties
index ca0e84ecd7..a4ed95c701 100644
--- a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/SyncopeWebApplication_ja.properties
+++ b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/SyncopeWebApplication_ja.properties
@@ -78,3 +78,5 @@ domains=\u30c9\u30e1\u30a4\u30f3
 nomatch=No matches found
 tooLargeFile=File is too large, max upload file size is ${maxUploadSizeB} bytes (${maxUploadSizeMB} MB). 
 confirmDelegation=Do you really want to switch user?
+topology=Topology
+engagements=Engagements
diff --git a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/SyncopeWebApplication_pt_BR.properties b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/SyncopeWebApplication_pt_BR.properties
index 142ca0eb03..cd5fe7769e 100644
--- a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/SyncopeWebApplication_pt_BR.properties
+++ b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/SyncopeWebApplication_pt_BR.properties
@@ -80,3 +80,5 @@ domains=Dom\u00ednios
 nomatch=Nenhuma correspond\u00eancia encontrada
 tooLargeFile=File is too large, max upload file size is ${maxUploadSizeB} bytes (${maxUploadSizeMB} MB). 
 confirmDelegation=Do you really want to switch user?
+topology=Topology
+engagements=Engagements
diff --git a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/SyncopeWebApplication_ru.properties b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/SyncopeWebApplication_ru.properties
index 9c58582f02..dd5cd0fbc2 100644
--- a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/SyncopeWebApplication_ru.properties
+++ b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/SyncopeWebApplication_ru.properties
@@ -79,3 +79,5 @@ domains=Domains
 nomatch=No matches found
 tooLargeFile=File is too large, max upload file size is ${maxUploadSizeB} bytes (${maxUploadSizeMB} MB). 
 confirmDelegation=Do you really want to switch user?
+topology=Topology
+engagements=Engagements
diff --git a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/implementations/MyRecipientsProvider.groovy b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/implementations/MyCommand.groovy
similarity index 69%
copy from client/idrepo/console/src/main/resources/org/apache/syncope/client/console/implementations/MyRecipientsProvider.groovy
copy to client/idrepo/console/src/main/resources/org/apache/syncope/client/console/implementations/MyCommand.groovy
index 6009cf9ea0..3b59310f2a 100644
--- a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/implementations/MyRecipientsProvider.groovy
+++ b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/implementations/MyCommand.groovy
@@ -1,3 +1,4 @@
+
 /*
  * Licensed to the Apache Software Foundation (ASF) under one
  * or more contributor license agreements.  See the NOTICE file
@@ -17,15 +18,13 @@
  * under the License.
  */
 import groovy.transform.CompileStatic
-import org.apache.syncope.core.persistence.api.attrvalue.validation.Validator
-import org.apache.syncope.core.persistence.api.entity.Notification
-import org.apache.syncope.core.provisioning.api.notification.RecipientsProvider
+import org.apache.syncope.common.lib.command.CommandArgs
+import org.apache.syncope.core.logic.api.Command
 
 @CompileStatic
-class MyRecipientsProvider implements RecipientsProvider {
-  
-  @Override
-  Set<String> provideRecipients(Notification notification) {
-    return List.of();
+class MyCommand implements Command<CommandArgs> {
+
+  String run(CommandArgs args) {
+    return "SUCCESS"
   }
 }
diff --git a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/implementations/MyLogicActions.groovy b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/implementations/MyLogicActions.groovy
index f92be605ec..b176acd1d1 100644
--- a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/implementations/MyLogicActions.groovy
+++ b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/implementations/MyLogicActions.groovy
@@ -21,7 +21,7 @@ import org.apache.syncope.common.lib.request.AnyCR
 import org.apache.syncope.common.lib.request.AnyUR
 import org.apache.syncope.common.lib.to.AnyTO
 import org.apache.syncope.common.lib.to.PropagationStatus
-import org.apache.syncope.core.provisioning.api.LogicActions
+import org.apache.syncope.core.logic.api.LogicActions
 
 @CompileStatic
 class MyLogicActions implements LogicActions {
diff --git a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/implementations/MyRecipientsProvider.groovy b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/implementations/MyRecipientsProvider.groovy
index 6009cf9ea0..1fe53ff713 100644
--- a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/implementations/MyRecipientsProvider.groovy
+++ b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/implementations/MyRecipientsProvider.groovy
@@ -17,7 +17,7 @@
  * under the License.
  */
 import groovy.transform.CompileStatic
-import org.apache.syncope.core.persistence.api.attrvalue.validation.Validator
+import java.util.Set
 import org.apache.syncope.core.persistence.api.entity.Notification
 import org.apache.syncope.core.provisioning.api.notification.RecipientsProvider
 
@@ -26,6 +26,6 @@ class MyRecipientsProvider implements RecipientsProvider {
   
   @Override
   Set<String> provideRecipients(Notification notification) {
-    return List.of();
+    return Set.of();
   }
 }
diff --git a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/pages/BasePage.html b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/pages/BasePage.html
index 505bbf2ee4..7ca271e928 100644
--- a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/pages/BasePage.html
+++ b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/pages/BasePage.html
@@ -127,6 +127,7 @@ under the License.
             <ul class="nav nav-pills nav-sidebar flex-column nav-child-indent nav-compact" data-widget="treeview" role="menu" data-accordion="false">
               <li class="nav-item" wicket:id="dashboardLI"><a href="#" class="nav-link" wicket:id="dashboard"><i class="nav-icon fa fa-tachometer-alt"></i><p><wicket:message key="dashboard"/></p></a></li>
               <li class="nav-item" wicket:id="realmsLI"><a href="#" class="nav-link" wicket:id="realms"><i class="nav-icon fa fa-folder-open"></i><p><wicket:message key="realms"/></p></a></li>
+              <li class="nav-item" wicket:id="engagementsLI"><a href="#" class="nav-link" wicket:id="engagements"><i class="nav-icon fas fa-tasks"></i><p><wicket:message key="engagements"/></p></a></li>
               <li class="nav-item" wicket:id="reportsLI"><a href="#" class="nav-link" wicket:id="reports"><i class="nav-icon fa fa-chart-pie"></i><p><wicket:message key="reports"/></p></a></li>
               <wicket:container wicket:id="idmPages">
                 <li class="nav-item" wicket:id="idmPageLI">
diff --git a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/pages/Engagements.html b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/pages/Engagements.html
new file mode 100644
index 0000000000..0a67f877c9
--- /dev/null
+++ b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/pages/Engagements.html
@@ -0,0 +1,47 @@
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+-->
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns:wicket="http://wicket.apache.org">
+  <wicket:extend>
+    <section class="content-header">
+      <div class="container-fluid">
+        <div class="row mb-2">
+          <div class="col-sm-6">
+            <h1>&nbsp;</h1>
+          </div>
+          <div class="col-sm-6">
+            <ol class="breadcrumb float-sm-right">
+              <li class="breadcrumb-item">
+                <a wicket:id="dashboardBr"><i class="fa fa-tachometer-alt"></i> <wicket:message key="dashboard"></wicket:message></a>
+              </li>
+              <li class="breadcrumb-item active"><wicket:message key="engagements"/></li>
+            </ol>
+          </div>
+        </div>
+      </div><!-- /.container-fluid -->
+    </section>
+
+    <section class="content" wicket:id="content">
+      <div class="container-fluid">
+        <div class="card card-outline">
+          <div class="card-body" wicket:id="tabbedPanel"/>
+        </div>
+      </div>
+    </section>
+  </wicket:extend>
+</html>
diff --git a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/pages/Reports.properties b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/pages/Engagements.properties
similarity index 88%
copy from client/idrepo/console/src/main/resources/org/apache/syncope/client/console/pages/Reports.properties
copy to client/idrepo/console/src/main/resources/org/apache/syncope/client/console/pages/Engagements.properties
index 78e6676a99..f46483841f 100644
--- a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/pages/Reports.properties
+++ b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/pages/Engagements.properties
@@ -14,5 +14,9 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
-reports=Reports
-report.templates=Templates
+
+schedTasks=Scheduled Tasks
+macroTasks=Macro
+commands=Commands
+arguments=Arguments
+any.edit=Run ${key}
diff --git a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/pages/Reports.properties b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/pages/Engagements_fr_CA.properties
similarity index 88%
copy from client/idrepo/console/src/main/resources/org/apache/syncope/client/console/pages/Reports.properties
copy to client/idrepo/console/src/main/resources/org/apache/syncope/client/console/pages/Engagements_fr_CA.properties
index 78e6676a99..f46483841f 100644
--- a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/pages/Reports.properties
+++ b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/pages/Engagements_fr_CA.properties
@@ -14,5 +14,9 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
-reports=Reports
-report.templates=Templates
+
+schedTasks=Scheduled Tasks
+macroTasks=Macro
+commands=Commands
+arguments=Arguments
+any.edit=Run ${key}
diff --git a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/pages/Reports_pt_BR.properties b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/pages/Engagements_it.properties
similarity index 88%
copy from client/idrepo/console/src/main/resources/org/apache/syncope/client/console/pages/Reports_pt_BR.properties
copy to client/idrepo/console/src/main/resources/org/apache/syncope/client/console/pages/Engagements_it.properties
index e675da7407..da0fafe645 100644
--- a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/pages/Reports_pt_BR.properties
+++ b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/pages/Engagements_it.properties
@@ -14,5 +14,9 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
-reports=Reports
-report.templates=Template
+
+schedTasks=Task Programmati
+macroTasks=Macro
+commands=Comandi
+arguments=Argomenti
+any.edit=Esegui ${key}
diff --git a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/pages/Reports.properties b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/pages/Engagements_ja.properties
similarity index 88%
copy from client/idrepo/console/src/main/resources/org/apache/syncope/client/console/pages/Reports.properties
copy to client/idrepo/console/src/main/resources/org/apache/syncope/client/console/pages/Engagements_ja.properties
index 78e6676a99..f46483841f 100644
--- a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/pages/Reports.properties
+++ b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/pages/Engagements_ja.properties
@@ -14,5 +14,9 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
-reports=Reports
-report.templates=Templates
+
+schedTasks=Scheduled Tasks
+macroTasks=Macro
+commands=Commands
+arguments=Arguments
+any.edit=Run ${key}
diff --git a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/pages/Reports_pt_BR.properties b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/pages/Engagements_pt_BR.properties
similarity index 87%
copy from client/idrepo/console/src/main/resources/org/apache/syncope/client/console/pages/Reports_pt_BR.properties
copy to client/idrepo/console/src/main/resources/org/apache/syncope/client/console/pages/Engagements_pt_BR.properties
index e675da7407..39b6776c98 100644
--- a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/pages/Reports_pt_BR.properties
+++ b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/pages/Engagements_pt_BR.properties
@@ -14,5 +14,9 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
-reports=Reports
-report.templates=Template
+
+schedTasks=ScheduledScheduled Tasks
+macroTasks=Macro
+commands=Commands
+arguments=Arguments
+any.edit=Run ${key}
diff --git a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/pages/Reports.properties b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/pages/Engagements_ru.properties
similarity index 88%
copy from client/idrepo/console/src/main/resources/org/apache/syncope/client/console/pages/Reports.properties
copy to client/idrepo/console/src/main/resources/org/apache/syncope/client/console/pages/Engagements_ru.properties
index 78e6676a99..f46483841f 100644
--- a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/pages/Reports.properties
+++ b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/pages/Engagements_ru.properties
@@ -14,5 +14,9 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
-reports=Reports
-report.templates=Templates
+
+schedTasks=Scheduled Tasks
+macroTasks=Macro
+commands=Commands
+arguments=Arguments
+any.edit=Run ${key}
diff --git a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/pages/Reports.properties b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/pages/Reports.properties
index 78e6676a99..a13d504168 100644
--- a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/pages/Reports.properties
+++ b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/pages/Reports.properties
@@ -14,5 +14,5 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
-reports=Reports
+
 report.templates=Templates
diff --git a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/pages/Reports_it.properties b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/pages/Reports_it.properties
index 732d43d536..7a2a58b8ac 100644
--- a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/pages/Reports_it.properties
+++ b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/pages/Reports_it.properties
@@ -14,5 +14,5 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
-reports=Report
+
 report.templates=Template
diff --git a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/pages/Reports_ja.properties b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/pages/Reports_ja.properties
index eaec821347..2c4b891886 100644
--- a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/pages/Reports_ja.properties
+++ b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/pages/Reports_ja.properties
@@ -14,5 +14,5 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
-reports=\u30ec\u30dd\u30fc\u30c8
+
 report.templates=\u30c6\u30f3\u30d7\u30ec\u30fc\u30c8
diff --git a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/pages/Reports_pt_BR.properties b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/pages/Reports_pt_BR.properties
index e675da7407..7a2a58b8ac 100644
--- a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/pages/Reports_pt_BR.properties
+++ b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/pages/Reports_pt_BR.properties
@@ -14,5 +14,5 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
-reports=Reports
+
 report.templates=Template
diff --git a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/pages/Reports_ru.properties b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/pages/Reports_ru.properties
index c1a1b3f815..bd7b281867 100644
--- a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/pages/Reports_ru.properties
+++ b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/pages/Reports_ru.properties
@@ -14,6 +14,5 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
-#
-reports=\u041e\u0442\u0447\u0435\u0442\u044b
+
 report.templates=\u0428\u0430\u0431\u043b\u043e\u043d\u044b
diff --git a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/panels/CommandsPanel.html b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/panels/CommandsPanel.html
new file mode 100644
index 0000000000..9375b3de50
--- /dev/null
+++ b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/panels/CommandsPanel.html
@@ -0,0 +1,36 @@
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+-->
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns:wicket="http://wicket.apache.org">
+  <wicket:panel>
+    <div wicket:id="searchBox">
+      <form wicket:id="form">
+        <div class="input-group mb-3">
+          <span wicket:id="filter">[FILTER]</span>
+          <span class="input-group-btn">
+            <button type="button" class="btn btn-default btn-flat" wicket:id="search">
+              <span class="fas fa-search" aria-hidden="true"></span>
+            </button>
+          </span>
+        </div>
+      </form>
+    </div>
+
+    <div wicket:id="commands"></div>
+  </wicket:panel>
+</html>
diff --git a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/tasks/CommandComposeWizardBuilder$CommandArgs.html b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/tasks/CommandComposeWizardBuilder$CommandArgs.html
new file mode 100644
index 0000000000..7772c93b7b
--- /dev/null
+++ b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/tasks/CommandComposeWizardBuilder$CommandArgs.html
@@ -0,0 +1,23 @@
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+-->
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns:wicket="http://wicket.apache.org">
+  <wicket:panel>
+    <span wicket:id="bean"/>
+  </wicket:panel>
+</html>
diff --git a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/tasks/CommandComposeWizardBuilder$Profile.html b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/tasks/CommandComposeWizardBuilder$Profile.html
new file mode 100644
index 0000000000..8f5d94de16
--- /dev/null
+++ b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/tasks/CommandComposeWizardBuilder$Profile.html
@@ -0,0 +1,26 @@
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+-->
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns:wicket="http://wicket.apache.org">
+  <wicket:panel>
+    <div class="form-group">
+      <label for="command"><wicket:message key="command"/></label>
+      <input type="text" class="form-control col-xs-4"  wicket:id="command"/>
+    </div>
+  </wicket:panel>
+</html>
diff --git a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/pages/Reports_it.properties b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/tasks/CommandComposeWizardBuilder$Profile.properties
similarity index 95%
copy from client/idrepo/console/src/main/resources/org/apache/syncope/client/console/pages/Reports_it.properties
copy to client/idrepo/console/src/main/resources/org/apache/syncope/client/console/tasks/CommandComposeWizardBuilder$Profile.properties
index 732d43d536..8b0724c04b 100644
--- a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/pages/Reports_it.properties
+++ b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/tasks/CommandComposeWizardBuilder$Profile.properties
@@ -14,5 +14,5 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
-reports=Report
-report.templates=Template
+
+command=Command
diff --git a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/pages/Reports_it.properties b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/tasks/CommandComposeWizardBuilder$Profile_fr_CA.properties
similarity index 95%
copy from client/idrepo/console/src/main/resources/org/apache/syncope/client/console/pages/Reports_it.properties
copy to client/idrepo/console/src/main/resources/org/apache/syncope/client/console/tasks/CommandComposeWizardBuilder$Profile_fr_CA.properties
index 732d43d536..8b0724c04b 100644
--- a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/pages/Reports_it.properties
+++ b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/tasks/CommandComposeWizardBuilder$Profile_fr_CA.properties
@@ -14,5 +14,5 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
-reports=Report
-report.templates=Template
+
+command=Command
diff --git a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/pages/Reports_it.properties b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/tasks/CommandComposeWizardBuilder$Profile_it.properties
similarity index 95%
copy from client/idrepo/console/src/main/resources/org/apache/syncope/client/console/pages/Reports_it.properties
copy to client/idrepo/console/src/main/resources/org/apache/syncope/client/console/tasks/CommandComposeWizardBuilder$Profile_it.properties
index 732d43d536..91b2647095 100644
--- a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/pages/Reports_it.properties
+++ b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/tasks/CommandComposeWizardBuilder$Profile_it.properties
@@ -14,5 +14,5 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
-reports=Report
-report.templates=Template
+
+command=Comando
diff --git a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/pages/Reports_it.properties b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/tasks/CommandComposeWizardBuilder$Profile_ja.properties
similarity index 95%
copy from client/idrepo/console/src/main/resources/org/apache/syncope/client/console/pages/Reports_it.properties
copy to client/idrepo/console/src/main/resources/org/apache/syncope/client/console/tasks/CommandComposeWizardBuilder$Profile_ja.properties
index 732d43d536..8b0724c04b 100644
--- a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/pages/Reports_it.properties
+++ b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/tasks/CommandComposeWizardBuilder$Profile_ja.properties
@@ -14,5 +14,5 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
-reports=Report
-report.templates=Template
+
+command=Command
diff --git a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/pages/Reports_it.properties b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/tasks/CommandComposeWizardBuilder$Profile_pt_BR.properties
similarity index 95%
copy from client/idrepo/console/src/main/resources/org/apache/syncope/client/console/pages/Reports_it.properties
copy to client/idrepo/console/src/main/resources/org/apache/syncope/client/console/tasks/CommandComposeWizardBuilder$Profile_pt_BR.properties
index 732d43d536..8b0724c04b 100644
--- a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/pages/Reports_it.properties
+++ b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/tasks/CommandComposeWizardBuilder$Profile_pt_BR.properties
@@ -14,5 +14,5 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
-reports=Report
-report.templates=Template
+
+command=Command
diff --git a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/pages/Reports_it.properties b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/tasks/CommandComposeWizardBuilder$Profile_ru.properties
similarity index 95%
copy from client/idrepo/console/src/main/resources/org/apache/syncope/client/console/pages/Reports_it.properties
copy to client/idrepo/console/src/main/resources/org/apache/syncope/client/console/tasks/CommandComposeWizardBuilder$Profile_ru.properties
index 732d43d536..8b0724c04b 100644
--- a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/pages/Reports_it.properties
+++ b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/tasks/CommandComposeWizardBuilder$Profile_ru.properties
@@ -14,5 +14,5 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
-reports=Report
-report.templates=Template
+
+command=Command
diff --git a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/pages/Reports_pt_BR.properties b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/tasks/MacroTaskDirectoryPanel.properties
similarity index 87%
copy from client/idrepo/console/src/main/resources/org/apache/syncope/client/console/pages/Reports_pt_BR.properties
copy to client/idrepo/console/src/main/resources/org/apache/syncope/client/console/tasks/MacroTaskDirectoryPanel.properties
index e675da7407..2c072ba2df 100644
--- a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/pages/Reports_pt_BR.properties
+++ b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/tasks/MacroTaskDirectoryPanel.properties
@@ -14,5 +14,7 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
-reports=Reports
-report.templates=Template
+
+command.conf=Command configuration for ${name}
+continueOnError=Continue on error
+saveExecs=Save executions
diff --git a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/pages/Reports_pt_BR.properties b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/tasks/MacroTaskDirectoryPanel_fr_CA.properties
similarity index 87%
copy from client/idrepo/console/src/main/resources/org/apache/syncope/client/console/pages/Reports_pt_BR.properties
copy to client/idrepo/console/src/main/resources/org/apache/syncope/client/console/tasks/MacroTaskDirectoryPanel_fr_CA.properties
index e675da7407..2c072ba2df 100644
--- a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/pages/Reports_pt_BR.properties
+++ b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/tasks/MacroTaskDirectoryPanel_fr_CA.properties
@@ -14,5 +14,7 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
-reports=Reports
-report.templates=Template
+
+command.conf=Command configuration for ${name}
+continueOnError=Continue on error
+saveExecs=Save executions
diff --git a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/pages/Reports_pt_BR.properties b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/tasks/MacroTaskDirectoryPanel_it.properties
similarity index 86%
copy from client/idrepo/console/src/main/resources/org/apache/syncope/client/console/pages/Reports_pt_BR.properties
copy to client/idrepo/console/src/main/resources/org/apache/syncope/client/console/tasks/MacroTaskDirectoryPanel_it.properties
index e675da7407..91e32ecf6e 100644
--- a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/pages/Reports_pt_BR.properties
+++ b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/tasks/MacroTaskDirectoryPanel_it.properties
@@ -14,5 +14,7 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
-reports=Reports
-report.templates=Template
+
+command.conf=Configurazione dei comandi per ${name}
+continueOnError=Continuare in caso di errore
+saveExecs=Salvare esecuzioni
diff --git a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/pages/Reports_pt_BR.properties b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/tasks/MacroTaskDirectoryPanel_ja.properties
similarity index 87%
copy from client/idrepo/console/src/main/resources/org/apache/syncope/client/console/pages/Reports_pt_BR.properties
copy to client/idrepo/console/src/main/resources/org/apache/syncope/client/console/tasks/MacroTaskDirectoryPanel_ja.properties
index e675da7407..2c072ba2df 100644
--- a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/pages/Reports_pt_BR.properties
+++ b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/tasks/MacroTaskDirectoryPanel_ja.properties
@@ -14,5 +14,7 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
-reports=Reports
-report.templates=Template
+
+command.conf=Command configuration for ${name}
+continueOnError=Continue on error
+saveExecs=Save executions
diff --git a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/pages/Reports_pt_BR.properties b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/tasks/MacroTaskDirectoryPanel_pt_BR.properties
similarity index 87%
copy from client/idrepo/console/src/main/resources/org/apache/syncope/client/console/pages/Reports_pt_BR.properties
copy to client/idrepo/console/src/main/resources/org/apache/syncope/client/console/tasks/MacroTaskDirectoryPanel_pt_BR.properties
index e675da7407..2c072ba2df 100644
--- a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/pages/Reports_pt_BR.properties
+++ b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/tasks/MacroTaskDirectoryPanel_pt_BR.properties
@@ -14,5 +14,7 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
-reports=Reports
-report.templates=Template
+
+command.conf=Command configuration for ${name}
+continueOnError=Continue on error
+saveExecs=Save executions
diff --git a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/pages/Reports_pt_BR.properties b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/tasks/MacroTaskDirectoryPanel_ru.properties
similarity index 87%
copy from client/idrepo/console/src/main/resources/org/apache/syncope/client/console/pages/Reports_pt_BR.properties
copy to client/idrepo/console/src/main/resources/org/apache/syncope/client/console/tasks/MacroTaskDirectoryPanel_ru.properties
index e675da7407..2c072ba2df 100644
--- a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/pages/Reports_pt_BR.properties
+++ b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/tasks/MacroTaskDirectoryPanel_ru.properties
@@ -14,5 +14,7 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
-reports=Reports
-report.templates=Template
+
+command.conf=Command configuration for ${name}
+continueOnError=Continue on error
+saveExecs=Save executions
diff --git a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/tasks/SchedTaskDirectoryPanel.properties b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/tasks/SchedTaskDirectoryPanel.properties
index a14b3f6141..70dbbd8e89 100644
--- a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/tasks/SchedTaskDirectoryPanel.properties
+++ b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/tasks/SchedTaskDirectoryPanel.properties
@@ -30,3 +30,4 @@ pullMode=Pull Mode
 reconFilterBuilder=Reconciliation Filter Builder
 actions=Actions
 sourceRealm=Source Realm
+realm=Realm
diff --git a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/tasks/SchedTaskDirectoryPanel_fr_CA.properties b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/tasks/SchedTaskDirectoryPanel_fr_CA.properties
index 4fdb593d21..cc45c5c4fa 100644
--- a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/tasks/SchedTaskDirectoryPanel_fr_CA.properties
+++ b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/tasks/SchedTaskDirectoryPanel_fr_CA.properties
@@ -17,15 +17,16 @@
 name=Nom
 description=Description
 destinationRealm=Domaine de destination
-jobDelegate=D�l�gu� � la t�che
-lastExec=Derni�re ex�cution
-nextExec=Prochaine ex�cution
+jobDelegate=D\u00e9l\u00e9gu\u00e9 \u00e0 la t\u00e2che
+lastExec=Derni\u00e8re ex\u00e9cution
+nextExec=Prochaine ex\u00e9cution
 active=Actif
 any.edit=Modifier ${name}
-any.new=Nouvelle t�che
+any.new=Nouvelle t\u00e2che
 any.finish=Soumettre ${name}
 any.cancel=Annuler ${name}
 pullMode=Mode Extraction
-reconFilterBuilder=G�n�rateur de filtre de rapprochement
+reconFilterBuilder=G\u00e9n\u00e9rateur de filtre de rapprochement
 actions=Actions
 sourceRealm=Domaine source
+realm=Realm
diff --git a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/tasks/SchedTaskDirectoryPanel_it.properties b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/tasks/SchedTaskDirectoryPanel_it.properties
index f261aec1b2..0444fd6dc5 100644
--- a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/tasks/SchedTaskDirectoryPanel_it.properties
+++ b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/tasks/SchedTaskDirectoryPanel_it.properties
@@ -30,3 +30,4 @@ pullMode=Pull Mode
 reconFilterBuilder=Reconciliation Filter Builder
 actions=Actions
 sourceRealm=Realm sorgente
+realm=Realm
diff --git a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/tasks/SchedTaskDirectoryPanel_ja.properties b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/tasks/SchedTaskDirectoryPanel_ja.properties
index e30d011da2..8d8cf9bfb9 100644
--- a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/tasks/SchedTaskDirectoryPanel_ja.properties
+++ b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/tasks/SchedTaskDirectoryPanel_ja.properties
@@ -30,3 +30,4 @@ pullMode=\u30d7\u30eb\u30e2\u30fc\u30c9
 reconFilterBuilder=\u30d5\u30a3\u30eb\u30bf\u30fc\u30d3\u30eb\u30c0\u30fc\u306e\u7167\u5408
 actions=\u30a2\u30af\u30b7\u30e7\u30f3
 sourceRealm=\u30bd\u30fc\u30b9\u30ec\u30eb\u30e0
+realm=Realm
diff --git a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/tasks/SchedTaskDirectoryPanel_pt_BR.properties b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/tasks/SchedTaskDirectoryPanel_pt_BR.properties
index a14b3f6141..70dbbd8e89 100644
--- a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/tasks/SchedTaskDirectoryPanel_pt_BR.properties
+++ b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/tasks/SchedTaskDirectoryPanel_pt_BR.properties
@@ -30,3 +30,4 @@ pullMode=Pull Mode
 reconFilterBuilder=Reconciliation Filter Builder
 actions=Actions
 sourceRealm=Source Realm
+realm=Realm
diff --git a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/tasks/SchedTaskDirectoryPanel_ru.properties b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/tasks/SchedTaskDirectoryPanel_ru.properties
index 9339ef79ad..99490c04cd 100644
--- a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/tasks/SchedTaskDirectoryPanel_ru.properties
+++ b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/tasks/SchedTaskDirectoryPanel_ru.properties
@@ -41,3 +41,4 @@ pullMode=\u0420\u0435\u0436\u0438\u043c \u043f\u043e\u043b\u0443\u0447\u0435\u04
 reconFilterBuilder=\u0424\u0438\u043b\u044c\u0442\u0440 \u0440\u0435\u043a\u043e\u043d\u0441\u0438\u043b\u0438\u0430\u0446\u0438\u0438
 actions=\u0414\u0435\u0439\u0441\u0442\u0432\u0438\u044f
 sourceRealm=Source Realm
+realm=Realm
diff --git a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/tasks/SchedTaskWizardBuilder$Profile.html b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/tasks/SchedTaskWizardBuilder$Profile.html
index 56ceeea006..a4b5615847 100644
--- a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/tasks/SchedTaskWizardBuilder$Profile.html
+++ b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/tasks/SchedTaskWizardBuilder$Profile.html
@@ -24,6 +24,12 @@ under the License.
 
     <div class="form-group"><span wicket:id="jobDelegate">[jobDelegate]</span></div>
 
+    <span wicket:id="macroTaskSpecifics">
+      <div class="form-group"><span wicket:id="realm">[realm]</span></div>
+      <div class="form-group"><span wicket:id="continueOnError">[continueOnError]</span></div>
+      <div class="form-group"><span wicket:id="saveExecs">[saveExecs]</span></div>
+    </span>      
+
     <span wicket:id="pullTaskSpecifics">
       <div class="form-group"><span wicket:id="destinationRealm">[destinationRealm]</span></div>
       <div class="form-group"><span wicket:id="pullMode">[pullMode]</span></div>
diff --git a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/tasks/SchedTaskWizardBuilder$Profile_fr_CA.properties b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/tasks/SchedTaskWizardBuilder$Profile_fr_CA.properties
index e034aac3d6..4c3c876a23 100644
--- a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/tasks/SchedTaskWizardBuilder$Profile_fr_CA.properties
+++ b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/tasks/SchedTaskWizardBuilder$Profile_fr_CA.properties
@@ -17,18 +17,18 @@
 name=Nom
 description=Description
 jobDelegate=Classe
-matchingRule=R�gle correspondante
-unmatchingRule=R�gle non correspondante
-performCreate=Permettre cr�ation
-performUpdate=Permettre mise ďż˝ jour
+matchingRule=R\u00e8gle correspondante
+unmatchingRule=R\u00e8gle non correspondante
+performCreate=Permettre cr\u00e9ation
+performUpdate=Permettre mise \u00e0 jour
 performDelete=Permettre suppression
 syncStatus=Statut sync
-lastExec=Derni�re ex�cution
-nextExec=Prochaine ex�cution
-detail=D�tails
+lastExec=Derni\u00e8re ex\u00e9cution
+nextExec=Prochaine ex\u00e9cution
+detail=D\u00e9tails
 delete=Supprimer
 edit=Modifier
-execute=Ex�cuter
-executeDryRun=Test ďż˝ blanc
+execute=Ex\u00e9cuter
+executeDryRun=Test \u00e0 blanc
 latestExecStatus=Dernier statut
-remediation=Remise en �tat
+remediation=Remise en \u00e9tat
diff --git a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/wizards/CommandWizardBuilder$CommandArgs.html b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/wizards/CommandWizardBuilder$CommandArgs.html
new file mode 100644
index 0000000000..7772c93b7b
--- /dev/null
+++ b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/wizards/CommandWizardBuilder$CommandArgs.html
@@ -0,0 +1,23 @@
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+-->
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns:wicket="http://wicket.apache.org">
+  <wicket:panel>
+    <span wicket:id="bean"/>
+  </wicket:panel>
+</html>
diff --git a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/task/TaskUtils.java b/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/command/CommandArgs.java
similarity index 67%
copy from core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/task/TaskUtils.java
copy to common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/command/CommandArgs.java
index e8f5c15d38..5a66bae16d 100644
--- a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/task/TaskUtils.java
+++ b/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/command/CommandArgs.java
@@ -16,20 +16,14 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.syncope.core.persistence.api.entity.task;
+package org.apache.syncope.common.lib.command;
 
-import org.apache.syncope.common.lib.to.TaskTO;
-import org.apache.syncope.common.lib.types.TaskType;
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+import org.apache.syncope.common.lib.BaseBean;
 
-public interface TaskUtils {
+@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY, property = "_class")
+public class CommandArgs implements BaseBean {
 
-    TaskType getType();
+    private static final long serialVersionUID = -85050010490462751L;
 
-    <T extends Task<T>> T newTask();
-
-    <T extends TaskTO> T newTaskTO();
-
-    <T extends Task<T>> Class<T> taskClass();
-
-    <T extends TaskTO> Class<T> taskTOClass();
 }
diff --git a/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/command/CommandOutput.java b/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/command/CommandOutput.java
new file mode 100644
index 0000000000..896833a0c3
--- /dev/null
+++ b/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/command/CommandOutput.java
@@ -0,0 +1,66 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.common.lib.command;
+
+public class CommandOutput extends CommandTO {
+
+    private static final long serialVersionUID = 7711356516501958110L;
+
+    public static class Builder extends CommandTO.Builder {
+
+        public Builder(final String key) {
+            super(key);
+        }
+
+        public Builder(final CommandTO commandTO) {
+            super(commandTO.getKey());
+            args(commandTO.getArgs());
+        }
+
+        @Override
+        protected CommandOutput newInstance() {
+            return new CommandOutput();
+        }
+
+        @Override
+        public Builder args(final CommandArgs args) {
+            return (Builder) super.args(args);
+        }
+
+        public Builder output(final String output) {
+            ((CommandOutput) getInstance()).setOutput(output);
+            return this;
+        }
+
+        @Override
+        public CommandOutput build() {
+            return (CommandOutput) super.build();
+        }
+    }
+
+    private String output;
+
+    public String getOutput() {
+        return output;
+    }
+
+    public void setOutput(final String output) {
+        this.output = output;
+    }
+}
diff --git a/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/command/CommandTO.java b/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/command/CommandTO.java
new file mode 100644
index 0000000000..c9bd8d1206
--- /dev/null
+++ b/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/command/CommandTO.java
@@ -0,0 +1,77 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.common.lib.command;
+
+import javax.ws.rs.PathParam;
+import org.apache.syncope.common.lib.BaseBean;
+
+public class CommandTO implements BaseBean {
+
+    private static final long serialVersionUID = 7711356516501958110L;
+
+    public static class Builder {
+
+        protected CommandTO instance;
+
+        public Builder(final String key) {
+            getInstance().setKey(key);
+        }
+
+        protected CommandTO newInstance() {
+            return new CommandTO();
+        }
+
+        protected final CommandTO getInstance() {
+            if (instance == null) {
+                instance = newInstance();
+            }
+            return instance;
+        }
+
+        public Builder args(final CommandArgs args) {
+            getInstance().setArgs(args);
+            return this;
+        }
+
+        public CommandTO build() {
+            return getInstance();
+        }
+    }
+
+    private String key;
+
+    private CommandArgs args;
+
+    public String getKey() {
+        return key;
+    }
+
+    @PathParam("key")
+    public void setKey(final String key) {
+        this.key = key;
+    }
+
+    public CommandArgs getArgs() {
+        return args;
+    }
+
+    public void setArgs(final CommandArgs args) {
+        this.args = args;
+    }
+}
diff --git a/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/to/MacroTaskTO.java b/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/to/MacroTaskTO.java
new file mode 100644
index 0000000000..01bf44eb6f
--- /dev/null
+++ b/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/to/MacroTaskTO.java
@@ -0,0 +1,113 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.common.lib.to;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
+import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.commons.lang3.builder.EqualsBuilder;
+import org.apache.commons.lang3.builder.HashCodeBuilder;
+import org.apache.syncope.common.lib.command.CommandTO;
+
+@Schema(allOf = { SchedTaskTO.class }, discriminatorProperty = "_class")
+public class MacroTaskTO extends SchedTaskTO {
+
+    private static final long serialVersionUID = -2387363212408909094L;
+
+    private String realm;
+
+    private final List<CommandTO> commands = new ArrayList<>();
+
+    private boolean continueOnError;
+
+    private boolean saveExecs = true;
+
+    @JacksonXmlProperty(localName = "_class", isAttribute = true)
+    @JsonProperty("_class")
+    @Schema(name = "_class", required = true, example = "org.apache.syncope.common.lib.to.MacroTaskTO")
+    @Override
+    public String getDiscriminator() {
+        return getClass().getName();
+    }
+
+    public String getRealm() {
+        return realm;
+    }
+
+    public void setRealm(final String realm) {
+        this.realm = realm;
+    }
+
+    @JacksonXmlElementWrapper(localName = "commands")
+    @JacksonXmlProperty(localName = "command")
+    public List<CommandTO> getCommands() {
+        return commands;
+    }
+
+    public boolean isContinueOnError() {
+        return continueOnError;
+    }
+
+    public void setContinueOnError(final boolean continueOnError) {
+        this.continueOnError = continueOnError;
+    }
+
+    public boolean isSaveExecs() {
+        return saveExecs;
+    }
+
+    public void setSaveExecs(final boolean saveExecs) {
+        this.saveExecs = saveExecs;
+    }
+
+    @Override
+    public int hashCode() {
+        return new HashCodeBuilder().
+                appendSuper(super.hashCode()).
+                append(realm).
+                append(commands).
+                append(continueOnError).
+                append(saveExecs).
+                build();
+    }
+
+    @Override
+    public boolean equals(final Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        final MacroTaskTO other = (MacroTaskTO) obj;
+        return new EqualsBuilder().
+                appendSuper(super.equals(obj)).
+                append(realm, other.realm).
+                append(commands, other.commands).
+                append(continueOnError, other.continueOnError).
+                append(saveExecs, other.saveExecs).
+                build();
+    }
+}
diff --git a/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/to/SchedTaskTO.java b/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/to/SchedTaskTO.java
index 49a854955d..054d6531dd 100644
--- a/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/to/SchedTaskTO.java
+++ b/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/to/SchedTaskTO.java
@@ -25,7 +25,9 @@ import java.time.OffsetDateTime;
 import org.apache.commons.lang3.builder.EqualsBuilder;
 import org.apache.commons.lang3.builder.HashCodeBuilder;
 
-@Schema(allOf = { TaskTO.class }, subTypes = { ProvisioningTaskTO.class }, discriminatorProperty = "_class")
+@Schema(allOf = { TaskTO.class },
+        subTypes = { ProvisioningTaskTO.class, MacroTaskTO.class },
+        discriminatorProperty = "_class")
 public class SchedTaskTO extends TaskTO implements NamedEntityTO {
 
     private static final long serialVersionUID = -5722284116974636425L;
diff --git a/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/types/ClientExceptionType.java b/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/types/ClientExceptionType.java
index 8dc10d55f6..555488be6f 100644
--- a/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/types/ClientExceptionType.java
+++ b/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/types/ClientExceptionType.java
@@ -22,7 +22,6 @@ import javax.ws.rs.core.Response;
 
 public enum ClientExceptionType {
 
-    AssociatedAnys(Response.Status.BAD_REQUEST),
     AssociatedResources(Response.Status.BAD_REQUEST),
     Composite(Response.Status.BAD_REQUEST),
     ConcurrentModification(Response.Status.PRECONDITION_FAILED),
@@ -70,6 +69,7 @@ public enum ClientExceptionType {
     InvalidRequest(Response.Status.BAD_REQUEST),
     InvalidValues(Response.Status.BAD_REQUEST),
     NotFound(Response.Status.NOT_FOUND),
+    RealmContains(Response.Status.BAD_REQUEST),
     RequiredValuesMissing(Response.Status.BAD_REQUEST),
     RESTValidation(Response.Status.BAD_REQUEST),
     GroupOwnership(Response.Status.BAD_REQUEST),
@@ -77,6 +77,7 @@ public enum ClientExceptionType {
     Scheduling(Response.Status.BAD_REQUEST),
     DelegatedAdministration(Response.Status.FORBIDDEN),
     Reconciliation(Response.Status.BAD_REQUEST),
+    RunError(Response.Status.INTERNAL_SERVER_ERROR),
     Unknown(Response.Status.BAD_REQUEST),
     Workflow(Response.Status.BAD_REQUEST);
 
diff --git a/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/types/IdRepoEntitlement.java b/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/types/IdRepoEntitlement.java
index 3fe99d4526..ed84d42e6d 100644
--- a/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/types/IdRepoEntitlement.java
+++ b/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/types/IdRepoEntitlement.java
@@ -240,6 +240,8 @@ public final class IdRepoEntitlement {
 
     public static final String DELEGATION_DELETE = "DELEGATION_DELETE";
 
+    public static final String COMMAND_RUN = "COMMAND_RUN";
+
     public static final String LOGGER_LIST = "LOGGER_LIST";
 
     public static final String LOGGER_UPDATE = "LOGGER_UPDATE";
diff --git a/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/types/IdRepoImplementationType.java b/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/types/IdRepoImplementationType.java
index 5ffaabf9d0..21df4a1b7e 100644
--- a/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/types/IdRepoImplementationType.java
+++ b/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/types/IdRepoImplementationType.java
@@ -37,6 +37,8 @@ public final class IdRepoImplementationType {
 
     public static final String VALIDATOR = "VALIDATOR";
 
+    public static final String COMMAND = "COMMAND";
+
     public static final String RECIPIENTS_PROVIDER = "RECIPIENTS_PROVIDER";
 
     public static final String AUDIT_APPENDER = "AUDIT_APPENDER";
@@ -49,8 +51,9 @@ public final class IdRepoImplementationType {
             Pair.of(ACCOUNT_RULE, "org.apache.syncope.core.persistence.api.dao.AccountRule"),
             Pair.of(PASSWORD_RULE, "org.apache.syncope.core.persistence.api.dao.PasswordRule"),
             Pair.of(TASKJOB_DELEGATE, "org.apache.syncope.core.provisioning.api.job.SchedTaskJobDelegate"),
-            Pair.of(LOGIC_ACTIONS, "org.apache.syncope.core.provisioning.api.LogicActions"),
+            Pair.of(LOGIC_ACTIONS, "org.apache.syncope.core.logic.api.LogicActions"),
             Pair.of(VALIDATOR, "org.apache.syncope.core.persistence.api.attrvalue.validation.PlainAttrValueValidator"),
+            Pair.of(COMMAND, "org.apache.syncope.core.logic.api.Command"),
             Pair.of(RECIPIENTS_PROVIDER, "org.apache.syncope.core.provisioning.api.notification.RecipientsProvider"),
             Pair.of(AUDIT_APPENDER, "org.apache.syncope.core.logic.audit.AuditAppender"),
             Pair.of(ITEM_TRANSFORMER, "org.apache.syncope.core.provisioning.api.data.ItemTransformer"));
diff --git a/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/types/TaskType.java b/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/types/TaskType.java
index 97d79df00a..2fc7efd8b5 100644
--- a/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/types/TaskType.java
+++ b/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/types/TaskType.java
@@ -18,6 +18,7 @@
  */
 package org.apache.syncope.common.lib.types;
 
+import org.apache.syncope.common.lib.to.MacroTaskTO;
 import org.apache.syncope.common.lib.to.NotificationTaskTO;
 import org.apache.syncope.common.lib.to.PropagationTaskTO;
 import org.apache.syncope.common.lib.to.PullTaskTO;
@@ -31,7 +32,8 @@ public enum TaskType {
     NOTIFICATION(NotificationTaskTO.class),
     SCHEDULED(SchedTaskTO.class),
     PULL(PullTaskTO.class),
-    PUSH(PushTaskTO.class);
+    PUSH(PushTaskTO.class),
+    MACRO(MacroTaskTO.class);
 
     private final Class<? extends TaskTO> toClass;
 
@@ -52,6 +54,8 @@ public enum TaskType {
                 ? TaskType.NOTIFICATION
                 : PropagationTaskTO.class.isAssignableFrom(clazz)
                 ? TaskType.PROPAGATION
+                : MacroTaskTO.class.isAssignableFrom(clazz)
+                ? TaskType.MACRO
                 : TaskType.SCHEDULED;
     }
 }
diff --git a/common/idrepo/rest-api/src/main/java/org/apache/syncope/common/rest/api/beans/CommandQuery.java b/common/idrepo/rest-api/src/main/java/org/apache/syncope/common/rest/api/beans/CommandQuery.java
new file mode 100644
index 0000000000..a588b3d2a0
--- /dev/null
+++ b/common/idrepo/rest-api/src/main/java/org/apache/syncope/common/rest/api/beans/CommandQuery.java
@@ -0,0 +1,55 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.common.rest.api.beans;
+
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.media.Schema;
+import javax.ws.rs.QueryParam;
+import org.apache.syncope.common.rest.api.service.JAXRSService;
+
+public class CommandQuery extends AbstractQuery {
+
+    private static final long serialVersionUID = -8792519310029596796L;
+
+    public static class Builder extends AbstractQuery.Builder<CommandQuery, Builder> {
+
+        @Override
+        protected CommandQuery newInstance() {
+            return new CommandQuery();
+        }
+
+        public Builder keyword(final String keyword) {
+            getInstance().setKeyword(keyword);
+            return this;
+        }
+    }
+
+    private String keyword;
+
+    @Parameter(name = JAXRSService.PARAM_KEYWORD, description = "keyword to match", schema =
+            @Schema(implementation = String.class))
+    public String getKeyword() {
+        return keyword;
+    }
+
+    @QueryParam(JAXRSService.PARAM_KEYWORD)
+    public void setKeyword(final String keyword) {
+        this.keyword = keyword;
+    }
+}
diff --git a/common/idrepo/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/CommandService.java b/common/idrepo/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/CommandService.java
new file mode 100644
index 0000000000..6fbef8a7b7
--- /dev/null
+++ b/common/idrepo/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/CommandService.java
@@ -0,0 +1,80 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.common.rest.api.service;
+
+import io.swagger.v3.oas.annotations.security.SecurityRequirement;
+import io.swagger.v3.oas.annotations.security.SecurityRequirements;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import javax.ws.rs.BeanParam;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+import org.apache.syncope.common.lib.command.CommandOutput;
+import org.apache.syncope.common.lib.command.CommandTO;
+import org.apache.syncope.common.lib.to.PagedResult;
+import org.apache.syncope.common.rest.api.RESTHeaders;
+import org.apache.syncope.common.rest.api.beans.CommandQuery;
+
+/**
+ * REST operations for commands.
+ */
+@Tag(name = "Commands")
+@SecurityRequirements({
+    @SecurityRequirement(name = "BasicAuthentication"),
+    @SecurityRequirement(name = "Bearer") })
+@Path("commands")
+public interface CommandService extends JAXRSService {
+
+    /**
+     * Returns a paged list of all commands.
+     *
+     * @param query query conditions
+     * @return list of all commands.
+     */
+    @GET
+    @Produces({ MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML, MediaType.APPLICATION_XML })
+    PagedResult<CommandTO> search(@BeanParam CommandQuery query);
+
+    /**
+     * Returns the command for the given key, if found.
+     *
+     * @param key command key
+     * @return the command for the given key, if found
+     */
+    @GET
+    @Path("{key}")
+    @Produces({ MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML, MediaType.APPLICATION_XML })
+    CommandTO read(@PathParam("key") String key);
+
+    /**
+     * Runs the given command with the given arguments and returns the resulting output.
+     *
+     * @param command command to run, with arguments
+     * @return command output
+     */
+    @POST
+    @Path("{key}")
+    @Consumes({ MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML, MediaType.APPLICATION_XML })
+    @Produces({ MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML, MediaType.APPLICATION_XML })
+    CommandOutput run(CommandTO command);
+}
diff --git a/common/idrepo/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/JAXRSService.java b/common/idrepo/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/JAXRSService.java
index 51a59cc576..a947ddbe71 100644
--- a/common/idrepo/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/JAXRSService.java
+++ b/common/idrepo/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/JAXRSService.java
@@ -28,6 +28,8 @@ public interface JAXRSService {
 
     String PARAM_ORDERBY = "orderby";
 
+    String PARAM_KEYWORD = "keyword";
+
     String PARAM_RESOURCE = "resource";
 
     String PARAM_NOTIFICATION = "notification";
diff --git a/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/AbstractAnyLogic.java b/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/AbstractAnyLogic.java
index 813c7e7301..d0776080a3 100644
--- a/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/AbstractAnyLogic.java
+++ b/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/AbstractAnyLogic.java
@@ -33,13 +33,13 @@ import org.apache.syncope.common.lib.to.AnyTO;
 import org.apache.syncope.common.lib.to.PropagationStatus;
 import org.apache.syncope.common.lib.to.ProvisioningResult;
 import org.apache.syncope.common.lib.types.ClientExceptionType;
+import org.apache.syncope.core.logic.api.LogicActions;
 import org.apache.syncope.core.persistence.api.dao.AnyTypeDAO;
 import org.apache.syncope.core.persistence.api.dao.RealmDAO;
 import org.apache.syncope.core.persistence.api.dao.search.OrderByClause;
 import org.apache.syncope.core.persistence.api.dao.search.SearchCond;
 import org.apache.syncope.core.persistence.api.entity.AnyType;
 import org.apache.syncope.core.persistence.api.entity.Realm;
-import org.apache.syncope.core.provisioning.api.LogicActions;
 import org.apache.syncope.core.provisioning.java.utils.TemplateUtils;
 import org.apache.syncope.core.spring.implementation.ImplementationManager;
 
diff --git a/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/AnyObjectLogic.java b/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/AnyObjectLogic.java
index 509821e117..cfcae26dd8 100644
--- a/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/AnyObjectLogic.java
+++ b/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/AnyObjectLogic.java
@@ -40,6 +40,7 @@ import org.apache.syncope.common.lib.types.AnyEntitlement;
 import org.apache.syncope.common.lib.types.AnyTypeKind;
 import org.apache.syncope.common.lib.types.ClientExceptionType;
 import org.apache.syncope.common.lib.types.PatchOperation;
+import org.apache.syncope.core.logic.api.LogicActions;
 import org.apache.syncope.core.persistence.api.dao.AnyObjectDAO;
 import org.apache.syncope.core.persistence.api.dao.AnySearchDAO;
 import org.apache.syncope.core.persistence.api.dao.AnyTypeDAO;
@@ -51,7 +52,6 @@ import org.apache.syncope.core.persistence.api.entity.AnyType;
 import org.apache.syncope.core.persistence.api.entity.Realm;
 import org.apache.syncope.core.persistence.api.entity.anyobject.AnyObject;
 import org.apache.syncope.core.provisioning.api.AnyObjectProvisioningManager;
-import org.apache.syncope.core.provisioning.api.LogicActions;
 import org.apache.syncope.core.provisioning.api.data.AnyObjectDataBinder;
 import org.apache.syncope.core.provisioning.api.utils.RealmUtils;
 import org.apache.syncope.core.provisioning.java.utils.TemplateUtils;
diff --git a/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/CommandLogic.java b/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/CommandLogic.java
new file mode 100644
index 0000000000..907a343e12
--- /dev/null
+++ b/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/CommandLogic.java
@@ -0,0 +1,130 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.core.logic;
+
+import java.lang.reflect.Method;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.stream.Collectors;
+import org.apache.commons.lang3.tuple.Pair;
+import org.apache.syncope.common.lib.SyncopeClientException;
+import org.apache.syncope.common.lib.command.CommandArgs;
+import org.apache.syncope.common.lib.command.CommandTO;
+import org.apache.syncope.common.lib.to.EntityTO;
+import org.apache.syncope.common.lib.types.ClientExceptionType;
+import org.apache.syncope.common.lib.types.IdRepoEntitlement;
+import org.apache.syncope.common.lib.types.IdRepoImplementationType;
+import org.apache.syncope.core.logic.api.Command;
+import org.apache.syncope.core.persistence.api.dao.ImplementationDAO;
+import org.apache.syncope.core.persistence.api.dao.NotFoundException;
+import org.apache.syncope.core.persistence.api.entity.Implementation;
+import org.apache.syncope.core.spring.implementation.ImplementationManager;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.transaction.annotation.Transactional;
+
+public class CommandLogic extends AbstractLogic<EntityTO> {
+
+    protected final ImplementationDAO implementationDAO;
+
+    protected final Map<String, Command<?>> perContextCommands = new ConcurrentHashMap<>();
+
+    public CommandLogic(final ImplementationDAO implementationDAO) {
+        this.implementationDAO = implementationDAO;
+    }
+
+    @PreAuthorize("hasRole('" + IdRepoEntitlement.IMPLEMENTATION_LIST + "')")
+    @Transactional(readOnly = true)
+    public Pair<Integer, List<CommandTO>> search(final int page, final int size, final String keyword) {
+        List<Implementation> result = implementationDAO.findByTypeAndKeyword(IdRepoImplementationType.COMMAND, keyword);
+
+        int count = result.size();
+
+        List<CommandTO> commands = result.stream().
+                skip((page - 1) * size).
+                limit(size).
+                map(command -> {
+                    try {
+                        return new CommandTO.Builder(command.getKey()).
+                                args(ImplementationManager.emptyArgs(command)).build();
+                    } catch (Exception e) {
+                        LOG.error("Could not get arg class for {}", command, e);
+                        return null;
+                    }
+                }).
+                filter(Objects::nonNull).
+                collect(Collectors.toList());
+
+        return Pair.of(count, commands);
+    }
+
+    @PreAuthorize("hasRole('" + IdRepoEntitlement.IMPLEMENTATION_READ + "')")
+    @Transactional(readOnly = true)
+    public CommandTO read(final String key) {
+        Implementation impl = Optional.ofNullable(implementationDAO.find(key)).
+                orElseThrow(() -> new NotFoundException("Implementation " + key));
+
+        try {
+            return new CommandTO.Builder(impl.getKey()).
+                    args(ImplementationManager.emptyArgs(impl)).build();
+        } catch (Exception e) {
+            SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.InvalidImplementation);
+            sce.getElements().add("Could not build " + impl.getKey());
+            throw sce;
+        }
+    }
+
+    @PreAuthorize("hasRole('" + IdRepoEntitlement.COMMAND_RUN + "')")
+    @SuppressWarnings("unchecked")
+    public String run(final CommandTO command) {
+        Implementation impl = Optional.ofNullable(implementationDAO.find(command.getKey())).
+                orElseThrow(() -> new NotFoundException("Implementation " + command.getKey()));
+
+        Command<CommandArgs> runnable;
+        try {
+            runnable = (Command<CommandArgs>) ImplementationManager.build(
+                    impl,
+                    () -> perContextCommands.get(impl.getKey()),
+                    instance -> perContextCommands.put(impl.getKey(), instance));
+        } catch (Exception e) {
+            SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.InvalidImplementation);
+            sce.getElements().add("Could not build " + impl.getKey());
+            throw sce;
+        }
+
+        try {
+            return runnable.run(command.getArgs());
+        } catch (Exception e) {
+            LOG.error("While running {} on {}", command.getKey(), command.getArgs(), e);
+
+            SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.RunError);
+            sce.getElements().add(e.getMessage());
+            throw sce;
+        }
+    }
+
+    @Override
+    protected EntityTO resolveReference(final Method method, final Object... args)
+            throws UnresolvedReferenceException {
+
+        throw new UnsupportedOperationException();
+    }
+}
diff --git a/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/GroupLogic.java b/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/GroupLogic.java
index 4891d5f0d4..52410edd51 100644
--- a/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/GroupLogic.java
+++ b/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/GroupLogic.java
@@ -45,6 +45,7 @@ import org.apache.syncope.common.lib.types.ImplementationEngine;
 import org.apache.syncope.common.lib.types.JobType;
 import org.apache.syncope.common.lib.types.PatchOperation;
 import org.apache.syncope.common.lib.types.ProvisionAction;
+import org.apache.syncope.core.logic.api.LogicActions;
 import org.apache.syncope.core.persistence.api.dao.AnySearchDAO;
 import org.apache.syncope.core.persistence.api.dao.AnyTypeDAO;
 import org.apache.syncope.core.persistence.api.dao.GroupDAO;
@@ -61,7 +62,6 @@ import org.apache.syncope.core.persistence.api.entity.Realm;
 import org.apache.syncope.core.persistence.api.entity.group.Group;
 import org.apache.syncope.core.persistence.api.entity.task.SchedTask;
 import org.apache.syncope.core.provisioning.api.GroupProvisioningManager;
-import org.apache.syncope.core.provisioning.api.LogicActions;
 import org.apache.syncope.core.provisioning.api.data.GroupDataBinder;
 import org.apache.syncope.core.provisioning.api.data.TaskDataBinder;
 import org.apache.syncope.core.provisioning.api.job.JobManager;
diff --git a/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/IdRepoLogicContext.java b/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/IdRepoLogicContext.java
index 1c38c68d3e..9a4e62889a 100644
--- a/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/IdRepoLogicContext.java
+++ b/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/IdRepoLogicContext.java
@@ -34,6 +34,7 @@ import org.apache.syncope.core.persistence.api.dao.AnyTypeClassDAO;
 import org.apache.syncope.core.persistence.api.dao.AnyTypeDAO;
 import org.apache.syncope.core.persistence.api.dao.ApplicationDAO;
 import org.apache.syncope.core.persistence.api.dao.AuditConfDAO;
+import org.apache.syncope.core.persistence.api.dao.CASSPClientAppDAO;
 import org.apache.syncope.core.persistence.api.dao.DelegationDAO;
 import org.apache.syncope.core.persistence.api.dao.DerSchemaDAO;
 import org.apache.syncope.core.persistence.api.dao.DynRealmDAO;
@@ -43,6 +44,7 @@ import org.apache.syncope.core.persistence.api.dao.GroupDAO;
 import org.apache.syncope.core.persistence.api.dao.ImplementationDAO;
 import org.apache.syncope.core.persistence.api.dao.MailTemplateDAO;
 import org.apache.syncope.core.persistence.api.dao.NotificationDAO;
+import org.apache.syncope.core.persistence.api.dao.OIDCRPClientAppDAO;
 import org.apache.syncope.core.persistence.api.dao.PlainSchemaDAO;
 import org.apache.syncope.core.persistence.api.dao.PolicyDAO;
 import org.apache.syncope.core.persistence.api.dao.RealmDAO;
@@ -51,6 +53,7 @@ import org.apache.syncope.core.persistence.api.dao.ReportDAO;
 import org.apache.syncope.core.persistence.api.dao.ReportExecDAO;
 import org.apache.syncope.core.persistence.api.dao.ReportTemplateDAO;
 import org.apache.syncope.core.persistence.api.dao.RoleDAO;
+import org.apache.syncope.core.persistence.api.dao.SAML2SPClientAppDAO;
 import org.apache.syncope.core.persistence.api.dao.SecurityQuestionDAO;
 import org.apache.syncope.core.persistence.api.dao.TaskDAO;
 import org.apache.syncope.core.persistence.api.dao.TaskExecDAO;
@@ -233,6 +236,12 @@ public class IdRepoLogicContext {
                 auditManager);
     }
 
+    @ConditionalOnMissingBean
+    @Bean
+    public CommandLogic commandLogic(final ImplementationDAO implementationDAO) {
+        return new CommandLogic(implementationDAO);
+    }
+
     @ConditionalOnMissingBean
     @Bean
     public FIQLQueryLogic fiqlQueryLogic(
@@ -363,10 +372,23 @@ public class IdRepoLogicContext {
             final RealmDataBinder binder,
             final RealmDAO realmDAO,
             final AnySearchDAO anySearchDAO,
+            final TaskDAO taskDAO,
+            final CASSPClientAppDAO casSPClientAppDAO,
+            final OIDCRPClientAppDAO oidcRPClientAppDAO,
+            final SAML2SPClientAppDAO saml2SPClientAppDAO,
             final PropagationManager propagationManager,
             final PropagationTaskExecutor taskExecutor) {
 
-        return new RealmLogic(realmDAO, anySearchDAO, binder, propagationManager, taskExecutor);
+        return new RealmLogic(
+                realmDAO,
+                anySearchDAO,
+                taskDAO,
+                casSPClientAppDAO,
+                oidcRPClientAppDAO,
+                saml2SPClientAppDAO,
+                binder,
+                propagationManager,
+                taskExecutor);
     }
 
     @ConditionalOnMissingBean
diff --git a/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/ImplementationLogic.java b/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/ImplementationLogic.java
index 487bb94c42..09853e398f 100644
--- a/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/ImplementationLogic.java
+++ b/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/ImplementationLogic.java
@@ -197,6 +197,10 @@ public class ImplementationLogic extends AbstractTransactionalLogic<Implementati
                 inUse = !taskDAO.findByDelegate(implementation).isEmpty();
                 break;
 
+            case IdRepoImplementationType.COMMAND:
+                inUse = !taskDAO.findByCommand(implementation).isEmpty();
+                break;
+
             case IdMImplementationType.RECON_FILTER_BUILDER:
                 inUse = !taskDAO.findByReconFilterBuilder(implementation).isEmpty();
                 break;
diff --git a/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/RealmLogic.java b/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/RealmLogic.java
index 966f9abb78..cce7a068fc 100644
--- a/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/RealmLogic.java
+++ b/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/RealmLogic.java
@@ -36,9 +36,13 @@ import org.apache.syncope.common.lib.types.ClientExceptionType;
 import org.apache.syncope.common.lib.types.IdRepoEntitlement;
 import org.apache.syncope.common.lib.types.ResourceOperation;
 import org.apache.syncope.core.persistence.api.dao.AnySearchDAO;
+import org.apache.syncope.core.persistence.api.dao.CASSPClientAppDAO;
 import org.apache.syncope.core.persistence.api.dao.DuplicateException;
 import org.apache.syncope.core.persistence.api.dao.NotFoundException;
+import org.apache.syncope.core.persistence.api.dao.OIDCRPClientAppDAO;
 import org.apache.syncope.core.persistence.api.dao.RealmDAO;
+import org.apache.syncope.core.persistence.api.dao.SAML2SPClientAppDAO;
+import org.apache.syncope.core.persistence.api.dao.TaskDAO;
 import org.apache.syncope.core.persistence.api.dao.search.AnyCond;
 import org.apache.syncope.core.persistence.api.dao.search.AttrCond;
 import org.apache.syncope.core.persistence.api.dao.search.SearchCond;
@@ -60,6 +64,14 @@ public class RealmLogic extends AbstractTransactionalLogic<RealmTO> {
 
     protected final AnySearchDAO searchDAO;
 
+    protected final TaskDAO taskDAO;
+
+    protected final CASSPClientAppDAO casSPClientAppDAO;
+
+    protected final OIDCRPClientAppDAO oidcRPClientAppDAO;
+
+    protected final SAML2SPClientAppDAO saml2SPClientAppDAO;
+
     protected final RealmDataBinder binder;
 
     protected final PropagationManager propagationManager;
@@ -69,12 +81,20 @@ public class RealmLogic extends AbstractTransactionalLogic<RealmTO> {
     public RealmLogic(
             final RealmDAO realmDAO,
             final AnySearchDAO searchDAO,
+            final TaskDAO taskDAO,
+            final CASSPClientAppDAO casSPClientAppDAO,
+            final OIDCRPClientAppDAO oidcRPClientAppDAO,
+            final SAML2SPClientAppDAO saml2SPClientAppDAO,
             final RealmDataBinder binder,
             final PropagationManager propagationManager,
             final PropagationTaskExecutor taskExecutor) {
 
         this.realmDAO = realmDAO;
         this.searchDAO = searchDAO;
+        this.taskDAO = taskDAO;
+        this.casSPClientAppDAO = casSPClientAppDAO;
+        this.oidcRPClientAppDAO = oidcRPClientAppDAO;
+        this.saml2SPClientAppDAO = saml2SPClientAppDAO;
         this.binder = binder;
         this.propagationManager = propagationManager;
         this.taskExecutor = taskExecutor;
@@ -212,14 +232,21 @@ public class RealmLogic extends AbstractTransactionalLogic<RealmTO> {
         int users = searchDAO.count(realm, true, adminRealms, allMatchingCond, AnyTypeKind.USER);
         int groups = searchDAO.count(realm, true, adminRealms, allMatchingCond, AnyTypeKind.GROUP);
         int anyObjects = searchDAO.count(realm, true, adminRealms, allMatchingCond, AnyTypeKind.ANY_OBJECT);
-
-        if (users + groups + anyObjects > 0) {
-            SyncopeClientException containedAnys = SyncopeClientException.build(ClientExceptionType.AssociatedAnys);
-            containedAnys.getElements().add(users + " user(s)");
-            containedAnys.getElements().add(groups + " group(s)");
-            containedAnys.getElements().add(anyObjects + " anyObject(s)");
-            throw containedAnys;
+        int macroTasks = taskDAO.findByRealm(realm).size();
+        int clientApps = casSPClientAppDAO.findByRealm(realm).size()
+                + saml2SPClientAppDAO.findByRealm(realm).size()
+                + oidcRPClientAppDAO.findByRealm(realm).size();
+
+        if (users + groups + anyObjects + macroTasks + clientApps > 0) {
+            SyncopeClientException realmContains = SyncopeClientException.build(ClientExceptionType.RealmContains);
+            realmContains.getElements().add(users + " user(s)");
+            realmContains.getElements().add(groups + " group(s)");
+            realmContains.getElements().add(anyObjects + " anyObject(s)");
+            realmContains.getElements().add(macroTasks + " command task(s)");
+            realmContains.getElements().add(clientApps + " client app(s)");
+            throw realmContains;
         }
+
         PropagationByResource<String> propByRes = new PropagationByResource<>();
         propByRes.addAll(ResourceOperation.DELETE, realm.getResourceKeys());
         List<PropagationTaskInfo> taskInfos = propagationManager.createTasks(realm, propByRes, null);
diff --git a/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/TaskLogic.java b/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/TaskLogic.java
index 91a1d4932c..9eb7051e5f 100644
--- a/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/TaskLogic.java
+++ b/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/TaskLogic.java
@@ -25,6 +25,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Optional;
+import java.util.Set;
 import java.util.stream.Collectors;
 import javax.ws.rs.core.Response;
 import org.apache.commons.lang3.ArrayUtils;
@@ -34,6 +35,7 @@ import org.apache.syncope.common.keymaster.client.api.ConfParamOps;
 import org.apache.syncope.common.lib.SyncopeClientException;
 import org.apache.syncope.common.lib.to.ExecTO;
 import org.apache.syncope.common.lib.to.JobTO;
+import org.apache.syncope.common.lib.to.MacroTaskTO;
 import org.apache.syncope.common.lib.to.PropagationTaskTO;
 import org.apache.syncope.common.lib.to.SchedTaskTO;
 import org.apache.syncope.common.lib.to.TaskTO;
@@ -52,6 +54,9 @@ import org.apache.syncope.core.persistence.api.dao.NotificationDAO;
 import org.apache.syncope.core.persistence.api.dao.TaskDAO;
 import org.apache.syncope.core.persistence.api.dao.TaskExecDAO;
 import org.apache.syncope.core.persistence.api.dao.search.OrderByClause;
+import org.apache.syncope.core.persistence.api.entity.ExternalResource;
+import org.apache.syncope.core.persistence.api.entity.Notification;
+import org.apache.syncope.core.persistence.api.entity.task.MacroTask;
 import org.apache.syncope.core.persistence.api.entity.task.NotificationTask;
 import org.apache.syncope.core.persistence.api.entity.task.PropagationTask;
 import org.apache.syncope.core.persistence.api.entity.task.SchedTask;
@@ -69,6 +74,7 @@ import org.apache.syncope.core.provisioning.api.utils.ExceptionUtils2;
 import org.apache.syncope.core.provisioning.java.job.TaskJob;
 import org.apache.syncope.core.provisioning.java.propagation.DefaultPropagationReporter;
 import org.apache.syncope.core.spring.security.AuthContextUtils;
+import org.apache.syncope.core.spring.security.DelegatedAdministrationException;
 import org.identityconnectors.framework.common.objects.ObjectClass;
 import org.quartz.JobDataMap;
 import org.quartz.JobKey;
@@ -124,6 +130,13 @@ public class TaskLogic extends AbstractExecutableLogic<TaskTO> {
         this.taskUtilsFactory = taskUtilsFactory;
     }
 
+    protected void securityChecks(final String entitlement, final String realm) {
+        Set<String> authRealms = AuthContextUtils.getAuthorizations().get(entitlement);
+        if (authRealms.stream().noneMatch(r -> realm.startsWith(r))) {
+            throw new DelegatedAdministrationException(realm, MacroTask.class.getSimpleName(), null);
+        }
+    }
+
     @PreAuthorize("hasRole('" + IdRepoEntitlement.TASK_CREATE + "')")
     public <T extends SchedTaskTO> T createSchedTask(final TaskType type, final T taskTO) {
         TaskUtils taskUtils = taskUtilsFactory.getInstance(taskTO);
@@ -132,6 +145,11 @@ public class TaskLogic extends AbstractExecutableLogic<TaskTO> {
             sce.getElements().add("Found " + type + ", expected " + taskUtils.getType());
             throw sce;
         }
+
+        if (taskUtils.getType() == TaskType.MACRO) {
+            securityChecks(IdRepoEntitlement.TASK_CREATE, ((MacroTaskTO) taskTO).getRealm());
+        }
+
         SchedTask task = binder.createSchedTask(taskTO, taskUtils);
         task = taskDAO.save(task);
 
@@ -166,6 +184,11 @@ public class TaskLogic extends AbstractExecutableLogic<TaskTO> {
             throw sce;
         }
 
+        if (taskUtils.getType() == TaskType.MACRO) {
+            securityChecks(IdRepoEntitlement.TASK_UPDATE, ((MacroTask) task).getRealm().getFullPath());
+            securityChecks(IdRepoEntitlement.TASK_UPDATE, ((MacroTaskTO) taskTO).getRealm());
+        }
+
         binder.updateSchedTask(task, taskTO, taskUtils);
         task = taskDAO.save(task);
         try {
@@ -187,7 +210,6 @@ public class TaskLogic extends AbstractExecutableLogic<TaskTO> {
 
     @PreAuthorize("hasRole('" + IdRepoEntitlement.TASK_LIST + "')")
     @Transactional(readOnly = true)
-    @SuppressWarnings("unchecked")
     public <T extends TaskTO> Pair<Integer, List<T>> search(
             final TaskType type,
             final String resource,
@@ -204,12 +226,32 @@ public class TaskLogic extends AbstractExecutableLogic<TaskTO> {
                 throw new IllegalArgumentException("type is required");
             }
 
+            ExternalResource resourceObj = resourceDAO.find(resource);
+            if (resource != null && resourceObj == null) {
+                throw new IllegalArgumentException("Missing External Resource: " + resource);
+            }
+
+            Notification notificationObj = notificationDAO.find(notification);
+            if (notification != null && notificationObj == null) {
+                throw new IllegalArgumentException("Missing Notification: " + notification);
+            }
+
             int count = taskDAO.count(
-                    type, resourceDAO.find(resource), notificationDAO.find(notification), anyTypeKind, entityKey);
+                    type,
+                    resourceObj,
+                    notificationObj,
+                    anyTypeKind,
+                    entityKey);
 
             List<T> result = taskDAO.findAll(
-                    type, resourceDAO.find(resource), notificationDAO.find(notification), anyTypeKind, entityKey,
-                    page, size, orderByClauses).stream().
+                    type,
+                    resourceObj,
+                    notificationObj,
+                    anyTypeKind,
+                    entityKey,
+                    page,
+                    size,
+                    orderByClauses).stream().
                     <T>map(task -> binder.getTaskTO(task, taskUtilsFactory.getInstance(type), details)).
                     collect(Collectors.toList());
 
@@ -236,6 +278,10 @@ public class TaskLogic extends AbstractExecutableLogic<TaskTO> {
             throw sce;
         }
 
+        if (taskUtils.getType() == TaskType.MACRO) {
+            securityChecks(IdRepoEntitlement.TASK_READ, ((MacroTask) task).getRealm().getFullPath());
+        }
+
         return binder.getTaskTO(task, taskUtilsFactory.getInstance(task), details);
     }
 
@@ -249,11 +295,11 @@ public class TaskLogic extends AbstractExecutableLogic<TaskTO> {
             throw sce;
         }
 
-        TaskUtils taskUtil = taskUtilsFactory.getInstance(task);
+        TaskUtils taskUtils = taskUtilsFactory.getInstance(task);
         String executor = AuthContextUtils.getUsername();
 
         ExecTO result = null;
-        switch (taskUtil.getType()) {
+        switch (taskUtils.getType()) {
             case PROPAGATION:
                 PropagationTask propagationTask = (PropagationTask) task;
                 PropagationTaskInfo taskInfo = new PropagationTaskInfo(
@@ -281,6 +327,11 @@ public class TaskLogic extends AbstractExecutableLogic<TaskTO> {
             case SCHEDULED:
             case PULL:
             case PUSH:
+            case MACRO:
+                if (taskUtils.getType() == TaskType.MACRO) {
+                    securityChecks(IdRepoEntitlement.TASK_EXECUTE, ((MacroTask) task).getRealm().getFullPath());
+                }
+
                 if (!((SchedTask) task).isActive()) {
                     SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.Scheduling);
                     sce.getElements().add("Task " + key + " is not active");
@@ -337,6 +388,10 @@ public class TaskLogic extends AbstractExecutableLogic<TaskTO> {
             throw sce;
         }
 
+        if (taskUtils.getType() == TaskType.MACRO) {
+            securityChecks(IdRepoEntitlement.TASK_DELETE, ((MacroTask) task).getRealm().getFullPath());
+        }
+
         T taskToDelete = binder.getTaskTO(task, taskUtils, true);
 
         if (TaskType.SCHEDULED == taskUtils.getType()
@@ -357,10 +412,14 @@ public class TaskLogic extends AbstractExecutableLogic<TaskTO> {
 
         Task<?> task = taskDAO.find(key).orElseThrow(() -> new NotFoundException("Task " + key));
 
+        if (task instanceof MacroTask) {
+            securityChecks(IdRepoEntitlement.TASK_READ, ((MacroTask) task).getRealm().getFullPath());
+        }
+
         Integer count = taskExecDAO.count(task);
 
         List<ExecTO> result = taskExecDAO.findAll(task, page, size, orderByClauses).stream().
-                map(taskExec -> binder.getExecTO(taskExec)).collect(Collectors.toList());
+                map(exec -> binder.getExecTO(exec)).collect(Collectors.toList());
 
         return Pair.of(count, result);
     }
@@ -369,18 +428,36 @@ public class TaskLogic extends AbstractExecutableLogic<TaskTO> {
     @Override
     public List<ExecTO> listRecentExecutions(final int max) {
         return taskExecDAO.findRecent(max).stream().
-                map(taskExec -> binder.getExecTO(taskExec)).collect(Collectors.toList());
+                map(exec -> {
+                    try {
+                        if (exec.getTask() instanceof MacroTask) {
+                            securityChecks(IdRepoEntitlement.TASK_DELETE,
+                                    ((MacroTask) exec.getTask()).getRealm().getFullPath());
+                        }
+
+                        return binder.getExecTO(exec);
+                    } catch (DelegatedAdministrationException e) {
+                        LOG.error("Skip executions for command task", e);
+                        return null;
+                    }
+                }).
+                filter(Objects::nonNull).
+                collect(Collectors.toList());
     }
 
     @PreAuthorize("hasRole('" + IdRepoEntitlement.TASK_DELETE + "')")
     @Override
     public ExecTO deleteExecution(final String execKey) {
-        TaskExec<?> taskExec = taskExecDAO.find(execKey).
+        TaskExec<?> exec = taskExecDAO.find(execKey).
                 orElseThrow(() -> new NotFoundException("Task execution " + execKey));
 
-        ExecTO taskExecutionToDelete = binder.getExecTO(taskExec);
-        taskExecDAO.delete(taskExec);
-        return taskExecutionToDelete;
+        if (exec.getTask() instanceof MacroTask) {
+            securityChecks(IdRepoEntitlement.TASK_DELETE, ((MacroTask) exec.getTask()).getRealm().getFullPath());
+        }
+
+        ExecTO executionToDelete = binder.getExecTO(exec);
+        taskExecDAO.delete(exec);
+        return executionToDelete;
     }
 
     @PreAuthorize("hasRole('" + IdRepoEntitlement.TASK_DELETE + "')")
@@ -402,6 +479,11 @@ public class TaskLogic extends AbstractExecutableLogic<TaskTO> {
             batchResponseItems.add(item);
 
             try {
+                if (exec.getTask() instanceof MacroTask) {
+                    securityChecks(IdRepoEntitlement.TASK_DELETE,
+                            ((MacroTask) exec.getTask()).getRealm().getFullPath());
+                }
+
                 taskExecDAO.delete(exec);
                 item.setStatus(Response.Status.OK.getStatusCode());
             } catch (Exception e) {
@@ -435,6 +517,10 @@ public class TaskLogic extends AbstractExecutableLogic<TaskTO> {
     public JobTO getJob(final String key) {
         Task<?> task = taskDAO.find(key).orElseThrow(() -> new NotFoundException("Task " + key));
 
+        if (task instanceof MacroTask) {
+            securityChecks(IdRepoEntitlement.TASK_READ, ((MacroTask) task).getRealm().getFullPath());
+        }
+
         JobTO jobTO = null;
         try {
             jobTO = getJobTO(JobNamer.getJobKey(task), false);
@@ -456,6 +542,10 @@ public class TaskLogic extends AbstractExecutableLogic<TaskTO> {
     public void actionJob(final String key, final JobAction action) {
         Task<?> task = taskDAO.find(key).orElseThrow(() -> new NotFoundException("Task " + key));
 
+        if (task instanceof MacroTask) {
+            securityChecks(IdRepoEntitlement.TASK_EXECUTE, ((MacroTask) task).getRealm().getFullPath());
+        }
+
         doActionJob(JobNamer.getJobKey(task), action);
     }
 
diff --git a/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/UserLogic.java b/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/UserLogic.java
index 28fc62443d..023d5e21fc 100644
--- a/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/UserLogic.java
+++ b/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/UserLogic.java
@@ -45,6 +45,7 @@ import org.apache.syncope.common.lib.types.AnyTypeKind;
 import org.apache.syncope.common.lib.types.ClientExceptionType;
 import org.apache.syncope.common.lib.types.IdRepoEntitlement;
 import org.apache.syncope.common.lib.types.PatchOperation;
+import org.apache.syncope.core.logic.api.LogicActions;
 import org.apache.syncope.core.persistence.api.dao.AccessTokenDAO;
 import org.apache.syncope.core.persistence.api.dao.AnySearchDAO;
 import org.apache.syncope.core.persistence.api.dao.AnyTypeDAO;
@@ -59,7 +60,6 @@ import org.apache.syncope.core.persistence.api.entity.AccessToken;
 import org.apache.syncope.core.persistence.api.entity.Realm;
 import org.apache.syncope.core.persistence.api.entity.group.Group;
 import org.apache.syncope.core.persistence.api.entity.user.User;
-import org.apache.syncope.core.provisioning.api.LogicActions;
 import org.apache.syncope.core.provisioning.api.UserProvisioningManager;
 import org.apache.syncope.core.provisioning.api.data.UserDataBinder;
 import org.apache.syncope.core.provisioning.api.serialization.POJOHelper;
diff --git a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/task/TaskUtils.java b/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/api/Command.java
similarity index 67%
copy from core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/task/TaskUtils.java
copy to core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/api/Command.java
index e8f5c15d38..88047c89f5 100644
--- a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/task/TaskUtils.java
+++ b/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/api/Command.java
@@ -16,20 +16,12 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.syncope.core.persistence.api.entity.task;
+package org.apache.syncope.core.logic.api;
 
-import org.apache.syncope.common.lib.to.TaskTO;
-import org.apache.syncope.common.lib.types.TaskType;
+import org.apache.syncope.common.lib.command.CommandArgs;
 
-public interface TaskUtils {
+@FunctionalInterface
+public interface Command<A extends CommandArgs> {
 
-    TaskType getType();
-
-    <T extends Task<T>> T newTask();
-
-    <T extends TaskTO> T newTaskTO();
-
-    <T extends Task<T>> Class<T> taskClass();
-
-    <T extends TaskTO> Class<T> taskTOClass();
+    String run(A args);
 }
diff --git a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/LogicActions.java b/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/api/LogicActions.java
similarity index 97%
rename from core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/LogicActions.java
rename to core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/api/LogicActions.java
index 5c5b766a3f..7ace5cef63 100644
--- a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/LogicActions.java
+++ b/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/api/LogicActions.java
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.syncope.core.provisioning.api;
+package org.apache.syncope.core.logic.api;
 
 import java.util.List;
 import org.apache.syncope.common.lib.request.AnyCR;
diff --git a/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/init/ClassPathScanImplementationLookup.java b/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/init/ClassPathScanImplementationLookup.java
index a9e3194667..b1aa633cf8 100644
--- a/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/init/ClassPathScanImplementationLookup.java
+++ b/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/init/ClassPathScanImplementationLookup.java
@@ -33,6 +33,8 @@ import org.apache.syncope.common.lib.report.ReportletConf;
 import org.apache.syncope.common.lib.types.IdMImplementationType;
 import org.apache.syncope.common.lib.types.IdRepoImplementationType;
 import org.apache.syncope.common.lib.types.ImplementationTypesHolder;
+import org.apache.syncope.core.logic.api.Command;
+import org.apache.syncope.core.logic.api.LogicActions;
 import org.apache.syncope.core.logic.audit.AuditAppender;
 import org.apache.syncope.core.logic.audit.JdbcAuditAppender;
 import org.apache.syncope.core.persistence.api.ImplementationLookup;
@@ -47,7 +49,6 @@ import org.apache.syncope.core.persistence.api.dao.PushCorrelationRule;
 import org.apache.syncope.core.persistence.api.dao.PushCorrelationRuleConfClass;
 import org.apache.syncope.core.persistence.api.dao.Reportlet;
 import org.apache.syncope.core.persistence.api.dao.ReportletConfClass;
-import org.apache.syncope.core.provisioning.api.LogicActions;
 import org.apache.syncope.core.provisioning.api.ProvisionSorter;
 import org.apache.syncope.core.provisioning.api.data.ItemTransformer;
 import org.apache.syncope.core.provisioning.api.job.SchedTaskJobDelegate;
@@ -243,6 +244,10 @@ public class ClassPathScanImplementationLookup implements ImplementationLookup {
                 if (ProvisionSorter.class.isAssignableFrom(clazz) && !isAbstractClazz) {
                     classNames.get(IdMImplementationType.PROVISION_SORTER).add(bd.getBeanClassName());
                 }
+
+                if (Command.class.isAssignableFrom(clazz) && !isAbstractClazz) {
+                    classNames.get(IdRepoImplementationType.COMMAND).add(bd.getBeanClassName());
+                }
             } catch (Throwable t) {
                 LOG.warn("Could not inspect class {}", bd.getBeanClassName(), t);
             }
diff --git a/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/job/MacroRunJobDelegate.java b/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/job/MacroRunJobDelegate.java
new file mode 100644
index 0000000000..9dab5397aa
--- /dev/null
+++ b/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/job/MacroRunJobDelegate.java
@@ -0,0 +1,92 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.core.logic.job;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import org.apache.syncope.common.lib.command.CommandArgs;
+import org.apache.syncope.core.logic.api.Command;
+import org.apache.syncope.core.persistence.api.dao.ImplementationDAO;
+import org.apache.syncope.core.persistence.api.entity.Implementation;
+import org.apache.syncope.core.persistence.api.entity.task.MacroTask;
+import org.apache.syncope.core.persistence.api.entity.task.TaskExec;
+import org.apache.syncope.core.provisioning.api.serialization.POJOHelper;
+import org.apache.syncope.core.provisioning.java.job.AbstractSchedTaskJobDelegate;
+import org.apache.syncope.core.spring.implementation.ImplementationManager;
+import org.quartz.JobExecutionContext;
+import org.quartz.JobExecutionException;
+import org.springframework.beans.factory.annotation.Autowired;
+
+public class MacroRunJobDelegate extends AbstractSchedTaskJobDelegate<MacroTask> {
+
+    @Autowired
+    protected ImplementationDAO implementationDAO;
+
+    protected final Map<String, Command<?>> perContextCommands = new ConcurrentHashMap<>();
+
+    @SuppressWarnings("unchecked")
+    @Override
+    protected String doExecute(final boolean dryRun, final String executor, final JobExecutionContext context)
+            throws JobExecutionException {
+
+        StringBuilder output = new StringBuilder();
+        for (int i = 0; i < task.getCommands().size(); i++) {
+            Implementation command = task.getCommands().get(i);
+
+            Command<CommandArgs> runnable;
+            try {
+                runnable = (Command<CommandArgs>) ImplementationManager.build(
+                        command,
+                        () -> perContextCommands.get(command.getKey()),
+                        instance -> perContextCommands.put(command.getKey(), instance));
+            } catch (Exception e) {
+                throw new JobExecutionException("Could not build " + command.getKey(), e);
+            }
+
+            String args = POJOHelper.serialize(task.getCommandArgs().get(i));
+
+            output.append("Command[").append(i).append("]: ").
+                    append(command.getKey()).append(" ").append(args).append("\n");
+            if (dryRun) {
+                output.append(command).append(' ').append(args);
+            } else {
+                try {
+                    output.append(runnable.run(task.getCommandArgs().get(i)));
+                } catch (Exception e) {
+                    if (task.isContinueOnError()) {
+                        output.append("Continuing on error: <").append(e.getMessage()).append('>');
+                        LOG.error("While running {} with args {}, continuing on error",
+                                command.getKey(), args, e);
+                    } else {
+                        throw new RuntimeException("While running " + command.getKey(), e);
+                    }
+                }
+            }
+            output.append("\n\n");
+        }
+
+        output.append("COMPLETED");
+        return output.toString();
+    }
+
+    @Override
+    protected boolean hasToBeRegistered(final TaskExec<?> execution) {
+        return task.isSaveExecs();
+    }
+}
diff --git a/core/idrepo/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/IdRepoRESTCXFContext.java b/core/idrepo/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/IdRepoRESTCXFContext.java
index 398ae2013b..aebfacf737 100644
--- a/core/idrepo/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/IdRepoRESTCXFContext.java
+++ b/core/idrepo/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/IdRepoRESTCXFContext.java
@@ -52,6 +52,7 @@ import org.apache.syncope.common.rest.api.service.AnyTypeClassService;
 import org.apache.syncope.common.rest.api.service.AnyTypeService;
 import org.apache.syncope.common.rest.api.service.ApplicationService;
 import org.apache.syncope.common.rest.api.service.AuditService;
+import org.apache.syncope.common.rest.api.service.CommandService;
 import org.apache.syncope.common.rest.api.service.DelegationService;
 import org.apache.syncope.common.rest.api.service.DynRealmService;
 import org.apache.syncope.common.rest.api.service.FIQLQueryService;
@@ -77,6 +78,7 @@ import org.apache.syncope.core.logic.AnyTypeClassLogic;
 import org.apache.syncope.core.logic.AnyTypeLogic;
 import org.apache.syncope.core.logic.ApplicationLogic;
 import org.apache.syncope.core.logic.AuditLogic;
+import org.apache.syncope.core.logic.CommandLogic;
 import org.apache.syncope.core.logic.DelegationLogic;
 import org.apache.syncope.core.logic.DynRealmLogic;
 import org.apache.syncope.core.logic.FIQLQueryLogic;
@@ -108,6 +110,7 @@ import org.apache.syncope.core.rest.cxf.service.AnyTypeClassServiceImpl;
 import org.apache.syncope.core.rest.cxf.service.AnyTypeServiceImpl;
 import org.apache.syncope.core.rest.cxf.service.ApplicationServiceImpl;
 import org.apache.syncope.core.rest.cxf.service.AuditServiceImpl;
+import org.apache.syncope.core.rest.cxf.service.CommandServiceImpl;
 import org.apache.syncope.core.rest.cxf.service.DelegationServiceImpl;
 import org.apache.syncope.core.rest.cxf.service.DynRealmServiceImpl;
 import org.apache.syncope.core.rest.cxf.service.FIQLQueryServiceImpl;
@@ -375,6 +378,12 @@ public class IdRepoRESTCXFContext {
         return new AuditServiceImpl(auditLogic);
     }
 
+    @ConditionalOnMissingBean
+    @Bean
+    public CommandService commandService(final CommandLogic commandLogic) {
+        return new CommandServiceImpl(commandLogic);
+    }
+
     @ConditionalOnMissingBean
     @Bean
     public FIQLQueryService fiqlQueryService(final FIQLQueryLogic fiqlQueryLogic) {
diff --git a/core/idrepo/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/CommandServiceImpl.java b/core/idrepo/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/CommandServiceImpl.java
new file mode 100644
index 0000000000..62c997671f
--- /dev/null
+++ b/core/idrepo/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/CommandServiceImpl.java
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.core.rest.cxf.service;
+
+import java.util.List;
+import org.apache.commons.lang3.tuple.Pair;
+import org.apache.syncope.common.lib.command.CommandOutput;
+import org.apache.syncope.common.lib.command.CommandTO;
+import org.apache.syncope.common.lib.to.PagedResult;
+import org.apache.syncope.common.rest.api.beans.CommandQuery;
+import org.apache.syncope.common.rest.api.service.CommandService;
+import org.apache.syncope.core.logic.CommandLogic;
+import org.springframework.stereotype.Service;
+
+@Service
+public class CommandServiceImpl extends AbstractService implements CommandService {
+
+    protected final CommandLogic logic;
+
+    public CommandServiceImpl(final CommandLogic logic) {
+        this.logic = logic;
+    }
+
+    @Override
+    public PagedResult<CommandTO> search(final CommandQuery query) {
+        String keyword = query.getKeyword() == null ? null : query.getKeyword().replace('*', '%');
+        Pair<Integer, List<CommandTO>> result = logic.search(query.getPage(), query.getSize(), keyword);
+        return buildPagedResult(result.getRight(), query.getPage(), query.getSize(), result.getLeft());
+    }
+
+    @Override
+    public CommandTO read(final String key) {
+        return logic.read(key);
+    }
+
+    @Override
+    public CommandOutput run(final CommandTO command) {
+        return new CommandOutput.Builder(command).output(logic.run(command)).build();
+    }
+}
diff --git a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/ImplementationDAO.java b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/ImplementationDAO.java
index 44b19e9b70..919dffafa1 100644
--- a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/ImplementationDAO.java
+++ b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/ImplementationDAO.java
@@ -27,6 +27,8 @@ public interface ImplementationDAO extends DAO<Implementation> {
 
     List<Implementation> findByType(String type);
 
+    List<Implementation> findByTypeAndKeyword(String type, String keyword);
+
     List<Implementation> findAll();
 
     Implementation save(Implementation implementation);
diff --git a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/TaskDAO.java b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/TaskDAO.java
index 6e03859157..58dd858e96 100644
--- a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/TaskDAO.java
+++ b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/TaskDAO.java
@@ -29,6 +29,8 @@ import org.apache.syncope.core.persistence.api.dao.search.OrderByClause;
 import org.apache.syncope.core.persistence.api.entity.ExternalResource;
 import org.apache.syncope.core.persistence.api.entity.Implementation;
 import org.apache.syncope.core.persistence.api.entity.Notification;
+import org.apache.syncope.core.persistence.api.entity.Realm;
+import org.apache.syncope.core.persistence.api.entity.task.MacroTask;
 import org.apache.syncope.core.persistence.api.entity.task.PullTask;
 import org.apache.syncope.core.persistence.api.entity.task.PushTask;
 import org.apache.syncope.core.persistence.api.entity.task.SchedTask;
@@ -50,6 +52,10 @@ public interface TaskDAO extends DAO<Task<?>> {
 
     List<PushTask> findByPushActions(Implementation pushActions);
 
+    List<MacroTask> findByCommand(Implementation delegate);
+
+    List<MacroTask> findByRealm(Realm realm);
+
     <T extends Task<T>> List<T> findToExec(TaskType type);
 
     <T extends Task<T>> List<T> findAll(TaskType type);
diff --git a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/EntityFactory.java b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/EntityFactory.java
index ff2fd744dd..a74c4900b4 100644
--- a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/EntityFactory.java
+++ b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/EntityFactory.java
@@ -18,19 +18,15 @@
  */
 package org.apache.syncope.core.persistence.api.entity;
 
-import org.apache.syncope.common.lib.types.TaskType;
 import org.apache.syncope.core.persistence.api.dao.AnySearchDAO;
 import org.apache.syncope.core.persistence.api.entity.anyobject.AnyObject;
 import org.apache.syncope.core.persistence.api.entity.group.Group;
-import org.apache.syncope.core.persistence.api.entity.task.TaskExec;
 import org.apache.syncope.core.persistence.api.entity.user.User;
 
 public interface EntityFactory {
 
     <E extends Entity> E newEntity(Class<E> reference);
 
-    <E extends TaskExec<?>> E newTaskExec(TaskType taskType);
-
     ConnPoolConf newConnPoolConf();
 
     Class<? extends User> userClass();
diff --git a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/ImplementationDAO.java b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/task/MacroTask.java
similarity index 60%
copy from core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/ImplementationDAO.java
copy to core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/task/MacroTask.java
index 44b19e9b70..29c68e044e 100644
--- a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/ImplementationDAO.java
+++ b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/task/MacroTask.java
@@ -16,20 +16,30 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.syncope.core.persistence.api.dao;
+package org.apache.syncope.core.persistence.api.entity.task;
 
 import java.util.List;
+import org.apache.syncope.common.lib.command.CommandArgs;
 import org.apache.syncope.core.persistence.api.entity.Implementation;
+import org.apache.syncope.core.persistence.api.entity.Realm;
 
-public interface ImplementationDAO extends DAO<Implementation> {
+public interface MacroTask extends SchedTask {
 
-    Implementation find(String key);
+    Realm getRealm();
 
-    List<Implementation> findByType(String type);
+    void setRealm(Realm realm);
 
-    List<Implementation> findAll();
+    void add(Implementation command, CommandArgs args);
 
-    Implementation save(Implementation implementation);
+    List<? extends Implementation> getCommands();
 
-    void delete(String key);
+    List<CommandArgs> getCommandArgs();
+
+    boolean isContinueOnError();
+
+    void setContinueOnError(boolean continueOnError);
+
+    boolean isSaveExecs();
+
+    void setSaveExecs(boolean saveExecs);
 }
diff --git a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/task/TaskUtils.java b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/task/TaskUtils.java
index e8f5c15d38..c5c8336559 100644
--- a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/task/TaskUtils.java
+++ b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/task/TaskUtils.java
@@ -27,9 +27,19 @@ public interface TaskUtils {
 
     <T extends Task<T>> T newTask();
 
+    <E extends TaskExec<?>> E newTaskExec();
+
     <T extends TaskTO> T newTaskTO();
 
     <T extends Task<T>> Class<T> taskClass();
 
     <T extends TaskTO> Class<T> taskTOClass();
+
+    String getTaskTable();
+
+    Class<? extends Task<?>> getTaskEntity();
+
+    String getTaskExecTable();
+
+    Class<? extends TaskExec<?>> getTaskExecEntity();
 }
diff --git a/core/persistence-jpa-json/src/main/java/org/apache/syncope/core/persistence/jpa/entity/anyobject/JPAJSONAnyObjectListener.java b/core/persistence-jpa-json/src/main/java/org/apache/syncope/core/persistence/jpa/entity/anyobject/JPAJSONAnyObjectListener.java
index 0770da0498..0d4fdeeacd 100644
--- a/core/persistence-jpa-json/src/main/java/org/apache/syncope/core/persistence/jpa/entity/anyobject/JPAJSONAnyObjectListener.java
+++ b/core/persistence-jpa-json/src/main/java/org/apache/syncope/core/persistence/jpa/entity/anyobject/JPAJSONAnyObjectListener.java
@@ -32,10 +32,13 @@ import org.apache.syncope.core.provisioning.api.serialization.POJOHelper;
 
 public class JPAJSONAnyObjectListener extends JPAJSONEntityListener<AnyObject> {
 
+    protected static final TypeReference<List<JPAJSONAPlainAttr>> TYPEREF =
+            new TypeReference<List<JPAJSONAPlainAttr>>() {
+    };
+
     @Override
     protected List<? extends JSONPlainAttr<AnyObject>> getAttrs(final String plainAttrsJSON) {
-        return POJOHelper.deserialize(plainAttrsJSON, new TypeReference<List<JPAJSONAPlainAttr>>() {
-        });
+        return POJOHelper.deserialize(plainAttrsJSON, TYPEREF);
     }
 
     @PostLoad
diff --git a/core/persistence-jpa-json/src/main/java/org/apache/syncope/core/persistence/jpa/entity/group/JPAJSONGroupListener.java b/core/persistence-jpa-json/src/main/java/org/apache/syncope/core/persistence/jpa/entity/group/JPAJSONGroupListener.java
index 2c9fb34c83..ff071bb4f3 100644
--- a/core/persistence-jpa-json/src/main/java/org/apache/syncope/core/persistence/jpa/entity/group/JPAJSONGroupListener.java
+++ b/core/persistence-jpa-json/src/main/java/org/apache/syncope/core/persistence/jpa/entity/group/JPAJSONGroupListener.java
@@ -32,10 +32,13 @@ import org.apache.syncope.core.provisioning.api.serialization.POJOHelper;
 
 public class JPAJSONGroupListener extends JPAJSONEntityListener<Group> {
 
+    protected static final TypeReference<List<JPAJSONGPlainAttr>> TYPEREF =
+            new TypeReference<List<JPAJSONGPlainAttr>>() {
+    };
+
     @Override
     protected List<? extends JSONPlainAttr<Group>> getAttrs(final String plainAttrsJSON) {
-        return POJOHelper.deserialize(plainAttrsJSON, new TypeReference<List<JPAJSONGPlainAttr>>() {
-        });
+        return POJOHelper.deserialize(plainAttrsJSON, TYPEREF);
     }
 
     @PostLoad
diff --git a/core/persistence-jpa-json/src/main/java/org/apache/syncope/core/persistence/jpa/entity/user/JPAJSONLinkedAccountListener.java b/core/persistence-jpa-json/src/main/java/org/apache/syncope/core/persistence/jpa/entity/user/JPAJSONLinkedAccountListener.java
index 26436a4a16..8b54e0ffb4 100644
--- a/core/persistence-jpa-json/src/main/java/org/apache/syncope/core/persistence/jpa/entity/user/JPAJSONLinkedAccountListener.java
+++ b/core/persistence-jpa-json/src/main/java/org/apache/syncope/core/persistence/jpa/entity/user/JPAJSONLinkedAccountListener.java
@@ -32,10 +32,13 @@ import org.apache.syncope.core.provisioning.api.serialization.POJOHelper;
 
 public class JPAJSONLinkedAccountListener extends JPAJSONEntityListener<User> {
 
+    protected static final TypeReference<List<JPAJSONLAPlainAttr>> TYPEREF =
+            new TypeReference<List<JPAJSONLAPlainAttr>>() {
+    };
+
     @Override
     protected List<? extends JSONLAPlainAttr> getAttrs(final String plainAttrsJSON) {
-        return POJOHelper.deserialize(plainAttrsJSON, new TypeReference<List<JPAJSONLAPlainAttr>>() {
-        });
+        return POJOHelper.deserialize(plainAttrsJSON, TYPEREF);
     }
 
     @PostLoad
diff --git a/core/persistence-jpa-json/src/main/java/org/apache/syncope/core/persistence/jpa/entity/user/JPAJSONUserListener.java b/core/persistence-jpa-json/src/main/java/org/apache/syncope/core/persistence/jpa/entity/user/JPAJSONUserListener.java
index a2dadab626..0ea44a238e 100644
--- a/core/persistence-jpa-json/src/main/java/org/apache/syncope/core/persistence/jpa/entity/user/JPAJSONUserListener.java
+++ b/core/persistence-jpa-json/src/main/java/org/apache/syncope/core/persistence/jpa/entity/user/JPAJSONUserListener.java
@@ -32,10 +32,13 @@ import org.apache.syncope.core.provisioning.api.serialization.POJOHelper;
 
 public class JPAJSONUserListener extends JPAJSONEntityListener<User> {
 
+    protected static final TypeReference<List<JPAJSONUPlainAttr>> TYPEREF =
+            new TypeReference<List<JPAJSONUPlainAttr>>() {
+    };
+
     @Override
     protected List<? extends JSONPlainAttr<User>> getAttrs(final String plainAttrsJSON) {
-        return POJOHelper.deserialize(plainAttrsJSON, new TypeReference<List<JPAJSONUPlainAttr>>() {
-        });
+        return POJOHelper.deserialize(plainAttrsJSON, TYPEREF);
     }
 
     @PostLoad
diff --git a/core/persistence-jpa-json/src/main/resources/domains/jpa-json/MasterContent.xml b/core/persistence-jpa-json/src/main/resources/domains/jpa-json/MasterContent.xml
index bfb31ffff2..fc6c0f4c16 100644
--- a/core/persistence-jpa-json/src/main/resources/domains/jpa-json/MasterContent.xml
+++ b/core/persistence-jpa-json/src/main/resources/domains/jpa-json/MasterContent.xml
@@ -39,6 +39,9 @@ under the License.
   <Implementation id="BinaryValidator" type="VALIDATOR" engine="JAVA"
                   body="org.apache.syncope.core.persistence.jpa.attrvalue.validation.BinaryValidator"/>
 
+  <Implementation id="MacroRunJobDelegate" type="TASKJOB_DELEGATE" engine="JAVA"
+                  body="org.apache.syncope.core.logic.job.MacroRunJobDelegate"/>
+
   <Implementation id="PullJobDelegate" type="TASKJOB_DELEGATE" engine="JAVA"
                   body="org.apache.syncope.core.provisioning.java.pushpull.PullJobDelegate"/>
   <Implementation id="PushJobDelegate" type="TASKJOB_DELEGATE" engine="JAVA"
diff --git a/core/persistence-jpa-json/src/test/resources/domains/MasterContent.xml b/core/persistence-jpa-json/src/test/resources/domains/MasterContent.xml
index 18e866db53..bed2f381a2 100644
--- a/core/persistence-jpa-json/src/test/resources/domains/MasterContent.xml
+++ b/core/persistence-jpa-json/src/test/resources/domains/MasterContent.xml
@@ -680,10 +680,14 @@ under the License.
   <VirSchema id="virtualdata" READONLY="0" anyTypeClass_id="minimal user"
              resource_id="resource-db-virattr" anyType_id="USER" extAttrName="USERNAME"/>
 
+  <Implementation id="MacroRunJobDelegate" type="TASKJOB_DELEGATE" engine="JAVA"
+                  body="org.apache.syncope.core.logic.job.MacroRunJobDelegate"/>
+
   <Implementation id="PullJobDelegate" type="TASKJOB_DELEGATE" engine="JAVA"
                   body="org.apache.syncope.core.provisioning.java.pushpull.PullJobDelegate"/>
   <Implementation id="PushJobDelegate" type="TASKJOB_DELEGATE" engine="JAVA"
                   body="org.apache.syncope.core.provisioning.java.pushpull.PushJobDelegate"/>
+
   <PropagationTask id="1e697572-b896-484c-ae7f-0c8f63fcbc6c" operation="UPDATE"
                    objectClassName="__ACCOUNT__" resource_id="ws-target-resource-2" anyTypeKind="USER" entityKey="1417acbe-cbf6-4277-9372-e75e04f97000"
                    propagationData='{"attributes":[{"name":"__PASSWORD__","value":[{"readOnly":false,"disposed":false,"encryptedBytes":"m9nh2US0Sa6m+cXccCq0Xw==","base64SHA1Hash":"GFJ69qfjxEOdrmt+9q+0Cw2uz60="}]},{"name":"__NAME__","value":["userId"],"nameValue":"userId"},{"name":"fullname","value":["fullname"]},{"name":"type","value":["type"]}]}'/>
diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/PersistenceContext.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/PersistenceContext.java
index cf95524230..97822c456e 100644
--- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/PersistenceContext.java
+++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/PersistenceContext.java
@@ -299,8 +299,8 @@ public class PersistenceContext {
 
     @ConditionalOnMissingBean
     @Bean
-    public TaskUtilsFactory taskUtilsFactory(final @Lazy EntityFactory entityFactory) {
-        return new JPATaskUtilsFactory(entityFactory);
+    public TaskUtilsFactory taskUtilsFactory() {
+        return new JPATaskUtilsFactory();
     }
 
     @ConditionalOnMissingBean
@@ -581,13 +581,8 @@ public class PersistenceContext {
 
     @ConditionalOnMissingBean
     @Bean
-    public RealmDAO realmDAO(
-            final @Lazy RoleDAO roleDAO,
-            final @Lazy CASSPClientAppDAO casSPClientAppDAO,
-            final @Lazy OIDCRPClientAppDAO oidcRPClientAppDAO,
-            final @Lazy SAML2SPClientAppDAO saml2SPClientAppDAO) {
-
-        return new JPARealmDAO(roleDAO, casSPClientAppDAO, oidcRPClientAppDAO, saml2SPClientAppDAO);
+    public RealmDAO realmDAO(final @Lazy RoleDAO roleDAO) {
+        return new JPARealmDAO(roleDAO);
     }
 
     @ConditionalOnMissingBean
@@ -678,14 +673,19 @@ public class PersistenceContext {
 
     @ConditionalOnMissingBean
     @Bean
-    public TaskDAO taskDAO(final RemediationDAO remediationDAO) {
-        return new JPATaskDAO(remediationDAO);
+    public TaskDAO taskDAO(
+            final RealmDAO realmDAO,
+            final RemediationDAO remediationDAO,
+            final TaskUtilsFactory taskUtilsFactory,
+            final SecurityProperties securityProperties) {
+
+        return new JPATaskDAO(realmDAO, remediationDAO, taskUtilsFactory, securityProperties);
     }
 
     @ConditionalOnMissingBean
     @Bean
-    public TaskExecDAO taskExecDAO(final TaskDAO taskDAO) {
-        return new JPATaskExecDAO(taskDAO);
+    public TaskExecDAO taskExecDAO(final TaskDAO taskDAO, final TaskUtilsFactory taskUtilsFactory) {
+        return new JPATaskExecDAO(taskDAO, taskUtilsFactory);
     }
 
     @ConditionalOnMissingBean
diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/DefaultPullCorrelationRule.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/DefaultPullCorrelationRule.java
index 6d8dad2e0e..ada4b93904 100644
--- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/DefaultPullCorrelationRule.java
+++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/DefaultPullCorrelationRule.java
@@ -60,9 +60,8 @@ public class DefaultPullCorrelationRule implements PullCorrelationRule {
         List<SearchCond> searchConds = new ArrayList<>();
 
         conf.getSchemas().forEach(schema -> {
-            Item item = mappingItems.get(schema);
-            Attribute attr = Optional.ofNullable(item).
-                    map(item1 -> syncDelta.getObject().getAttributeByName(item1.getExtAttrName())).orElse(null);
+            Attribute attr = Optional.ofNullable(mappingItems.get(schema)).
+                    map(item -> syncDelta.getObject().getAttributeByName(item.getExtAttrName())).orElse(null);
             if (attr == null) {
                 throw new IllegalArgumentException(
                         "Connector object does not contains the attributes to perform the search: " + schema);
diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPAImplementationDAO.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPAImplementationDAO.java
index be98681114..b063cf39fa 100644
--- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPAImplementationDAO.java
+++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPAImplementationDAO.java
@@ -20,6 +20,7 @@ package org.apache.syncope.core.persistence.jpa.dao;
 
 import java.util.List;
 import javax.persistence.TypedQuery;
+import org.apache.commons.lang3.StringUtils;
 import org.apache.syncope.core.persistence.api.dao.ImplementationDAO;
 import org.apache.syncope.core.persistence.api.entity.Implementation;
 import org.apache.syncope.core.persistence.jpa.entity.JPAImplementation;
@@ -37,12 +38,29 @@ public class JPAImplementationDAO extends AbstractDAO<Implementation> implements
     @Override
     public List<Implementation> findByType(final String type) {
         TypedQuery<Implementation> query = entityManager().createQuery(
-                "SELECT e FROM " + JPAImplementation.class.getSimpleName() + " e WHERE e.type=:type",
+                "SELECT e FROM " + JPAImplementation.class.getSimpleName() + " e WHERE e.type=:type ORDER BY e.id ASC",
                 Implementation.class);
         query.setParameter("type", type);
         return query.getResultList();
     }
 
+    @Override
+    public List<Implementation> findByTypeAndKeyword(final String type, final String keyword) {
+        if (StringUtils.isBlank(keyword)) {
+            return findByType(type);
+        }
+
+        TypedQuery<Implementation> query = entityManager().createQuery(
+                "SELECT e FROM " + JPAImplementation.class.getSimpleName() + " e "
+                + "WHERE e.type=:type "
+                + "AND e.id LIKE :keyword "
+                + "ORDER BY e.id ASC",
+                Implementation.class);
+        query.setParameter("type", type);
+        query.setParameter("keyword", keyword);
+        return query.getResultList();
+    }
+
     @Override
     public List<Implementation> findAll() {
         TypedQuery<Implementation> query = entityManager().createQuery(
diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPARealmDAO.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPARealmDAO.java
index 9affc78f08..000824a700 100644
--- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPARealmDAO.java
+++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPARealmDAO.java
@@ -25,12 +25,9 @@ import javax.persistence.NoResultException;
 import javax.persistence.TypedQuery;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.syncope.common.lib.SyncopeConstants;
-import org.apache.syncope.core.persistence.api.dao.CASSPClientAppDAO;
 import org.apache.syncope.core.persistence.api.dao.MalformedPathException;
-import org.apache.syncope.core.persistence.api.dao.OIDCRPClientAppDAO;
 import org.apache.syncope.core.persistence.api.dao.RealmDAO;
 import org.apache.syncope.core.persistence.api.dao.RoleDAO;
-import org.apache.syncope.core.persistence.api.dao.SAML2SPClientAppDAO;
 import org.apache.syncope.core.persistence.api.entity.ExternalResource;
 import org.apache.syncope.core.persistence.api.entity.Implementation;
 import org.apache.syncope.core.persistence.api.entity.Realm;
@@ -49,22 +46,8 @@ public class JPARealmDAO extends AbstractDAO<Realm> implements RealmDAO {
 
     protected final RoleDAO roleDAO;
 
-    protected final CASSPClientAppDAO casSPClientAppDAO;
-
-    protected final OIDCRPClientAppDAO oidcRPClientAppDAO;
-
-    protected final SAML2SPClientAppDAO saml2SPClientAppDAO;
-
-    public JPARealmDAO(
-            final RoleDAO roleDAO,
-            final CASSPClientAppDAO casSPClientAppDAO,
-            final OIDCRPClientAppDAO oidcRPClientAppDAO,
-            final SAML2SPClientAppDAO saml2SPClientAppDAO) {
-
+    public JPARealmDAO(final RoleDAO roleDAO) {
         this.roleDAO = roleDAO;
-        this.casSPClientAppDAO = casSPClientAppDAO;
-        this.oidcRPClientAppDAO = oidcRPClientAppDAO;
-        this.saml2SPClientAppDAO = saml2SPClientAppDAO;
     }
 
     @Override
@@ -258,10 +241,6 @@ public class JPARealmDAO extends AbstractDAO<Realm> implements RealmDAO {
         findDescendants(realm).forEach(toBeDeleted -> {
             roleDAO.findByRealm(toBeDeleted).forEach(role -> role.getRealms().remove(toBeDeleted));
 
-            casSPClientAppDAO.findByRealm(toBeDeleted).forEach(clientApp -> clientApp.setRealm(null));
-            oidcRPClientAppDAO.findByRealm(toBeDeleted).forEach(clientApp -> clientApp.setRealm(null));
-            saml2SPClientAppDAO.findByRealm(toBeDeleted).forEach(clientApp -> clientApp.setRealm(null));
-
             toBeDeleted.setParent(null);
 
             entityManager().remove(toBeDeleted);
diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPATaskDAO.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPATaskDAO.java
index 54e032243c..844d825bf7 100644
--- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPATaskDAO.java
+++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPATaskDAO.java
@@ -22,6 +22,7 @@ import java.lang.reflect.Field;
 import java.time.OffsetDateTime;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Objects;
 import java.util.Optional;
 import java.util.stream.Collectors;
 import javax.persistence.ManyToOne;
@@ -33,125 +34,64 @@ import org.apache.commons.lang3.StringUtils;
 import org.apache.syncope.common.lib.to.PropagationTaskTO;
 import org.apache.syncope.common.lib.types.AnyTypeKind;
 import org.apache.syncope.common.lib.types.ExecStatus;
+import org.apache.syncope.common.lib.types.IdRepoEntitlement;
 import org.apache.syncope.common.lib.types.TaskType;
+import org.apache.syncope.core.persistence.api.dao.RealmDAO;
 import org.apache.syncope.core.persistence.api.dao.RemediationDAO;
 import org.apache.syncope.core.persistence.api.dao.TaskDAO;
 import org.apache.syncope.core.persistence.api.dao.search.OrderByClause;
 import org.apache.syncope.core.persistence.api.entity.ExternalResource;
 import org.apache.syncope.core.persistence.api.entity.Implementation;
 import org.apache.syncope.core.persistence.api.entity.Notification;
-import org.apache.syncope.core.persistence.api.entity.task.NotificationTask;
+import org.apache.syncope.core.persistence.api.entity.Realm;
+import org.apache.syncope.core.persistence.api.entity.task.MacroTask;
 import org.apache.syncope.core.persistence.api.entity.task.PropagationTask;
 import org.apache.syncope.core.persistence.api.entity.task.PullTask;
 import org.apache.syncope.core.persistence.api.entity.task.PushTask;
 import org.apache.syncope.core.persistence.api.entity.task.SchedTask;
 import org.apache.syncope.core.persistence.api.entity.task.Task;
+import org.apache.syncope.core.persistence.api.entity.task.TaskUtilsFactory;
+import org.apache.syncope.core.persistence.jpa.entity.task.JPAMacroTask;
 import org.apache.syncope.core.persistence.jpa.entity.task.JPANotificationTask;
 import org.apache.syncope.core.persistence.jpa.entity.task.JPAPropagationTask;
 import org.apache.syncope.core.persistence.jpa.entity.task.JPAPropagationTaskExec;
 import org.apache.syncope.core.persistence.jpa.entity.task.JPAPullTask;
 import org.apache.syncope.core.persistence.jpa.entity.task.JPAPushTask;
 import org.apache.syncope.core.persistence.jpa.entity.task.JPASchedTask;
+import org.apache.syncope.core.spring.security.AuthContextUtils;
+import org.apache.syncope.core.spring.security.SecurityProperties;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.util.CollectionUtils;
 import org.springframework.util.ReflectionUtils;
 
 public class JPATaskDAO extends AbstractDAO<Task<?>> implements TaskDAO {
 
-    public static TaskType getTaskType(final Task<?> task) {
-        if (task instanceof NotificationTask) {
-            return TaskType.NOTIFICATION;
-        }
-
-        if (task instanceof PropagationTask) {
-            return TaskType.PROPAGATION;
-        }
-
-        if (task instanceof PushTask) {
-            return TaskType.PUSH;
-        }
-
-        if (task instanceof PullTask) {
-            return TaskType.PULL;
-        }
-
-        if (task instanceof SchedTask) {
-            return TaskType.SCHEDULED;
-        }
-
-        return null;
-    }
-
-    public static String getEntityTableName(final TaskType type) {
-        String result = null;
-
-        switch (type) {
-            case NOTIFICATION:
-                result = JPANotificationTask.TABLE;
-                break;
-
-            case PROPAGATION:
-                result = JPAPropagationTask.TABLE;
-                break;
+    protected final RealmDAO realmDAO;
 
-            case PUSH:
-                result = JPAPushTask.TABLE;
-                break;
-
-            case SCHEDULED:
-                result = JPASchedTask.TABLE;
-                break;
-
-            case PULL:
-                result = JPAPullTask.TABLE;
-                break;
-
-            default:
-        }
-
-        return result;
-    }
-
-    public static Class<? extends Task<?>> getEntityReference(final TaskType type) {
-        Class<? extends Task<?>> result = null;
-
-        switch (type) {
-            case NOTIFICATION:
-                result = JPANotificationTask.class;
-                break;
-
-            case PROPAGATION:
-                result = JPAPropagationTask.class;
-                break;
-
-            case PUSH:
-                result = JPAPushTask.class;
-                break;
+    protected final RemediationDAO remediationDAO;
 
-            case SCHEDULED:
-                result = JPASchedTask.class;
-                break;
+    protected final TaskUtilsFactory taskUtilsFactory;
 
-            case PULL:
-                result = JPAPullTask.class;
-                break;
+    protected final SecurityProperties securityProperties;
 
-            default:
-        }
+    public JPATaskDAO(
+            final RealmDAO realmDAO,
+            final RemediationDAO remediationDAO,
+            final TaskUtilsFactory taskUtilsFactory,
+            final SecurityProperties securityProperties) {
 
-        return result;
-    }
-
-    protected final RemediationDAO remediationDAO;
-
-    public JPATaskDAO(final RemediationDAO remediationDAO) {
+        this.realmDAO = realmDAO;
         this.remediationDAO = remediationDAO;
+        this.taskUtilsFactory = taskUtilsFactory;
+        this.securityProperties = securityProperties;
     }
 
     @Transactional(readOnly = true)
     @Override
     public boolean exists(final TaskType type, final String key) {
-        Query query = entityManager().createNativeQuery("SELECT id FROM " + getEntityTableName(type) + " WHERE id=?");
+        Query query = entityManager().createNativeQuery("SELECT id FROM "
+                + taskUtilsFactory.getInstance(type).getTaskTable()
+                + " WHERE id=?");
         query.setParameter(1, key);
 
         return !query.getResultList().isEmpty();
@@ -161,7 +101,7 @@ public class JPATaskDAO extends AbstractDAO<Task<?>> implements TaskDAO {
     @SuppressWarnings("unchecked")
     @Override
     public <T extends Task<T>> T find(final TaskType type, final String key) {
-        return (T) entityManager().find(getEntityReference(type), key);
+        return (T) entityManager().find(taskUtilsFactory.getInstance(type).getTaskEntity(), key);
     }
 
     @Override
@@ -173,6 +113,9 @@ public class JPATaskDAO extends AbstractDAO<Task<?>> implements TaskDAO {
         if (task == null) {
             task = find(TaskType.PUSH, key);
         }
+        if (task == null) {
+            task = find(TaskType.MACRO, key);
+        }
         if (task == null) {
             task = find(TaskType.PROPAGATION, key);
         }
@@ -223,9 +166,28 @@ public class JPATaskDAO extends AbstractDAO<Task<?>> implements TaskDAO {
         return query.getResultList();
     }
 
+    @Override
+    public List<MacroTask> findByRealm(final Realm realm) {
+        TypedQuery<MacroTask> query = entityManager().createQuery(
+                "SELECT e FROM " + JPAMacroTask.class.getSimpleName() + " e "
+                + "WHERE e.realm=:realm", MacroTask.class);
+        query.setParameter("realm", realm);
+
+        return query.getResultList();
+    }
+
+    @Override
+    public List<MacroTask> findByCommand(final Implementation command) {
+        TypedQuery<MacroTask> query = entityManager().createQuery("SELECT e FROM " + JPAMacroTask.class.getSimpleName()
+                + " e WHERE :command MEMBER OF e.commands", MacroTask.class);
+        query.setParameter("command", command);
+
+        return query.getResultList();
+    }
+
     protected final <T extends Task<T>> StringBuilder buildFindAllQueryJPA(final TaskType type) {
         StringBuilder builder = new StringBuilder("SELECT t FROM ").
-                append(getEntityReference(type).getSimpleName()).
+                append(taskUtilsFactory.getInstance(type).getTaskEntity().getSimpleName()).
                 append(" t WHERE ");
         if (type == TaskType.SCHEDULED) {
             builder.append("t.id NOT IN (SELECT t.id FROM ").
@@ -262,6 +224,11 @@ public class JPATaskDAO extends AbstractDAO<Task<?>> implements TaskDAO {
         return findAll(type, null, null, null, null, -1, -1, List.of());
     }
 
+    protected int setParameter(final List<Object> parameters, final Object parameter) {
+        parameters.add(parameter);
+        return parameters.size();
+    }
+
     protected StringBuilder buildFindAllQuery(
             final TaskType type,
             final ExternalResource resource,
@@ -269,7 +236,7 @@ public class JPATaskDAO extends AbstractDAO<Task<?>> implements TaskDAO {
             final AnyTypeKind anyTypeKind,
             final String entityKey,
             final boolean orderByTaskExecInfo,
-            final List<Object> queryParameters) {
+            final List<Object> parameters) {
 
         if (resource != null
                 && type != TaskType.PROPAGATION && type != TaskType.PUSH && type != TaskType.PULL) {
@@ -287,55 +254,60 @@ public class JPATaskDAO extends AbstractDAO<Task<?>> implements TaskDAO {
             throw new IllegalArgumentException(type + " is not related to notifications");
         }
 
-        String table = getEntityTableName(type);
-        StringBuilder queryString = new StringBuilder("SELECT ").append(table).append(".*");
+        String taskTable = taskUtilsFactory.getInstance(type).getTaskTable();
+        StringBuilder queryString = new StringBuilder("SELECT ").append(taskTable).append(".*");
 
         if (orderByTaskExecInfo) {
-            queryString.append(',').append(JPATaskExecDAO.getEntityTableName(type)).append(".startDate AS startDate").
-                    append(',').append(JPATaskExecDAO.getEntityTableName(type)).append(".endDate AS endDate").
-                    append(',').append(JPATaskExecDAO.getEntityTableName(type)).append(".status AS status").
-                    append(" FROM ").append(table).
-                    append(',').append(JPATaskExecDAO.getEntityTableName(type)).append(',').append("(SELECT ").
-                    append(JPATaskExecDAO.getEntityTableName(type)).append(".task_id, ").
-                    append("MAX(").append(JPATaskExecDAO.getEntityTableName(type)).append(".startDate) AS startDate").
-                    append(" FROM ").append(JPATaskExecDAO.getEntityTableName(type)).
-                    append(" GROUP BY ").append(JPATaskExecDAO.getEntityTableName(type)).append(".task_id) GRP").
+            String taskExecTable = taskUtilsFactory.getInstance(type).getTaskExecTable();
+            queryString.append(',').append(taskExecTable).append(".startDate AS startDate").
+                    append(',').append(taskExecTable).append(".endDate AS endDate").
+                    append(',').append(taskExecTable).append(".status AS status").
+                    append(" FROM ").append(taskTable).
+                    append(',').append(taskExecTable).append(',').append("(SELECT ").
+                    append(taskExecTable).append(".task_id, ").
+                    append("MAX(").append(taskExecTable).append(".startDate) AS startDate").
+                    append(" FROM ").append(taskExecTable).
+                    append(" GROUP BY ").append(taskExecTable).append(".task_id) GRP").
                     append(" WHERE ").
-                    append(table).append(".id=").append(JPATaskExecDAO.getEntityTableName(type)).append(".task_id").
-                    append(" AND ").append(table).append(".id=").append("GRP.task_id").
+                    append(taskTable).append(".id=").append(taskExecTable).append(".task_id").
+                    append(" AND ").append(taskTable).append(".id=").append("GRP.task_id").
                     append(" AND ").
-                    append(JPATaskExecDAO.getEntityTableName(type)).append(".startDate=").append("GRP.startDate");
+                    append(taskExecTable).append(".startDate=").append("GRP.startDate");
         } else {
-            queryString.append(", null AS startDate, null AS endDate, null AS status FROM ").append(table).
-                    append(" WHERE 1=1 ");
+            queryString.append(", null AS startDate, null AS endDate, null AS status FROM ").append(taskTable).
+                    append(" WHERE 1=1");
         }
 
         queryString.append(' ');
 
         if (resource != null) {
-            queryParameters.add(resource.getKey());
-
             queryString.append(" AND ").
-                    append(table).
-                    append(".resource_id=?").append(queryParameters.size());
+                    append(taskTable).append(".resource_id=?").append(setParameter(parameters, resource.getKey()));
         }
         if (notification != null) {
-            queryParameters.add(notification.getKey());
-
             queryString.append(" AND ").
-                    append(table).
-                    append(".notification_id=?").append(queryParameters.size());
+                    append(taskTable).
+                    append(".notification_id=?").append(setParameter(parameters, notification.getKey()));
         }
         if (anyTypeKind != null && entityKey != null) {
-            queryParameters.add(anyTypeKind.name());
-            queryParameters.add(entityKey);
-
             queryString.append(" AND ").
-                    append(table).
-                    append(".anyTypeKind=?").append(queryParameters.size() - 1).
+                    append(taskTable).append(".anyTypeKind=?").append(setParameter(parameters, anyTypeKind.name())).
                     append(" AND ").
-                    append(table).
-                    append(".entityKey=?").append(queryParameters.size());
+                    append(taskTable).append(".entityKey=?").append(setParameter(parameters, entityKey));
+        }
+        if (type == TaskType.MACRO
+                && !AuthContextUtils.getUsername().equals(securityProperties.getAdminUser())) {
+
+            String realmKeysArg = AuthContextUtils.getAuthorizations().get(IdRepoEntitlement.TASK_LIST).stream().
+                    map(realmDAO::findByFullPath).
+                    filter(Objects::nonNull).
+                    flatMap(r -> realmDAO.findDescendants(r).stream()).
+                    map(Realm::getKey).
+                    distinct().
+                    map(realmKey -> "?" + setParameter(parameters, realmKey)).
+                    collect(Collectors.joining(","));
+            queryString.append(" AND ").
+                    append(taskTable).append(".realm_id IN (").append(realmKeysArg).append(")");
         }
 
         return queryString;
@@ -400,7 +372,7 @@ public class JPATaskDAO extends AbstractDAO<Task<?>> implements TaskDAO {
             final int itemsPerPage,
             final List<OrderByClause> orderByClauses) {
 
-        List<Object> queryParameters = new ArrayList<>();
+        List<Object> parameters = new ArrayList<>();
 
         boolean orderByTaskExecInfo = orderByClauses.stream().
                 anyMatch(clause -> clause.getField().equals("start")
@@ -415,7 +387,7 @@ public class JPATaskDAO extends AbstractDAO<Task<?>> implements TaskDAO {
                 anyTypeKind,
                 entityKey,
                 orderByTaskExecInfo,
-                queryParameters);
+                parameters);
 
         if (orderByTaskExecInfo) {
             // UNION with tasks without executions...
@@ -427,20 +399,22 @@ public class JPATaskDAO extends AbstractDAO<Task<?>> implements TaskDAO {
                             anyTypeKind,
                             entityKey,
                             false,
-                            queryParameters)).
+                            parameters)).
                     append(" AND id NOT IN ").
-                    append("(SELECT task_id AS id FROM ").append(JPATaskExecDAO.getEntityTableName(type)).append(')').
+                    append("(SELECT task_id AS id FROM ").
+                    append(taskUtilsFactory.getInstance(type).getTaskExecTable()).
+                    append(')').
                     append(")) T");
         } else {
             queryString.insert(0, "SELECT T.id FROM (").append(") T");
         }
 
-        queryString.append(toOrderByStatement(getEntityReference(type), orderByClauses));
+        queryString.append(toOrderByStatement(taskUtilsFactory.getInstance(type).getTaskEntity(), orderByClauses));
 
         Query query = entityManager().createNativeQuery(queryString.toString());
 
-        for (int i = 1; i <= queryParameters.size(); i++) {
-            query.setParameter(i, queryParameters.get(i - 1));
+        for (int i = 1; i <= parameters.size(); i++) {
+            query.setParameter(i, parameters.get(i - 1));
         }
 
         query.setFirstResult(itemsPerPage * (page <= 0 ? 0 : page - 1));
@@ -476,18 +450,19 @@ public class JPATaskDAO extends AbstractDAO<Task<?>> implements TaskDAO {
             final AnyTypeKind anyTypeKind,
             final String entityKey) {
 
-        List<Object> queryParameters = new ArrayList<>();
+        List<Object> parameters = new ArrayList<>();
 
         StringBuilder queryString =
-                buildFindAllQuery(type, resource, notification, anyTypeKind, entityKey, false, queryParameters);
+                buildFindAllQuery(type, resource, notification, anyTypeKind, entityKey, false, parameters);
 
+        String table = taskUtilsFactory.getInstance(type).getTaskTable();
         Query query = entityManager().createNativeQuery(StringUtils.replaceOnce(
                 queryString.toString(),
-                "SELECT " + getEntityTableName(type) + ".*, null AS startDate, null AS endDate, null AS status",
-                "SELECT COUNT(" + getEntityTableName(type) + ".id)"));
+                "SELECT " + table + ".*, null AS startDate, null AS endDate, null AS status",
+                "SELECT COUNT(" + table + ".id)"));
 
-        for (int i = 1; i <= queryParameters.size(); i++) {
-            query.setParameter(i, queryParameters.get(i - 1));
+        for (int i = 1; i <= parameters.size(); i++) {
+            query.setParameter(i, parameters.get(i - 1));
         }
 
         return ((Number) query.getSingleResult()).intValue();
diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPATaskExecDAO.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPATaskExecDAO.java
index dbf6645e6e..e6f08ab786 100644
--- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPATaskExecDAO.java
+++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPATaskExecDAO.java
@@ -31,87 +31,26 @@ import org.apache.syncope.core.persistence.api.dao.TaskExecDAO;
 import org.apache.syncope.core.persistence.api.dao.search.OrderByClause;
 import org.apache.syncope.core.persistence.api.entity.task.Task;
 import org.apache.syncope.core.persistence.api.entity.task.TaskExec;
+import org.apache.syncope.core.persistence.api.entity.task.TaskUtilsFactory;
 import org.apache.syncope.core.persistence.jpa.entity.task.AbstractTaskExec;
-import org.apache.syncope.core.persistence.jpa.entity.task.JPANotificationTaskExec;
-import org.apache.syncope.core.persistence.jpa.entity.task.JPAPropagationTaskExec;
-import org.apache.syncope.core.persistence.jpa.entity.task.JPAPullTaskExec;
-import org.apache.syncope.core.persistence.jpa.entity.task.JPAPushTaskExec;
-import org.apache.syncope.core.persistence.jpa.entity.task.JPASchedTaskExec;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.util.ReflectionUtils;
 
 public class JPATaskExecDAO extends AbstractDAO<TaskExec<?>> implements TaskExecDAO {
 
-    public static String getEntityTableName(final TaskType type) {
-        String result = null;
-
-        switch (type) {
-            case NOTIFICATION:
-                result = JPANotificationTaskExec.TABLE;
-                break;
-
-            case PROPAGATION:
-                result = JPAPropagationTaskExec.TABLE;
-                break;
-
-            case PUSH:
-                result = JPAPushTaskExec.TABLE;
-                break;
-
-            case SCHEDULED:
-                result = JPASchedTaskExec.TABLE;
-                break;
-
-            case PULL:
-                result = JPAPullTaskExec.TABLE;
-                break;
-
-            default:
-        }
-
-        return result;
-    }
-
-    protected static Class<? extends TaskExec<?>> getEntityReference(final TaskType type) {
-        Class<? extends TaskExec<?>> result = null;
-
-        switch (type) {
-            case NOTIFICATION:
-                result = JPANotificationTaskExec.class;
-                break;
-
-            case PROPAGATION:
-                result = JPAPropagationTaskExec.class;
-                break;
-
-            case PUSH:
-                result = JPAPushTaskExec.class;
-                break;
-
-            case SCHEDULED:
-                result = JPASchedTaskExec.class;
-                break;
-
-            case PULL:
-                result = JPAPullTaskExec.class;
-                break;
-
-            default:
-        }
-
-        return result;
-    }
-
     protected final TaskDAO taskDAO;
 
-    public JPATaskExecDAO(final TaskDAO taskDAO) {
+    protected final TaskUtilsFactory taskUtilsFactory;
+
+    public JPATaskExecDAO(final TaskDAO taskDAO, final TaskUtilsFactory taskUtilsFactory) {
         this.taskDAO = taskDAO;
+        this.taskUtilsFactory = taskUtilsFactory;
     }
 
     @SuppressWarnings("unchecked")
     @Override
     public <T extends Task<T>> TaskExec<T> find(final TaskType type, final String key) {
-        return (TaskExec<T>) entityManager().find(getEntityReference(type), key);
+        return (TaskExec<T>) entityManager().find(taskUtilsFactory.getInstance(type).getTaskExecEntity(), key);
     }
 
     @Override
@@ -123,6 +62,9 @@ public class JPATaskExecDAO extends AbstractDAO<TaskExec<?>> implements TaskExec
         if (task == null) {
             task = find(TaskType.PUSH, key);
         }
+        if (task == null) {
+            task = find(TaskType.MACRO, key);
+        }
         if (task == null) {
             task = find(TaskType.PROPAGATION, key);
         }
@@ -136,7 +78,7 @@ public class JPATaskExecDAO extends AbstractDAO<TaskExec<?>> implements TaskExec
     @SuppressWarnings("unchecked")
     protected <T extends Task<T>> List<TaskExec<T>> findRecent(final TaskType type, final int max) {
         Query query = entityManager().createQuery(
-                "SELECT e FROM " + getEntityReference(type).getSimpleName() + " e "
+                "SELECT e FROM " + taskUtilsFactory.getInstance(type).getTaskExecEntity().getSimpleName() + " e "
                 + "WHERE e.end IS NOT NULL ORDER BY e.end DESC");
         query.setMaxResults(max);
 
@@ -161,7 +103,7 @@ public class JPATaskExecDAO extends AbstractDAO<TaskExec<?>> implements TaskExec
     @SuppressWarnings("unchecked")
     protected TaskExec<?> findLatest(final TaskType type, final Task<?> task, final String field) {
         Query query = entityManager().createQuery(
-                "SELECT e FROM " + getEntityReference(type).getSimpleName() + " e "
+                "SELECT e FROM " + taskUtilsFactory.getInstance(type).getTaskExecEntity().getSimpleName() + " e "
                 + "WHERE e.task=:task ORDER BY e." + field + " DESC");
         query.setParameter("task", task);
         query.setMaxResults(1);
@@ -192,7 +134,7 @@ public class JPATaskExecDAO extends AbstractDAO<TaskExec<?>> implements TaskExec
             final OffsetDateTime endedAfter) {
 
         StringBuilder queryString = new StringBuilder("SELECT e FROM ").
-                append(getEntityReference(JPATaskDAO.getTaskType(task)).getSimpleName()).
+                append(taskUtilsFactory.getInstance(task).getTaskExecEntity().getSimpleName()).
                 append(" e WHERE e.task=:task ");
 
         if (startedBefore != null) {
@@ -230,7 +172,7 @@ public class JPATaskExecDAO extends AbstractDAO<TaskExec<?>> implements TaskExec
     @Override
     public int count(final Task<?> task) {
         Query countQuery = entityManager().createNativeQuery(
-                "SELECT COUNT(e.id) FROM " + getEntityTableName(JPATaskDAO.getTaskType(task)) + " e "
+                "SELECT COUNT(e.id) FROM " + taskUtilsFactory.getInstance(task).getTaskExecTable() + " e "
                 + "WHERE e.task_id=?1");
         countQuery.setParameter(1, task.getKey());
 
@@ -261,7 +203,7 @@ public class JPATaskExecDAO extends AbstractDAO<TaskExec<?>> implements TaskExec
             final Task<?> task, final int page, final int itemsPerPage, final List<OrderByClause> orderByClauses) {
 
         String queryString = "SELECT e "
-                + "FROM " + getEntityReference(JPATaskDAO.getTaskType(task)).getSimpleName() + " e "
+                + "FROM " + taskUtilsFactory.getInstance(task).getTaskExecEntity().getSimpleName() + " e "
                 + "WHERE e.task=:task "
                 + toOrderByStatement(orderByClauses);
 
diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/JPAConnInstance.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/JPAConnInstance.java
index 2b0b1d8d56..253e6b272e 100644
--- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/JPAConnInstance.java
+++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/JPAConnInstance.java
@@ -58,6 +58,10 @@ public class JPAConnInstance extends AbstractGeneratedKeyEntity implements ConnI
 
     public static final String TABLE = "ConnInstance";
 
+    protected static final TypeReference<Set<ConnectorCapability>> TYPEREF =
+            new TypeReference<Set<ConnectorCapability>>() {
+    };
+
     private static final int DEFAULT_TIMEOUT = 10;
 
     @ManyToOne(fetch = FetchType.EAGER, optional = false)
@@ -246,9 +250,7 @@ public class JPAConnInstance extends AbstractGeneratedKeyEntity implements ConnI
             getCapabilities().clear();
         }
         if (capabilities != null) {
-            getCapabilities().addAll(
-                    POJOHelper.deserialize(capabilities, new TypeReference<Set<ConnectorCapability>>() {
-                    }));
+            getCapabilities().addAll(POJOHelper.deserialize(capabilities, TYPEREF));
         }
     }
 
diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/JPAEntityFactory.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/JPAEntityFactory.java
index 1da1fb5d43..0b9ee7c7df 100644
--- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/JPAEntityFactory.java
+++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/JPAEntityFactory.java
@@ -18,7 +18,6 @@
  */
 package org.apache.syncope.core.persistence.jpa.entity;
 
-import org.apache.syncope.common.lib.types.TaskType;
 import org.apache.syncope.core.persistence.api.dao.AnySearchDAO;
 import org.apache.syncope.core.persistence.api.entity.AccessToken;
 import org.apache.syncope.core.persistence.api.entity.AnyAbout;
@@ -86,13 +85,13 @@ import org.apache.syncope.core.persistence.api.entity.policy.PullPolicy;
 import org.apache.syncope.core.persistence.api.entity.policy.PushCorrelationRuleEntity;
 import org.apache.syncope.core.persistence.api.entity.policy.PushPolicy;
 import org.apache.syncope.core.persistence.api.entity.task.AnyTemplatePullTask;
+import org.apache.syncope.core.persistence.api.entity.task.MacroTask;
 import org.apache.syncope.core.persistence.api.entity.task.NotificationTask;
 import org.apache.syncope.core.persistence.api.entity.task.PropagationTask;
 import org.apache.syncope.core.persistence.api.entity.task.PullTask;
 import org.apache.syncope.core.persistence.api.entity.task.PushTask;
 import org.apache.syncope.core.persistence.api.entity.task.PushTaskAnyFilter;
 import org.apache.syncope.core.persistence.api.entity.task.SchedTask;
-import org.apache.syncope.core.persistence.api.entity.task.TaskExec;
 import org.apache.syncope.core.persistence.api.entity.user.DynRoleMembership;
 import org.apache.syncope.core.persistence.api.entity.user.LAPlainAttr;
 import org.apache.syncope.core.persistence.api.entity.user.LAPlainAttrUniqueValue;
@@ -140,17 +139,13 @@ import org.apache.syncope.core.persistence.jpa.entity.policy.JPAPullPolicy;
 import org.apache.syncope.core.persistence.jpa.entity.policy.JPAPushCorrelationRuleEntity;
 import org.apache.syncope.core.persistence.jpa.entity.policy.JPAPushPolicy;
 import org.apache.syncope.core.persistence.jpa.entity.task.JPAAnyTemplatePullTask;
+import org.apache.syncope.core.persistence.jpa.entity.task.JPAMacroTask;
 import org.apache.syncope.core.persistence.jpa.entity.task.JPANotificationTask;
-import org.apache.syncope.core.persistence.jpa.entity.task.JPANotificationTaskExec;
 import org.apache.syncope.core.persistence.jpa.entity.task.JPAPropagationTask;
-import org.apache.syncope.core.persistence.jpa.entity.task.JPAPropagationTaskExec;
 import org.apache.syncope.core.persistence.jpa.entity.task.JPAPullTask;
-import org.apache.syncope.core.persistence.jpa.entity.task.JPAPullTaskExec;
 import org.apache.syncope.core.persistence.jpa.entity.task.JPAPushTask;
 import org.apache.syncope.core.persistence.jpa.entity.task.JPAPushTaskAnyFilter;
-import org.apache.syncope.core.persistence.jpa.entity.task.JPAPushTaskExec;
 import org.apache.syncope.core.persistence.jpa.entity.task.JPASchedTask;
-import org.apache.syncope.core.persistence.jpa.entity.task.JPASchedTaskExec;
 import org.apache.syncope.core.persistence.jpa.entity.user.JPADynRoleMembership;
 import org.apache.syncope.core.persistence.jpa.entity.user.JPALAPlainAttr;
 import org.apache.syncope.core.persistence.jpa.entity.user.JPALAPlainAttrUniqueValue;
@@ -281,6 +276,8 @@ public class JPAEntityFactory implements EntityFactory {
             result = (E) new JPAPushTask();
         } else if (reference.equals(PullTask.class)) {
             result = (E) new JPAPullTask();
+        } else if (reference.equals(MacroTask.class)) {
+            result = (E) new JPAMacroTask();
         } else if (reference.equals(SchedTask.class)) {
             result = (E) new JPASchedTask();
         } else if (reference.equals(PushTaskAnyFilter.class)) {
@@ -348,43 +345,6 @@ public class JPAEntityFactory implements EntityFactory {
         return result;
     }
 
-    @SuppressWarnings("unchecked")
-    @Override
-    public <E extends TaskExec<?>> E newTaskExec(final TaskType taskType) {
-        E result;
-
-        switch (taskType) {
-            case NOTIFICATION:
-                result = (E) new JPANotificationTaskExec();
-                break;
-
-            case PROPAGATION:
-                result = (E) new JPAPropagationTaskExec();
-                break;
-
-            case PULL:
-                result = (E) new JPAPullTaskExec();
-                break;
-
-            case PUSH:
-                result = (E) new JPAPushTaskExec();
-                break;
-
-            case SCHEDULED:
-                result = (E) new JPASchedTaskExec();
-                break;
-
-            default:
-                result = null;
-        }
-
-        if (result instanceof AbstractGeneratedKeyEntity) {
-            ((AbstractGeneratedKeyEntity) result).setKey(SecureRandomUtils.generateRandomUUID().toString());
-        }
-
-        return result;
-    }
-
     @Override
     public ConnPoolConf newConnPoolConf() {
         return new JPAConnPoolConf();
diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/JPAExternalResource.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/JPAExternalResource.java
index 3ecabb3837..5a5b1602a1 100644
--- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/JPAExternalResource.java
+++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/JPAExternalResource.java
@@ -81,6 +81,14 @@ public class JPAExternalResource extends AbstractProvidedKeyEntity implements Ex
 
     public static final String TABLE = "ExternalResource";
 
+    protected static final TypeReference<Set<ConnectorCapability>> CAPABILITY_TYPEREF =
+            new TypeReference<Set<ConnectorCapability>>() {
+    };
+
+    protected static final TypeReference<List<Provision>> PROVISION_TYPEREF =
+            new TypeReference<List<Provision>>() {
+    };
+
     /**
      * Should this resource enforce the mandatory constraints?
      */
@@ -397,14 +405,10 @@ public class JPAExternalResource extends AbstractProvidedKeyEntity implements Ex
             getProvisions().clear();
         }
         if (capabilitiesOverride != null) {
-            getCapabilitiesOverride().addAll(
-                    POJOHelper.deserialize(capabilitiesOverride, new TypeReference<Set<ConnectorCapability>>() {
-                    }));
+            getCapabilitiesOverride().addAll(POJOHelper.deserialize(capabilitiesOverride, CAPABILITY_TYPEREF));
         }
         if (provisions != null) {
-            getProvisions().addAll(
-                    POJOHelper.deserialize(provisions, new TypeReference<List<Provision>>() {
-                    }));
+            getProvisions().addAll(POJOHelper.deserialize(provisions, PROVISION_TYPEREF));
         }
     }
 
diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/JPANotification.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/JPANotification.java
index be5dde761a..a3e7a82ce4 100644
--- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/JPANotification.java
+++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/JPANotification.java
@@ -56,6 +56,9 @@ public class JPANotification extends AbstractGeneratedKeyEntity implements Notif
 
     public static final String TABLE = "Notification";
 
+    protected static final TypeReference<List<String>> TYPEREF = new TypeReference<List<String>>() {
+    };
+
     @Lob
     private String events;
 
@@ -225,14 +228,10 @@ public class JPANotification extends AbstractGeneratedKeyEntity implements Notif
             getStaticRecipients().clear();
         }
         if (events != null) {
-            getEvents().addAll(
-                    POJOHelper.deserialize(events, new TypeReference<List<String>>() {
-                    }));
+            getEvents().addAll(POJOHelper.deserialize(events, TYPEREF));
         }
         if (staticRecipients != null) {
-            getStaticRecipients().addAll(
-                    POJOHelper.deserialize(staticRecipients, new TypeReference<List<String>>() {
-                    }));
+            getStaticRecipients().addAll(POJOHelper.deserialize(staticRecipients, TYPEREF));
         }
     }
 
diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/JPARole.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/JPARole.java
index ce773105da..6834b15cb4 100644
--- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/JPARole.java
+++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/JPARole.java
@@ -62,6 +62,9 @@ public class JPARole extends AbstractProvidedKeyEntity implements Role {
 
     public static final String TABLE = "SyncopeRole";
 
+    protected static final TypeReference<Set<String>> TYPEREF = new TypeReference<Set<String>>() {
+    };
+
     @Lob
     private String entitlements;
 
@@ -177,8 +180,7 @@ public class JPARole extends AbstractProvidedKeyEntity implements Role {
         }
         if (entitlements != null) {
             getEntitlements().addAll(
-                    POJOHelper.deserialize(entitlements, new TypeReference<Set<String>>() {
-                    }));
+                    POJOHelper.deserialize(entitlements, TYPEREF));
         }
     }
 
diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/am/AbstractClientApp.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/am/AbstractClientApp.java
index 60301830ab..2241f7bc29 100644
--- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/am/AbstractClientApp.java
+++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/am/AbstractClientApp.java
@@ -44,6 +44,9 @@ public class AbstractClientApp extends AbstractGeneratedKeyEntity implements Cli
 
     private static final long serialVersionUID = 7422422526695279794L;
 
+    protected static final TypeReference<List<Attr>> ATTR_TYPEREF = new TypeReference<List<Attr>>() {
+    };
+
     @Column(unique = true, nullable = false)
     private String name;
 
@@ -159,8 +162,7 @@ public class AbstractClientApp extends AbstractGeneratedKeyEntity implements Cli
     public List<Attr> getProperties() {
         return properties == null
                 ? new ArrayList<>(0)
-                : POJOHelper.deserialize(properties, new TypeReference<>() {
-                });
+                : POJOHelper.deserialize(properties, ATTR_TYPEREF);
     }
 
     @Override
diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/am/JPAAttrRepo.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/am/JPAAttrRepo.java
index 9f452b69fc..b9b8adb137 100644
--- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/am/JPAAttrRepo.java
+++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/am/JPAAttrRepo.java
@@ -46,9 +46,12 @@ import org.apache.syncope.core.provisioning.api.serialization.POJOHelper;
 @Table(name = JPAAttrRepo.TABLE)
 public class JPAAttrRepo extends AbstractProvidedKeyEntity implements AttrRepo {
 
+    private static final long serialVersionUID = 7337970107878689617L;
+
     public static final String TABLE = "AttrRepo";
 
-    private static final long serialVersionUID = 7337970107878689617L;
+    protected static final TypeReference<List<Item>> TYPEREF = new TypeReference<List<Item>>() {
+    };
 
     private String description;
 
@@ -123,9 +126,7 @@ public class JPAAttrRepo extends AbstractProvidedKeyEntity implements AttrRepo {
             getItems().clear();
         }
         if (items != null) {
-            getItems().addAll(
-                    POJOHelper.deserialize(items, new TypeReference<List<Item>>() {
-                    }));
+            getItems().addAll(POJOHelper.deserialize(items, TYPEREF));
         }
     }
 
diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/am/JPAAuthModule.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/am/JPAAuthModule.java
index 527fe81de6..3bbaf9d8b0 100644
--- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/am/JPAAuthModule.java
+++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/am/JPAAuthModule.java
@@ -46,9 +46,12 @@ import org.apache.syncope.core.provisioning.api.serialization.POJOHelper;
 @Table(name = JPAAuthModule.TABLE)
 public class JPAAuthModule extends AbstractProvidedKeyEntity implements AuthModule {
 
+    private static final long serialVersionUID = 5681033638234853077L;
+
     public static final String TABLE = "AuthModule";
 
-    private static final long serialVersionUID = 5681033638234853077L;
+    protected static final TypeReference<List<Item>> TYPEREF = new TypeReference<List<Item>>() {
+    };
 
     private String description;
 
@@ -123,9 +126,7 @@ public class JPAAuthModule extends AbstractProvidedKeyEntity implements AuthModu
             getItems().clear();
         }
         if (items != null) {
-            getItems().addAll(
-                    POJOHelper.deserialize(items, new TypeReference<List<Item>>() {
-                    }));
+            getItems().addAll(POJOHelper.deserialize(items, TYPEREF));
         }
     }
 
diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/am/JPAAuthProfile.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/am/JPAAuthProfile.java
index 7ac8baa57a..e8349e11ac 100644
--- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/am/JPAAuthProfile.java
+++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/am/JPAAuthProfile.java
@@ -41,9 +41,28 @@ import org.apache.syncope.core.provisioning.api.serialization.POJOHelper;
         @UniqueConstraint(columnNames = { "owner" }))
 public class JPAAuthProfile extends AbstractGeneratedKeyEntity implements AuthProfile {
 
+    private static final long serialVersionUID = 57352617217394093L;
+
     public static final String TABLE = "AuthProfile";
 
-    private static final long serialVersionUID = 57352617217394093L;
+    protected static final TypeReference<List<GoogleMfaAuthToken>> GOOGLE_MFA_TOKENS_TYPEREF =
+            new TypeReference<List<GoogleMfaAuthToken>>() {
+    };
+
+    protected static final TypeReference<List<GoogleMfaAuthAccount>> GOOGLE_MFA_ACCOUNTS_TYPEREF =
+            new TypeReference<List<GoogleMfaAuthAccount>>() {
+    };
+
+    protected static final TypeReference<List<U2FDevice>> U2F_TYPEREF = new TypeReference<List<U2FDevice>>() {
+    };
+
+    protected static final TypeReference<List<ImpersonationAccount>> IMPERSONATION_TYPEREF =
+            new TypeReference<List<ImpersonationAccount>>() {
+    };
+
+    protected static final TypeReference<List<WebAuthnDeviceCredential>> WEBAUTHN_TYPEREF =
+            new TypeReference<List<WebAuthnDeviceCredential>>() {
+    };
 
     @Column(nullable = false)
     private String owner;
@@ -76,8 +95,7 @@ public class JPAAuthProfile extends AbstractGeneratedKeyEntity implements AuthPr
     @Override
     public List<GoogleMfaAuthToken> getGoogleMfaAuthTokens() {
         return Optional.ofNullable(googleMfaAuthTokens).
-                map(v -> POJOHelper.deserialize(v, new TypeReference<List<GoogleMfaAuthToken>>() {
-        })).orElseGet(() -> new ArrayList<>(0));
+                map(v -> POJOHelper.deserialize(v, GOOGLE_MFA_TOKENS_TYPEREF)).orElseGet(() -> new ArrayList<>(0));
     }
 
     @Override
@@ -88,8 +106,7 @@ public class JPAAuthProfile extends AbstractGeneratedKeyEntity implements AuthPr
     @Override
     public List<GoogleMfaAuthAccount> getGoogleMfaAuthAccounts() {
         return Optional.ofNullable(googleMfaAuthAccounts).
-                map(v -> POJOHelper.deserialize(v, new TypeReference<List<GoogleMfaAuthAccount>>() {
-        })).orElseGet(() -> new ArrayList<>(0));
+                map(v -> POJOHelper.deserialize(v, GOOGLE_MFA_ACCOUNTS_TYPEREF)).orElseGet(() -> new ArrayList<>(0));
     }
 
     @Override
@@ -100,8 +117,7 @@ public class JPAAuthProfile extends AbstractGeneratedKeyEntity implements AuthPr
     @Override
     public List<U2FDevice> getU2FRegisteredDevices() {
         return Optional.ofNullable(u2fRegisteredDevices).
-                map(v -> POJOHelper.deserialize(v, new TypeReference<List<U2FDevice>>() {
-        })).orElseGet(() -> new ArrayList<>(0));
+                map(v -> POJOHelper.deserialize(v, U2F_TYPEREF)).orElseGet(() -> new ArrayList<>(0));
     }
 
     @Override
@@ -112,8 +128,7 @@ public class JPAAuthProfile extends AbstractGeneratedKeyEntity implements AuthPr
     @Override
     public List<ImpersonationAccount> getImpersonationAccounts() {
         return Optional.ofNullable(impersonationAccounts).
-                map(v -> POJOHelper.deserialize(v, new TypeReference<List<ImpersonationAccount>>() {
-        })).orElseGet(() -> new ArrayList<>(0));
+                map(v -> POJOHelper.deserialize(v, IMPERSONATION_TYPEREF)).orElseGet(() -> new ArrayList<>(0));
     }
 
     @Override
@@ -124,8 +139,7 @@ public class JPAAuthProfile extends AbstractGeneratedKeyEntity implements AuthPr
     @Override
     public List<WebAuthnDeviceCredential> getWebAuthnDeviceCredentials() {
         return Optional.ofNullable(webAuthnDeviceCredentials).
-                map(v -> POJOHelper.deserialize(v, new TypeReference<List<WebAuthnDeviceCredential>>() {
-        })).orElseGet(() -> new ArrayList<>(0));
+                map(v -> POJOHelper.deserialize(v, WEBAUTHN_TYPEREF)).orElseGet(() -> new ArrayList<>(0));
     }
 
     @Override
diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/am/JPAOIDCRPClientApp.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/am/JPAOIDCRPClientApp.java
index 3ca91b1b80..fbd172e512 100644
--- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/am/JPAOIDCRPClientApp.java
+++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/am/JPAOIDCRPClientApp.java
@@ -47,6 +47,17 @@ public class JPAOIDCRPClientApp extends AbstractClientApp implements OIDCRPClien
 
     public static final String TABLE = "OIDCRPClientApp";
 
+    protected static final TypeReference<Set<String>> STRING_TYPEREF = new TypeReference<Set<String>>() {
+    };
+
+    protected static final TypeReference<Set<OIDCGrantType>> GRANT_TYPE_TYPEREF =
+            new TypeReference<Set<OIDCGrantType>>() {
+    };
+
+    protected static final TypeReference<Set<OIDCResponseType>> RESPONSE_TYPE_TYPEREF =
+            new TypeReference<Set<OIDCResponseType>>() {
+    };
+
     @Column(unique = true, nullable = false)
     private String clientId;
 
@@ -173,19 +184,13 @@ public class JPAOIDCRPClientApp extends AbstractClientApp implements OIDCRPClien
             getSupportedResponseTypes().clear();
         }
         if (redirectUris != null) {
-            getRedirectUris().addAll(
-                    POJOHelper.deserialize(redirectUris, new TypeReference<Set<String>>() {
-                    }));
+            getRedirectUris().addAll(POJOHelper.deserialize(redirectUris, STRING_TYPEREF));
         }
         if (supportedGrantTypes != null) {
-            getSupportedGrantTypes().addAll(
-                    POJOHelper.deserialize(supportedGrantTypes, new TypeReference<Set<OIDCGrantType>>() {
-                    }));
+            getSupportedGrantTypes().addAll(POJOHelper.deserialize(supportedGrantTypes, GRANT_TYPE_TYPEREF));
         }
         if (supportedResponseTypes != null) {
-            getSupportedResponseTypes().addAll(
-                    POJOHelper.deserialize(supportedResponseTypes, new TypeReference<Set<OIDCResponseType>>() {
-                    }));
+            getSupportedResponseTypes().addAll(POJOHelper.deserialize(supportedResponseTypes, RESPONSE_TYPE_TYPEREF));
         }
     }
 
diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/am/JPASAML2SPClientApp.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/am/JPASAML2SPClientApp.java
index 555f5d0086..7b9c05e9f2 100644
--- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/am/JPASAML2SPClientApp.java
+++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/am/JPASAML2SPClientApp.java
@@ -42,9 +42,16 @@ import org.apache.syncope.core.provisioning.api.serialization.POJOHelper;
 @Table(name = JPASAML2SPClientApp.TABLE)
 public class JPASAML2SPClientApp extends AbstractClientApp implements SAML2SPClientApp {
 
+    private static final long serialVersionUID = 6422422526695279794L;
+
     public static final String TABLE = "SAML2SPClientApp";
 
-    private static final long serialVersionUID = 6422422526695279794L;
+    protected static final TypeReference<Set<String>> STRING_TYPEREF = new TypeReference<Set<String>>() {
+    };
+
+    protected static final TypeReference<List<XmlSecAlgorithm>> XMLSECAGO_TYPEREF =
+            new TypeReference<List<XmlSecAlgorithm>>() {
+    };
 
     @Column(unique = true, nullable = false)
     private String entityId;
@@ -289,45 +296,31 @@ public class JPASAML2SPClientApp extends AbstractClientApp implements SAML2SPCli
         }
         if (assertionAudiences != null) {
             getAssertionAudiences().addAll(
-                    POJOHelper.deserialize(assertionAudiences,
-                            new TypeReference<Set<String>>() {
-                    }));
+                    POJOHelper.deserialize(assertionAudiences, STRING_TYPEREF));
         }
         if (signingSignatureAlgorithms != null) {
             getSigningSignatureAlgorithms().addAll(
-                    POJOHelper.deserialize(signingSignatureAlgorithms,
-                            new TypeReference<List<XmlSecAlgorithm>>() {
-                    }));
+                    POJOHelper.deserialize(signingSignatureAlgorithms, XMLSECAGO_TYPEREF));
         }
         if (signingSignatureReferenceDigestMethods != null) {
             getSigningSignatureReferenceDigestMethods().addAll(
-                    POJOHelper.deserialize(signingSignatureReferenceDigestMethods,
-                            new TypeReference<List<XmlSecAlgorithm>>() {
-                    }));
+                    POJOHelper.deserialize(signingSignatureReferenceDigestMethods, XMLSECAGO_TYPEREF));
         }
         if (encryptionDataAlgorithms != null) {
             getEncryptionDataAlgorithms().addAll(
-                    POJOHelper.deserialize(encryptionDataAlgorithms,
-                            new TypeReference<List<XmlSecAlgorithm>>() {
-                    }));
+                    POJOHelper.deserialize(encryptionDataAlgorithms, XMLSECAGO_TYPEREF));
         }
         if (encryptionKeyAlgorithms != null) {
             getEncryptionKeyAlgorithms().addAll(
-                    POJOHelper.deserialize(encryptionKeyAlgorithms,
-                            new TypeReference<List<XmlSecAlgorithm>>() {
-                    }));
+                    POJOHelper.deserialize(encryptionKeyAlgorithms, XMLSECAGO_TYPEREF));
         }
         if (signingSignatureBlackListedAlgorithms != null) {
             getSigningSignatureBlackListedAlgorithms().addAll(
-                    POJOHelper.deserialize(signingSignatureBlackListedAlgorithms,
-                            new TypeReference<List<XmlSecAlgorithm>>() {
-                    }));
+                    POJOHelper.deserialize(signingSignatureBlackListedAlgorithms, XMLSECAGO_TYPEREF));
         }
         if (encryptionBlackListedAlgorithms != null) {
             getEncryptionBlackListedAlgorithms().addAll(
-                    POJOHelper.deserialize(encryptionBlackListedAlgorithms,
-                            new TypeReference<List<XmlSecAlgorithm>>() {
-                    }));
+                    POJOHelper.deserialize(encryptionBlackListedAlgorithms, XMLSECAGO_TYPEREF));
         }
     }
 
diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/am/JPAWAConfigEntry.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/am/JPAWAConfigEntry.java
index 4b5c4cd6fc..eea214c74d 100644
--- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/am/JPAWAConfigEntry.java
+++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/am/JPAWAConfigEntry.java
@@ -31,9 +31,12 @@ import org.apache.syncope.core.provisioning.api.serialization.POJOHelper;
 @Table(name = JPAWAConfigEntry.TABLE)
 public class JPAWAConfigEntry extends AbstractProvidedKeyEntity implements WAConfigEntry {
 
+    private static final long serialVersionUID = 6422422526695279794L;
+
     public static final String TABLE = "WAConfigEntry";
 
-    private static final long serialVersionUID = 6422422526695279794L;
+    protected static TypeReference<List<String>> TYPEREF = new TypeReference<List<String>>() {
+    };
 
     @Lob
     private String waConfigValues;
@@ -42,8 +45,7 @@ public class JPAWAConfigEntry extends AbstractProvidedKeyEntity implements WACon
     public List<String> getValues() {
         return waConfigValues == null
                 ? List.of()
-                : POJOHelper.deserialize(waConfigValues, new TypeReference<>() {
-        });
+                : POJOHelper.deserialize(waConfigValues, TYPEREF);
     }
 
     @Override
diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/task/JPAMacroTask.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/task/JPAMacroTask.java
new file mode 100644
index 0000000000..20fdcc5269
--- /dev/null
+++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/task/JPAMacroTask.java
@@ -0,0 +1,178 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.core.persistence.jpa.entity.task;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import java.util.ArrayList;
+import java.util.List;
+import javax.persistence.CascadeType;
+import javax.persistence.Entity;
+import javax.persistence.FetchType;
+import javax.persistence.JoinColumn;
+import javax.persistence.JoinTable;
+import javax.persistence.Lob;
+import javax.persistence.ManyToMany;
+import javax.persistence.ManyToOne;
+import javax.persistence.OneToMany;
+import javax.persistence.PostLoad;
+import javax.persistence.PostPersist;
+import javax.persistence.PostUpdate;
+import javax.persistence.PrePersist;
+import javax.persistence.PreUpdate;
+import javax.persistence.Table;
+import javax.persistence.Transient;
+import javax.persistence.UniqueConstraint;
+import javax.validation.constraints.NotNull;
+import org.apache.syncope.common.lib.command.CommandArgs;
+import org.apache.syncope.common.lib.types.IdRepoImplementationType;
+import org.apache.syncope.core.persistence.api.entity.Implementation;
+import org.apache.syncope.core.persistence.api.entity.Realm;
+import org.apache.syncope.core.persistence.api.entity.task.MacroTask;
+import org.apache.syncope.core.persistence.api.entity.task.SchedTask;
+import org.apache.syncope.core.persistence.api.entity.task.TaskExec;
+import org.apache.syncope.core.persistence.jpa.entity.JPAImplementation;
+import org.apache.syncope.core.persistence.jpa.entity.JPARealm;
+import org.apache.syncope.core.provisioning.api.serialization.POJOHelper;
+
+@Entity
+@Table(name = JPAMacroTask.TABLE)
+public class JPAMacroTask extends JPASchedTask implements MacroTask {
+
+    private static final long serialVersionUID = 8261850094316787406L;
+
+    public static final String TABLE = "MacroTask";
+
+    protected static final TypeReference<List<CommandArgs>> TYPEREF = new TypeReference<List<CommandArgs>>() {
+    };
+
+    @ManyToOne(fetch = FetchType.EAGER, optional = false)
+    private JPARealm realm;
+
+    @NotNull
+    private Boolean continueOnError = false;
+
+    @NotNull
+    private Boolean saveExecs = true;
+
+    @ManyToMany(fetch = FetchType.EAGER)
+    @JoinTable(name = TABLE + "Commands",
+            joinColumns =
+            @JoinColumn(name = "task_id"),
+            inverseJoinColumns =
+            @JoinColumn(name = "implementation_id"),
+            uniqueConstraints =
+            @UniqueConstraint(columnNames = { "task_id", "implementation_id" }))
+    private List<JPAImplementation> commands = new ArrayList<>();
+
+    @Lob
+    private String commandArgs;
+
+    @Transient
+    private final List<CommandArgs> commandArgsList = new ArrayList<>();
+
+    @OneToMany(targetEntity = JPAMacroTaskExec.class,
+            cascade = CascadeType.ALL, orphanRemoval = true, mappedBy = "task")
+    private List<TaskExec<SchedTask>> executions = new ArrayList<>();
+
+    @Override
+    public Realm getRealm() {
+        return realm;
+    }
+
+    @Override
+    public void setRealm(final Realm realm) {
+        checkType(realm, JPARealm.class);
+        this.realm = (JPARealm) realm;
+    }
+
+    @Override
+    public void add(final Implementation command, final CommandArgs args) {
+        checkType(command, JPAImplementation.class);
+        checkImplementationType(command, IdRepoImplementationType.COMMAND);
+        commands.add((JPAImplementation) command);
+
+        getCommandArgs().add(args);
+    }
+
+    @Override
+    public List<JPAImplementation> getCommands() {
+        return commands;
+    }
+
+    @Override
+    public List<CommandArgs> getCommandArgs() {
+        return commandArgsList;
+    }
+
+    @Override
+    public boolean isContinueOnError() {
+        return continueOnError == null ? false : continueOnError;
+    }
+
+    @Override
+    public void setContinueOnError(final boolean continueOnError) {
+        this.continueOnError = continueOnError;
+    }
+
+    @Override
+    public boolean isSaveExecs() {
+        return saveExecs == null ? true : saveExecs;
+    }
+
+    @Override
+    public void setSaveExecs(final boolean saveExecs) {
+        this.saveExecs = saveExecs;
+    }
+
+    @Override
+    protected Class<? extends TaskExec<SchedTask>> executionClass() {
+        return JPAMacroTaskExec.class;
+    }
+
+    @Override
+    protected List<TaskExec<SchedTask>> executions() {
+        return executions;
+    }
+
+    protected void json2list(final boolean clearFirst) {
+        if (clearFirst) {
+            getCommandArgs().clear();
+        }
+        if (commandArgs != null) {
+            getCommandArgs().addAll(POJOHelper.deserialize(commandArgs, TYPEREF));
+        }
+    }
+
+    @PostLoad
+    public void postLoad() {
+        json2list(false);
+    }
+
+    @PostPersist
+    @PostUpdate
+    public void postSave() {
+        json2list(true);
+    }
+
+    @PrePersist
+    @PreUpdate
+    public void list2json() {
+        commandArgs = POJOHelper.serialize(getCommandArgs(), TYPEREF);
+    }
+}
diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/task/JPAMacroTaskExec.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/task/JPAMacroTaskExec.java
new file mode 100644
index 0000000000..a0e877a08a
--- /dev/null
+++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/task/JPAMacroTaskExec.java
@@ -0,0 +1,49 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.core.persistence.jpa.entity.task;
+
+import javax.persistence.Entity;
+import javax.persistence.ManyToOne;
+import javax.persistence.Table;
+import org.apache.syncope.core.persistence.api.entity.task.MacroTask;
+import org.apache.syncope.core.persistence.api.entity.task.SchedTask;
+import org.apache.syncope.core.persistence.api.entity.task.TaskExec;
+
+@Entity
+@Table(name = JPAMacroTaskExec.TABLE)
+public class JPAMacroTaskExec extends AbstractTaskExec<SchedTask> implements TaskExec<SchedTask> {
+
+    private static final long serialVersionUID = 1909033231464074554L;
+
+    public static final String TABLE = "MacroTaskExec";
+
+    @ManyToOne(optional = false)
+    private JPAMacroTask task;
+
+    @Override
+    public MacroTask getTask() {
+        return task;
+    }
+
+    @Override
+    public void setTask(final SchedTask task) {
+        checkType(task, MacroTask.class);
+        this.task = (JPAMacroTask) task;
+    }
+}
diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/task/JPANotificationTask.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/task/JPANotificationTask.java
index aeb7c5f137..995b98bb51 100644
--- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/task/JPANotificationTask.java
+++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/task/JPANotificationTask.java
@@ -54,6 +54,9 @@ public class JPANotificationTask extends AbstractTask<NotificationTask> implemen
 
     public static final String TABLE = "NotificationTask";
 
+    protected static final TypeReference<List<String>> TYPEREF = new TypeReference<List<String>>() {
+    };
+
     @NotNull
     @ManyToOne
     private JPANotification notification;
@@ -205,9 +208,7 @@ public class JPANotificationTask extends AbstractTask<NotificationTask> implemen
             getRecipients().clear();
         }
         if (recipients != null) {
-            getRecipients().addAll(
-                    POJOHelper.deserialize(recipients, new TypeReference<List<String>>() {
-                    }));
+            getRecipients().addAll(POJOHelper.deserialize(recipients, TYPEREF));
         }
     }
 
diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/task/JPASchedTask.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/task/JPASchedTask.java
index aca10f3a64..cd680aedae 100644
--- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/task/JPASchedTask.java
+++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/task/JPASchedTask.java
@@ -65,16 +65,6 @@ public class JPASchedTask extends AbstractTask<SchedTask> implements SchedTask {
             cascade = CascadeType.ALL, orphanRemoval = true, mappedBy = "task")
     private List<TaskExec<SchedTask>> executions = new ArrayList<>();
 
-    @Override
-    protected Class<? extends TaskExec<SchedTask>> executionClass() {
-        return JPASchedTaskExec.class;
-    }
-
-    @Override
-    protected List<TaskExec<SchedTask>> executions() {
-        return executions;
-    }
-
     @Override
     public Implementation getJobDelegate() {
         return jobDelegate;
@@ -136,4 +126,14 @@ public class JPASchedTask extends AbstractTask<SchedTask> implements SchedTask {
     public void setActive(final boolean active) {
         this.active = active;
     }
+
+    @Override
+    protected Class<? extends TaskExec<SchedTask>> executionClass() {
+        return JPASchedTaskExec.class;
+    }
+
+    @Override
+    protected List<TaskExec<SchedTask>> executions() {
+        return executions;
+    }
 }
diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/task/JPATaskUtils.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/task/JPATaskUtils.java
index df6f3a8ad1..42276473fa 100644
--- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/task/JPATaskUtils.java
+++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/task/JPATaskUtils.java
@@ -18,6 +18,7 @@
  */
 package org.apache.syncope.core.persistence.jpa.entity.task;
 
+import org.apache.syncope.common.lib.to.MacroTaskTO;
 import org.apache.syncope.common.lib.to.NotificationTaskTO;
 import org.apache.syncope.common.lib.to.PropagationTaskTO;
 import org.apache.syncope.common.lib.to.PullTaskTO;
@@ -25,24 +26,23 @@ import org.apache.syncope.common.lib.to.PushTaskTO;
 import org.apache.syncope.common.lib.to.SchedTaskTO;
 import org.apache.syncope.common.lib.to.TaskTO;
 import org.apache.syncope.common.lib.types.TaskType;
-import org.apache.syncope.core.persistence.api.entity.EntityFactory;
+import org.apache.syncope.core.persistence.api.entity.task.MacroTask;
 import org.apache.syncope.core.persistence.api.entity.task.NotificationTask;
 import org.apache.syncope.core.persistence.api.entity.task.PropagationTask;
 import org.apache.syncope.core.persistence.api.entity.task.PullTask;
 import org.apache.syncope.core.persistence.api.entity.task.PushTask;
 import org.apache.syncope.core.persistence.api.entity.task.SchedTask;
 import org.apache.syncope.core.persistence.api.entity.task.Task;
+import org.apache.syncope.core.persistence.api.entity.task.TaskExec;
 import org.apache.syncope.core.persistence.api.entity.task.TaskUtils;
+import org.apache.syncope.core.spring.security.SecureRandomUtils;
 
 @SuppressWarnings("unchecked")
 public final class JPATaskUtils implements TaskUtils {
 
-    protected final EntityFactory entityFactory;
-
     protected final TaskType type;
 
-    protected JPATaskUtils(final EntityFactory entityFactory, final TaskType type) {
-        this.entityFactory = entityFactory;
+    protected JPATaskUtils(final TaskType type) {
         this.type = type;
     }
 
@@ -72,6 +72,10 @@ public final class JPATaskUtils implements TaskUtils {
                 result = (Class<T>) PushTask.class;
                 break;
 
+            case MACRO:
+                result = (Class<T>) MacroTask.class;
+                break;
+
             case NOTIFICATION:
                 result = (Class<T>) NotificationTask.class;
                 break;
@@ -88,26 +92,74 @@ public final class JPATaskUtils implements TaskUtils {
 
         switch (type) {
             case PROPAGATION:
-                result = (T) entityFactory.newEntity(PropagationTask.class);
+                result = (T) new JPAPropagationTask();
                 break;
 
             case SCHEDULED:
-                result = (T) entityFactory.newEntity(SchedTask.class);
+                result = (T) new JPASchedTask();
                 break;
 
             case PULL:
-                result = (T) entityFactory.newEntity(PullTask.class);
+                result = (T) new JPAPullTask();
                 break;
 
             case PUSH:
-                result = (T) entityFactory.newEntity(PushTask.class);
+                result = (T) new JPAPushTask();
+                break;
+
+            case MACRO:
+                result = (T) new JPAMacroTask();
                 break;
 
             case NOTIFICATION:
-                result = (T) entityFactory.newEntity(NotificationTask.class);
+                result = (T) new JPANotificationTask();
+                break;
+
+            default:
+        }
+
+        if (result != null) {
+            ((AbstractTask<?>) result).setKey(SecureRandomUtils.generateRandomUUID().toString());
+        }
+
+        return result;
+    }
+
+    @Override
+    public <E extends TaskExec<?>> E newTaskExec() {
+        E result;
+
+        switch (type) {
+            case NOTIFICATION:
+                result = (E) new JPANotificationTaskExec();
+                break;
+
+            case PROPAGATION:
+                result = (E) new JPAPropagationTaskExec();
+                break;
+
+            case PULL:
+                result = (E) new JPAPullTaskExec();
+                break;
+
+            case PUSH:
+                result = (E) new JPAPushTaskExec();
+                break;
+
+            case MACRO:
+                result = (E) new JPAMacroTaskExec();
+                break;
+
+            case SCHEDULED:
+                result = (E) new JPASchedTaskExec();
                 break;
 
             default:
+                result = null;
+        }
+
+        if (result != null) {
+            ((AbstractTaskExec<?>) result).setKey(SecureRandomUtils.generateRandomUUID().toString());
         }
 
         return result;
@@ -134,6 +186,10 @@ public final class JPATaskUtils implements TaskUtils {
                 result = (Class<T>) PushTaskTO.class;
                 break;
 
+            case MACRO:
+                result = (Class<T>) MacroTaskTO.class;
+                break;
+
             case NOTIFICATION:
                 result = (Class<T>) NotificationTaskTO.class;
                 break;
@@ -153,4 +209,144 @@ public final class JPATaskUtils implements TaskUtils {
             return null;
         }
     }
+
+    @Override
+    public String getTaskTable() {
+        String result = null;
+
+        switch (type) {
+            case NOTIFICATION:
+                result = JPANotificationTask.TABLE;
+                break;
+
+            case PROPAGATION:
+                result = JPAPropagationTask.TABLE;
+                break;
+
+            case PUSH:
+                result = JPAPushTask.TABLE;
+                break;
+
+            case PULL:
+                result = JPAPullTask.TABLE;
+                break;
+
+            case MACRO:
+                result = JPAMacroTask.TABLE;
+                break;
+
+            case SCHEDULED:
+                result = JPASchedTask.TABLE;
+                break;
+
+            default:
+        }
+
+        return result;
+    }
+
+    @Override
+    public Class<? extends Task<?>> getTaskEntity() {
+        Class<? extends Task<?>> result = null;
+
+        switch (type) {
+            case NOTIFICATION:
+                result = JPANotificationTask.class;
+                break;
+
+            case PROPAGATION:
+                result = JPAPropagationTask.class;
+                break;
+
+            case PUSH:
+                result = JPAPushTask.class;
+                break;
+
+            case PULL:
+                result = JPAPullTask.class;
+                break;
+
+            case MACRO:
+                result = JPAMacroTask.class;
+                break;
+
+            case SCHEDULED:
+                result = JPASchedTask.class;
+                break;
+
+            default:
+        }
+
+        return result;
+    }
+
+    @Override
+    public String getTaskExecTable() {
+        String result = null;
+
+        switch (type) {
+            case NOTIFICATION:
+                result = JPANotificationTaskExec.TABLE;
+                break;
+
+            case PROPAGATION:
+                result = JPAPropagationTaskExec.TABLE;
+                break;
+
+            case SCHEDULED:
+                result = JPASchedTaskExec.TABLE;
+                break;
+
+            case PUSH:
+                result = JPAPushTaskExec.TABLE;
+                break;
+
+            case PULL:
+                result = JPAPullTaskExec.TABLE;
+                break;
+
+            case MACRO:
+                result = JPAMacroTaskExec.TABLE;
+                break;
+
+            default:
+        }
+
+        return result;
+    }
+
+    @Override
+    public Class<? extends TaskExec<?>> getTaskExecEntity() {
+        Class<? extends TaskExec<?>> result = null;
+
+        switch (type) {
+            case NOTIFICATION:
+                result = JPANotificationTaskExec.class;
+                break;
+
+            case PROPAGATION:
+                result = JPAPropagationTaskExec.class;
+                break;
+
+            case SCHEDULED:
+                result = JPASchedTaskExec.class;
+                break;
+
+            case PUSH:
+                result = JPAPushTaskExec.class;
+                break;
+
+            case PULL:
+                result = JPAPullTaskExec.class;
+                break;
+
+            case MACRO:
+                result = JPAMacroTaskExec.class;
+                break;
+
+            default:
+        }
+
+        return result;
+    }
 }
diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/task/JPATaskUtilsFactory.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/task/JPATaskUtilsFactory.java
index 53c9575641..35cce33c29 100644
--- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/task/JPATaskUtilsFactory.java
+++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/task/JPATaskUtilsFactory.java
@@ -20,6 +20,7 @@ package org.apache.syncope.core.persistence.jpa.entity.task;
 
 import java.util.HashMap;
 import java.util.Map;
+import org.apache.syncope.common.lib.to.MacroTaskTO;
 import org.apache.syncope.common.lib.to.NotificationTaskTO;
 import org.apache.syncope.common.lib.to.PropagationTaskTO;
 import org.apache.syncope.common.lib.to.PullTaskTO;
@@ -27,7 +28,7 @@ import org.apache.syncope.common.lib.to.PushTaskTO;
 import org.apache.syncope.common.lib.to.SchedTaskTO;
 import org.apache.syncope.common.lib.to.TaskTO;
 import org.apache.syncope.common.lib.types.TaskType;
-import org.apache.syncope.core.persistence.api.entity.EntityFactory;
+import org.apache.syncope.core.persistence.api.entity.task.MacroTask;
 import org.apache.syncope.core.persistence.api.entity.task.NotificationTask;
 import org.apache.syncope.core.persistence.api.entity.task.PropagationTask;
 import org.apache.syncope.core.persistence.api.entity.task.PullTask;
@@ -40,21 +41,15 @@ import org.apache.syncope.core.spring.ApplicationContextProvider;
 
 public class JPATaskUtilsFactory implements TaskUtilsFactory {
 
-    protected final EntityFactory entityFactory;
-
     protected final Map<TaskType, TaskUtils> instances = new HashMap<>(5);
 
-    public JPATaskUtilsFactory(final EntityFactory entityFactory) {
-        this.entityFactory = entityFactory;
-    }
-
     @Override
     public TaskUtils getInstance(final TaskType type) {
         TaskUtils instance;
         synchronized (instances) {
             instance = instances.get(type);
             if (instance == null) {
-                instance = new JPATaskUtils(entityFactory, type);
+                instance = new JPATaskUtils(type);
                 ApplicationContextProvider.getBeanFactory().autowireBean(instance);
                 instances.put(type, instance);
             }
@@ -70,6 +65,8 @@ public class JPATaskUtilsFactory implements TaskUtilsFactory {
             type = TaskType.PULL;
         } else if (task instanceof PushTask) {
             type = TaskType.PUSH;
+        } else if (task instanceof MacroTask) {
+            type = TaskType.MACRO;
         } else if (task instanceof SchedTask) {
             type = TaskType.SCHEDULED;
         } else if (task instanceof PropagationTask) {
@@ -96,6 +93,8 @@ public class JPATaskUtilsFactory implements TaskUtilsFactory {
             type = TaskType.PULL;
         } else if (taskClass == PushTaskTO.class) {
             type = TaskType.PUSH;
+        } else if (taskClass == MacroTaskTO.class) {
+            type = TaskType.MACRO;
         } else {
             throw new IllegalArgumentException("Invalid TaskTO class: " + taskClass.getName());
         }
diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/user/JPAUser.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/user/JPAUser.java
index 28fad86e1d..abd9b71e6f 100644
--- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/user/JPAUser.java
+++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/user/JPAUser.java
@@ -78,13 +78,16 @@ public class JPAUser
 
     public static final String TABLE = "SyncopeUser";
 
-    private static final Encryptor ENCRYPTOR = Encryptor.getInstance();
+    protected static final Encryptor ENCRYPTOR = Encryptor.getInstance();
+
+    protected static final TypeReference<List<String>> TYPEREF = new TypeReference<List<String>>() {
+    };
 
     @Column(nullable = true)
-    private String password;
+    protected String password;
 
     @Transient
-    private String clearPassword;
+    protected String clearPassword;
 
     @ManyToMany(fetch = FetchType.EAGER)
     @JoinTable(joinColumns =
@@ -93,53 +96,53 @@ public class JPAUser
             @JoinColumn(name = "role_id"),
             uniqueConstraints =
             @UniqueConstraint(columnNames = { "user_id", "role_id" }))
-    private List<JPARole> roles = new ArrayList<>();
+    protected List<JPARole> roles = new ArrayList<>();
 
     @OneToMany(cascade = CascadeType.ALL, mappedBy = "owner")
     @Valid
-    private List<JPAUPlainAttr> plainAttrs = new ArrayList<>();
+    protected List<JPAUPlainAttr> plainAttrs = new ArrayList<>();
 
     @Column(nullable = true)
-    private String status;
+    protected String status;
 
     @Lob
-    private String token;
+    protected String token;
 
-    private OffsetDateTime tokenExpireTime;
+    protected OffsetDateTime tokenExpireTime;
 
     @Column(nullable = true)
     @Enumerated(EnumType.STRING)
-    private CipherAlgorithm cipherAlgorithm;
+    protected CipherAlgorithm cipherAlgorithm;
 
     @Lob
-    private String passwordHistory;
+    protected String passwordHistory;
 
     /**
      * Subsequent failed logins.
      */
     @Column(nullable = true)
-    private Integer failedLogins;
+    protected Integer failedLogins;
 
     /**
      * Username/Login.
      */
     @Column(unique = true)
     @NotNull(message = "Blank username")
-    private String username;
+    protected String username;
 
     /**
      * Last successful login date.
      */
-    private OffsetDateTime lastLoginDate;
+    protected OffsetDateTime lastLoginDate;
 
     /**
      * Change password date.
      */
-    private OffsetDateTime changePwdDate;
+    protected OffsetDateTime changePwdDate;
 
-    private Boolean suspended = false;
+    protected Boolean suspended = false;
 
-    private Boolean mustChangePassword = false;
... 2301 lines suppressed ...