thingsboard-aplcache

Changes

application/src/main/java/org/thingsboard/server/actors/rule/ChainProcessingContext.java 117(+0 -117)

application/src/main/java/org/thingsboard/server/actors/rule/RuleActor.java 90(+0 -90)

application/src/main/java/org/thingsboard/server/actors/rule/RuleActorMessageProcessor.java 345(+0 -345)

application/src/main/java/org/thingsboard/server/actors/rule/RuleActorMetaData.java 107(+0 -107)

application/src/main/java/org/thingsboard/server/actors/rule/RuleProcessingContext.java 115(+0 -115)

application/src/main/java/org/thingsboard/server/actors/shared/rule/RuleManager.java 135(+0 -135)

dao/src/test/java/org/thingsboard/server/dao/service/rule/BaseRuleServiceTest.java 163(+0 -163)

pom.xml 8(+7 -1)

ui/package.json 3(+2 -1)

ui/server.js 20(+20 -0)

Details

diff --git a/application/pom.xml b/application/pom.xml
index 8449246..6da40a9 100644
--- a/application/pom.xml
+++ b/application/pom.xml
@@ -54,6 +54,14 @@
             <artifactId>extensions-api</artifactId>
         </dependency>
         <dependency>
+            <groupId>org.thingsboard.rule-engine</groupId>
+            <artifactId>rule-engine-api</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.thingsboard.rule-engine</groupId>
+            <artifactId>rule-engine-components</artifactId>
+        </dependency>
+        <dependency>
             <groupId>org.thingsboard</groupId>
             <artifactId>extensions-core</artifactId>
         </dependency>
diff --git a/application/src/main/data/upgrade/1.5.0/schema_update.cql b/application/src/main/data/upgrade/1.5.0/schema_update.cql
index aa8b10b..5cdaede 100644
--- a/application/src/main/data/upgrade/1.5.0/schema_update.cql
+++ b/application/src/main/data/upgrade/1.5.0/schema_update.cql
@@ -69,6 +69,7 @@ CREATE TABLE IF NOT EXISTS  thingsboard.rule_chain (
     search_text text,
     first_rule_node_id uuid,
     root boolean,
+    debug_mode boolean,
     configuration text,
     additional_info text,
     PRIMARY KEY (id, tenant_id)
@@ -85,9 +86,12 @@ CREATE TABLE IF NOT EXISTS  thingsboard.rule_node (
     id uuid,
     type text,
     name text,
+    debug_mode boolean,
     search_text text,
     configuration text,
     additional_info text,
     PRIMARY KEY (id)
 );
 
+ALTER TABLE thingsboard.device ADD last_connect bigint;
+ALTER TABLE thingsboard.device ADD last_update bigint;
\ No newline at end of file
diff --git a/application/src/main/data/upgrade/1.5.0/schema_update.sql b/application/src/main/data/upgrade/1.5.0/schema_update.sql
index 0043ed5..ab91166 100644
--- a/application/src/main/data/upgrade/1.5.0/schema_update.sql
+++ b/application/src/main/data/upgrade/1.5.0/schema_update.sql
@@ -21,6 +21,7 @@ CREATE TABLE IF NOT EXISTS rule_chain (
     name varchar(255),
     first_rule_node_id varchar(31),
     root boolean,
+    debug_mode boolean,
     search_text varchar(255),
     tenant_id varchar(31)
 );
@@ -31,5 +32,9 @@ CREATE TABLE IF NOT EXISTS rule_node (
     configuration varchar(10000000),
     type varchar(255),
     name varchar(255),
+    debug_mode boolean,
     search_text varchar(255)
-);
\ No newline at end of file
+);
+
+ALTER TABLE device ADD COLUMN IF NOT EXISTS last_connect BIGINT;
+ALTER TABLE device ADD COLUMN IF NOT EXISTS last_update BIGINT;
\ No newline at end of file
diff --git a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
index 77953c9..50d2530 100644
--- a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
+++ b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
@@ -25,15 +25,19 @@ import com.typesafe.config.Config;
 import com.typesafe.config.ConfigFactory;
 import lombok.Getter;
 import lombok.Setter;
+import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Component;
+import org.thingsboard.rule.engine.api.ListeningExecutor;
+import org.thingsboard.rule.engine.api.MailService;
 import org.thingsboard.server.actors.service.ActorService;
 import org.thingsboard.server.common.data.DataConstants;
 import org.thingsboard.server.common.data.Event;
 import org.thingsboard.server.common.data.id.EntityId;
 import org.thingsboard.server.common.data.id.TenantId;
 import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
+import org.thingsboard.server.common.msg.TbMsg;
 import org.thingsboard.server.common.msg.cluster.ServerAddress;
 import org.thingsboard.server.common.transport.auth.DeviceAuthService;
 import org.thingsboard.server.controller.plugin.PluginWebSocketMsgEndpoint;
@@ -46,116 +50,200 @@ import org.thingsboard.server.dao.device.DeviceService;
 import org.thingsboard.server.dao.event.EventService;
 import org.thingsboard.server.dao.plugin.PluginService;
 import org.thingsboard.server.dao.relation.RelationService;
+import org.thingsboard.server.dao.rule.RuleChainService;
 import org.thingsboard.server.dao.rule.RuleService;
 import org.thingsboard.server.dao.tenant.TenantService;
 import org.thingsboard.server.dao.timeseries.TimeseriesService;
+import org.thingsboard.server.dao.user.UserService;
 import org.thingsboard.server.service.cluster.discovery.DiscoveryService;
 import org.thingsboard.server.service.cluster.routing.ClusterRoutingService;
 import org.thingsboard.server.service.cluster.rpc.ClusterRpcService;
 import org.thingsboard.server.service.component.ComponentDiscoveryService;
+import org.thingsboard.server.service.executors.DbCallbackExecutorService;
+import org.thingsboard.server.service.mail.MailExecutorService;
+import org.thingsboard.server.service.script.JsExecutorService;
+import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService;
 
+import java.io.IOException;
 import java.io.PrintWriter;
 import java.io.StringWriter;
 import java.util.Optional;
 
+@Slf4j
 @Component
 public class ActorSystemContext {
     private static final String AKKA_CONF_FILE_NAME = "actor-system.conf";
 
     protected final ObjectMapper mapper = new ObjectMapper();
 
-    @Getter @Setter private ActorService actorService;
+    @Getter
+    @Setter
+    private ActorService actorService;
 
     @Autowired
-    @Getter private DiscoveryService discoveryService;
+    @Getter
+    private DiscoveryService discoveryService;
 
     @Autowired
-    @Getter @Setter private ComponentDiscoveryService componentService;
+    @Getter
+    @Setter
+    private ComponentDiscoveryService componentService;
 
     @Autowired
-    @Getter private ClusterRoutingService routingService;
+    @Getter
+    private ClusterRoutingService routingService;
 
     @Autowired
-    @Getter private ClusterRpcService rpcService;
+    @Getter
+    private ClusterRpcService rpcService;
 
     @Autowired
-    @Getter private DeviceAuthService deviceAuthService;
+    @Getter
+    private DeviceAuthService deviceAuthService;
 
     @Autowired
-    @Getter private DeviceService deviceService;
+    @Getter
+    private DeviceService deviceService;
 
     @Autowired
-    @Getter private AssetService assetService;
+    @Getter
+    private AssetService assetService;
 
     @Autowired
-    @Getter private TenantService tenantService;
+    @Getter
+    private TenantService tenantService;
 
     @Autowired
-    @Getter private CustomerService customerService;
+    @Getter
+    private CustomerService customerService;
 
     @Autowired
-    @Getter private RuleService ruleService;
+    @Getter
+    private UserService userService;
 
     @Autowired
-    @Getter private PluginService pluginService;
+    @Getter
+    private RuleService ruleService;
 
     @Autowired
-    @Getter private TimeseriesService tsService;
+    @Getter
+    private RuleChainService ruleChainService;
 
     @Autowired
-    @Getter private AttributesService attributesService;
+    @Getter
+    private PluginService pluginService;
 
     @Autowired
-    @Getter private EventService eventService;
+    @Getter
+    private TimeseriesService tsService;
 
     @Autowired
-    @Getter private AlarmService alarmService;
+    @Getter
+    private AttributesService attributesService;
 
     @Autowired
-    @Getter private RelationService relationService;
+    @Getter
+    private EventService eventService;
 
     @Autowired
-    @Getter private AuditLogService auditLogService;
+    @Getter
+    private AlarmService alarmService;
 
     @Autowired
-    @Getter @Setter private PluginWebSocketMsgEndpoint wsMsgEndpoint;
+    @Getter
+    private RelationService relationService;
+
+    @Autowired
+    @Getter
+    private AuditLogService auditLogService;
+
+    @Autowired
+    @Getter
+    private TelemetrySubscriptionService tsSubService;
+
+    @Autowired
+    @Getter
+    @Setter
+    private PluginWebSocketMsgEndpoint wsMsgEndpoint;
+
+    @Autowired
+    @Getter
+    private JsExecutorService jsExecutor;
+
+    @Autowired
+    @Getter
+    private MailExecutorService mailExecutor;
+
+    @Autowired
+    @Getter
+    private DbCallbackExecutorService dbCallbackExecutor;
+
+    @Autowired
+    @Getter
+    private MailService mailService;
 
     @Value("${actors.session.sync.timeout}")
-    @Getter private long syncSessionTimeout;
+    @Getter
+    private long syncSessionTimeout;
 
     @Value("${actors.plugin.termination.delay}")
-    @Getter private long pluginActorTerminationDelay;
+    @Getter
+    private long pluginActorTerminationDelay;
 
     @Value("${actors.plugin.processing.timeout}")
-    @Getter private long pluginProcessingTimeout;
+    @Getter
+    private long pluginProcessingTimeout;
 
     @Value("${actors.plugin.error_persist_frequency}")
-    @Getter private long pluginErrorPersistFrequency;
+    @Getter
+    private long pluginErrorPersistFrequency;
+
+    @Value("${actors.rule.chain.error_persist_frequency}")
+    @Getter
+    private long ruleChainErrorPersistFrequency;
+
+    @Value("${actors.rule.node.error_persist_frequency}")
+    @Getter
+    private long ruleNodeErrorPersistFrequency;
 
     @Value("${actors.rule.termination.delay}")
-    @Getter private long ruleActorTerminationDelay;
+    @Getter
+    private long ruleActorTerminationDelay;
 
     @Value("${actors.rule.error_persist_frequency}")
-    @Getter private long ruleErrorPersistFrequency;
+    @Getter
+    private long ruleErrorPersistFrequency;
 
     @Value("${actors.statistics.enabled}")
-    @Getter private boolean statisticsEnabled;
+    @Getter
+    private boolean statisticsEnabled;
 
     @Value("${actors.statistics.persist_frequency}")
-    @Getter private long statisticsPersistFrequency;
+    @Getter
+    private long statisticsPersistFrequency;
 
     @Value("${actors.tenant.create_components_on_init}")
-    @Getter private boolean tenantComponentsInitEnabled;
+    @Getter
+    private boolean tenantComponentsInitEnabled;
 
-    @Getter @Setter private ActorSystem actorSystem;
+    @Getter
+    @Setter
+    private ActorSystem actorSystem;
 
-    @Getter @Setter private ActorRef appActor;
+    @Getter
+    @Setter
+    private ActorRef appActor;
 
-    @Getter @Setter private ActorRef sessionManagerActor;
+    @Getter
+    @Setter
+    private ActorRef sessionManagerActor;
 
-    @Getter @Setter private ActorRef statsActor;
+    @Getter
+    @Setter
+    private ActorRef statsActor;
 
-    @Getter private final Config config;
+    @Getter
+    private final Config config;
 
     public ActorSystemContext() {
         config = ConfigFactory.parseResources(AKKA_CONF_FILE_NAME).withFallback(ConfigFactory.load());
@@ -187,7 +275,7 @@ public class ActorSystemContext {
         eventService.save(event);
     }
 
-    private String toString(Exception e) {
+    private String toString(Throwable e) {
         StringWriter sw = new StringWriter();
         e.printStackTrace(new PrintWriter(sw));
         return sw.toString();
@@ -207,4 +295,60 @@ public class ActorSystemContext {
     private JsonNode toBodyJson(ServerAddress server, String method, String body) {
         return mapper.createObjectNode().put("server", server.toString()).put("method", method).put("error", body);
     }
+
+    public String getServerAddress() {
+        return discoveryService.getCurrentServer().getServerAddress().toString();
+    }
+
+    public void persistDebugInput(TenantId tenantId, EntityId entityId, TbMsg tbMsg) {
+        persistDebug(tenantId, entityId, "IN", tbMsg, null);
+    }
+
+    public void persistDebugInput(TenantId tenantId, EntityId entityId, TbMsg tbMsg, Throwable error) {
+        persistDebug(tenantId, entityId, "IN", tbMsg, error);
+    }
+
+    public void persistDebugOutput(TenantId tenantId, EntityId entityId, TbMsg tbMsg, Throwable error) {
+        persistDebug(tenantId, entityId, "OUT", tbMsg, error);
+    }
+
+    public void persistDebugOutput(TenantId tenantId, EntityId entityId, TbMsg tbMsg) {
+        persistDebug(tenantId, entityId, "OUT", tbMsg, null);
+    }
+
+    private void persistDebug(TenantId tenantId, EntityId entityId, String type, TbMsg tbMsg, Throwable error) {
+        try {
+            Event event = new Event();
+            event.setTenantId(tenantId);
+            event.setEntityId(entityId);
+            event.setType(DataConstants.DEBUG_RULE_NODE);
+
+            String metadata = mapper.writeValueAsString(tbMsg.getMetaData().getData());
+
+            ObjectNode node = mapper.createObjectNode()
+                    .put("type", type)
+                    .put("server", getServerAddress())
+                    .put("entityId", tbMsg.getOriginator().getId().toString())
+                    .put("entityName", tbMsg.getOriginator().getEntityType().name())
+                    .put("msgId", tbMsg.getId().toString())
+                    .put("msgType", tbMsg.getType())
+                    .put("dataType", tbMsg.getDataType().name())
+                    .put("data", tbMsg.getData())
+                    .put("metadata", metadata);
+
+            if (error != null) {
+                node = node.put("error", toString(error));
+            }
+
+            event.setBody(node);
+            eventService.save(event);
+        } catch (IOException ex) {
+            log.warn("Failed to persist rule node debug message", ex);
+        }
+    }
+
+    public static Exception toException(Throwable error) {
+        return Exception.class.isInstance(error) ? (Exception) error : new Exception(error);
+    }
+
 }
diff --git a/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java b/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java
index b475277..a75158f 100644
--- a/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java
@@ -22,48 +22,41 @@ import akka.event.LoggingAdapter;
 import akka.japi.Function;
 import org.thingsboard.server.actors.ActorSystemContext;
 import org.thingsboard.server.actors.plugin.PluginTerminationMsg;
-import org.thingsboard.server.actors.service.ContextAwareActor;
+import org.thingsboard.server.actors.ruleChain.RuleChainManagerActor;
 import org.thingsboard.server.actors.service.ContextBasedCreator;
 import org.thingsboard.server.actors.service.DefaultActorService;
-import org.thingsboard.server.actors.shared.plugin.PluginManager;
 import org.thingsboard.server.actors.shared.plugin.SystemPluginManager;
-import org.thingsboard.server.actors.shared.rule.RuleManager;
-import org.thingsboard.server.actors.shared.rule.SystemRuleManager;
-import org.thingsboard.server.actors.tenant.RuleChainDeviceMsg;
+import org.thingsboard.server.actors.shared.rulechain.SystemRuleChainManager;
 import org.thingsboard.server.actors.tenant.TenantActor;
 import org.thingsboard.server.common.data.Tenant;
 import org.thingsboard.server.common.data.id.PluginId;
-import org.thingsboard.server.common.data.id.RuleId;
+import org.thingsboard.server.common.data.id.RuleChainId;
 import org.thingsboard.server.common.data.id.TenantId;
 import org.thingsboard.server.common.data.page.PageDataIterable;
+import org.thingsboard.server.common.msg.TbActorMsg;
 import org.thingsboard.server.common.msg.cluster.ClusterEventMsg;
 import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
 import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
+import org.thingsboard.server.common.msg.system.ServiceToRuleEngineMsg;
 import org.thingsboard.server.dao.model.ModelConstants;
 import org.thingsboard.server.dao.tenant.TenantService;
 import org.thingsboard.server.extensions.api.device.ToDeviceActorNotificationMsg;
 import org.thingsboard.server.extensions.api.plugins.msg.ToPluginActorMsg;
-import org.thingsboard.server.extensions.api.rules.ToRuleActorMsg;
 import scala.concurrent.duration.Duration;
 
 import java.util.HashMap;
 import java.util.Map;
-import java.util.Optional;
 
-public class AppActor extends ContextAwareActor {
+public class AppActor extends RuleChainManagerActor {
 
     private final LoggingAdapter logger = Logging.getLogger(getContext().system(), this);
 
     public static final TenantId SYSTEM_TENANT = new TenantId(ModelConstants.NULL_UUID);
-    private final RuleManager ruleManager;
-    private final PluginManager pluginManager;
     private final TenantService tenantService;
     private final Map<TenantId, ActorRef> tenantActors;
 
     private AppActor(ActorSystemContext systemContext) {
-        super(systemContext);
-        this.ruleManager = new SystemRuleManager(systemContext);
-        this.pluginManager = new SystemPluginManager(systemContext);
+        super(systemContext, new SystemRuleChainManager(systemContext), new SystemPluginManager(systemContext));
         this.tenantService = systemContext.getTenantService();
         this.tenantActors = new HashMap<>();
     }
@@ -77,8 +70,7 @@ public class AppActor extends ContextAwareActor {
     public void preStart() {
         logger.info("Starting main system actor.");
         try {
-            ruleManager.init(this.context());
-            pluginManager.init(this.context());
+            initRuleChains();
 
             if (systemContext.isTenantComponentsInitEnabled()) {
                 PageDataIterable<Tenant> tenantIterator = new PageDataIterable<>(tenantService::findTenants, ENTITY_PACK_LIMIT);
@@ -96,29 +88,51 @@ public class AppActor extends ContextAwareActor {
     }
 
     @Override
-    public void onReceive(Object msg) throws Exception {
-        logger.debug("Received message: {}", msg);
-        if (msg instanceof ToDeviceActorMsg) {
-            processDeviceMsg((ToDeviceActorMsg) msg);
-        } else if (msg instanceof ToPluginActorMsg) {
-            onToPluginMsg((ToPluginActorMsg) msg);
-        } else if (msg instanceof ToRuleActorMsg) {
-            onToRuleMsg((ToRuleActorMsg) msg);
-        } else if (msg instanceof ToDeviceActorNotificationMsg) {
-            onToDeviceActorMsg((ToDeviceActorNotificationMsg) msg);
-        } else if (msg instanceof Terminated) {
-            processTermination((Terminated) msg);
-        } else if (msg instanceof ClusterEventMsg) {
-            broadcast(msg);
-        } else if (msg instanceof ComponentLifecycleMsg) {
-            onComponentLifecycleMsg((ComponentLifecycleMsg) msg);
-        } else if (msg instanceof PluginTerminationMsg) {
-            onPluginTerminated((PluginTerminationMsg) msg);
+    protected boolean process(TbActorMsg msg) {
+        switch (msg.getMsgType()) {
+            case COMPONENT_LIFE_CYCLE_MSG:
+                onComponentLifecycleMsg((ComponentLifecycleMsg) msg);
+                break;
+            case SERVICE_TO_RULE_ENGINE_MSG:
+                onServiceToRuleEngineMsg((ServiceToRuleEngineMsg) msg);
+                break;
+            default:
+                return false;
+        }
+        return true;
+    }
+
+    private void onServiceToRuleEngineMsg(ServiceToRuleEngineMsg msg) {
+        if (SYSTEM_TENANT.equals(msg.getTenantId())) {
+            //TODO: ashvayka handle this.
         } else {
-            logger.warning("Unknown message: {}!", msg);
+            getOrCreateTenantActor(msg.getTenantId()).tell(msg, self());
         }
     }
 
+
+//    @Override
+//    public void onReceive(Object msg) throws Exception {
+//        logger.debug("Received message: {}", msg);
+//        if (msg instanceof ToDeviceActorMsg) {
+//            processDeviceMsg((ToDeviceActorMsg) msg);
+//        } else if (msg instanceof ToPluginActorMsg) {
+//            onToPluginMsg((ToPluginActorMsg) msg);
+//        } else if (msg instanceof ToDeviceActorNotificationMsg) {
+//            onToDeviceActorMsg((ToDeviceActorNotificationMsg) msg);
+//        } else if (msg instanceof Terminated) {
+//            processTermination((Terminated) msg);
+//        } else if (msg instanceof ClusterEventMsg) {
+//            broadcast(msg);
+//        } else if (msg instanceof ComponentLifecycleMsg) {
+//            onComponentLifecycleMsg((ComponentLifecycleMsg) msg);
+//        } else if (msg instanceof PluginTerminationMsg) {
+//            onPluginTerminated((PluginTerminationMsg) msg);
+//        } else {
+//            logger.warning("Unknown message: {}!", msg);
+//        }
+//    }
+
     private void onPluginTerminated(PluginTerminationMsg msg) {
         pluginManager.remove(msg.getId());
     }
@@ -128,20 +142,10 @@ public class AppActor extends ContextAwareActor {
         tenantActors.values().forEach(actorRef -> actorRef.tell(msg, ActorRef.noSender()));
     }
 
-    private void onToRuleMsg(ToRuleActorMsg msg) {
-        ActorRef target;
-        if (SYSTEM_TENANT.equals(msg.getTenantId())) {
-            target = ruleManager.getOrCreateRuleActor(this.context(), msg.getRuleId());
-        } else {
-            target = getOrCreateTenantActor(msg.getTenantId());
-        }
-        target.tell(msg, ActorRef.noSender());
-    }
-
     private void onToPluginMsg(ToPluginActorMsg msg) {
         ActorRef target;
         if (SYSTEM_TENANT.equals(msg.getPluginTenantId())) {
-            target = pluginManager.getOrCreatePluginActor(this.context(), msg.getPluginId());
+            target = pluginManager.getOrCreateActor(this.context(), msg.getPluginId());
         } else {
             target = getOrCreateTenantActor(msg.getPluginTenantId());
         }
@@ -149,26 +153,16 @@ public class AppActor extends ContextAwareActor {
     }
 
     private void onComponentLifecycleMsg(ComponentLifecycleMsg msg) {
-        ActorRef target = null;
+        ActorRef target;
         if (SYSTEM_TENANT.equals(msg.getTenantId())) {
-            Optional<PluginId> pluginId = msg.getPluginId();
-            Optional<RuleId> ruleId = msg.getRuleId();
-            if (pluginId.isPresent()) {
-                target = pluginManager.getOrCreatePluginActor(this.context(), pluginId.get());
-            } else if (ruleId.isPresent()) {
-                Optional<ActorRef> ref = ruleManager.update(this.context(), ruleId.get(), msg.getEvent());
-                if (ref.isPresent()) {
-                    target = ref.get();
-                } else {
-                    logger.debug("Failed to find actor for rule: [{}]", ruleId);
-                    return;
-                }
-            }
+            target = getEntityActorRef(msg.getEntityId());
         } else {
             target = getOrCreateTenantActor(msg.getTenantId());
         }
         if (target != null) {
             target.tell(msg, ActorRef.noSender());
+        } else {
+            logger.debug("Invalid component lifecycle msg: {}", msg);
         }
     }
 
@@ -180,7 +174,7 @@ public class AppActor extends ContextAwareActor {
         TenantId tenantId = toDeviceActorMsg.getTenantId();
         ActorRef tenantActor = getOrCreateTenantActor(tenantId);
         if (toDeviceActorMsg.getPayload().getMsgType().requiresRulesProcessing()) {
-            tenantActor.tell(new RuleChainDeviceMsg(toDeviceActorMsg, ruleManager.getRuleChain(this.context())), context().self());
+//            tenantActor.tell(new RuleChainDeviceMsg(toDeviceActorMsg, ruleManager.getRuleChain(this.context())), context().self());
         } else {
             tenantActor.tell(toDeviceActorMsg, context().self());
         }
diff --git a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActor.java b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActor.java
index 861c405..87bc992 100644
--- a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActor.java
@@ -18,19 +18,19 @@ package org.thingsboard.server.actors.device;
 import akka.event.Logging;
 import akka.event.LoggingAdapter;
 import org.thingsboard.server.actors.ActorSystemContext;
-import org.thingsboard.server.actors.rule.RulesProcessedMsg;
 import org.thingsboard.server.actors.service.ContextAwareActor;
 import org.thingsboard.server.actors.service.ContextBasedCreator;
-import org.thingsboard.server.actors.tenant.RuleChainDeviceMsg;
 import org.thingsboard.server.common.data.id.DeviceId;
 import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.msg.TbActorMsg;
 import org.thingsboard.server.common.msg.cluster.ClusterEventMsg;
 import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
 import org.thingsboard.server.extensions.api.device.DeviceAttributesEventNotificationMsg;
 import org.thingsboard.server.extensions.api.device.DeviceCredentialsUpdateNotificationMsg;
 import org.thingsboard.server.extensions.api.device.DeviceNameOrTypeUpdateMsg;
 import org.thingsboard.server.extensions.api.device.ToDeviceActorNotificationMsg;
-import org.thingsboard.server.extensions.api.plugins.msg.*;
+import org.thingsboard.server.extensions.api.plugins.msg.TimeoutMsg;
+import org.thingsboard.server.extensions.api.plugins.msg.ToDeviceRpcRequestPluginMsg;
 
 public class DeviceActor extends ContextAwareActor {
 
@@ -48,12 +48,17 @@ public class DeviceActor extends ContextAwareActor {
     }
 
     @Override
+    protected boolean process(TbActorMsg msg) {
+        return false;
+    }
+
+    @Override
     public void onReceive(Object msg) throws Exception {
-        if (msg instanceof RuleChainDeviceMsg) {
-            processor.process(context(), (RuleChainDeviceMsg) msg);
-        } else if (msg instanceof RulesProcessedMsg) {
-            processor.onRulesProcessedMsg(context(), (RulesProcessedMsg) msg);
-        } else if (msg instanceof ToDeviceActorMsg) {
+//        if (msg instanceof RuleChainDeviceMsg) {
+//            processor.process(context(), (RuleChainDeviceMsg) msg);
+//        } else if (msg instanceof RulesProcessedMsg) {
+//            processor.onRulesProcessedMsg(context(), (RulesProcessedMsg) msg);
+        if (msg instanceof ToDeviceActorMsg) {
             processor.process(context(), (ToDeviceActorMsg) msg);
         } else if (msg instanceof ToDeviceActorNotificationMsg) {
             if (msg instanceof DeviceAttributesEventNotificationMsg) {
diff --git a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java
index 21112bf..3644a49 100644
--- a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java
@@ -19,9 +19,7 @@ import akka.actor.ActorContext;
 import akka.actor.ActorRef;
 import akka.event.LoggingAdapter;
 import org.thingsboard.server.actors.ActorSystemContext;
-import org.thingsboard.server.actors.rule.*;
 import org.thingsboard.server.actors.shared.AbstractContextAwareMsgProcessor;
-import org.thingsboard.server.actors.tenant.RuleChainDeviceMsg;
 import org.thingsboard.server.common.data.DataConstants;
 import org.thingsboard.server.common.data.Device;
 import org.thingsboard.server.common.data.id.DeviceId;
@@ -37,15 +35,10 @@ import org.thingsboard.server.common.msg.session.FromDeviceMsg;
 import org.thingsboard.server.common.msg.session.MsgType;
 import org.thingsboard.server.common.msg.session.SessionType;
 import org.thingsboard.server.common.msg.session.ToDeviceMsg;
-import org.thingsboard.server.extensions.api.device.*;
-import org.thingsboard.server.extensions.api.plugins.msg.FromDeviceRpcResponse;
-import org.thingsboard.server.extensions.api.plugins.msg.RpcError;
-import org.thingsboard.server.extensions.api.plugins.msg.TimeoutIntMsg;
-import org.thingsboard.server.extensions.api.plugins.msg.TimeoutMsg;
-import org.thingsboard.server.extensions.api.plugins.msg.ToDeviceRpcRequest;
-import org.thingsboard.server.extensions.api.plugins.msg.ToDeviceRpcRequestBody;
-import org.thingsboard.server.extensions.api.plugins.msg.ToDeviceRpcRequestPluginMsg;
-import org.thingsboard.server.extensions.api.plugins.msg.ToPluginRpcResponseDeviceMsg;
+import org.thingsboard.server.extensions.api.device.DeviceAttributes;
+import org.thingsboard.server.extensions.api.device.DeviceAttributesEventNotificationMsg;
+import org.thingsboard.server.extensions.api.device.DeviceNameOrTypeUpdateMsg;
+import org.thingsboard.server.extensions.api.plugins.msg.*;
 
 import java.util.*;
 import java.util.concurrent.ExecutionException;
@@ -230,18 +223,18 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
         }
     }
 
-    void process(ActorContext context, RuleChainDeviceMsg srcMsg) {
-        ChainProcessingMetaData md = new ChainProcessingMetaData(srcMsg.getRuleChain(),
-                srcMsg.getToDeviceActorMsg(), new DeviceMetaData(deviceId, deviceName, deviceType, deviceAttributes), context.self());
-        ChainProcessingContext ctx = new ChainProcessingContext(md);
-        if (ctx.getChainLength() > 0) {
-            RuleProcessingMsg msg = new RuleProcessingMsg(ctx);
-            ActorRef ruleActorRef = ctx.getCurrentActor();
-            ruleActorRef.tell(msg, ActorRef.noSender());
-        } else {
-            context.self().tell(new RulesProcessedMsg(ctx), context.self());
-        }
-    }
+//    void process(ActorContext context, RuleChainDeviceMsg srcMsg) {
+//        ChainProcessingMetaData md = new ChainProcessingMetaData(srcMsg.getRuleChain(),
+//                srcMsg.getToDeviceActorMsg(), new DeviceMetaData(deviceId, deviceName, deviceType, deviceAttributes), context.self());
+//        ChainProcessingContext ctx = new ChainProcessingContext(md);
+//        if (ctx.getChainLength() > 0) {
+//            RuleProcessingMsg msg = new RuleProcessingMsg(ctx);
+//            ActorRef ruleActorRef = ctx.getCurrentActor();
+//            ruleActorRef.tell(msg, ActorRef.noSender());
+//        } else {
+//            context.self().tell(new RulesProcessedMsg(ctx), context.self());
+//        }
+//    }
 
     void processRpcResponses(ActorContext context, ToDeviceActorMsg msg) {
         SessionId sessionId = msg.getSessionId();
@@ -302,18 +295,18 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
         );
     }
 
-    void onRulesProcessedMsg(ActorContext context, RulesProcessedMsg msg) {
-        ChainProcessingContext ctx = msg.getCtx();
-        ToDeviceActorMsg inMsg = ctx.getInMsg();
-        SessionId sid = inMsg.getSessionId();
-        ToDeviceSessionActorMsg response;
-        if (ctx.getResponse() != null) {
-            response = new BasicToDeviceSessionActorMsg(ctx.getResponse(), sid);
-        } else {
-            response = new BasicToDeviceSessionActorMsg(ctx.getError(), sid);
-        }
-        sendMsgToSessionActor(response, inMsg.getServerAddress());
-    }
+//    void onRulesProcessedMsg(ActorContext context, RulesProcessedMsg msg) {
+//        ChainProcessingContext ctx = msg.getCtx();
+//        ToDeviceActorMsg inMsg = ctx.getInMsg();
+//        SessionId sid = inMsg.getSessionId();
+//        ToDeviceSessionActorMsg response;
+//        if (ctx.getResponse() != null) {
+//            response = new BasicToDeviceSessionActorMsg(ctx.getResponse(), sid);
+//        } else {
+//            response = new BasicToDeviceSessionActorMsg(ctx.getError(), sid);
+//        }
+//        sendMsgToSessionActor(response, inMsg.getServerAddress());
+//    }
 
     private void processSubscriptionCommands(ActorContext context, ToDeviceActorMsg msg) {
         SessionId sessionId = msg.getSessionId();
diff --git a/application/src/main/java/org/thingsboard/server/actors/plugin/PluginActor.java b/application/src/main/java/org/thingsboard/server/actors/plugin/PluginActor.java
index 265da38..88278f3 100644
--- a/application/src/main/java/org/thingsboard/server/actors/plugin/PluginActor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/plugin/PluginActor.java
@@ -23,6 +23,7 @@ import org.thingsboard.server.actors.service.ContextBasedCreator;
 import org.thingsboard.server.actors.stats.StatsPersistTick;
 import org.thingsboard.server.common.data.id.PluginId;
 import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.msg.TbActorMsg;
 import org.thingsboard.server.common.msg.cluster.ClusterEventMsg;
 import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
 import org.thingsboard.server.extensions.api.plugins.msg.TimeoutMsg;
@@ -41,6 +42,12 @@ public class PluginActor extends ComponentActor<PluginId, PluginActorMessageProc
     }
 
     @Override
+    protected boolean process(TbActorMsg msg) {
+        //TODO Move everything here, to work with TbActorMsg
+        return false;
+    }
+
+    @Override
     public void onReceive(Object msg) throws Exception {
         if (msg instanceof PluginWebsocketMsg) {
             onWebsocketMsg((PluginWebsocketMsg<?>) msg);
diff --git a/application/src/main/java/org/thingsboard/server/actors/plugin/PluginActorMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/plugin/PluginActorMessageProcessor.java
index 6e78e20..f6bf54d 100644
--- a/application/src/main/java/org/thingsboard/server/actors/plugin/PluginActorMessageProcessor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/plugin/PluginActorMessageProcessor.java
@@ -28,9 +28,14 @@ import org.thingsboard.server.common.data.plugin.ComponentType;
 import org.thingsboard.server.common.data.plugin.PluginMetaData;
 import org.thingsboard.server.common.msg.cluster.ClusterEventMsg;
 import org.thingsboard.server.common.msg.cluster.ServerAddress;
+import org.thingsboard.server.common.msg.core.BasicStatusCodeResponse;
+import org.thingsboard.server.common.msg.session.FromDeviceRequestMsg;
+import org.thingsboard.server.common.msg.session.MsgType;
 import org.thingsboard.server.extensions.api.plugins.Plugin;
 import org.thingsboard.server.extensions.api.plugins.PluginInitializationException;
 import org.thingsboard.server.extensions.api.plugins.msg.FromDeviceRpcResponse;
+import org.thingsboard.server.extensions.api.plugins.msg.ResponsePluginToRuleMsg;
+import org.thingsboard.server.extensions.api.plugins.msg.RuleToPluginMsg;
 import org.thingsboard.server.extensions.api.plugins.msg.TimeoutMsg;
 import org.thingsboard.server.extensions.api.plugins.rest.PluginRestMsg;
 import org.thingsboard.server.extensions.api.plugins.rpc.PluginRpcMsg;
@@ -57,7 +62,7 @@ public class PluginActorMessageProcessor extends ComponentMsgProcessor<PluginId>
     }
 
     @Override
-    public void start() throws Exception {
+    public void start(ActorContext context) throws Exception {
         logger.info("[{}] Going to start plugin actor.", entityId);
         pluginMd = systemContext.getPluginService().findPluginById(entityId);
         if (pluginMd == null) {
@@ -76,7 +81,7 @@ public class PluginActorMessageProcessor extends ComponentMsgProcessor<PluginId>
     }
 
     @Override
-    public void stop() throws Exception {
+    public void stop(ActorContext context) throws Exception {
         onStop();
     }
 
@@ -98,7 +103,20 @@ public class PluginActorMessageProcessor extends ComponentMsgProcessor<PluginId>
 
     public void onRuleToPluginMsg(RuleToPluginMsgWrapper msg) throws RuleException {
         if (state == ComponentLifecycleState.ACTIVE) {
-            pluginImpl.process(trustedCtx, msg.getRuleTenantId(), msg.getRuleId(), msg.getMsg());
+            try {
+                pluginImpl.process(trustedCtx, msg.getRuleTenantId(), msg.getRuleId(), msg.getMsg());
+            } catch (Exception ex) {
+                logger.debug("[{}] Failed to process RuleToPlugin msg: [{}] [{}]", tenantId, msg.getMsg(), ex);
+                RuleToPluginMsg ruleMsg = msg.getMsg();
+                MsgType responceMsgType = MsgType.RULE_ENGINE_ERROR;
+                Integer requestId = 0;
+                if (ruleMsg.getPayload() instanceof FromDeviceRequestMsg) {
+                    requestId = ((FromDeviceRequestMsg) ruleMsg.getPayload()).getRequestId();
+                }
+                trustedCtx.reply(
+                        new ResponsePluginToRuleMsg(ruleMsg.getUid(), tenantId, msg.getRuleId(),
+                                BasicStatusCodeResponse.onError(responceMsgType, requestId, ex)));
+            }
         } else {
             //TODO: reply with plugin suspended message
         }
@@ -191,7 +209,7 @@ public class PluginActorMessageProcessor extends ComponentMsgProcessor<PluginId>
             if (pluginImpl != null) {
                 pluginImpl.stop(trustedCtx);
             }
-            start();
+            start(context);
         }
     }
 
@@ -217,7 +235,7 @@ public class PluginActorMessageProcessor extends ComponentMsgProcessor<PluginId>
             pluginImpl.resume(trustedCtx);
             logger.info("[{}] Plugin resumed.", entityId);
         } else {
-            start();
+            start(context);
         }
     }
 
diff --git a/application/src/main/java/org/thingsboard/server/actors/plugin/PluginProcessingContext.java b/application/src/main/java/org/thingsboard/server/actors/plugin/PluginProcessingContext.java
index ce95ee4..10f16ce 100644
--- a/application/src/main/java/org/thingsboard/server/actors/plugin/PluginProcessingContext.java
+++ b/application/src/main/java/org/thingsboard/server/actors/plugin/PluginProcessingContext.java
@@ -36,6 +36,7 @@ import org.thingsboard.server.common.data.page.TextPageLink;
 import org.thingsboard.server.common.data.plugin.PluginMetaData;
 import org.thingsboard.server.common.data.relation.EntityRelation;
 import org.thingsboard.server.common.data.relation.RelationTypeGroup;
+import org.thingsboard.server.common.data.rule.RuleChain;
 import org.thingsboard.server.common.data.rule.RuleMetaData;
 import org.thingsboard.server.common.msg.cluster.ServerAddress;
 import org.thingsboard.server.extensions.api.device.DeviceAttributesEventNotificationMsg;
@@ -330,6 +331,9 @@ public final class PluginProcessingContext implements PluginContext {
                 case RULE:
                     validateRule(ctx, entityId, callback);
                     return;
+                case RULE_CHAIN:
+                    validateRuleChain(ctx, entityId, callback);
+                    return;
                 case PLUGIN:
                     validatePlugin(ctx, entityId, callback);
                     return;
@@ -411,6 +415,28 @@ public final class PluginProcessingContext implements PluginContext {
         }
     }
 
+    private void validateRuleChain(final PluginApiCallSecurityContext ctx, EntityId entityId, ValidationCallback callback) {
+        if (ctx.isCustomerUser()) {
+            callback.onSuccess(this, ValidationResult.accessDenied(CUSTOMER_USER_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION));
+        } else {
+            ListenableFuture<RuleChain> ruleChainFuture = pluginCtx.ruleChainService.findRuleChainByIdAsync(new RuleChainId(entityId.getId()));
+            Futures.addCallback(ruleChainFuture, getCallback(callback, ruleChain -> {
+                if (ruleChain == null) {
+                    return ValidationResult.entityNotFound("Rule chain with requested id wasn't found!");
+                } else {
+                    if (ctx.isTenantAdmin() && !ruleChain.getTenantId().equals(ctx.getTenantId())) {
+                        return ValidationResult.accessDenied("Rule chain doesn't belong to the current Tenant!");
+                    } else if (ctx.isSystemAdmin() && !ruleChain.getTenantId().isNullUid()) {
+                        return ValidationResult.accessDenied("Rule chain is not in system scope!");
+                    } else {
+                        return ValidationResult.ok();
+                    }
+                }
+            }));
+        }
+    }
+
+
     private void validatePlugin(final PluginApiCallSecurityContext ctx, EntityId entityId, ValidationCallback callback) {
         if (ctx.isCustomerUser()) {
             callback.onSuccess(this, ValidationResult.accessDenied(CUSTOMER_USER_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION));
diff --git a/application/src/main/java/org/thingsboard/server/actors/plugin/SharedPluginProcessingContext.java b/application/src/main/java/org/thingsboard/server/actors/plugin/SharedPluginProcessingContext.java
index 06138a3..1828970 100644
--- a/application/src/main/java/org/thingsboard/server/actors/plugin/SharedPluginProcessingContext.java
+++ b/application/src/main/java/org/thingsboard/server/actors/plugin/SharedPluginProcessingContext.java
@@ -32,6 +32,7 @@ import org.thingsboard.server.dao.customer.CustomerService;
 import org.thingsboard.server.dao.device.DeviceService;
 import org.thingsboard.server.dao.plugin.PluginService;
 import org.thingsboard.server.dao.relation.RelationService;
+import org.thingsboard.server.dao.rule.RuleChainService;
 import org.thingsboard.server.dao.rule.RuleService;
 import org.thingsboard.server.dao.tenant.TenantService;
 import org.thingsboard.server.dao.timeseries.TimeseriesService;
@@ -56,6 +57,7 @@ public final class SharedPluginProcessingContext {
     final AssetService assetService;
     final DeviceService deviceService;
     final RuleService ruleService;
+    final RuleChainService ruleChainService;
     final PluginService pluginService;
     final CustomerService customerService;
     final TenantService tenantService;
@@ -84,6 +86,7 @@ public final class SharedPluginProcessingContext {
         this.rpcService = sysContext.getRpcService();
         this.routingService = sysContext.getRoutingService();
         this.ruleService = sysContext.getRuleService();
+        this.ruleChainService = sysContext.getRuleChainService();
         this.pluginService = sysContext.getPluginService();
         this.customerService = sysContext.getCustomerService();
         this.tenantService = sysContext.getTenantService();
diff --git a/application/src/main/java/org/thingsboard/server/actors/rpc/RpcManagerActor.java b/application/src/main/java/org/thingsboard/server/actors/rpc/RpcManagerActor.java
index 9290a8f..ba20013 100644
--- a/application/src/main/java/org/thingsboard/server/actors/rpc/RpcManagerActor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/rpc/RpcManagerActor.java
@@ -23,6 +23,7 @@ import org.thingsboard.server.actors.ActorSystemContext;
 import org.thingsboard.server.actors.service.ContextAwareActor;
 import org.thingsboard.server.actors.service.ContextBasedCreator;
 import org.thingsboard.server.actors.service.DefaultActorService;
+import org.thingsboard.server.common.msg.TbActorMsg;
 import org.thingsboard.server.common.msg.cluster.ClusterEventMsg;
 import org.thingsboard.server.common.msg.cluster.ServerAddress;
 import org.thingsboard.server.gen.cluster.ClusterAPIProtos;
@@ -57,6 +58,12 @@ public class RpcManagerActor extends ContextAwareActor {
     }
 
     @Override
+    protected boolean process(TbActorMsg msg) {
+        //TODO Move everything here, to work with TbActorMsg
+        return false;
+    }
+
+    @Override
     public void onReceive(Object msg) throws Exception {
         if (msg instanceof RpcSessionTellMsg) {
             onMsg((RpcSessionTellMsg) msg);
diff --git a/application/src/main/java/org/thingsboard/server/actors/rpc/RpcSessionActor.java b/application/src/main/java/org/thingsboard/server/actors/rpc/RpcSessionActor.java
index db029fa..a187444 100644
--- a/application/src/main/java/org/thingsboard/server/actors/rpc/RpcSessionActor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/rpc/RpcSessionActor.java
@@ -23,6 +23,7 @@ import io.grpc.stub.StreamObserver;
 import org.thingsboard.server.actors.ActorSystemContext;
 import org.thingsboard.server.actors.service.ContextAwareActor;
 import org.thingsboard.server.actors.service.ContextBasedCreator;
+import org.thingsboard.server.common.msg.TbActorMsg;
 import org.thingsboard.server.common.msg.cluster.ServerAddress;
 import org.thingsboard.server.gen.cluster.ClusterAPIProtos;
 import org.thingsboard.server.gen.cluster.ClusterRpcServiceGrpc;
@@ -48,6 +49,12 @@ public class RpcSessionActor extends ContextAwareActor {
     }
 
     @Override
+    protected boolean process(TbActorMsg msg) {
+        //TODO Move everything here, to work with TbActorMsg
+        return false;
+    }
+
+    @Override
     public void onReceive(Object msg) throws Exception {
         if (msg instanceof RpcSessionTellMsg) {
             tell((RpcSessionTellMsg) msg);
diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java
new file mode 100644
index 0000000..8ffd378
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java
@@ -0,0 +1,209 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.server.actors.ruleChain;
+
+import akka.actor.ActorRef;
+import akka.actor.Cancellable;
+import com.google.common.base.Function;
+import org.thingsboard.rule.engine.api.*;
+import org.thingsboard.server.actors.ActorSystemContext;
+import org.thingsboard.server.common.data.id.RuleNodeId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.rule.RuleNode;
+import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.cluster.ServerAddress;
+import org.thingsboard.server.dao.alarm.AlarmService;
+import org.thingsboard.server.dao.asset.AssetService;
+import org.thingsboard.server.dao.attributes.AttributesService;
+import org.thingsboard.server.dao.customer.CustomerService;
+import org.thingsboard.server.dao.device.DeviceService;
+import org.thingsboard.server.dao.plugin.PluginService;
+import org.thingsboard.server.dao.relation.RelationService;
+import org.thingsboard.server.dao.rule.RuleChainService;
+import org.thingsboard.server.dao.timeseries.TimeseriesService;
+import org.thingsboard.server.dao.user.UserService;
+import org.thingsboard.server.service.script.NashornJsEngine;
+import scala.concurrent.duration.Duration;
+
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Created by ashvayka on 19.03.18.
+ */
+class DefaultTbContext implements TbContext {
+
+    private static final Function<? super List<Void>, ? extends Void> LIST_VOID_FUNCTION = v -> null;
+    private final ActorSystemContext mainCtx;
+    private final RuleNodeCtx nodeCtx;
+
+    public DefaultTbContext(ActorSystemContext mainCtx, RuleNodeCtx nodeCtx) {
+        this.mainCtx = mainCtx;
+        this.nodeCtx = nodeCtx;
+    }
+
+    @Override
+    public void tellNext(TbMsg msg) {
+        tellNext(msg, (String) null);
+    }
+
+    @Override
+    public void tellNext(TbMsg msg, String relationType) {
+        if (nodeCtx.getSelf().isDebugMode()) {
+            mainCtx.persistDebugOutput(nodeCtx.getTenantId(), nodeCtx.getSelf().getId(), msg);
+        }
+        nodeCtx.getChainActor().tell(new RuleNodeToRuleChainTellNextMsg(nodeCtx.getSelf().getId(), relationType, msg), nodeCtx.getSelfActor());
+    }
+
+    @Override
+    public void tellSelf(TbMsg msg, long delayMs) {
+        //TODO: add persistence layer
+        scheduleMsgWithDelay(new RuleNodeToSelfMsg(msg), delayMs, nodeCtx.getSelfActor());
+    }
+
+    private void scheduleMsgWithDelay(Object msg, long delayInMs, ActorRef target) {
+        mainCtx.getScheduler().scheduleOnce(Duration.create(delayInMs, TimeUnit.MILLISECONDS), target, msg, mainCtx.getActorSystem().dispatcher(), nodeCtx.getSelfActor());
+    }
+
+    @Override
+    public void tellOthers(TbMsg msg) {
+        throw new RuntimeException("Not Implemented!");
+    }
+
+    @Override
+    public void tellSibling(TbMsg msg, ServerAddress address) {
+        throw new RuntimeException("Not Implemented!");
+    }
+
+    @Override
+    public void spawn(TbMsg msg) {
+        throw new RuntimeException("Not Implemented!");
+    }
+
+    @Override
+    public void ack(TbMsg msg) {
+
+    }
+
+    @Override
+    public void tellError(TbMsg msg, Throwable th) {
+        if (nodeCtx.getSelf().isDebugMode()) {
+            mainCtx.persistDebugOutput(nodeCtx.getTenantId(), nodeCtx.getSelf().getId(), msg, th);
+        }
+        nodeCtx.getSelfActor().tell(new RuleNodeToSelfErrorMsg(msg, th), nodeCtx.getSelfActor());
+    }
+
+    @Override
+    public void updateSelf(RuleNode self) {
+        nodeCtx.setSelf(self);
+    }
+
+    @Override
+    public RuleNodeId getSelfId() {
+        return nodeCtx.getSelf().getId();
+    }
+
+    @Override
+    public TenantId getTenantId() {
+        return nodeCtx.getTenantId();
+    }
+
+    @Override
+    public void tellNext(TbMsg msg, Set<String> relationTypes) {
+        relationTypes.forEach(type -> tellNext(msg, type));
+    }
+
+    @Override
+    public ListeningExecutor getJsExecutor() {
+        return mainCtx.getJsExecutor();
+    }
+
+    @Override
+    public ListeningExecutor getMailExecutor() {
+        return mainCtx.getMailExecutor();
+    }
+
+    @Override
+    public ListeningExecutor getDbCallbackExecutor() {
+        return mainCtx.getDbCallbackExecutor();
+    }
+
+    @Override
+    public ScriptEngine createJsScriptEngine(String script, String functionName, String... argNames) {
+        return new NashornJsEngine(script, functionName, argNames);
+    }
+
+    @Override
+    public AttributesService getAttributesService() {
+        return mainCtx.getAttributesService();
+    }
+
+    @Override
+    public CustomerService getCustomerService() {
+        return mainCtx.getCustomerService();
+    }
+
+    @Override
+    public UserService getUserService() {
+        return mainCtx.getUserService();
+    }
+
+    @Override
+    public PluginService getPluginService() {
+        return mainCtx.getPluginService();
+    }
+
+    @Override
+    public AssetService getAssetService() {
+        return mainCtx.getAssetService();
+    }
+
+    @Override
+    public DeviceService getDeviceService() {
+        return mainCtx.getDeviceService();
+    }
+
+    @Override
+    public AlarmService getAlarmService() {
+        return mainCtx.getAlarmService();
+    }
+
+    @Override
+    public RuleChainService getRuleChainService() {
+        return mainCtx.getRuleChainService();
+    }
+
+    @Override
+    public TimeseriesService getTimeseriesService() {
+        return mainCtx.getTsService();
+    }
+
+    @Override
+    public RuleEngineTelemetryService getTelemetryService() {
+        return mainCtx.getTsSubService();
+    }
+
+    @Override
+    public RelationService getRelationService() {
+        return mainCtx.getRelationService();
+    }
+
+    @Override
+    public MailService getMailService() {
+        return mainCtx.getMailService();
+    }
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActor.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActor.java
new file mode 100644
index 0000000..f539e32
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActor.java
@@ -0,0 +1,88 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.server.actors.ruleChain;
+
+import akka.actor.OneForOneStrategy;
+import akka.actor.SupervisorStrategy;
+import org.thingsboard.server.actors.ActorSystemContext;
+import org.thingsboard.server.actors.service.ComponentActor;
+import org.thingsboard.server.actors.service.ContextBasedCreator;
+import org.thingsboard.server.common.data.id.RuleChainId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.msg.TbActorMsg;
+import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
+import org.thingsboard.server.common.msg.system.ServiceToRuleEngineMsg;
+import scala.concurrent.duration.Duration;
+
+public class RuleChainActor extends ComponentActor<RuleChainId, RuleChainActorMessageProcessor> {
+
+    private RuleChainActor(ActorSystemContext systemContext, TenantId tenantId, RuleChainId ruleChainId) {
+        super(systemContext, tenantId, ruleChainId);
+        setProcessor(new RuleChainActorMessageProcessor(tenantId, ruleChainId, systemContext,
+                logger, context().parent(), context().self()));
+    }
+
+    @Override
+    protected boolean process(TbActorMsg msg) {
+        switch (msg.getMsgType()) {
+            case COMPONENT_LIFE_CYCLE_MSG:
+                onComponentLifecycleMsg((ComponentLifecycleMsg) msg);
+                break;
+            case SERVICE_TO_RULE_ENGINE_MSG:
+                processor.onServiceToRuleEngineMsg((ServiceToRuleEngineMsg) msg);
+                break;
+            case RULE_TO_RULE_CHAIN_TELL_NEXT_MSG:
+                processor.onTellNext((RuleNodeToRuleChainTellNextMsg) msg);
+                break;
+            default:
+                return false;
+        }
+        return true;
+    }
+
+    public static class ActorCreator extends ContextBasedCreator<RuleChainActor> {
+        private static final long serialVersionUID = 1L;
+
+        private final TenantId tenantId;
+        private final RuleChainId ruleChainId;
+
+        public ActorCreator(ActorSystemContext context, TenantId tenantId, RuleChainId pluginId) {
+            super(context);
+            this.tenantId = tenantId;
+            this.ruleChainId = pluginId;
+        }
+
+        @Override
+        public RuleChainActor create() throws Exception {
+            return new RuleChainActor(context, tenantId, ruleChainId);
+        }
+    }
+
+    @Override
+    protected long getErrorPersistFrequency() {
+        return systemContext.getRuleChainErrorPersistFrequency();
+    }
+
+    @Override
+    public SupervisorStrategy supervisorStrategy() {
+        return strategy;
+    }
+
+    private final SupervisorStrategy strategy = new OneForOneStrategy(3, Duration.create("1 minute"), t -> {
+        logAndPersist("Unknown Failure", ActorSystemContext.toException(t));
+        return SupervisorStrategy.resume();
+    });
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java
new file mode 100644
index 0000000..d588a63
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java
@@ -0,0 +1,195 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.server.actors.ruleChain;
+
+import akka.actor.ActorContext;
+import akka.actor.ActorRef;
+import akka.actor.Props;
+import akka.event.LoggingAdapter;
+import org.thingsboard.server.actors.ActorSystemContext;
+import org.thingsboard.server.actors.service.DefaultActorService;
+import org.thingsboard.server.actors.shared.ComponentMsgProcessor;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.RuleChainId;
+import org.thingsboard.server.common.data.id.RuleNodeId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
+import org.thingsboard.server.common.data.plugin.ComponentLifecycleState;
+import org.thingsboard.server.common.data.relation.EntityRelation;
+import org.thingsboard.server.common.data.rule.RuleChain;
+import org.thingsboard.server.common.data.rule.RuleNode;
+import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.cluster.ClusterEventMsg;
+import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
+import org.thingsboard.server.common.msg.system.ServiceToRuleEngineMsg;
+import org.thingsboard.server.dao.rule.RuleChainService;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * @author Andrew Shvayka
+ */
+public class RuleChainActorMessageProcessor extends ComponentMsgProcessor<RuleChainId> {
+
+    private final ActorRef parent;
+    private final ActorRef self;
+    private final Map<RuleNodeId, RuleNodeCtx> nodeActors;
+    private final Map<RuleNodeId, List<RuleNodeRelation>> nodeRoutes;
+    private final RuleChainService service;
+
+    private RuleNodeId firstId;
+    private RuleNodeCtx firstNode;
+
+    RuleChainActorMessageProcessor(TenantId tenantId, RuleChainId ruleChainId, ActorSystemContext systemContext
+            , LoggingAdapter logger, ActorRef parent, ActorRef self) {
+        super(systemContext, logger, tenantId, ruleChainId);
+        this.parent = parent;
+        this.self = self;
+        this.nodeActors = new HashMap<>();
+        this.nodeRoutes = new HashMap<>();
+        this.service = systemContext.getRuleChainService();
+    }
+
+    @Override
+    public void start(ActorContext context) throws Exception {
+        RuleChain ruleChain = service.findRuleChainById(entityId);
+        List<RuleNode> ruleNodeList = service.getRuleChainNodes(entityId);
+        // Creating and starting the actors;
+        for (RuleNode ruleNode : ruleNodeList) {
+            ActorRef ruleNodeActor = createRuleNodeActor(context, ruleNode);
+            nodeActors.put(ruleNode.getId(), new RuleNodeCtx(tenantId, self, ruleNodeActor, ruleNode));
+        }
+        initRoutes(ruleChain, ruleNodeList);
+    }
+
+    @Override
+    public void onUpdate(ActorContext context) throws Exception {
+        RuleChain ruleChain = service.findRuleChainById(entityId);
+        List<RuleNode> ruleNodeList = service.getRuleChainNodes(entityId);
+
+        for (RuleNode ruleNode : ruleNodeList) {
+            RuleNodeCtx existing = nodeActors.get(ruleNode.getId());
+            if (existing == null) {
+                ActorRef ruleNodeActor = createRuleNodeActor(context, ruleNode);
+                nodeActors.put(ruleNode.getId(), new RuleNodeCtx(tenantId, self, ruleNodeActor, ruleNode));
+            } else {
+                existing.setSelf(ruleNode);
+                existing.getSelfActor().tell(new ComponentLifecycleMsg(tenantId, existing.getSelf().getId(), ComponentLifecycleEvent.UPDATED), self);
+            }
+        }
+
+        Set<RuleNodeId> existingNodes = ruleNodeList.stream().map(RuleNode::getId).collect(Collectors.toSet());
+        List<RuleNodeId> removedRules = nodeActors.keySet().stream().filter(node -> !existingNodes.contains(node)).collect(Collectors.toList());
+        removedRules.forEach(ruleNodeId -> {
+            RuleNodeCtx removed = nodeActors.remove(ruleNodeId);
+            removed.getSelfActor().tell(new ComponentLifecycleMsg(tenantId, removed.getSelf().getId(), ComponentLifecycleEvent.DELETED), self);
+        });
+
+        initRoutes(ruleChain, ruleNodeList);
+    }
+
+    @Override
+    public void stop(ActorContext context) throws Exception {
+        nodeActors.values().stream().map(RuleNodeCtx::getSelfActor).forEach(context::stop);
+        nodeActors.clear();
+        nodeRoutes.clear();
+        context.stop(self);
+    }
+
+    @Override
+    public void onClusterEventMsg(ClusterEventMsg msg) throws Exception {
+
+    }
+
+    private ActorRef createRuleNodeActor(ActorContext context, RuleNode ruleNode) {
+        String dispatcherName = tenantId.getId().equals(EntityId.NULL_UUID) ?
+                DefaultActorService.SYSTEM_RULE_DISPATCHER_NAME : DefaultActorService.TENANT_RULE_DISPATCHER_NAME;
+        return context.actorOf(
+                Props.create(new RuleNodeActor.ActorCreator(systemContext, tenantId, entityId, ruleNode.getId()))
+                        .withDispatcher(dispatcherName), ruleNode.getId().toString());
+    }
+
+    private void initRoutes(RuleChain ruleChain, List<RuleNode> ruleNodeList) {
+        nodeRoutes.clear();
+        // Populating the routes map;
+        for (RuleNode ruleNode : ruleNodeList) {
+            List<EntityRelation> relations = service.getRuleNodeRelations(ruleNode.getId());
+            for (EntityRelation relation : relations) {
+                if (relation.getTo().getEntityType() == EntityType.RULE_NODE) {
+                    RuleNodeCtx ruleNodeCtx = nodeActors.get(new RuleNodeId(relation.getTo().getId()));
+                    if (ruleNodeCtx == null) {
+                        throw new IllegalArgumentException("Rule Node [" + relation.getFrom() + "] has invalid relation to Rule node [" + relation.getTo() + "]");
+                    }
+                }
+                nodeRoutes.computeIfAbsent(ruleNode.getId(), k -> new ArrayList<>())
+                        .add(new RuleNodeRelation(ruleNode.getId(), relation.getTo(), relation.getType()));
+            }
+        }
+
+        firstId = ruleChain.getFirstRuleNodeId();
+        firstNode = nodeActors.get(ruleChain.getFirstRuleNodeId());
+        state = ComponentLifecycleState.ACTIVE;
+    }
+
+    void onServiceToRuleEngineMsg(ServiceToRuleEngineMsg envelope) {
+        checkActive();
+        TbMsg tbMsg = envelope.getTbMsg();
+        //TODO: push to queue and act on ack in async way
+        pushMsgToNode(firstNode, tbMsg);
+    }
+
+    void onTellNext(RuleNodeToRuleChainTellNextMsg envelope) {
+        checkActive();
+        RuleNodeId originator = envelope.getOriginator();
+        String targetRelationType = envelope.getRelationType();
+        List<RuleNodeRelation> relations = nodeRoutes.get(originator);
+        if (relations == null) {
+            return;
+        }
+        boolean copy = relations.size() > 1;
+        for (RuleNodeRelation relation : relations) {
+            TbMsg msg = envelope.getMsg();
+            if (copy) {
+                msg = msg.copy();
+            }
+            if (targetRelationType == null || targetRelationType.equalsIgnoreCase(relation.getType())) {
+                switch (relation.getOut().getEntityType()) {
+                    case RULE_NODE:
+                        RuleNodeId targetRuleNodeId = new RuleNodeId(relation.getOut().getId());
+                        RuleNodeCtx targetRuleNode = nodeActors.get(targetRuleNodeId);
+                        pushMsgToNode(targetRuleNode, msg);
+                        break;
+                    case RULE_CHAIN:
+//                        TODO: implement
+                        break;
+                }
+            }
+        }
+    }
+
+    private void pushMsgToNode(RuleNodeCtx nodeCtx, TbMsg msg) {
+        if (nodeCtx != null) {
+            nodeCtx.getSelfActor().tell(new RuleChainToRuleNodeMsg(new DefaultTbContext(systemContext, nodeCtx), msg), self);
+        }
+    }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainManagerActor.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainManagerActor.java
new file mode 100644
index 0000000..940bd5b
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainManagerActor.java
@@ -0,0 +1,61 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.server.actors.ruleChain;
+
+import akka.actor.ActorRef;
+import org.thingsboard.server.actors.ActorSystemContext;
+import org.thingsboard.server.actors.service.ContextAwareActor;
+import org.thingsboard.server.actors.shared.plugin.PluginManager;
+import org.thingsboard.server.actors.shared.rulechain.RuleChainManager;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.PluginId;
+import org.thingsboard.server.common.data.id.RuleChainId;
+import org.thingsboard.server.dao.rule.RuleChainService;
+
+/**
+ * Created by ashvayka on 15.03.18.
+ */
+public abstract class RuleChainManagerActor extends ContextAwareActor {
+
+    protected final RuleChainManager ruleChainManager;
+    protected final PluginManager pluginManager;
+    protected final RuleChainService ruleChainService;
+
+    public RuleChainManagerActor(ActorSystemContext systemContext, RuleChainManager ruleChainManager, PluginManager pluginManager) {
+        super(systemContext);
+        this.ruleChainManager = ruleChainManager;
+        this.pluginManager = pluginManager;
+        this.ruleChainService = systemContext.getRuleChainService();
+    }
+
+    protected void initRuleChains() {
+        pluginManager.init(this.context());
+        ruleChainManager.init(this.context());
+    }
+
+    protected ActorRef getEntityActorRef(EntityId entityId) {
+        ActorRef target = null;
+        switch (entityId.getEntityType()) {
+            case PLUGIN:
+                target = pluginManager.getOrCreateActor(this.context(), (PluginId) entityId);
+                break;
+            case RULE_CHAIN:
+                target = ruleChainManager.getOrCreateActor(this.context(), (RuleChainId) entityId);
+                break;
+        }
+        return target;
+    }
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainToRuleNodeMsg.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainToRuleNodeMsg.java
new file mode 100644
index 0000000..e7d866c
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainToRuleNodeMsg.java
@@ -0,0 +1,37 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.server.actors.ruleChain;
+
+import lombok.Data;
+import org.thingsboard.rule.engine.api.TbContext;
+import org.thingsboard.server.common.msg.MsgType;
+import org.thingsboard.server.common.msg.TbActorMsg;
+import org.thingsboard.server.common.msg.TbMsg;
+
+/**
+ * Created by ashvayka on 19.03.18.
+ */
+@Data
+final class RuleChainToRuleNodeMsg implements TbActorMsg {
+
+    private final TbContext ctx;
+    private final TbMsg msg;
+
+    @Override
+    public MsgType getMsgType() {
+        return MsgType.RULE_CHAIN_TO_RULE_MSG;
+    }
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActor.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActor.java
new file mode 100644
index 0000000..f7ca0d8
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActor.java
@@ -0,0 +1,109 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.server.actors.ruleChain;
+
+import org.thingsboard.server.actors.ActorSystemContext;
+import org.thingsboard.server.actors.service.ComponentActor;
+import org.thingsboard.server.actors.service.ContextBasedCreator;
+import org.thingsboard.server.common.data.id.RuleChainId;
+import org.thingsboard.server.common.data.id.RuleNodeId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.msg.TbActorMsg;
+import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
+
+public class RuleNodeActor extends ComponentActor<RuleNodeId, RuleNodeActorMessageProcessor> {
+
+    private final RuleChainId ruleChainId;
+
+    private RuleNodeActor(ActorSystemContext systemContext, TenantId tenantId, RuleChainId ruleChainId, RuleNodeId ruleNodeId) {
+        super(systemContext, tenantId, ruleNodeId);
+        this.ruleChainId = ruleChainId;
+        setProcessor(new RuleNodeActorMessageProcessor(tenantId, ruleChainId, ruleNodeId, systemContext,
+                logger, context().parent(), context().self()));
+    }
+
+    @Override
+    protected boolean process(TbActorMsg msg) {
+        switch (msg.getMsgType()) {
+            case COMPONENT_LIFE_CYCLE_MSG:
+                onComponentLifecycleMsg((ComponentLifecycleMsg) msg);
+                break;
+            case RULE_CHAIN_TO_RULE_MSG:
+                onRuleChainToRuleNodeMsg((RuleChainToRuleNodeMsg) msg);
+                break;
+            case RULE_TO_SELF_ERROR_MSG:
+                onRuleNodeToSelfErrorMsg((RuleNodeToSelfErrorMsg) msg);
+                break;
+            case RULE_TO_SELF_MSG:
+                onRuleNodeToSelfMsg((RuleNodeToSelfMsg) msg);
+                break;
+            default:
+                return false;
+        }
+        return true;
+    }
+
+    private void onRuleNodeToSelfMsg(RuleNodeToSelfMsg msg) {
+        logger.debug("[{}] Going to process rule msg: {}", id, msg.getMsg());
+        try {
+            processor.onRuleToSelfMsg(msg);
+            increaseMessagesProcessedCount();
+        } catch (Exception e) {
+            logAndPersist("onRuleMsg", e);
+        }
+    }
+
+    private void onRuleChainToRuleNodeMsg(RuleChainToRuleNodeMsg msg) {
+        logger.debug("[{}] Going to process rule msg: {}", id, msg.getMsg());
+        try {
+            processor.onRuleChainToRuleNodeMsg(msg);
+            increaseMessagesProcessedCount();
+        } catch (Exception e) {
+            logAndPersist("onRuleMsg", e);
+        }
+    }
+
+    private void onRuleNodeToSelfErrorMsg(RuleNodeToSelfErrorMsg msg) {
+        logAndPersist("onRuleMsg", ActorSystemContext.toException(msg.getError()));
+    }
+
+    public static class ActorCreator extends ContextBasedCreator<RuleNodeActor> {
+        private static final long serialVersionUID = 1L;
+
+        private final TenantId tenantId;
+        private final RuleChainId ruleChainId;
+        private final RuleNodeId ruleNodeId;
+
+        public ActorCreator(ActorSystemContext context, TenantId tenantId, RuleChainId ruleChainId, RuleNodeId ruleNodeId) {
+            super(context);
+            this.tenantId = tenantId;
+            this.ruleChainId = ruleChainId;
+            this.ruleNodeId = ruleNodeId;
+
+        }
+
+        @Override
+        public RuleNodeActor create() throws Exception {
+            return new RuleNodeActor(context, tenantId, ruleChainId, ruleNodeId);
+        }
+    }
+
+    @Override
+    protected long getErrorPersistFrequency() {
+        return systemContext.getRuleNodeErrorPersistFrequency();
+    }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActorMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActorMessageProcessor.java
new file mode 100644
index 0000000..ea857db
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActorMessageProcessor.java
@@ -0,0 +1,116 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.server.actors.ruleChain;
+
+import akka.actor.ActorContext;
+import akka.actor.ActorRef;
+import akka.event.LoggingAdapter;
+import org.thingsboard.rule.engine.api.TbContext;
+import org.thingsboard.rule.engine.api.TbNode;
+import org.thingsboard.rule.engine.api.TbNodeConfiguration;
+import org.thingsboard.rule.engine.api.TbNodeException;
+import org.thingsboard.server.actors.ActorSystemContext;
+import org.thingsboard.server.actors.shared.ComponentMsgProcessor;
+import org.thingsboard.server.common.data.id.RuleChainId;
+import org.thingsboard.server.common.data.id.RuleNodeId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.plugin.ComponentLifecycleState;
+import org.thingsboard.server.common.data.rule.RuleNode;
+import org.thingsboard.server.common.msg.cluster.ClusterEventMsg;
+import org.thingsboard.server.dao.rule.RuleChainService;
+
+import java.util.concurrent.ExecutionException;
+
+/**
+ * @author Andrew Shvayka
+ */
+public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor<RuleNodeId> {
+
+    private final ActorRef parent;
+    private final ActorRef self;
+    private final RuleChainService service;
+    private RuleNode ruleNode;
+    private TbNode tbNode;
+    private TbContext defaultCtx;
+
+    RuleNodeActorMessageProcessor(TenantId tenantId, RuleChainId ruleChainId, RuleNodeId ruleNodeId, ActorSystemContext systemContext
+            , LoggingAdapter logger, ActorRef parent, ActorRef self) {
+        super(systemContext, logger, tenantId, ruleNodeId);
+        this.parent = parent;
+        this.self = self;
+        this.service = systemContext.getRuleChainService();
+        this.ruleNode = systemContext.getRuleChainService().findRuleNodeById(entityId);
+        this.defaultCtx = new DefaultTbContext(systemContext, new RuleNodeCtx(tenantId, parent, self, ruleNode));
+    }
+
+    @Override
+    public void start(ActorContext context) throws Exception {
+        tbNode = initComponent(ruleNode);
+        state = ComponentLifecycleState.ACTIVE;
+    }
+
+    @Override
+    public void onUpdate(ActorContext context) throws Exception {
+        RuleNode newRuleNode = systemContext.getRuleChainService().findRuleNodeById(entityId);
+        boolean restartRequired = !(ruleNode.getType().equals(newRuleNode.getType())
+                && ruleNode.getConfiguration().equals(newRuleNode.getConfiguration()));
+        this.ruleNode = newRuleNode;
+        this.defaultCtx.updateSelf(newRuleNode);
+        if (restartRequired) {
+            if (tbNode != null) {
+                tbNode.destroy();
+            }
+            start(context);
+        }
+    }
+
+    @Override
+    public void stop(ActorContext context) throws Exception {
+        if (tbNode != null) {
+            tbNode.destroy();
+        }
+        context.stop(self);
+    }
+
+    @Override
+    public void onClusterEventMsg(ClusterEventMsg msg) throws Exception {
+
+    }
+
+    public void onRuleToSelfMsg(RuleNodeToSelfMsg msg) throws Exception {
+        checkActive();
+        if (ruleNode.isDebugMode()) {
+            systemContext.persistDebugInput(tenantId, entityId, msg.getMsg());
+        }
+        tbNode.onMsg(defaultCtx, msg.getMsg());
+    }
+
+    void onRuleChainToRuleNodeMsg(RuleChainToRuleNodeMsg msg) throws Exception {
+        checkActive();
+        if (ruleNode.isDebugMode()) {
+            systemContext.persistDebugInput(tenantId, entityId, msg.getMsg());
+        }
+        tbNode.onMsg(msg.getCtx(), msg.getMsg());
+    }
+
+    private TbNode initComponent(RuleNode ruleNode) throws Exception {
+        Class<?> componentClazz = Class.forName(ruleNode.getType());
+        TbNode tbNode = (TbNode) (componentClazz.newInstance());
+        tbNode.init(defaultCtx, new TbNodeConfiguration(ruleNode.getConfiguration()));
+        return tbNode;
+    }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeCtx.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeCtx.java
new file mode 100644
index 0000000..10fcc8b
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeCtx.java
@@ -0,0 +1,34 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.server.actors.ruleChain;
+
+import akka.actor.ActorRef;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.rule.RuleNode;
+
+/**
+ * Created by ashvayka on 19.03.18.
+ */
+@Data
+@AllArgsConstructor
+final class RuleNodeCtx {
+    private final TenantId tenantId;
+    private final ActorRef chainActor;
+    private final ActorRef selfActor;
+    private RuleNode self;
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeRelation.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeRelation.java
new file mode 100644
index 0000000..7861e54
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeRelation.java
@@ -0,0 +1,32 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.server.actors.ruleChain;
+
+import lombok.Data;
+import org.thingsboard.server.common.data.id.EntityId;
+
+/**
+ * Created by ashvayka on 19.03.18.
+ */
+
+@Data
+final class RuleNodeRelation {
+
+    private final EntityId in;
+    private final EntityId out;
+    private final String type;
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeToRuleChainTellNextMsg.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeToRuleChainTellNextMsg.java
new file mode 100644
index 0000000..054284d
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeToRuleChainTellNextMsg.java
@@ -0,0 +1,39 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.server.actors.ruleChain;
+
+import lombok.Data;
+import org.thingsboard.server.common.data.id.RuleNodeId;
+import org.thingsboard.server.common.msg.MsgType;
+import org.thingsboard.server.common.msg.TbActorMsg;
+import org.thingsboard.server.common.msg.TbMsg;
+
+/**
+ * Created by ashvayka on 19.03.18.
+ */
+@Data
+final class RuleNodeToRuleChainTellNextMsg implements TbActorMsg {
+
+    private final RuleNodeId originator;
+    private final String relationType;
+    private final TbMsg msg;
+
+    @Override
+    public MsgType getMsgType() {
+        return MsgType.RULE_TO_RULE_CHAIN_TELL_NEXT_MSG;
+    }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeToSelfErrorMsg.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeToSelfErrorMsg.java
new file mode 100644
index 0000000..e6248f1
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeToSelfErrorMsg.java
@@ -0,0 +1,37 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.server.actors.ruleChain;
+
+import lombok.Data;
+import org.thingsboard.server.common.msg.MsgType;
+import org.thingsboard.server.common.msg.TbActorMsg;
+import org.thingsboard.server.common.msg.TbMsg;
+
+/**
+ * Created by ashvayka on 19.03.18.
+ */
+@Data
+final class RuleNodeToSelfErrorMsg implements TbActorMsg {
+
+    private final TbMsg msg;
+    private final Throwable error;
+
+    @Override
+    public MsgType getMsgType() {
+        return MsgType.RULE_TO_SELF_ERROR_MSG;
+    }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeToSelfMsg.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeToSelfMsg.java
new file mode 100644
index 0000000..5c5af42
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeToSelfMsg.java
@@ -0,0 +1,37 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.server.actors.ruleChain;
+
+import lombok.Data;
+import org.thingsboard.server.common.data.id.RuleNodeId;
+import org.thingsboard.server.common.msg.MsgType;
+import org.thingsboard.server.common.msg.TbActorMsg;
+import org.thingsboard.server.common.msg.TbMsg;
+
+/**
+ * Created by ashvayka on 19.03.18.
+ */
+@Data
+final class RuleNodeToSelfMsg implements TbActorMsg {
+
+    private final TbMsg msg;
+
+    @Override
+    public MsgType getMsgType() {
+        return MsgType.RULE_TO_SELF_MSG;
+    }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/service/ActorService.java b/application/src/main/java/org/thingsboard/server/actors/service/ActorService.java
index baae376..0be0385 100644
--- a/application/src/main/java/org/thingsboard/server/actors/service/ActorService.java
+++ b/application/src/main/java/org/thingsboard/server/actors/service/ActorService.java
@@ -15,20 +15,19 @@
  */
 package org.thingsboard.server.actors.service;
 
-import org.thingsboard.server.common.data.id.DeviceId;
-import org.thingsboard.server.common.data.id.PluginId;
-import org.thingsboard.server.common.data.id.RuleId;
-import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.id.*;
 import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
+import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.system.ServiceToRuleEngineMsg;
 import org.thingsboard.server.common.transport.SessionMsgProcessor;
 import org.thingsboard.server.service.cluster.discovery.DiscoveryServiceListener;
 import org.thingsboard.server.service.cluster.rpc.RpcMsgListener;
 
 public interface ActorService extends SessionMsgProcessor, WebSocketMsgProcessor, RestMsgProcessor, RpcMsgListener, DiscoveryServiceListener {
 
-    void onPluginStateChange(TenantId tenantId, PluginId pluginId, ComponentLifecycleEvent state);
+    void onEntityStateChange(TenantId tenantId, EntityId entityId, ComponentLifecycleEvent state);
 
-    void onRuleStateChange(TenantId tenantId, RuleId ruleId, ComponentLifecycleEvent state);
+    void onMsg(ServiceToRuleEngineMsg msg);
 
     void onCredentialsUpdate(TenantId tenantId, DeviceId deviceId);
 
diff --git a/application/src/main/java/org/thingsboard/server/actors/service/ComponentActor.java b/application/src/main/java/org/thingsboard/server/actors/service/ComponentActor.java
index 76b9be9..6aa68d3 100644
--- a/application/src/main/java/org/thingsboard/server/actors/service/ComponentActor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/service/ComponentActor.java
@@ -54,7 +54,7 @@ public abstract class ComponentActor<T extends EntityId, P extends ComponentMsgP
     @Override
     public void preStart() {
         try {
-            processor.start();
+            processor.start(context());
             logLifecycleEvent(ComponentLifecycleEvent.STARTED);
             if (systemContext.isStatisticsEnabled()) {
                 scheduleStatsPersistTick();
@@ -78,7 +78,7 @@ public abstract class ComponentActor<T extends EntityId, P extends ComponentMsgP
     @Override
     public void postStop() {
         try {
-            processor.stop();
+            processor.stop(context());
             logLifecycleEvent(ComponentLifecycleEvent.STOPPED);
         } catch (Exception e) {
             logger.warning("[{}][{}] Failed to stop {} processor: {}", tenantId, id, id.getEntityType(), e.getMessage());
@@ -141,7 +141,6 @@ public abstract class ComponentActor<T extends EntityId, P extends ComponentMsgP
         messagesProcessed++;
     }
 
-
     protected void logAndPersist(String method, Exception e) {
         logAndPersist(method, e, false);
     }
diff --git a/application/src/main/java/org/thingsboard/server/actors/service/ContextAwareActor.java b/application/src/main/java/org/thingsboard/server/actors/service/ContextAwareActor.java
index 825c971..1d9c671 100644
--- a/application/src/main/java/org/thingsboard/server/actors/service/ContextAwareActor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/service/ContextAwareActor.java
@@ -16,9 +16,13 @@
 package org.thingsboard.server.actors.service;
 
 import akka.actor.UntypedActor;
+import akka.event.Logging;
+import akka.event.LoggingAdapter;
 import org.thingsboard.server.actors.ActorSystemContext;
+import org.thingsboard.server.common.msg.TbActorMsg;
 
 public abstract class ContextAwareActor extends UntypedActor {
+    protected final LoggingAdapter logger = Logging.getLogger(getContext().system(), this);
 
     public static final int ENTITY_PACK_LIMIT = 1024;
 
@@ -28,4 +32,20 @@ public abstract class ContextAwareActor extends UntypedActor {
         super();
         this.systemContext = systemContext;
     }
+
+    @Override
+    public void onReceive(Object msg) throws Exception {
+        if (logger.isDebugEnabled()) {
+            logger.debug("Processing msg: {}", msg);
+        }
+        if (msg instanceof TbActorMsg) {
+            if(!process((TbActorMsg) msg)){
+                logger.warning("Unknown message: {}!", msg);
+            }
+        } else {
+            logger.warning("Unknown message: {}!", msg);
+        }
+    }
+
+    protected abstract boolean process(TbActorMsg msg);
 }
diff --git a/application/src/main/java/org/thingsboard/server/actors/service/DefaultActorService.java b/application/src/main/java/org/thingsboard/server/actors/service/DefaultActorService.java
index bb84a30..fbb5a14 100644
--- a/application/src/main/java/org/thingsboard/server/actors/service/DefaultActorService.java
+++ b/application/src/main/java/org/thingsboard/server/actors/service/DefaultActorService.java
@@ -30,16 +30,14 @@ import org.thingsboard.server.actors.rpc.RpcSessionCreateRequestMsg;
 import org.thingsboard.server.actors.rpc.RpcSessionTellMsg;
 import org.thingsboard.server.actors.session.SessionManagerActor;
 import org.thingsboard.server.actors.stats.StatsActor;
-import org.thingsboard.server.common.data.id.DeviceId;
-import org.thingsboard.server.common.data.id.PluginId;
-import org.thingsboard.server.common.data.id.RuleId;
-import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.id.*;
 import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
 import org.thingsboard.server.common.msg.aware.SessionAwareMsg;
 import org.thingsboard.server.common.msg.cluster.ClusterEventMsg;
 import org.thingsboard.server.common.msg.cluster.ServerAddress;
 import org.thingsboard.server.common.msg.cluster.ToAllNodesMsg;
 import org.thingsboard.server.common.msg.core.ToDeviceSessionActorMsg;
+import org.thingsboard.server.common.msg.system.ServiceToRuleEngineMsg;
 import org.thingsboard.server.extensions.api.device.DeviceNameOrTypeUpdateMsg;
 import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
 import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
@@ -129,6 +127,11 @@ public class DefaultActorService implements ActorService {
     }
 
     @Override
+    public void onMsg(ServiceToRuleEngineMsg msg) {
+        appActor.tell(msg, ActorRef.noSender());
+    }
+
+    @Override
     public void process(SessionAwareMsg msg) {
         log.debug("Processing session aware msg: {}", msg);
         sessionManagerActor.tell(msg, ActorRef.noSender());
@@ -212,15 +215,9 @@ public class DefaultActorService implements ActorService {
     }
 
     @Override
-    public void onPluginStateChange(TenantId tenantId, PluginId pluginId, ComponentLifecycleEvent state) {
-        log.trace("[{}] Processing onPluginStateChange event: {}", pluginId, state);
-        broadcast(ComponentLifecycleMsg.forPlugin(tenantId, pluginId, state));
-    }
-
-    @Override
-    public void onRuleStateChange(TenantId tenantId, RuleId ruleId, ComponentLifecycleEvent state) {
-        log.trace("[{}] Processing onRuleStateChange event: {}", ruleId, state);
-        broadcast(ComponentLifecycleMsg.forRule(tenantId, ruleId, state));
+    public void onEntityStateChange(TenantId tenantId, EntityId entityId, ComponentLifecycleEvent state) {
+        log.trace("[{}] Processing {} state change event: {}", tenantId, entityId.getEntityType(), state);
+        broadcast(new ComponentLifecycleMsg(tenantId, entityId, state));
     }
 
     @Override
diff --git a/application/src/main/java/org/thingsboard/server/actors/session/SessionActor.java b/application/src/main/java/org/thingsboard/server/actors/session/SessionActor.java
index 37827d6..9d324c5 100644
--- a/application/src/main/java/org/thingsboard/server/actors/session/SessionActor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/session/SessionActor.java
@@ -23,6 +23,7 @@ import org.thingsboard.server.actors.service.ContextAwareActor;
 import org.thingsboard.server.actors.service.ContextBasedCreator;
 import org.thingsboard.server.actors.shared.SessionTimeoutMsg;
 import org.thingsboard.server.common.data.id.SessionId;
+import org.thingsboard.server.common.msg.TbActorMsg;
 import org.thingsboard.server.common.msg.cluster.ClusterEventMsg;
 import org.thingsboard.server.common.msg.core.ToDeviceSessionActorMsg;
 import org.thingsboard.server.common.msg.session.ToDeviceActorSessionMsg;
@@ -61,6 +62,12 @@ public class SessionActor extends ContextAwareActor {
     }
 
     @Override
+    protected boolean process(TbActorMsg msg) {
+        //TODO Move everything here, to work with TbActorMsg
+        return false;
+    }
+
+    @Override
     public void onReceive(Object msg) throws Exception {
         logger.debug("[{}] Processing: {}.", sessionId, msg);
         if (msg instanceof ToDeviceActorSessionMsg) {
diff --git a/application/src/main/java/org/thingsboard/server/actors/session/SessionManagerActor.java b/application/src/main/java/org/thingsboard/server/actors/session/SessionManagerActor.java
index 9d67dab..b5b1791 100644
--- a/application/src/main/java/org/thingsboard/server/actors/session/SessionManagerActor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/session/SessionManagerActor.java
@@ -26,6 +26,7 @@ import org.thingsboard.server.actors.service.ContextBasedCreator;
 import org.thingsboard.server.actors.service.DefaultActorService;
 import org.thingsboard.server.actors.shared.SessionTimeoutMsg;
 import org.thingsboard.server.common.data.id.SessionId;
+import org.thingsboard.server.common.msg.TbActorMsg;
 import org.thingsboard.server.common.msg.aware.SessionAwareMsg;
 
 import akka.event.Logging;
@@ -49,6 +50,12 @@ public class SessionManagerActor extends ContextAwareActor {
     }
 
     @Override
+    protected boolean process(TbActorMsg msg) {
+        //TODO Move everything here, to work with TbActorMsg
+        return false;
+    }
+
+    @Override
     public void onReceive(Object msg) throws Exception {
         if (msg instanceof SessionCtrlMsg) {
             onSessionCtrlMsg((SessionCtrlMsg) msg);
diff --git a/application/src/main/java/org/thingsboard/server/actors/shared/AbstractContextAwareMsgProcessor.java b/application/src/main/java/org/thingsboard/server/actors/shared/AbstractContextAwareMsgProcessor.java
index 73b221f..e1313d2 100644
--- a/application/src/main/java/org/thingsboard/server/actors/shared/AbstractContextAwareMsgProcessor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/shared/AbstractContextAwareMsgProcessor.java
@@ -102,9 +102,6 @@ public abstract class AbstractContextAwareMsgProcessor {
             case FILTER:
                 configurationClazz = ((Filter) componentClazz.getAnnotation(Filter.class)).configuration();
                 break;
-            case PROCESSOR:
-                configurationClazz = ((Processor) componentClazz.getAnnotation(Processor.class)).configuration();
-                break;
             case ACTION:
                 configurationClazz = ((Action) componentClazz.getAnnotation(Action.class)).configuration();
                 break;
diff --git a/application/src/main/java/org/thingsboard/server/actors/shared/ComponentMsgProcessor.java b/application/src/main/java/org/thingsboard/server/actors/shared/ComponentMsgProcessor.java
index 18d32d9..e25d3a7 100644
--- a/application/src/main/java/org/thingsboard/server/actors/shared/ComponentMsgProcessor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/shared/ComponentMsgProcessor.java
@@ -20,12 +20,14 @@ import akka.event.LoggingAdapter;
 import org.thingsboard.server.actors.ActorSystemContext;
 import org.thingsboard.server.actors.stats.StatsPersistTick;
 import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.plugin.ComponentLifecycleState;
 import org.thingsboard.server.common.msg.cluster.ClusterEventMsg;
 
 public abstract class ComponentMsgProcessor<T> extends AbstractContextAwareMsgProcessor {
 
     protected final TenantId tenantId;
     protected final T entityId;
+    protected ComponentLifecycleState state;
 
     protected ComponentMsgProcessor(ActorSystemContext systemContext, LoggingAdapter logger, TenantId tenantId, T id) {
         super(systemContext, logger);
@@ -33,23 +35,44 @@ public abstract class ComponentMsgProcessor<T> extends AbstractContextAwareMsgPr
         this.entityId = id;
     }
 
-    public abstract void start() throws Exception;
+    public abstract void start(ActorContext context) throws Exception;
 
-    public abstract void stop() throws Exception;
+    public abstract void stop(ActorContext context) throws Exception;
 
-    public abstract void onCreated(ActorContext context) throws Exception;
+    public abstract void onClusterEventMsg(ClusterEventMsg msg) throws Exception;
 
-    public abstract void onUpdate(ActorContext context) throws Exception;
+    public void onCreated(ActorContext context) throws Exception {
+        start(context);
+    }
 
-    public abstract void onActivate(ActorContext context) throws Exception;
+    public void onUpdate(ActorContext context) throws Exception {
+        restart(context);
+    }
 
-    public abstract void onSuspend(ActorContext context) throws Exception;
+    public void onActivate(ActorContext context) throws Exception {
+        restart(context);
+    }
 
-    public abstract void onStop(ActorContext context) throws Exception;
+    public void onSuspend(ActorContext context) throws Exception {
+        stop(context);
+    }
 
-    public abstract void onClusterEventMsg(ClusterEventMsg msg) throws Exception;
+    public void onStop(ActorContext context) throws Exception {
+        stop(context);
+    }
+
+    private void restart(ActorContext context) throws Exception {
+        stop(context);
+        start(context);
+    }
 
     public void scheduleStatsPersistTick(ActorContext context, long statsPersistFrequency) {
         schedulePeriodicMsgWithDelay(context, new StatsPersistTick(), statsPersistFrequency, statsPersistFrequency);
     }
+
+    protected void checkActive() {
+        if (state != ComponentLifecycleState.ACTIVE) {
+            throw new IllegalStateException("Rule chain is not active!");
+        }
+    }
 }
diff --git a/application/src/main/java/org/thingsboard/server/actors/shared/EntityActorsManager.java b/application/src/main/java/org/thingsboard/server/actors/shared/EntityActorsManager.java
new file mode 100644
index 0000000..d4a1f34
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/shared/EntityActorsManager.java
@@ -0,0 +1,86 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.server.actors.shared;
+
+import akka.actor.ActorContext;
+import akka.actor.ActorRef;
+import akka.actor.Props;
+import akka.actor.UntypedActor;
+import akka.japi.Creator;
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.server.actors.ActorSystemContext;
+import org.thingsboard.server.actors.service.ContextAwareActor;
+import org.thingsboard.server.common.data.SearchTextBased;
+import org.thingsboard.server.common.data.SearchTextBasedWithAdditionalInfo;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.id.UUIDBased;
+import org.thingsboard.server.common.data.page.PageDataIterable;
+import org.thingsboard.server.common.data.plugin.PluginMetaData;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Created by ashvayka on 15.03.18.
+ */
+@Slf4j
+public abstract class EntityActorsManager<T extends EntityId, A extends UntypedActor, M extends SearchTextBased<? extends UUIDBased>> {
+
+    protected final ActorSystemContext systemContext;
+    protected final Map<T, ActorRef> actors;
+
+    public EntityActorsManager(ActorSystemContext systemContext) {
+        this.systemContext = systemContext;
+        this.actors = new HashMap<>();
+    }
+
+    protected abstract TenantId getTenantId();
+
+    protected abstract String getDispatcherName();
+
+    protected abstract Creator<A> creator(T entityId);
+
+    protected abstract PageDataIterable.FetchFunction<M> getFetchEntitiesFunction();
+
+    public void init(ActorContext context) {
+        for (M entity : new PageDataIterable<>(getFetchEntitiesFunction(), ContextAwareActor.ENTITY_PACK_LIMIT)) {
+            T entityId = (T) entity.getId();
+            log.debug("[{}|{}] Creating entity actor", entityId.getEntityType(), entityId.getId());
+            //TODO: remove this cast making UUIDBased subclass of EntityId an interface and vice versa.
+            ActorRef actorRef = getOrCreateActor(context, entityId);
+            visit(entity, actorRef);
+            log.debug("[{}|{}] Entity actor created.", entityId.getEntityType(), entityId.getId());
+        }
+    }
+
+    protected void visit(M entity, ActorRef actorRef) {}
+
+    public ActorRef getOrCreateActor(ActorContext context, T entityId) {
+        return actors.computeIfAbsent(entityId, eId ->
+                context.actorOf(Props.create(creator(eId))
+                        .withDispatcher(getDispatcherName()), eId.toString()));
+    }
+
+    public void broadcast(Object msg) {
+        actors.values().forEach(actorRef -> actorRef.tell(msg, ActorRef.noSender()));
+    }
+
+    public void remove(T id) {
+        actors.remove(id);
+    }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/shared/plugin/PluginManager.java b/application/src/main/java/org/thingsboard/server/actors/shared/plugin/PluginManager.java
index 4f5871f..3345e5f 100644
--- a/application/src/main/java/org/thingsboard/server/actors/shared/plugin/PluginManager.java
+++ b/application/src/main/java/org/thingsboard/server/actors/shared/plugin/PluginManager.java
@@ -15,63 +15,28 @@
  */
 package org.thingsboard.server.actors.shared.plugin;
 
-import akka.actor.ActorContext;
-import akka.actor.ActorRef;
-import akka.actor.Props;
+import akka.japi.Creator;
 import lombok.extern.slf4j.Slf4j;
 import org.thingsboard.server.actors.ActorSystemContext;
 import org.thingsboard.server.actors.plugin.PluginActor;
-import org.thingsboard.server.actors.service.ContextAwareActor;
+import org.thingsboard.server.actors.shared.EntityActorsManager;
 import org.thingsboard.server.common.data.id.PluginId;
-import org.thingsboard.server.common.data.id.TenantId;
-import org.thingsboard.server.common.data.page.PageDataIterable;
-import org.thingsboard.server.common.data.page.PageDataIterable.FetchFunction;
 import org.thingsboard.server.common.data.plugin.PluginMetaData;
 import org.thingsboard.server.dao.plugin.PluginService;
 
-import java.util.HashMap;
-import java.util.Map;
-
 @Slf4j
-public abstract class PluginManager {
+public abstract class PluginManager extends EntityActorsManager<PluginId, PluginActor, PluginMetaData> {
 
-    protected final ActorSystemContext systemContext;
     protected final PluginService pluginService;
-    protected final Map<PluginId, ActorRef> pluginActors;
 
     public PluginManager(ActorSystemContext systemContext) {
-        this.systemContext = systemContext;
+        super(systemContext);
         this.pluginService = systemContext.getPluginService();
-        this.pluginActors = new HashMap<>();
     }
 
-    public void init(ActorContext context) {
-        PageDataIterable<PluginMetaData> pluginIterator = new PageDataIterable<>(getFetchPluginsFunction(),
-                ContextAwareActor.ENTITY_PACK_LIMIT);
-        for (PluginMetaData plugin : pluginIterator) {
-            log.debug("[{}] Creating plugin actor", plugin.getId());
-            getOrCreatePluginActor(context, plugin.getId());
-            log.debug("Plugin actor created.");
-        }
+    @Override
+    public Creator<PluginActor> creator(PluginId entityId){
+        return new PluginActor.ActorCreator(systemContext, getTenantId(), entityId);
     }
 
-    abstract FetchFunction<PluginMetaData> getFetchPluginsFunction();
-
-    abstract TenantId getTenantId();
-
-    abstract String getDispatcherName();
-
-    public ActorRef getOrCreatePluginActor(ActorContext context, PluginId pluginId) {
-        return pluginActors.computeIfAbsent(pluginId, pId ->
-                context.actorOf(Props.create(new PluginActor.ActorCreator(systemContext, getTenantId(), pId))
-                        .withDispatcher(getDispatcherName()), pId.toString()));
-    }
-
-    public void broadcast(Object msg) {
-        pluginActors.values().forEach(actorRef -> actorRef.tell(msg, ActorRef.noSender()));
-    }
-
-    public void remove(PluginId id) {
-        pluginActors.remove(id);
-    }
 }
diff --git a/application/src/main/java/org/thingsboard/server/actors/shared/plugin/SystemPluginManager.java b/application/src/main/java/org/thingsboard/server/actors/shared/plugin/SystemPluginManager.java
index 0888e23..88c52a6 100644
--- a/application/src/main/java/org/thingsboard/server/actors/shared/plugin/SystemPluginManager.java
+++ b/application/src/main/java/org/thingsboard/server/actors/shared/plugin/SystemPluginManager.java
@@ -29,12 +29,12 @@ public class SystemPluginManager extends PluginManager {
     }
 
     @Override
-    FetchFunction<PluginMetaData> getFetchPluginsFunction() {
+    protected FetchFunction<PluginMetaData> getFetchEntitiesFunction() {
         return pluginService::findSystemPlugins;
     }
 
     @Override
-    TenantId getTenantId() {
+    protected TenantId getTenantId() {
         return BasePluginService.SYSTEM_TENANT;
     }
 
diff --git a/application/src/main/java/org/thingsboard/server/actors/shared/plugin/TenantPluginManager.java b/application/src/main/java/org/thingsboard/server/actors/shared/plugin/TenantPluginManager.java
index 14ea2aa..09115f0 100644
--- a/application/src/main/java/org/thingsboard/server/actors/shared/plugin/TenantPluginManager.java
+++ b/application/src/main/java/org/thingsboard/server/actors/shared/plugin/TenantPluginManager.java
@@ -19,6 +19,7 @@ import akka.actor.ActorContext;
 import org.thingsboard.server.actors.ActorSystemContext;
 import org.thingsboard.server.actors.service.DefaultActorService;
 import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.page.PageDataIterable;
 import org.thingsboard.server.common.data.page.PageDataIterable.FetchFunction;
 import org.thingsboard.server.common.data.plugin.PluginMetaData;
 
@@ -39,12 +40,12 @@ public class TenantPluginManager extends PluginManager {
     }
 
     @Override
-    FetchFunction<PluginMetaData> getFetchPluginsFunction() {
+    protected FetchFunction<PluginMetaData> getFetchEntitiesFunction() {
         return link -> pluginService.findTenantPlugins(tenantId, link);
     }
 
     @Override
-    TenantId getTenantId() {
+    protected TenantId getTenantId() {
         return tenantId;
     }
 
diff --git a/application/src/main/java/org/thingsboard/server/actors/shared/rulechain/RuleChainManager.java b/application/src/main/java/org/thingsboard/server/actors/shared/rulechain/RuleChainManager.java
new file mode 100644
index 0000000..ff0c52e
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/shared/rulechain/RuleChainManager.java
@@ -0,0 +1,59 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.server.actors.shared.rulechain;
+
+import akka.actor.ActorRef;
+import akka.japi.Creator;
+import lombok.Getter;
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.server.actors.ActorSystemContext;
+import org.thingsboard.server.actors.ruleChain.RuleChainActor;
+import org.thingsboard.server.actors.shared.EntityActorsManager;
+import org.thingsboard.server.common.data.id.RuleChainId;
+import org.thingsboard.server.common.data.rule.RuleChain;
+import org.thingsboard.server.dao.rule.RuleChainService;
+
+/**
+ * Created by ashvayka on 15.03.18.
+ */
+@Slf4j
+public abstract class RuleChainManager extends EntityActorsManager<RuleChainId, RuleChainActor, RuleChain> {
+
+    protected final RuleChainService service;
+    @Getter
+    protected RuleChain rootChain;
+    @Getter
+    protected ActorRef rootChainActor;
+
+    public RuleChainManager(ActorSystemContext systemContext) {
+        super(systemContext);
+        this.service = systemContext.getRuleChainService();
+    }
+
+    @Override
+    public Creator<RuleChainActor> creator(RuleChainId entityId) {
+        return new RuleChainActor.ActorCreator(systemContext, getTenantId(), entityId);
+    }
+
+    @Override
+    protected void visit(RuleChain entity, ActorRef actorRef) {
+        if (entity.isRoot()) {
+            rootChain = entity;
+            rootChainActor = actorRef;
+        }
+    }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/stats/StatsActor.java b/application/src/main/java/org/thingsboard/server/actors/stats/StatsActor.java
index ccc31cc..8623370 100644
--- a/application/src/main/java/org/thingsboard/server/actors/stats/StatsActor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/stats/StatsActor.java
@@ -24,6 +24,7 @@ import org.thingsboard.server.actors.service.ContextAwareActor;
 import org.thingsboard.server.actors.service.ContextBasedCreator;
 import org.thingsboard.server.common.data.DataConstants;
 import org.thingsboard.server.common.data.Event;
+import org.thingsboard.server.common.msg.TbActorMsg;
 import org.thingsboard.server.common.msg.cluster.ServerAddress;
 
 public class StatsActor extends ContextAwareActor {
@@ -36,6 +37,12 @@ public class StatsActor extends ContextAwareActor {
     }
 
     @Override
+    protected boolean process(TbActorMsg msg) {
+        //TODO Move everything here, to work with TbActorMsg\
+        return false;
+    }
+
+    @Override
     public void onReceive(Object msg) throws Exception {
         logger.debug("Received message: {}", msg);
         if (msg instanceof StatsPersistMsg) {
diff --git a/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java b/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java
index b923fe1..d53c054 100644
--- a/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java
@@ -15,52 +15,38 @@
  */
 package org.thingsboard.server.actors.tenant;
 
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Optional;
-
+import akka.actor.ActorRef;
+import akka.actor.Props;
+import akka.event.Logging;
+import akka.event.LoggingAdapter;
 import org.thingsboard.server.actors.ActorSystemContext;
 import org.thingsboard.server.actors.device.DeviceActor;
 import org.thingsboard.server.actors.plugin.PluginTerminationMsg;
-import org.thingsboard.server.actors.rule.ComplexRuleActorChain;
-import org.thingsboard.server.actors.rule.RuleActorChain;
-import org.thingsboard.server.actors.service.ContextAwareActor;
+import org.thingsboard.server.actors.ruleChain.RuleChainManagerActor;
 import org.thingsboard.server.actors.service.ContextBasedCreator;
 import org.thingsboard.server.actors.service.DefaultActorService;
-import org.thingsboard.server.actors.shared.plugin.PluginManager;
 import org.thingsboard.server.actors.shared.plugin.TenantPluginManager;
-import org.thingsboard.server.actors.shared.rule.RuleManager;
-import org.thingsboard.server.actors.shared.rule.TenantRuleManager;
+import org.thingsboard.server.actors.shared.rulechain.TenantRuleChainManager;
 import org.thingsboard.server.common.data.id.DeviceId;
-import org.thingsboard.server.common.data.id.PluginId;
-import org.thingsboard.server.common.data.id.RuleId;
 import org.thingsboard.server.common.data.id.TenantId;
-import org.thingsboard.server.common.msg.cluster.ClusterEventMsg;
+import org.thingsboard.server.common.msg.TbActorMsg;
 import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
-
-import akka.actor.ActorRef;
-import akka.actor.Props;
-import akka.event.Logging;
-import akka.event.LoggingAdapter;
 import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
+import org.thingsboard.server.common.msg.system.ServiceToRuleEngineMsg;
 import org.thingsboard.server.extensions.api.device.ToDeviceActorNotificationMsg;
 import org.thingsboard.server.extensions.api.plugins.msg.ToPluginActorMsg;
-import org.thingsboard.server.extensions.api.rules.ToRuleActorMsg;
 
-public class TenantActor extends ContextAwareActor {
+import java.util.HashMap;
+import java.util.Map;
 
-    private final LoggingAdapter logger = Logging.getLogger(getContext().system(), this);
+public class TenantActor extends RuleChainManagerActor {
 
     private final TenantId tenantId;
-    private final RuleManager ruleManager;
-    private final PluginManager pluginManager;
     private final Map<DeviceId, ActorRef> deviceActors;
 
     private TenantActor(ActorSystemContext systemContext, TenantId tenantId) {
-        super(systemContext);
+        super(systemContext, new TenantRuleChainManager(systemContext, tenantId), new TenantPluginManager(systemContext, tenantId));
         this.tenantId = tenantId;
-        this.ruleManager = new TenantRuleManager(systemContext, tenantId);
-        this.pluginManager = new TenantPluginManager(systemContext, tenantId);
         this.deviceActors = new HashMap<>();
     }
 
@@ -68,8 +54,7 @@ public class TenantActor extends ContextAwareActor {
     public void preStart() {
         logger.info("[{}] Starting tenant actor.", tenantId);
         try {
-            ruleManager.init(this.context());
-            pluginManager.init(this.context());
+            initRuleChains();
             logger.info("[{}] Tenant actor started.", tenantId);
         } catch (Exception e) {
             logger.error(e, "[{}] Unknown failure", tenantId);
@@ -77,29 +62,45 @@ public class TenantActor extends ContextAwareActor {
     }
 
     @Override
-    public void onReceive(Object msg) throws Exception {
-        logger.debug("[{}] Received message: {}", tenantId, msg);
-        if (msg instanceof RuleChainDeviceMsg) {
-            process((RuleChainDeviceMsg) msg);
-        } else if (msg instanceof ToDeviceActorMsg) {
-            onToDeviceActorMsg((ToDeviceActorMsg) msg);
-        } else if (msg instanceof ToPluginActorMsg) {
-            onToPluginMsg((ToPluginActorMsg) msg);
-        } else if (msg instanceof ToRuleActorMsg) {
-            onToRuleMsg((ToRuleActorMsg) msg);
-        } else if (msg instanceof ToDeviceActorNotificationMsg) {
-            onToDeviceActorMsg((ToDeviceActorNotificationMsg) msg);
-        } else if (msg instanceof ClusterEventMsg) {
-            broadcast(msg);
-        } else if (msg instanceof ComponentLifecycleMsg) {
-            onComponentLifecycleMsg((ComponentLifecycleMsg) msg);
-        } else if (msg instanceof PluginTerminationMsg) {
-            onPluginTerminated((PluginTerminationMsg) msg);
-        } else {
-            logger.warning("[{}] Unknown message: {}!", tenantId, msg);
+    protected boolean process(TbActorMsg msg) {
+        switch (msg.getMsgType()) {
+            case COMPONENT_LIFE_CYCLE_MSG:
+                onComponentLifecycleMsg((ComponentLifecycleMsg) msg);
+                break;
+            case SERVICE_TO_RULE_ENGINE_MSG:
+                onServiceToRuleEngineMsg((ServiceToRuleEngineMsg) msg);
+                break;
+            default:
+                return false;
         }
+        return true;
     }
 
+    private void onServiceToRuleEngineMsg(ServiceToRuleEngineMsg msg) {
+        ruleChainManager.getRootChainActor().tell(msg, self());
+    }
+
+
+//    @Override
+//    public void onReceive(Object msg) throws Exception {
+//        logger.debug("[{}] Received message: {}", tenantId, msg);
+//        if (msg instanceof ToDeviceActorMsg) {
+//            onToDeviceActorMsg((ToDeviceActorMsg) msg);
+//        } else if (msg instanceof ToPluginActorMsg) {
+//            onToPluginMsg((ToPluginActorMsg) msg);
+//        } else if (msg instanceof ToDeviceActorNotificationMsg) {
+//            onToDeviceActorMsg((ToDeviceActorNotificationMsg) msg);
+//        } else if (msg instanceof ClusterEventMsg) {
+//            broadcast(msg);
+//        } else if (msg instanceof ComponentLifecycleMsg) {
+//            onComponentLifecycleMsg((ComponentLifecycleMsg) msg);
+//        } else if (msg instanceof PluginTerminationMsg) {
+//            onPluginTerminated((PluginTerminationMsg) msg);
+//        } else {
+//            logger.warning("[{}] Unknown message: {}!", tenantId, msg);
+//        }
+//    }
+
     private void broadcast(Object msg) {
         pluginManager.broadcast(msg);
         deviceActors.values().forEach(actorRef -> actorRef.tell(msg, ActorRef.noSender()));
@@ -113,14 +114,9 @@ public class TenantActor extends ContextAwareActor {
         getOrCreateDeviceActor(msg.getDeviceId()).tell(msg, ActorRef.noSender());
     }
 
-    private void onToRuleMsg(ToRuleActorMsg msg) {
-        ActorRef target = ruleManager.getOrCreateRuleActor(this.context(), msg.getRuleId());
-        target.tell(msg, ActorRef.noSender());
-    }
-
     private void onToPluginMsg(ToPluginActorMsg msg) {
         if (msg.getPluginTenantId().equals(tenantId)) {
-            ActorRef pluginActor = pluginManager.getOrCreatePluginActor(this.context(), msg.getPluginId());
+            ActorRef pluginActor = pluginManager.getOrCreateActor(this.context(), msg.getPluginId());
             pluginActor.tell(msg, ActorRef.noSender());
         } else {
             context().parent().tell(msg, ActorRef.noSender());
@@ -128,23 +124,11 @@ public class TenantActor extends ContextAwareActor {
     }
 
     private void onComponentLifecycleMsg(ComponentLifecycleMsg msg) {
-        Optional<PluginId> pluginId = msg.getPluginId();
-        Optional<RuleId> ruleId = msg.getRuleId();
-        if (pluginId.isPresent()) {
-            ActorRef pluginActor = pluginManager.getOrCreatePluginActor(this.context(), pluginId.get());
-            pluginActor.tell(msg, ActorRef.noSender());
-        } else if (ruleId.isPresent()) {
-            ActorRef target;
-            Optional<ActorRef> ref = ruleManager.update(this.context(), ruleId.get(), msg.getEvent());
-            if (ref.isPresent()) {
-                target = ref.get();
-            } else {
-                logger.debug("Failed to find actor for rule: [{}]", ruleId);
-                return;
-            }
+        ActorRef target = getEntityActorRef(msg.getEntityId());
+        if (target != null) {
             target.tell(msg, ActorRef.noSender());
         } else {
-            logger.debug("[{}] Invalid component lifecycle msg.", tenantId);
+            logger.debug("Invalid component lifecycle msg: {}", msg);
         }
     }
 
@@ -152,13 +136,6 @@ public class TenantActor extends ContextAwareActor {
         pluginManager.remove(msg.getId());
     }
 
-    private void process(RuleChainDeviceMsg msg) {
-        ToDeviceActorMsg toDeviceActorMsg = msg.getToDeviceActorMsg();
-        ActorRef deviceActor = getOrCreateDeviceActor(toDeviceActorMsg.getDeviceId());
-        RuleActorChain tenantChain = ruleManager.getRuleChain(this.context());
-        RuleActorChain chain = new ComplexRuleActorChain(msg.getRuleChain(), tenantChain);
-        deviceActor.tell(new RuleChainDeviceMsg(toDeviceActorMsg, chain), context().self());
-    }
 
     private ActorRef getOrCreateDeviceActor(DeviceId deviceId) {
         return deviceActors.computeIfAbsent(deviceId, k -> context().actorOf(Props.create(new DeviceActor.ActorCreator(systemContext, tenantId, deviceId))
diff --git a/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java b/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java
index 2952529..24c533c 100644
--- a/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java
+++ b/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java
@@ -20,7 +20,6 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
 import org.springframework.boot.autoconfigure.security.SecurityProperties;
-import org.springframework.boot.context.properties.EnableConfigurationProperties;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.core.annotation.Order;
@@ -37,7 +36,6 @@ import org.springframework.security.web.authentication.AuthenticationFailureHand
 import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
 import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
 import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
-import org.springframework.web.cors.CorsUtils;
 import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
 import org.springframework.web.filter.CorsFilter;
 import org.thingsboard.server.dao.audit.AuditLogLevelFilter;
diff --git a/application/src/main/java/org/thingsboard/server/config/WebSocketConfiguration.java b/application/src/main/java/org/thingsboard/server/config/WebSocketConfiguration.java
index a75ecb1..2e5050a 100644
--- a/application/src/main/java/org/thingsboard/server/config/WebSocketConfiguration.java
+++ b/application/src/main/java/org/thingsboard/server/config/WebSocketConfiguration.java
@@ -17,9 +17,9 @@ package org.thingsboard.server.config;
 
 import java.util.Map;
 
-import org.thingsboard.server.exception.ThingsboardErrorCode;
-import org.thingsboard.server.exception.ThingsboardException;
-import org.thingsboard.server.controller.plugin.PluginWebSocketHandler;
+import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
+import org.thingsboard.server.controller.plugin.TbWebSocketHandler;
 import org.thingsboard.server.service.security.model.SecurityUser;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
@@ -54,7 +54,7 @@ public class WebSocketConfiguration implements WebSocketConfigurer {
 
     @Override
     public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
-        registry.addHandler(pluginWsHandler(), WS_PLUGIN_MAPPING).setAllowedOrigins("*")
+        registry.addHandler(wsHandler(), WS_PLUGIN_MAPPING).setAllowedOrigins("*")
                 .addInterceptors(new HttpSessionHandshakeInterceptor(), new HandshakeInterceptor() {
 
                     @Override
@@ -82,8 +82,8 @@ public class WebSocketConfiguration implements WebSocketConfigurer {
     }
 
     @Bean
-    public WebSocketHandler pluginWsHandler() {
-        return new PluginWebSocketHandler();
+    public WebSocketHandler wsHandler() {
+        return new TbWebSocketHandler();
     }
 
     protected SecurityUser getCurrentUser() throws ThingsboardException {
diff --git a/application/src/main/java/org/thingsboard/server/controller/AdminController.java b/application/src/main/java/org/thingsboard/server/controller/AdminController.java
index e9a6ba3..5a43125 100644
--- a/application/src/main/java/org/thingsboard/server/controller/AdminController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/AdminController.java
@@ -20,8 +20,8 @@ import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.*;
 import org.thingsboard.server.common.data.AdminSettings;
 import org.thingsboard.server.dao.settings.AdminSettingsService;
-import org.thingsboard.server.exception.ThingsboardException;
-import org.thingsboard.server.service.mail.MailService;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
+import org.thingsboard.rule.engine.api.MailService;
 import org.thingsboard.server.service.update.UpdateService;
 import org.thingsboard.server.service.update.model.UpdateMessage;
 
diff --git a/application/src/main/java/org/thingsboard/server/controller/AlarmController.java b/application/src/main/java/org/thingsboard/server/controller/AlarmController.java
index 1959f4e..81bcf7e 100644
--- a/application/src/main/java/org/thingsboard/server/controller/AlarmController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/AlarmController.java
@@ -23,8 +23,8 @@ import org.thingsboard.server.common.data.alarm.*;
 import org.thingsboard.server.common.data.id.*;
 import org.thingsboard.server.common.data.page.TimePageData;
 import org.thingsboard.server.common.data.page.TimePageLink;
-import org.thingsboard.server.exception.ThingsboardErrorCode;
-import org.thingsboard.server.exception.ThingsboardException;
+import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
 
 @RestController
 @RequestMapping("/api")
diff --git a/application/src/main/java/org/thingsboard/server/controller/AssetController.java b/application/src/main/java/org/thingsboard/server/controller/AssetController.java
index 9b43913..0e348f9 100644
--- a/application/src/main/java/org/thingsboard/server/controller/AssetController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/AssetController.java
@@ -33,8 +33,8 @@ import org.thingsboard.server.common.data.asset.AssetSearchQuery;
 import org.thingsboard.server.common.data.security.Authority;
 import org.thingsboard.server.dao.exception.IncorrectParameterException;
 import org.thingsboard.server.dao.model.ModelConstants;
-import org.thingsboard.server.exception.ThingsboardErrorCode;
-import org.thingsboard.server.exception.ThingsboardException;
+import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
 import org.thingsboard.server.service.security.model.SecurityUser;
 
 import java.util.ArrayList;
diff --git a/application/src/main/java/org/thingsboard/server/controller/AuditLogController.java b/application/src/main/java/org/thingsboard/server/controller/AuditLogController.java
index 75bcf2a..e8685c7 100644
--- a/application/src/main/java/org/thingsboard/server/controller/AuditLogController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/AuditLogController.java
@@ -24,7 +24,7 @@ import org.thingsboard.server.common.data.id.TenantId;
 import org.thingsboard.server.common.data.id.UserId;
 import org.thingsboard.server.common.data.page.TimePageData;
 import org.thingsboard.server.common.data.page.TimePageLink;
-import org.thingsboard.server.exception.ThingsboardException;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
 
 import java.util.UUID;
 
diff --git a/application/src/main/java/org/thingsboard/server/controller/AuthController.java b/application/src/main/java/org/thingsboard/server/controller/AuthController.java
index ef38d80..96ff516 100644
--- a/application/src/main/java/org/thingsboard/server/controller/AuthController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/AuthController.java
@@ -28,9 +28,9 @@ import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
 import org.springframework.web.bind.annotation.*;
 import org.thingsboard.server.common.data.User;
 import org.thingsboard.server.common.data.security.UserCredentials;
-import org.thingsboard.server.exception.ThingsboardErrorCode;
-import org.thingsboard.server.exception.ThingsboardException;
-import org.thingsboard.server.service.mail.MailService;
+import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
+import org.thingsboard.rule.engine.api.MailService;
 import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRepository;
 import org.thingsboard.server.service.security.model.SecurityUser;
 import org.thingsboard.server.service.security.model.UserPrincipal;
diff --git a/application/src/main/java/org/thingsboard/server/controller/BaseController.java b/application/src/main/java/org/thingsboard/server/controller/BaseController.java
index 83b304f..29436c1 100644
--- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/BaseController.java
@@ -15,12 +15,9 @@
  */
 package org.thingsboard.server.controller;
 
-import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.ObjectMapper;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.StringUtils;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.beans.factory.annotation.Value;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.context.SecurityContextHolder;
 import org.springframework.web.bind.annotation.ExceptionHandler;
@@ -30,7 +27,6 @@ import org.thingsboard.server.common.data.alarm.Alarm;
 import org.thingsboard.server.common.data.alarm.AlarmId;
 import org.thingsboard.server.common.data.alarm.AlarmInfo;
 import org.thingsboard.server.common.data.asset.Asset;
-import org.thingsboard.server.common.data.audit.ActionStatus;
 import org.thingsboard.server.common.data.audit.ActionType;
 import org.thingsboard.server.common.data.id.*;
 import org.thingsboard.server.common.data.page.TextPageLink;
@@ -49,6 +45,7 @@ import org.thingsboard.server.dao.audit.AuditLogService;
 import org.thingsboard.server.dao.customer.CustomerService;
 import org.thingsboard.server.dao.dashboard.DashboardService;
 import org.thingsboard.server.dao.device.DeviceCredentialsService;
+import org.thingsboard.server.dao.device.DeviceOfflineService;
 import org.thingsboard.server.dao.device.DeviceService;
 import org.thingsboard.server.dao.exception.DataValidationException;
 import org.thingsboard.server.dao.exception.IncorrectParameterException;
@@ -57,12 +54,13 @@ import org.thingsboard.server.dao.plugin.PluginService;
 import org.thingsboard.server.dao.relation.RelationService;
 import org.thingsboard.server.dao.rule.RuleChainService;
 import org.thingsboard.server.dao.rule.RuleService;
+import org.thingsboard.server.dao.tenant.TenantService;
 import org.thingsboard.server.dao.user.UserService;
 import org.thingsboard.server.dao.widget.WidgetTypeService;
 import org.thingsboard.server.dao.widget.WidgetsBundleService;
-import org.thingsboard.server.exception.ThingsboardErrorCode;
+import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
 import org.thingsboard.server.exception.ThingsboardErrorResponseHandler;
-import org.thingsboard.server.exception.ThingsboardException;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
 import org.thingsboard.server.service.component.ComponentDiscoveryService;
 import org.thingsboard.server.service.security.model.SecurityUser;
 
@@ -71,6 +69,7 @@ import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import java.util.List;
 import java.util.Optional;
+import java.util.Set;
 import java.util.UUID;
 
 import static org.thingsboard.server.dao.service.Validator.validateId;
@@ -85,6 +84,9 @@ public abstract class BaseController {
     private ThingsboardErrorResponseHandler errorResponseHandler;
 
     @Autowired
+    protected TenantService tenantService;
+
+    @Autowired
     protected CustomerService customerService;
 
     @Autowired
@@ -132,6 +134,9 @@ public abstract class BaseController {
     @Autowired
     protected AuditLogService auditLogService;
 
+    @Autowired
+    protected DeviceOfflineService offlineService;
+
     @ExceptionHandler(ThingsboardException.class)
     public void handleThingsboardException(ThingsboardException ex, HttpServletResponse response) {
         errorResponseHandler.handle(ex, response);
@@ -480,6 +485,15 @@ public abstract class BaseController {
         }
     }
 
+    List<ComponentDescriptor> checkComponentDescriptorsByTypes(Set<ComponentType> types) throws ThingsboardException {
+        try {
+            log.debug("[{}] Lookup component descriptors", types);
+            return componentDescriptorService.getComponents(types);
+        } catch (Exception e) {
+            throw handleException(e, false);
+        }
+    }
+
     List<ComponentDescriptor> checkPluginActionsByPluginClazz(String pluginClazz) throws ThingsboardException {
         try {
             checkComponentDescriptorByClazz(pluginClazz);
@@ -550,6 +564,8 @@ public abstract class BaseController {
                 throw new ThingsboardException(YOU_DON_T_HAVE_PERMISSION_TO_PERFORM_THIS_OPERATION,
                         ThingsboardErrorCode.PERMISSION_DENIED);
 
+            } else if (tenantId.getId().equals(ModelConstants.NULL_UUID)) {
+                ruleChain.setConfiguration(null);
             }
         }
         return ruleChain;
@@ -590,5 +606,8 @@ public abstract class BaseController {
         auditLogService.logEntityAction(user.getTenantId(), customerId, user.getId(), user.getName(), entityId, entity, actionType, e, additionalInfo);
     }
 
+    protected static Exception toException(Throwable error) {
+        return Exception.class.isInstance(error) ? (Exception) error : new Exception(error);
+    }
 
 }
diff --git a/application/src/main/java/org/thingsboard/server/controller/ComponentDescriptorController.java b/application/src/main/java/org/thingsboard/server/controller/ComponentDescriptorController.java
index e63a443..6313d61 100644
--- a/application/src/main/java/org/thingsboard/server/controller/ComponentDescriptorController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/ComponentDescriptorController.java
@@ -19,9 +19,11 @@ import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.*;
 import org.thingsboard.server.common.data.plugin.ComponentDescriptor;
 import org.thingsboard.server.common.data.plugin.ComponentType;
-import org.thingsboard.server.exception.ThingsboardException;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
 
+import java.util.HashSet;
 import java.util.List;
+import java.util.Set;
 
 @RestController
 @RequestMapping("/api")
@@ -52,6 +54,22 @@ public class ComponentDescriptorController extends BaseController {
     }
 
     @PreAuthorize("hasAnyAuthority('SYS_ADMIN','TENANT_ADMIN')")
+    @RequestMapping(value = "/components", params = {"componentTypes"}, method = RequestMethod.GET)
+    @ResponseBody
+    public List<ComponentDescriptor> getComponentDescriptorsByTypes(@RequestParam("componentTypes") String[] strComponentTypes) throws ThingsboardException {
+        checkArrayParameter("componentTypes", strComponentTypes);
+        try {
+            Set<ComponentType> componentTypes = new HashSet<>();
+            for (String strComponentType : strComponentTypes) {
+                componentTypes.add(ComponentType.valueOf(strComponentType));
+            }
+            return checkComponentDescriptorsByTypes(componentTypes);
+        } catch (Exception e) {
+            throw handleException(e);
+        }
+    }
+
+    @PreAuthorize("hasAnyAuthority('SYS_ADMIN','TENANT_ADMIN')")
     @RequestMapping(value = "/components/actions/{pluginClazz:.+}", method = RequestMethod.GET)
     @ResponseBody
     public List<ComponentDescriptor> getPluginActionsByPluginClazz(@PathVariable("pluginClazz") String pluginClazz) throws ThingsboardException {
diff --git a/application/src/main/java/org/thingsboard/server/controller/CustomerController.java b/application/src/main/java/org/thingsboard/server/controller/CustomerController.java
index b164702..7763f3c 100644
--- a/application/src/main/java/org/thingsboard/server/controller/CustomerController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/CustomerController.java
@@ -28,7 +28,7 @@ import org.thingsboard.server.common.data.id.CustomerId;
 import org.thingsboard.server.common.data.id.TenantId;
 import org.thingsboard.server.common.data.page.TextPageData;
 import org.thingsboard.server.common.data.page.TextPageLink;
-import org.thingsboard.server.exception.ThingsboardException;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
 
 @RestController
 @RequestMapping("/api")
diff --git a/application/src/main/java/org/thingsboard/server/controller/DashboardController.java b/application/src/main/java/org/thingsboard/server/controller/DashboardController.java
index d2952a1..4ec2bba 100644
--- a/application/src/main/java/org/thingsboard/server/controller/DashboardController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/DashboardController.java
@@ -27,9 +27,7 @@ import org.thingsboard.server.common.data.page.TextPageData;
 import org.thingsboard.server.common.data.page.TextPageLink;
 import org.thingsboard.server.common.data.page.TimePageData;
 import org.thingsboard.server.common.data.page.TimePageLink;
-import org.thingsboard.server.dao.exception.IncorrectParameterException;
-import org.thingsboard.server.dao.model.ModelConstants;
-import org.thingsboard.server.exception.ThingsboardException;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
 
 import java.util.HashSet;
 import java.util.Set;
diff --git a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java
index bceea54..f97603e 100644
--- a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java
@@ -23,9 +23,11 @@ import org.thingsboard.server.common.data.Customer;
 import org.thingsboard.server.common.data.Device;
 import org.thingsboard.server.common.data.EntitySubtype;
 import org.thingsboard.server.common.data.EntityType;
-import org.thingsboard.server.common.data.audit.ActionStatus;
 import org.thingsboard.server.common.data.audit.ActionType;
 import org.thingsboard.server.common.data.device.DeviceSearchQuery;
+import org.thingsboard.server.common.data.device.DeviceStatusQuery;
+import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
 import org.thingsboard.server.common.data.id.CustomerId;
 import org.thingsboard.server.common.data.id.DeviceId;
 import org.thingsboard.server.common.data.id.TenantId;
@@ -35,8 +37,6 @@ import org.thingsboard.server.common.data.security.Authority;
 import org.thingsboard.server.common.data.security.DeviceCredentials;
 import org.thingsboard.server.dao.exception.IncorrectParameterException;
 import org.thingsboard.server.dao.model.ModelConstants;
-import org.thingsboard.server.exception.ThingsboardErrorCode;
-import org.thingsboard.server.exception.ThingsboardException;
 import org.thingsboard.server.service.security.model.SecurityUser;
 
 import java.util.ArrayList;
@@ -70,7 +70,7 @@ public class DeviceController extends BaseController {
             device.setTenantId(getCurrentUser().getTenantId());
             if (getCurrentUser().getAuthority() == Authority.CUSTOMER_USER) {
                 if (device.getId() == null || device.getId().isNullUid() ||
-                    device.getCustomerId() == null || device.getCustomerId().isNullUid()) {
+                        device.getCustomerId() == null || device.getCustomerId().isNullUid()) {
                     throw new ThingsboardException("You don't have permission to perform this operation!",
                             ThingsboardErrorCode.PERMISSION_DENIED);
                 } else {
@@ -368,4 +368,32 @@ public class DeviceController extends BaseController {
             throw handleException(e);
         }
     }
+
+    @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
+    @RequestMapping(value = "/device/offline", method = RequestMethod.GET)
+    @ResponseBody
+    public List<Device> getOfflineDevices(@RequestParam("contactType") DeviceStatusQuery.ContactType contactType,
+                                          @RequestParam("threshold") long threshold) throws ThingsboardException {
+        try {
+            TenantId tenantId = getCurrentUser().getTenantId();
+            ListenableFuture<List<Device>> offlineDevices = offlineService.findOfflineDevices(tenantId.getId(), contactType, threshold);
+            return checkNotNull(offlineDevices.get());
+        } catch (Exception e) {
+            throw handleException(e);
+        }
+    }
+
+    @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
+    @RequestMapping(value = "/device/online", method = RequestMethod.GET)
+    @ResponseBody
+    public List<Device> getOnlineDevices(@RequestParam("contactType") DeviceStatusQuery.ContactType contactType,
+                                          @RequestParam("threshold") long threshold) throws ThingsboardException {
+        try {
+            TenantId tenantId = getCurrentUser().getTenantId();
+            ListenableFuture<List<Device>> offlineDevices = offlineService.findOnlineDevices(tenantId.getId(), contactType, threshold);
+            return checkNotNull(offlineDevices.get());
+        } catch (Exception e) {
+            throw handleException(e);
+        }
+    }
 }
diff --git a/application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java b/application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java
index 03054df..3a3b78b 100644
--- a/application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java
@@ -24,8 +24,8 @@ import org.thingsboard.server.common.data.relation.EntityRelation;
 import org.thingsboard.server.common.data.relation.EntityRelationInfo;
 import org.thingsboard.server.common.data.relation.RelationTypeGroup;
 import org.thingsboard.server.common.data.relation.EntityRelationsQuery;
-import org.thingsboard.server.exception.ThingsboardErrorCode;
-import org.thingsboard.server.exception.ThingsboardException;
+import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
 
 import java.util.List;
 
diff --git a/application/src/main/java/org/thingsboard/server/controller/EventController.java b/application/src/main/java/org/thingsboard/server/controller/EventController.java
index 331b15e..f67f113 100644
--- a/application/src/main/java/org/thingsboard/server/controller/EventController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/EventController.java
@@ -24,8 +24,8 @@ import org.thingsboard.server.common.data.page.TimePageData;
 import org.thingsboard.server.common.data.page.TimePageLink;
 import org.thingsboard.server.dao.event.EventService;
 import org.thingsboard.server.dao.model.ModelConstants;
-import org.thingsboard.server.exception.ThingsboardErrorCode;
-import org.thingsboard.server.exception.ThingsboardException;
+import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
 
 @RestController
 @RequestMapping("/api")
diff --git a/application/src/main/java/org/thingsboard/server/controller/HttpValidationCallback.java b/application/src/main/java/org/thingsboard/server/controller/HttpValidationCallback.java
new file mode 100644
index 0000000..fb1f3e7
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/controller/HttpValidationCallback.java
@@ -0,0 +1,32 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.server.controller;
+
+import com.google.common.util.concurrent.FutureCallback;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.context.request.async.DeferredResult;
+import org.thingsboard.server.service.security.ValidationCallback;
+
+/**
+ * Created by ashvayka on 21.02.17.
+ */
+public class HttpValidationCallback extends ValidationCallback<DeferredResult<ResponseEntity>> {
+
+    public HttpValidationCallback(DeferredResult<ResponseEntity> response, FutureCallback<DeferredResult<ResponseEntity>> action) {
+       super(response, action);
+    }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/controller/plugin/PluginApiController.java b/application/src/main/java/org/thingsboard/server/controller/plugin/PluginApiController.java
index 8e3cee4..8d25db3 100644
--- a/application/src/main/java/org/thingsboard/server/controller/plugin/PluginApiController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/plugin/PluginApiController.java
@@ -17,90 +17,69 @@ package org.thingsboard.server.controller.plugin;
 
 
 import lombok.extern.slf4j.Slf4j;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.http.HttpStatus;
-import org.springframework.http.RequestEntity;
-import org.springframework.http.ResponseEntity;
-import org.springframework.security.access.prepost.PreAuthorize;
-import org.springframework.web.bind.annotation.PathVariable;
 import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.ResponseStatus;
 import org.springframework.web.bind.annotation.RestController;
-import org.springframework.web.context.request.async.DeferredResult;
-import org.thingsboard.server.actors.service.ActorService;
-import org.thingsboard.server.common.data.id.CustomerId;
-import org.thingsboard.server.common.data.id.TenantId;
-import org.thingsboard.server.common.data.id.UserId;
-import org.thingsboard.server.common.data.plugin.PluginMetaData;
 import org.thingsboard.server.controller.BaseController;
-import org.thingsboard.server.dao.model.ModelConstants;
-import org.thingsboard.server.dao.plugin.PluginService;
-import org.thingsboard.server.exception.ThingsboardException;
-import org.thingsboard.server.extensions.api.plugins.PluginApiCallSecurityContext;
 import org.thingsboard.server.extensions.api.plugins.PluginConstants;
-import org.thingsboard.server.extensions.api.plugins.rest.BasicPluginRestMsg;
-import org.thingsboard.server.extensions.api.plugins.rest.RestRequest;
-
-import javax.servlet.http.HttpServletRequest;
 
 @RestController
 @RequestMapping(PluginConstants.PLUGIN_URL_PREFIX)
 @Slf4j
 public class PluginApiController extends BaseController {
 
-    @SuppressWarnings("rawtypes")
-    @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
-    @RequestMapping(value = "/{pluginToken}/**")
-    @ResponseStatus(value = HttpStatus.OK)
-    public DeferredResult<ResponseEntity> processRequest(
-            @PathVariable("pluginToken") String pluginToken,
-            RequestEntity<byte[]> requestEntity,
-            HttpServletRequest request)
-            throws ThingsboardException {
-        log.debug("[{}] Going to process requst uri: {}", pluginToken, requestEntity.getUrl());
-        DeferredResult<ResponseEntity> result = new DeferredResult<ResponseEntity>();
-        PluginMetaData pluginMd = pluginService.findPluginByApiToken(pluginToken);
-        if (pluginMd == null) {
-            result.setErrorResult(new PluginNotFoundException("Plugin with token: " + pluginToken + " not found!"));
-        } else {
-            TenantId tenantId = getCurrentUser().getTenantId();
-            CustomerId customerId = getCurrentUser().getCustomerId();
-            if (validatePluginAccess(pluginMd, tenantId, customerId)) {
-                if(tenantId != null && ModelConstants.NULL_UUID.equals(tenantId.getId())){
-                    tenantId = null;
-                }
-                UserId userId = getCurrentUser().getId();
-                String userName = getCurrentUser().getName();
-                PluginApiCallSecurityContext securityCtx = new PluginApiCallSecurityContext(pluginMd.getTenantId(), pluginMd.getId(),
-                        tenantId, customerId, userId, userName);
-                actorService.process(new BasicPluginRestMsg(securityCtx, new RestRequest(requestEntity, request), result));
-            } else {
-                result.setResult(new ResponseEntity<>(HttpStatus.FORBIDDEN));
-            }
-
-        }
-        return result;
-    }
-
-    public static boolean validatePluginAccess(PluginMetaData pluginMd, TenantId tenantId, CustomerId customerId) {
-        boolean systemAdministrator = tenantId == null || ModelConstants.NULL_UUID.equals(tenantId.getId());
-        boolean tenantAdministrator = !systemAdministrator && (customerId == null || ModelConstants.NULL_UUID.equals(customerId.getId()));
-        boolean systemPlugin = ModelConstants.NULL_UUID.equals(pluginMd.getTenantId().getId());
-
-        boolean validUser = false;
-        if (systemPlugin) {
-            if (pluginMd.isPublicAccess() || systemAdministrator) {
-                // All users can access public system plugins. Only system
-                // users can access private system plugins
-                validUser = true;
-            }
-        } else {
-            if ((pluginMd.isPublicAccess() || tenantAdministrator) && tenantId != null && tenantId.equals(pluginMd.getTenantId())) {
-                // All tenant users can access public tenant plugins. Only tenant
-                // administrator can access private tenant plugins
-                validUser = true;
-            }
-        }
-        return validUser;
-    }
+//    @SuppressWarnings("rawtypes")
+//    @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
+//    @RequestMapping(value = "/{pluginToken}/**")
+//    @ResponseStatus(value = HttpStatus.OK)
+//    public DeferredResult<ResponseEntity> processRequest(
+//            @PathVariable("pluginToken") String pluginToken,
+//            RequestEntity<byte[]> requestEntity,
+//            HttpServletRequest request)
+//            throws ThingsboardException {
+//        log.debug("[{}] Going to process requst uri: {}", pluginToken, requestEntity.getUrl());
+//        DeferredResult<ResponseEntity> result = new DeferredResult<ResponseEntity>();
+//        PluginMetaData pluginMd = pluginService.findPluginByApiToken(pluginToken);
+//        if (pluginMd == null) {
+//            result.setErrorResult(new PluginNotFoundException("Plugin with token: " + pluginToken + " not found!"));
+//        } else {
+//            TenantId tenantId = getCurrentUser().getTenantId();
+//            CustomerId customerId = getCurrentUser().getCustomerId();
+//            if (validatePluginAccess(pluginMd, tenantId, customerId)) {
+//                if(tenantId != null && ModelConstants.NULL_UUID.equals(tenantId.getId())){
+//                    tenantId = null;
+//                }
+//                UserId userId = getCurrentUser().getId();
+//                String userName = getCurrentUser().getName();
+//                PluginApiCallSecurityContext securityCtx = new PluginApiCallSecurityContext(pluginMd.getTenantId(), pluginMd.getId(),
+//                        tenantId, customerId, userId, userName);
+//                actorService.process(new BasicPluginRestMsg(securityCtx, new RestRequest(requestEntity, request), result));
+//            } else {
+//                result.setResult(new ResponseEntity<>(HttpStatus.FORBIDDEN));
+//            }
+//
+//        }
+//        return result;
+//    }
+//
+//    public static boolean validatePluginAccess(PluginMetaData pluginMd, TenantId tenantId, CustomerId customerId) {
+//        boolean systemAdministrator = tenantId == null || ModelConstants.NULL_UUID.equals(tenantId.getId());
+//        boolean tenantAdministrator = !systemAdministrator && (customerId == null || ModelConstants.NULL_UUID.equals(customerId.getId()));
+//        boolean systemPlugin = ModelConstants.NULL_UUID.equals(pluginMd.getTenantId().getId());
+//
+//        boolean validUser = false;
+//        if (systemPlugin) {
+//            if (pluginMd.isPublicAccess() || systemAdministrator) {
+//                // All users can access public system plugins. Only system
+//                // users can access private system plugins
+//                validUser = true;
+//            }
+//        } else {
+//            if ((pluginMd.isPublicAccess() || tenantAdministrator) && tenantId != null && tenantId.equals(pluginMd.getTenantId())) {
+//                // All tenant users can access public tenant plugins. Only tenant
+//                // administrator can access private tenant plugins
+//                validUser = true;
+//            }
+//        }
+//        return validUser;
+//    }
 }
diff --git a/application/src/main/java/org/thingsboard/server/controller/PluginController.java b/application/src/main/java/org/thingsboard/server/controller/PluginController.java
index 2c69248..3bc385d 100644
--- a/application/src/main/java/org/thingsboard/server/controller/PluginController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/PluginController.java
@@ -28,7 +28,7 @@ import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
 import org.thingsboard.server.common.data.plugin.PluginMetaData;
 import org.thingsboard.server.common.data.security.Authority;
 import org.thingsboard.server.dao.model.ModelConstants;
-import org.thingsboard.server.exception.ThingsboardException;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
 
 import java.util.List;
 
@@ -71,7 +71,7 @@ public class PluginController extends BaseController {
             boolean created = source.getId() == null;
             source.setTenantId(getCurrentUser().getTenantId());
             PluginMetaData plugin = checkNotNull(pluginService.savePlugin(source));
-            actorService.onPluginStateChange(plugin.getTenantId(), plugin.getId(),
+            actorService.onEntityStateChange(plugin.getTenantId(), plugin.getId(),
                     created ? ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED);
 
             logEntityAction(plugin.getId(), plugin,
@@ -97,7 +97,7 @@ public class PluginController extends BaseController {
             PluginId pluginId = new PluginId(toUUID(strPluginId));
             PluginMetaData plugin = checkPlugin(pluginService.findPluginById(pluginId));
             pluginService.activatePluginById(pluginId);
-            actorService.onPluginStateChange(plugin.getTenantId(), plugin.getId(), ComponentLifecycleEvent.ACTIVATED);
+            actorService.onEntityStateChange(plugin.getTenantId(), plugin.getId(), ComponentLifecycleEvent.ACTIVATED);
 
             logEntityAction(plugin.getId(), plugin,
                     null,
@@ -123,7 +123,7 @@ public class PluginController extends BaseController {
             PluginId pluginId = new PluginId(toUUID(strPluginId));
             PluginMetaData plugin = checkPlugin(pluginService.findPluginById(pluginId));
             pluginService.suspendPluginById(pluginId);
-            actorService.onPluginStateChange(plugin.getTenantId(), plugin.getId(), ComponentLifecycleEvent.SUSPENDED);
+            actorService.onEntityStateChange(plugin.getTenantId(), plugin.getId(), ComponentLifecycleEvent.SUSPENDED);
 
             logEntityAction(plugin.getId(), plugin,
                     null,
@@ -221,7 +221,7 @@ public class PluginController extends BaseController {
             PluginId pluginId = new PluginId(toUUID(strPluginId));
             PluginMetaData plugin = checkPlugin(pluginService.findPluginById(pluginId));
             pluginService.deletePluginById(pluginId);
-            actorService.onPluginStateChange(plugin.getTenantId(), plugin.getId(), ComponentLifecycleEvent.DELETED);
+            actorService.onEntityStateChange(plugin.getTenantId(), plugin.getId(), ComponentLifecycleEvent.DELETED);
 
             logEntityAction(pluginId, plugin,
                     null,
diff --git a/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java b/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java
index 012e077..48c9cd5 100644
--- a/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java
@@ -15,31 +15,46 @@
  */
 package org.thingsboard.server.controller;
 
+import com.datastax.driver.core.utils.UUIDs;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import lombok.extern.slf4j.Slf4j;
 import org.springframework.http.HttpStatus;
 import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.util.StringUtils;
 import org.springframework.web.bind.annotation.*;
+import org.thingsboard.rule.engine.api.ScriptEngine;
 import org.thingsboard.server.common.data.EntityType;
 import org.thingsboard.server.common.data.audit.ActionType;
-import org.thingsboard.server.common.data.id.PluginId;
 import org.thingsboard.server.common.data.id.RuleChainId;
 import org.thingsboard.server.common.data.id.TenantId;
 import org.thingsboard.server.common.data.page.TextPageData;
 import org.thingsboard.server.common.data.page.TextPageLink;
 import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
-import org.thingsboard.server.common.data.plugin.PluginMetaData;
 import org.thingsboard.server.common.data.rule.RuleChain;
+import org.thingsboard.server.common.data.rule.RuleChainMetaData;
 import org.thingsboard.server.common.data.security.Authority;
+import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.TbMsgMetaData;
 import org.thingsboard.server.dao.model.ModelConstants;
-import org.thingsboard.server.exception.ThingsboardException;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
+import org.thingsboard.server.service.script.NashornJsEngine;
 
 import java.util.List;
+import java.util.Map;
+import java.util.Set;
 
+@Slf4j
 @RestController
 @RequestMapping("/api")
 public class RuleChainController extends BaseController {
 
     public static final String RULE_CHAIN_ID = "ruleChainId";
 
+    private static final ObjectMapper objectMapper = new ObjectMapper();
+
     @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
     @RequestMapping(value = "/ruleChain/{ruleChainId}", method = RequestMethod.GET)
     @ResponseBody
@@ -54,6 +69,21 @@ public class RuleChainController extends BaseController {
     }
 
     @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
+    @RequestMapping(value = "/ruleChain/{ruleChainId}/metadata", method = RequestMethod.GET)
+    @ResponseBody
+    public RuleChainMetaData getRuleChainMetaData(@PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException {
+        checkParameter(RULE_CHAIN_ID, strRuleChainId);
+        try {
+            RuleChainId ruleChainId = new RuleChainId(toUUID(strRuleChainId));
+            checkRuleChain(ruleChainId);
+            return ruleChainService.loadRuleChainMetaData(ruleChainId);
+        } catch (Exception e) {
+            throw handleException(e);
+        }
+    }
+
+
+    @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
     @RequestMapping(value = "/ruleChain", method = RequestMethod.POST)
     @ResponseBody
     public RuleChain saveRuleChain(@RequestBody RuleChain ruleChain) throws ThingsboardException {
@@ -62,6 +92,9 @@ public class RuleChainController extends BaseController {
             ruleChain.setTenantId(getCurrentUser().getTenantId());
             RuleChain savedRuleChain = checkNotNull(ruleChainService.saveRuleChain(ruleChain));
 
+            actorService.onEntityStateChange(ruleChain.getTenantId(), savedRuleChain.getId(),
+                    created ? ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED);
+
             logEntityAction(savedRuleChain.getId(), savedRuleChain,
                     null,
                     created ? ActionType.ADDED : ActionType.UPDATED, null);
@@ -77,6 +110,30 @@ public class RuleChainController extends BaseController {
     }
 
     @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
+    @RequestMapping(value = "/ruleChain/metadata", method = RequestMethod.POST)
+    @ResponseBody
+    public RuleChainMetaData saveRuleChainMetaData(@RequestBody RuleChainMetaData ruleChainMetaData) throws ThingsboardException {
+        try {
+            RuleChain ruleChain = checkRuleChain(ruleChainMetaData.getRuleChainId());
+            RuleChainMetaData savedRuleChainMetaData = checkNotNull(ruleChainService.saveRuleChainMetaData(ruleChainMetaData));
+
+            actorService.onEntityStateChange(ruleChain.getTenantId(), ruleChain.getId(), ComponentLifecycleEvent.UPDATED);
+
+            logEntityAction(ruleChain.getId(), ruleChain,
+                    null,
+                    ActionType.UPDATED, null, ruleChainMetaData);
+
+            return savedRuleChainMetaData;
+        } catch (Exception e) {
+
+            logEntityAction(emptyId(EntityType.RULE_CHAIN), null,
+                    null, ActionType.UPDATED, e, ruleChainMetaData);
+
+            throw handleException(e);
+        }
+    }
+
+    @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
     @RequestMapping(value = "/ruleChains", params = {"limit"}, method = RequestMethod.GET)
     @ResponseBody
     public TextPageData<RuleChain> getRuleChains(
@@ -145,6 +202,8 @@ public class RuleChainController extends BaseController {
             RuleChain ruleChain = checkRuleChain(ruleChainId);
             ruleChainService.deleteRuleChainById(ruleChainId);
 
+            actorService.onEntityStateChange(ruleChain.getTenantId(), ruleChain.getId(), ComponentLifecycleEvent.DELETED);
+
             logEntityAction(ruleChainId, ruleChain,
                     null,
                     ActionType.DELETED, null, strRuleChainId);
@@ -158,4 +217,78 @@ public class RuleChainController extends BaseController {
         }
     }
 
+    @PreAuthorize("hasAuthority('TENANT_ADMIN')")
+    @RequestMapping(value = "/ruleChain/testScript", method = RequestMethod.POST)
+    @ResponseBody
+    public JsonNode testScript(@RequestBody JsonNode inputParams) throws ThingsboardException {
+        try {
+            String script = inputParams.get("script").asText();
+            String scriptType = inputParams.get("scriptType").asText();
+            String functionName = inputParams.get("functionName").asText();
+            JsonNode argNamesJson = inputParams.get("argNames");
+            String[] argNames = objectMapper.treeToValue(argNamesJson, String[].class);
+
+            String data = inputParams.get("msg").asText();
+            JsonNode metadataJson = inputParams.get("metadata");
+            Map<String, String> metadata = objectMapper.convertValue(metadataJson, new TypeReference<Map<String, String>>() {});
+            String msgType = inputParams.get("msgType").asText();
+            String output = "";
+            String errorText = "";
+            ScriptEngine engine = null;
+            try {
+                engine = new NashornJsEngine(script, functionName, argNames);
+                TbMsg inMsg = new TbMsg(UUIDs.timeBased(), msgType, null, new TbMsgMetaData(metadata), data);
+                switch (scriptType) {
+                    case "update":
+                        output = msgToOutput(engine.executeUpdate(inMsg));
+                        break;
+                    case "generate":
+                        output = msgToOutput(engine.executeGenerate(inMsg));
+                        break;
+                    case "filter":
+                        boolean result = engine.executeFilter(inMsg);
+                        output = Boolean.toString(result);
+                        break;
+                    case "switch":
+                        Set<String> states = engine.executeSwitch(inMsg);
+                        output = objectMapper.writeValueAsString(states);
+                        break;
+                    case "json":
+                        JsonNode json = engine.executeJson(inMsg);
+                        output = objectMapper.writeValueAsString(json);
+                        break;
+                    case "string":
+                        output = engine.executeToString(inMsg);
+                        break;
+                    default:
+                        throw new IllegalArgumentException("Unsupported script type: " + scriptType);
+                }
+            } catch (Exception e) {
+                log.error("Error evaluating JS function", e);
+                errorText = e.getMessage();
+            } finally {
+                if (engine != null) {
+                    engine.destroy();
+                }
+            }
+            ObjectNode result = objectMapper.createObjectNode();
+            result.put("output", output);
+            result.put("error", errorText);
+            return result;
+        } catch (Exception e) {
+            throw handleException(e);
+        }
+    }
+
+    private String msgToOutput(TbMsg msg) throws Exception {
+        ObjectNode msgData = objectMapper.createObjectNode();
+        if (!StringUtils.isEmpty(msg.getData())) {
+            msgData.set("msg", objectMapper.readTree(msg.getData()));
+        }
+        Map<String, String> metadata = msg.getMetaData().getData();
+        msgData.set("metadata", objectMapper.valueToTree(metadata));
+        msgData.put("msgType", msg.getType());
+        return objectMapper.writeValueAsString(msgData);
+    }
+
 }
diff --git a/application/src/main/java/org/thingsboard/server/controller/RuleController.java b/application/src/main/java/org/thingsboard/server/controller/RuleController.java
index e498c8f..4528d81 100644
--- a/application/src/main/java/org/thingsboard/server/controller/RuleController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/RuleController.java
@@ -28,7 +28,7 @@ import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
 import org.thingsboard.server.common.data.plugin.PluginMetaData;
 import org.thingsboard.server.common.data.rule.RuleMetaData;
 import org.thingsboard.server.common.data.security.Authority;
-import org.thingsboard.server.exception.ThingsboardException;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
 
 import java.util.List;
 
@@ -73,7 +73,7 @@ public class RuleController extends BaseController {
             boolean created = source.getId() == null;
             source.setTenantId(getCurrentUser().getTenantId());
             RuleMetaData rule = checkNotNull(ruleService.saveRule(source));
-            actorService.onRuleStateChange(rule.getTenantId(), rule.getId(),
+            actorService.onEntityStateChange(rule.getTenantId(), rule.getId(),
                     created ? ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED);
 
             logEntityAction(rule.getId(), rule,
@@ -99,7 +99,7 @@ public class RuleController extends BaseController {
             RuleId ruleId = new RuleId(toUUID(strRuleId));
             RuleMetaData rule = checkRule(ruleService.findRuleById(ruleId));
             ruleService.activateRuleById(ruleId);
-            actorService.onRuleStateChange(rule.getTenantId(), rule.getId(), ComponentLifecycleEvent.ACTIVATED);
+            actorService.onEntityStateChange(rule.getTenantId(), rule.getId(), ComponentLifecycleEvent.ACTIVATED);
 
             logEntityAction(rule.getId(), rule,
                     null,
@@ -125,7 +125,7 @@ public class RuleController extends BaseController {
             RuleId ruleId = new RuleId(toUUID(strRuleId));
             RuleMetaData rule = checkRule(ruleService.findRuleById(ruleId));
             ruleService.suspendRuleById(ruleId);
-            actorService.onRuleStateChange(rule.getTenantId(), rule.getId(), ComponentLifecycleEvent.SUSPENDED);
+            actorService.onEntityStateChange(rule.getTenantId(), rule.getId(), ComponentLifecycleEvent.SUSPENDED);
 
             logEntityAction(rule.getId(), rule,
                     null,
@@ -219,7 +219,7 @@ public class RuleController extends BaseController {
             RuleId ruleId = new RuleId(toUUID(strRuleId));
             RuleMetaData rule = checkRule(ruleService.findRuleById(ruleId));
             ruleService.deleteRuleById(ruleId);
-            actorService.onRuleStateChange(rule.getTenantId(), rule.getId(), ComponentLifecycleEvent.DELETED);
+            actorService.onEntityStateChange(rule.getTenantId(), rule.getId(), ComponentLifecycleEvent.DELETED);
 
             logEntityAction(ruleId, rule,
                     null,
diff --git a/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java b/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java
new file mode 100644
index 0000000..80ddc2b
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java
@@ -0,0 +1,586 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.server.controller;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.google.common.base.Function;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonParser;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.util.StringUtils;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.context.request.async.DeferredResult;
+import org.thingsboard.server.common.data.DataConstants;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.audit.ActionType;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.EntityIdFactory;
+import org.thingsboard.server.common.data.id.UUIDBased;
+import org.thingsboard.server.common.data.kv.Aggregation;
+import org.thingsboard.server.common.data.kv.AttributeKvEntry;
+import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry;
+import org.thingsboard.server.common.data.kv.BaseTsKvQuery;
+import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
+import org.thingsboard.server.common.data.kv.BooleanDataEntry;
+import org.thingsboard.server.common.data.kv.DoubleDataEntry;
+import org.thingsboard.server.common.data.kv.KvEntry;
+import org.thingsboard.server.common.data.kv.LongDataEntry;
+import org.thingsboard.server.common.data.kv.StringDataEntry;
+import org.thingsboard.server.common.data.kv.TsKvEntry;
+import org.thingsboard.server.common.data.kv.TsKvQuery;
+import org.thingsboard.server.common.msg.core.TelemetryUploadRequest;
+import org.thingsboard.server.common.transport.adaptor.JsonConverter;
+import org.thingsboard.server.dao.attributes.AttributesService;
+import org.thingsboard.server.dao.timeseries.TimeseriesService;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
+import org.thingsboard.server.extensions.api.exception.InvalidParametersException;
+import org.thingsboard.server.extensions.api.exception.UncheckedApiException;
+import org.thingsboard.server.extensions.api.plugins.PluginConstants;
+import org.thingsboard.server.extensions.core.plugin.telemetry.AttributeData;
+import org.thingsboard.server.extensions.core.plugin.telemetry.TsData;
+import org.thingsboard.server.service.security.AccessValidator;
+import org.thingsboard.server.service.security.model.SecurityUser;
+import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService;
+
+import javax.annotation.Nullable;
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.stream.Collectors;
+
+/**
+ * Created by ashvayka on 22.03.18.
+ */
+@RestController
+@RequestMapping(PluginConstants.TELEMETRY_URL_PREFIX)
+@Slf4j
+public class TelemetryController extends BaseController {
+
+    @Autowired
+    private AttributesService attributesService;
+
+    @Autowired
+    private TimeseriesService tsService;
+
+    @Autowired
+    private TelemetrySubscriptionService tsSubService;
+
+    @Autowired
+    private AccessValidator accessValidator;
+
+    private ExecutorService executor;
+
+    @PostConstruct
+    public void initExecutor() {
+        executor = Executors.newSingleThreadExecutor();
+    }
+
+    @PreDestroy
+    public void shutdownExecutor() {
+        if (executor != null) {
+            executor.shutdownNow();
+        }
+    }
+
+    @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
+    @RequestMapping(value = "/{entityType}/{entityId}/keys/attributes", method = RequestMethod.GET)
+    @ResponseBody
+    public DeferredResult<ResponseEntity> getAttributeKeys(
+            @PathVariable("entityType") String entityType, @PathVariable("entityId") String entityIdStr) throws ThingsboardException {
+        return accessValidator.validateEntityAndCallback(getCurrentUser(), entityType, entityIdStr, this::getAttributeKeysCallback);
+    }
+
+    @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
+    @RequestMapping(value = "/{entityType}/{entityId}/keys/attributes/{scope}", method = RequestMethod.GET)
+    @ResponseBody
+    public DeferredResult<ResponseEntity> getAttributeKeysByScope(
+            @PathVariable("entityType") String entityType, @PathVariable("entityId") String entityIdStr
+            , @PathVariable("scope") String scope) throws ThingsboardException {
+        return accessValidator.validateEntityAndCallback(getCurrentUser(), entityType, entityIdStr,
+                (result, entityId) -> getAttributeKeysCallback(result, entityId, scope));
+    }
+
+    @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
+    @RequestMapping(value = "/{entityType}/{entityId}/values/attributes", method = RequestMethod.GET)
+    @ResponseBody
+    public DeferredResult<ResponseEntity> getAttributes(
+            @PathVariable("entityType") String entityType, @PathVariable("entityId") String entityIdStr,
+            @RequestParam(name = "keys", required = false) String keysStr) throws ThingsboardException {
+        SecurityUser user = getCurrentUser();
+        return accessValidator.validateEntityAndCallback(getCurrentUser(), entityType, entityIdStr,
+                (result, entityId) -> getAttributeValuesCallback(result, user, entityId, null, keysStr));
+    }
+
+    @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
+    @RequestMapping(value = "/{entityType}/{entityId}/values/attributes/{scope}", method = RequestMethod.GET)
+    @ResponseBody
+    public DeferredResult<ResponseEntity> getAttributesByScope(
+            @PathVariable("entityType") String entityType, @PathVariable("entityId") String entityIdStr,
+            @PathVariable("scope") String scope,
+            @RequestParam(name = "keys", required = false) String keysStr) throws ThingsboardException {
+        SecurityUser user = getCurrentUser();
+        return accessValidator.validateEntityAndCallback(getCurrentUser(), entityType, entityIdStr,
+                (result, entityId) -> getAttributeValuesCallback(result, user, entityId, scope, keysStr));
+    }
+
+    @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
+    @RequestMapping(value = "/{entityType}/{entityId}/keys/timeseries", method = RequestMethod.GET)
+    @ResponseBody
+    public DeferredResult<ResponseEntity> getTimeseriesKeys(
+            @PathVariable("entityType") String entityType, @PathVariable("entityId") String entityIdStr) throws ThingsboardException {
+        return accessValidator.validateEntityAndCallback(getCurrentUser(), entityType, entityIdStr,
+                (result, entityId) -> {
+                    Futures.addCallback(tsService.findAllLatest(entityId), getTsKeysToResponseCallback(result));
+                });
+    }
+
+    @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
+    @RequestMapping(value = "/{entityType}/{entityId}/values/timeseries", method = RequestMethod.GET)
+    @ResponseBody
+    public DeferredResult<ResponseEntity> getLatestTimeseries(
+            @PathVariable("entityType") String entityType, @PathVariable("entityId") String entityIdStr,
+            @RequestParam(name = "keys", required = false) String keysStr) throws ThingsboardException {
+        SecurityUser user = getCurrentUser();
+
+        return accessValidator.validateEntityAndCallback(getCurrentUser(), entityType, entityIdStr,
+                (result, entityId) -> getLatestTimeseriesValuesCallback(result, user, entityId, keysStr));
+    }
+
+
+    @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
+    @RequestMapping(value = "/{entityType}/{entityId}/values/timeseries", method = RequestMethod.GET, params = {"keys", "startTs", "endTs"})
+    @ResponseBody
+    public DeferredResult<ResponseEntity> getTimeseries(
+            @PathVariable("entityType") String entityType, @PathVariable("entityId") String entityIdStr,
+            @RequestParam(name = "keys") String keys,
+            @RequestParam(name = "startTs") Long startTs,
+            @RequestParam(name = "endTs") Long endTs,
+            @RequestParam(name = "interval", defaultValue = "0") Long interval,
+            @RequestParam(name = "limit", defaultValue = "100") Integer limit,
+            @RequestParam(name = "agg", defaultValue = "NONE") String aggStr
+    ) throws ThingsboardException {
+        return accessValidator.validateEntityAndCallback(getCurrentUser(), entityType, entityIdStr,
+                (result, entityId) -> {
+                    // If interval is 0, convert this to a NONE aggregation, which is probably what the user really wanted
+                    Aggregation agg = interval == 0L ? Aggregation.valueOf(Aggregation.NONE.name()) : Aggregation.valueOf(aggStr);
+                    List<TsKvQuery> queries = toKeysList(keys).stream().map(key -> new BaseTsKvQuery(key, startTs, endTs, interval, limit, agg))
+                            .collect(Collectors.toList());
+
+                    Futures.addCallback(tsService.findAll(entityId, queries), getTsKvListCallback(result));
+                });
+    }
+
+    @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
+    @RequestMapping(value = "/{deviceId}/{scope}", method = RequestMethod.POST)
+    @ResponseBody
+    public DeferredResult<ResponseEntity> saveDeviceAttributes(@PathVariable("deviceId") String deviceIdStr, @PathVariable("scope") String scope,
+                                                               @RequestBody JsonNode request) throws ThingsboardException {
+        EntityId entityId = EntityIdFactory.getByTypeAndUuid(EntityType.DEVICE, deviceIdStr);
+        return saveAttributes(entityId, scope, request);
+    }
+
+    @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
+    @RequestMapping(value = "/{entityType}/{entityId}/{scope}", method = RequestMethod.POST)
+    @ResponseBody
+    public DeferredResult<ResponseEntity> saveEntityAttributesV1(@PathVariable("entityType") String entityType, @PathVariable("entityId") String entityIdStr,
+                                                                 @PathVariable("scope") String scope,
+                                                                 @RequestBody JsonNode request) throws ThingsboardException {
+        EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr);
+        return saveAttributes(entityId, scope, request);
+    }
+
+    @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
+    @RequestMapping(value = "/{entityType}/{entityId}/attributes/{scope}", method = RequestMethod.POST)
+    @ResponseBody
+    public DeferredResult<ResponseEntity> saveEntityAttributesV2(@PathVariable("entityType") String entityType, @PathVariable("entityId") String entityIdStr,
+                                                                 @PathVariable("scope") String scope,
+                                                                 @RequestBody JsonNode request) throws ThingsboardException {
+        EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr);
+        return saveAttributes(entityId, scope, request);
+    }
+
+    @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
+    @RequestMapping(value = "/{entityType}/{entityId}/timeseries/{scope}", method = RequestMethod.POST)
+    @ResponseBody
+    public DeferredResult<ResponseEntity> saveEntityTelemetry(@PathVariable("entityType") String entityType, @PathVariable("entityId") String entityIdStr,
+                                                              @PathVariable("scope") String scope,
+                                                              @RequestBody String requestBody) throws ThingsboardException {
+        EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr);
+        return saveTelemetry(entityId, requestBody, 0L);
+    }
+
+    @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
+    @RequestMapping(value = "/{entityType}/{entityId}/timeseries/{scope}/{ttl}", method = RequestMethod.POST)
+    @ResponseBody
+    public DeferredResult<ResponseEntity> saveEntityTelemetryWithTTL(@PathVariable("entityType") String entityType, @PathVariable("entityId") String entityIdStr,
+                                                                     @PathVariable("scope") String scope, @PathVariable("ttl") Long ttl,
+                                                                     @RequestBody String requestBody) throws ThingsboardException {
+        EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr);
+        return saveTelemetry(entityId, requestBody, ttl);
+    }
+
+    @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
+    @RequestMapping(value = "/{deviceId}/{scope}", method = RequestMethod.DELETE)
+    @ResponseBody
+    public DeferredResult<ResponseEntity> deleteEntityAttributes(@PathVariable("deviceId") String deviceIdStr,
+                                                                 @PathVariable("scope") String scope,
+                                                                 @RequestParam(name = "keys") String keysStr) throws ThingsboardException {
+        EntityId entityId = EntityIdFactory.getByTypeAndUuid(EntityType.DEVICE, deviceIdStr);
+        return deleteAttributes(entityId, scope, keysStr);
+    }
+
+    @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
+    @RequestMapping(value = "/{entityType}/{entityId}/{scope}", method = RequestMethod.DELETE)
+    @ResponseBody
+    public DeferredResult<ResponseEntity> deleteEntityAttributes(@PathVariable("entityType") String entityType, @PathVariable("entityId") String entityIdStr,
+                                                                 @PathVariable("scope") String scope,
+                                                                 @RequestParam(name = "keys") String keysStr) throws ThingsboardException {
+        EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr);
+        return deleteAttributes(entityId, scope, keysStr);
+    }
+
+    private DeferredResult<ResponseEntity> deleteAttributes(EntityId entityIdStr, String scope, String keysStr) throws ThingsboardException {
+        List<String> keys = toKeysList(keysStr);
+        if (keys.isEmpty()) {
+            return getImmediateDeferredResult("Empty keys: " + keysStr, HttpStatus.BAD_REQUEST);
+        }
+        SecurityUser user = getCurrentUser();
+        if (DataConstants.SERVER_SCOPE.equals(scope) ||
+                DataConstants.SHARED_SCOPE.equals(scope) ||
+                DataConstants.CLIENT_SCOPE.equals(scope)) {
+            return accessValidator.validateEntityAndCallback(getCurrentUser(), entityIdStr, (result, entityId) -> {
+                ListenableFuture<List<Void>> future = attributesService.removeAll(entityId, scope, keys);
+                Futures.addCallback(future, new FutureCallback<List<Void>>() {
+                    @Override
+                    public void onSuccess(@Nullable List<Void> tmp) {
+                        logAttributesDeleted(user, entityId, scope, keys, null);
+                        result.setResult(new ResponseEntity<>(HttpStatus.OK));
+                    }
+
+                    @Override
+                    public void onFailure(Throwable t) {
+                        logAttributesDeleted(user, entityId, scope, keys, t);
+                        result.setResult(new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR));
+                    }
+                }, executor);
+            });
+        } else {
+            return getImmediateDeferredResult("Invalid attribute scope: " + scope, HttpStatus.BAD_REQUEST);
+        }
+    }
+
+    private DeferredResult<ResponseEntity> saveAttributes(EntityId entityIdSrc, String scope, JsonNode json) throws ThingsboardException {
+        if (!DataConstants.SERVER_SCOPE.equals(scope) && !DataConstants.SHARED_SCOPE.equals(scope)) {
+            return getImmediateDeferredResult("Invalid scope: " + scope, HttpStatus.BAD_REQUEST);
+        }
+        if (json.isObject()) {
+            List<AttributeKvEntry> attributes = extractRequestAttributes(json);
+            if (attributes.isEmpty()) {
+                return getImmediateDeferredResult("No attributes data found in request body!", HttpStatus.BAD_REQUEST);
+            }
+            SecurityUser user = getCurrentUser();
+            return accessValidator.validateEntityAndCallback(getCurrentUser(), entityIdSrc, (result, entityId) -> {
+                tsSubService.saveAndNotify(entityId, scope, attributes, new FutureCallback<Void>() {
+                    @Override
+                    public void onSuccess(@Nullable Void tmp) {
+                        logAttributesUpdated(user, entityId, scope, attributes, null);
+                        result.setResult(new ResponseEntity(HttpStatus.OK));
+                    }
+
+                    @Override
+                    public void onFailure(Throwable t) {
+                        logAttributesUpdated(user, entityId, scope, attributes, t);
+                        AccessValidator.handleError(t, result, HttpStatus.INTERNAL_SERVER_ERROR);
+                    }
+                });
+            });
+        } else {
+            return getImmediateDeferredResult("Request is not a JSON object", HttpStatus.BAD_REQUEST);
+        }
+    }
+
+    private DeferredResult<ResponseEntity> saveTelemetry(EntityId entityIdSrc, String requestBody, long ttl) throws ThingsboardException {
+        TelemetryUploadRequest telemetryRequest;
+        JsonElement telemetryJson;
+        try {
+            telemetryJson = new JsonParser().parse(requestBody);
+        } catch (Exception e) {
+            return getImmediateDeferredResult("Unable to parse timeseries payload: Invalid JSON body!", HttpStatus.BAD_REQUEST);
+        }
+        try {
+            telemetryRequest = JsonConverter.convertToTelemetry(telemetryJson);
+        } catch (Exception e) {
+            return getImmediateDeferredResult("Unable to parse timeseries payload. Invalid JSON body: " + e.getMessage(), HttpStatus.BAD_REQUEST);
+        }
+        List<TsKvEntry> entries = new ArrayList<>();
+        for (Map.Entry<Long, List<KvEntry>> entry : telemetryRequest.getData().entrySet()) {
+            for (KvEntry kv : entry.getValue()) {
+                entries.add(new BasicTsKvEntry(entry.getKey(), kv));
+            }
+        }
+        if (entries.isEmpty()) {
+            return getImmediateDeferredResult("No timeseries data found in request body!", HttpStatus.BAD_REQUEST);
+        }
+        SecurityUser user = getCurrentUser();
+        return accessValidator.validateEntityAndCallback(getCurrentUser(), entityIdSrc, (result, entityId) -> {
+            tsSubService.saveAndNotify(entityId, entries, ttl, new FutureCallback<Void>() {
+                @Override
+                public void onSuccess(@Nullable Void tmp) {
+                    result.setResult(new ResponseEntity(HttpStatus.OK));
+                }
+
+                @Override
+                public void onFailure(Throwable t) {
+                    AccessValidator.handleError(t, result, HttpStatus.INTERNAL_SERVER_ERROR);
+                }
+            });
+        });
+    }
+
+    private void getLatestTimeseriesValuesCallback(@Nullable DeferredResult<ResponseEntity> result, SecurityUser user, EntityId entityId, String keys) {
+        ListenableFuture<List<TsKvEntry>> future;
+        if (StringUtils.isEmpty(keys)) {
+            future = tsService.findAllLatest(entityId);
+        } else {
+            future = tsService.findLatest(entityId, toKeysList(keys));
+        }
+        Futures.addCallback(future, getTsKvListCallback(result));
+    }
+
+    private void getAttributeValuesCallback(@Nullable DeferredResult<ResponseEntity> result, SecurityUser user, EntityId entityId, String scope, String keys) {
+        List<String> keyList = toKeysList(keys);
+        FutureCallback<List<AttributeKvEntry>> callback = getAttributeValuesToResponseCallback(result, user, scope, entityId, keyList);
+        if (!StringUtils.isEmpty(scope)) {
+            if (keyList != null && !keyList.isEmpty()) {
+                Futures.addCallback(attributesService.find(entityId, scope, keyList), callback);
+            } else {
+                Futures.addCallback(attributesService.findAll(entityId, scope), callback);
+            }
+        } else {
+            List<ListenableFuture<List<AttributeKvEntry>>> futures = new ArrayList<>();
+            for (String tmpScope : DataConstants.allScopes()) {
+                if (keyList != null && !keyList.isEmpty()) {
+                    futures.add(attributesService.find(entityId, tmpScope, keyList));
+                } else {
+                    futures.add(attributesService.findAll(entityId, tmpScope));
+                }
+            }
+
+            ListenableFuture<List<AttributeKvEntry>> future = mergeAllAttributesFutures(futures);
+
+            Futures.addCallback(future, callback);
+        }
+    }
+
+    private void getAttributeKeysCallback(@Nullable DeferredResult<ResponseEntity> result, EntityId entityId, String scope) {
+        Futures.addCallback(attributesService.findAll(entityId, scope), getAttributeKeysToResponseCallback(result));
+    }
+
+    private void getAttributeKeysCallback(@Nullable DeferredResult<ResponseEntity> result, EntityId entityId) {
+        List<ListenableFuture<List<AttributeKvEntry>>> futures = new ArrayList<>();
+        for (String scope : DataConstants.allScopes()) {
+            futures.add(attributesService.findAll(entityId, scope));
+        }
+
+        ListenableFuture<List<AttributeKvEntry>> future = mergeAllAttributesFutures(futures);
+
+        Futures.addCallback(future, getAttributeKeysToResponseCallback(result));
+    }
+
+    private FutureCallback<List<TsKvEntry>> getTsKeysToResponseCallback(final DeferredResult<ResponseEntity> response) {
+        return new FutureCallback<List<TsKvEntry>>() {
+            @Override
+            public void onSuccess(List<TsKvEntry> values) {
+                List<String> keys = values.stream().map(KvEntry::getKey).collect(Collectors.toList());
+                response.setResult(new ResponseEntity<>(keys, HttpStatus.OK));
+            }
+
+            @Override
+            public void onFailure(Throwable e) {
+                log.error("Failed to fetch attributes", e);
+                AccessValidator.handleError(e, response, HttpStatus.INTERNAL_SERVER_ERROR);
+            }
+        };
+    }
+
+    private FutureCallback<List<AttributeKvEntry>> getAttributeKeysToResponseCallback(final DeferredResult<ResponseEntity> response) {
+        return new FutureCallback<List<AttributeKvEntry>>() {
+
+            @Override
+            public void onSuccess(List<AttributeKvEntry> attributes) {
+                List<String> keys = attributes.stream().map(KvEntry::getKey).collect(Collectors.toList());
+                response.setResult(new ResponseEntity<>(keys, HttpStatus.OK));
+            }
+
+            @Override
+            public void onFailure(Throwable e) {
+                log.error("Failed to fetch attributes", e);
+                AccessValidator.handleError(e, response, HttpStatus.INTERNAL_SERVER_ERROR);
+            }
+        };
+    }
+
+    private FutureCallback<List<AttributeKvEntry>> getAttributeValuesToResponseCallback(final DeferredResult<ResponseEntity> response,
+                                                                                        final SecurityUser user, final String scope,
+                                                                                        final EntityId entityId, final List<String> keyList) {
+        return new FutureCallback<List<AttributeKvEntry>>() {
+            @Override
+            public void onSuccess(List<AttributeKvEntry> attributes) {
+                List<AttributeData> values = attributes.stream().map(attribute -> new AttributeData(attribute.getLastUpdateTs(),
+                        attribute.getKey(), attribute.getValue())).collect(Collectors.toList());
+                logAttributesRead(user, entityId, scope, keyList, null);
+                response.setResult(new ResponseEntity<>(values, HttpStatus.OK));
+            }
+
+            @Override
+            public void onFailure(Throwable e) {
+                log.error("Failed to fetch attributes", e);
+                logAttributesRead(user, entityId, scope, keyList, e);
+                AccessValidator.handleError(e, response, HttpStatus.INTERNAL_SERVER_ERROR);
+            }
+        };
+    }
+
+    private FutureCallback<List<TsKvEntry>> getTsKvListCallback(final DeferredResult<ResponseEntity> response) {
+        return new FutureCallback<List<TsKvEntry>>() {
+            @Override
+            public void onSuccess(List<TsKvEntry> data) {
+                Map<String, List<TsData>> result = new LinkedHashMap<>();
+                for (TsKvEntry entry : data) {
+                    result.computeIfAbsent(entry.getKey(), k -> new ArrayList<>())
+                            .add(new TsData(entry.getTs(), entry.getValueAsString()));
+                }
+                response.setResult(new ResponseEntity<>(result, HttpStatus.OK));
+            }
+
+            @Override
+            public void onFailure(Throwable e) {
+                log.error("Failed to fetch historical data", e);
+                AccessValidator.handleError(e, response, HttpStatus.INTERNAL_SERVER_ERROR);
+            }
+        };
+    }
+
+    private void logAttributesDeleted(SecurityUser user, EntityId entityId, String scope, List<String> keys, Throwable e) {
+        auditLogService.logEntityAction(
+                user.getTenantId(),
+                user.getCustomerId(),
+                user.getId(),
+                user.getName(),
+                (UUIDBased & EntityId) entityId,
+                null,
+                ActionType.ATTRIBUTES_DELETED,
+                toException(e),
+                scope,
+                keys);
+    }
+
+    private void logAttributesUpdated(SecurityUser user, EntityId entityId, String scope, List<AttributeKvEntry> attributes, Throwable e) {
+        auditLogService.logEntityAction(
+                user.getTenantId(),
+                user.getCustomerId(),
+                user.getId(),
+                user.getName(),
+                (UUIDBased & EntityId) entityId,
+                null,
+                ActionType.ATTRIBUTES_UPDATED,
+                toException(e),
+                scope,
+                attributes);
+    }
+
+
+    private void logAttributesRead(SecurityUser user, EntityId entityId, String scope, List<String> keys, Throwable e) {
+        auditLogService.logEntityAction(
+                user.getTenantId(),
+                user.getCustomerId(),
+                user.getId(),
+                user.getName(),
+                (UUIDBased & EntityId) entityId,
+                null,
+                ActionType.ATTRIBUTES_READ,
+                toException(e),
+                scope,
+                keys);
+    }
+
+    private ListenableFuture<List<AttributeKvEntry>> mergeAllAttributesFutures(List<ListenableFuture<List<AttributeKvEntry>>> futures) {
+        return Futures.transform(Futures.successfulAsList(futures),
+                (Function<? super List<List<AttributeKvEntry>>, ? extends List<AttributeKvEntry>>) input -> {
+                    List<AttributeKvEntry> tmp = new ArrayList<>();
+                    if (input != null) {
+                        input.forEach(tmp::addAll);
+                    }
+                    return tmp;
+                }, executor);
+    }
+
+    private List<String> toKeysList(String keys) {
+        List<String> keyList = null;
+        if (!StringUtils.isEmpty(keys)) {
+            keyList = Arrays.asList(keys.split(","));
+        }
+        return keyList;
+    }
+
+    private DeferredResult<ResponseEntity> getImmediateDeferredResult(String message, HttpStatus status) {
+        DeferredResult<ResponseEntity> result = new DeferredResult<>();
+        result.setResult(new ResponseEntity<>(message, status));
+        return result;
+    }
+
+    private List<AttributeKvEntry> extractRequestAttributes(JsonNode jsonNode) {
+        long ts = System.currentTimeMillis();
+        List<AttributeKvEntry> attributes = new ArrayList<>();
+        jsonNode.fields().forEachRemaining(entry -> {
+            String key = entry.getKey();
+            JsonNode value = entry.getValue();
+            if (entry.getValue().isTextual()) {
+                attributes.add(new BaseAttributeKvEntry(new StringDataEntry(key, value.textValue()), ts));
+            } else if (entry.getValue().isBoolean()) {
+                attributes.add(new BaseAttributeKvEntry(new BooleanDataEntry(key, value.booleanValue()), ts));
+            } else if (entry.getValue().isDouble()) {
+                attributes.add(new BaseAttributeKvEntry(new DoubleDataEntry(key, value.doubleValue()), ts));
+            } else if (entry.getValue().isNumber()) {
+                if (entry.getValue().isBigInteger()) {
+                    throw new UncheckedApiException(new InvalidParametersException("Big integer values are not supported!"));
+                } else {
+                    attributes.add(new BaseAttributeKvEntry(new LongDataEntry(key, value.longValue()), ts));
+                }
+            }
+        });
+        return attributes;
+    }
+}
diff --git a/application/src/main/java/org/thingsboard/server/controller/TenantController.java b/application/src/main/java/org/thingsboard/server/controller/TenantController.java
index 5acb4eb..bf49074 100644
--- a/application/src/main/java/org/thingsboard/server/controller/TenantController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/TenantController.java
@@ -24,7 +24,7 @@ import org.thingsboard.server.common.data.id.TenantId;
 import org.thingsboard.server.common.data.page.TextPageData;
 import org.thingsboard.server.common.data.page.TextPageLink;
 import org.thingsboard.server.dao.tenant.TenantService;
-import org.thingsboard.server.exception.ThingsboardException;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
 
 @RestController
 @RequestMapping("/api")
diff --git a/application/src/main/java/org/thingsboard/server/controller/UserController.java b/application/src/main/java/org/thingsboard/server/controller/UserController.java
index 2a1531a..52b207d 100644
--- a/application/src/main/java/org/thingsboard/server/controller/UserController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/UserController.java
@@ -29,9 +29,9 @@ import org.thingsboard.server.common.data.page.TextPageData;
 import org.thingsboard.server.common.data.page.TextPageLink;
 import org.thingsboard.server.common.data.security.Authority;
 import org.thingsboard.server.common.data.security.UserCredentials;
-import org.thingsboard.server.exception.ThingsboardErrorCode;
-import org.thingsboard.server.exception.ThingsboardException;
-import org.thingsboard.server.service.mail.MailService;
+import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
+import org.thingsboard.rule.engine.api.MailService;
 import org.thingsboard.server.service.security.model.SecurityUser;
 
 import javax.servlet.http.HttpServletRequest;
diff --git a/application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java b/application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java
index 757f765..bf89f13 100644
--- a/application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java
@@ -25,7 +25,7 @@ import org.thingsboard.server.common.data.page.TextPageLink;
 import org.thingsboard.server.common.data.security.Authority;
 import org.thingsboard.server.common.data.widget.WidgetsBundle;
 import org.thingsboard.server.dao.model.ModelConstants;
-import org.thingsboard.server.exception.ThingsboardException;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
 
 import java.util.List;
 
diff --git a/application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java b/application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java
index 44c7d94..43ece89 100644
--- a/application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java
@@ -23,7 +23,7 @@ import org.thingsboard.server.common.data.id.WidgetTypeId;
 import org.thingsboard.server.common.data.security.Authority;
 import org.thingsboard.server.common.data.widget.WidgetType;
 import org.thingsboard.server.dao.model.ModelConstants;
-import org.thingsboard.server.exception.ThingsboardException;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
 
 import java.util.List;
 
diff --git a/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponse.java b/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponse.java
index 3b897d6..031073f 100644
--- a/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponse.java
+++ b/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponse.java
@@ -16,6 +16,7 @@
 package org.thingsboard.server.exception;
 
 import org.springframework.http.HttpStatus;
+import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
 
 import java.util.Date;
 
diff --git a/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java b/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java
index c70c561..63fe17a 100644
--- a/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java
+++ b/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java
@@ -17,8 +17,6 @@ package org.thingsboard.server.exception;
 
 import com.fasterxml.jackson.databind.ObjectMapper;
 import lombok.extern.slf4j.Slf4j;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.http.HttpStatus;
 import org.springframework.http.MediaType;
@@ -27,6 +25,8 @@ import org.springframework.security.authentication.BadCredentialsException;
 import org.springframework.security.core.AuthenticationException;
 import org.springframework.security.web.access.AccessDeniedHandler;
 import org.springframework.stereotype.Component;
+import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
 import org.thingsboard.server.service.security.exception.AuthMethodNotSupportedException;
 import org.thingsboard.server.service.security.exception.JwtExpiredTokenException;
 
diff --git a/application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java b/application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java
index 0a6081d..9377756 100644
--- a/application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java
+++ b/application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java
@@ -16,6 +16,8 @@
 package org.thingsboard.server.service.component;
 
 import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.fasterxml.jackson.databind.JsonNode;
 import com.google.common.base.Charsets;
 import com.google.common.io.Resources;
 import lombok.extern.slf4j.Slf4j;
@@ -26,12 +28,14 @@ import org.springframework.context.annotation.ClassPathScanningCandidateComponen
 import org.springframework.core.env.Environment;
 import org.springframework.core.type.filter.AnnotationTypeFilter;
 import org.springframework.stereotype.Service;
+import org.thingsboard.rule.engine.api.*;
 import org.thingsboard.server.common.data.plugin.ComponentDescriptor;
 import org.thingsboard.server.common.data.plugin.ComponentType;
 import org.thingsboard.server.dao.component.ComponentDescriptorService;
 import org.thingsboard.server.extensions.api.component.*;
 
 import javax.annotation.PostConstruct;
+import java.io.IOException;
 import java.lang.annotation.Annotation;
 import java.util.*;
 import java.util.stream.Collectors;
@@ -66,6 +70,24 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe
         }
     }
 
+    private void registerRuleNodeComponents() {
+        Set<BeanDefinition> ruleNodeBeanDefinitions = getBeanDefinitions(RuleNode.class);
+        for (BeanDefinition def : ruleNodeBeanDefinitions) {
+            try {
+                String clazzName = def.getBeanClassName();
+                Class<?> clazz = Class.forName(clazzName);
+                RuleNode ruleNodeAnnotation = clazz.getAnnotation(RuleNode.class);
+                ComponentType type = ruleNodeAnnotation.type();
+                ComponentDescriptor component = scanAndPersistComponent(def, type);
+                components.put(component.getClazz(), component);
+                componentsMap.computeIfAbsent(type, k -> new ArrayList<>()).add(component);
+            } catch (Exception e) {
+                log.error("Can't initialize component {}, due to {}", def.getBeanClassName(), e.getMessage(), e);
+                throw new RuntimeException(e);
+            }
+        }
+    }
+
     private void registerComponents(ComponentType type, Class<? extends Annotation> annotation) {
         List<ComponentDescriptor> components = persist(getBeanDefinitions(annotation), type);
         componentsMap.put(type, components);
@@ -79,8 +101,7 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe
     private List<ComponentDescriptor> persist(Set<BeanDefinition> filterDefs, ComponentType type) {
         List<ComponentDescriptor> result = new ArrayList<>();
         for (BeanDefinition def : filterDefs) {
-            ComponentDescriptor scannedComponent = scanAndPersistComponent(def, type);
-            result.add(scannedComponent);
+            result.add(scanAndPersistComponent(def, type));
         }
         return result;
     }
@@ -93,23 +114,26 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe
             Class<?> clazz = Class.forName(clazzName);
             String descriptorResourceName;
             switch (type) {
+                case ENRICHMENT:
                 case FILTER:
-                    Filter filterAnnotation = clazz.getAnnotation(Filter.class);
-                    scannedComponent.setName(filterAnnotation.name());
-                    scannedComponent.setScope(filterAnnotation.scope());
-                    descriptorResourceName = filterAnnotation.descriptor();
-                    break;
-                case PROCESSOR:
-                    Processor processorAnnotation = clazz.getAnnotation(Processor.class);
-                    scannedComponent.setName(processorAnnotation.name());
-                    scannedComponent.setScope(processorAnnotation.scope());
-                    descriptorResourceName = processorAnnotation.descriptor();
-                    break;
+                case TRANSFORMATION:
                 case ACTION:
-                    Action actionAnnotation = clazz.getAnnotation(Action.class);
-                    scannedComponent.setName(actionAnnotation.name());
-                    scannedComponent.setScope(actionAnnotation.scope());
-                    descriptorResourceName = actionAnnotation.descriptor();
+                    RuleNode ruleNodeAnnotation = clazz.getAnnotation(RuleNode.class);
+                    scannedComponent.setName(ruleNodeAnnotation.name());
+                    scannedComponent.setScope(ruleNodeAnnotation.scope());
+                    NodeDefinition nodeDefinition = prepareNodeDefinition(ruleNodeAnnotation);
+                    ObjectNode configurationDescriptor = mapper.createObjectNode();
+                    JsonNode node = mapper.valueToTree(nodeDefinition);
+                    configurationDescriptor.set("nodeDefinition", node);
+                    scannedComponent.setConfigurationDescriptor(configurationDescriptor);
+                    break;
+                case OLD_ACTION:
+                    Action oldActionAnnotation = clazz.getAnnotation(Action.class);
+                    scannedComponent.setName(oldActionAnnotation.name());
+                    scannedComponent.setScope(oldActionAnnotation.scope());
+                    descriptorResourceName = oldActionAnnotation.descriptor();
+                    scannedComponent.setConfigurationDescriptor(mapper.readTree(
+                            Resources.toString(Resources.getResource(descriptorResourceName), Charsets.UTF_8)));
                     break;
                 case PLUGIN:
                     Plugin pluginAnnotation = clazz.getAnnotation(Plugin.class);
@@ -122,18 +146,18 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe
                                     log.error("Can't initialize plugin {}, due to missing action {}!", def.getBeanClassName(), actionClazz.getName());
                                     return new ClassNotFoundException("Action: " + actionClazz.getName() + "is missing!");
                                 });
-                        if (actionComponent.getType() != ComponentType.ACTION) {
+                        if (actionComponent.getType() != ComponentType.OLD_ACTION) {
                             log.error("Plugin {} action {} has wrong component type!", def.getBeanClassName(), actionClazz.getName(), actionComponent.getType());
                             throw new RuntimeException("Plugin " + def.getBeanClassName() + "action " + actionClazz.getName() + " has wrong component type!");
                         }
                     }
-                    scannedComponent.setActions(Arrays.stream(pluginAnnotation.actions()).map(action -> action.getName()).collect(Collectors.joining(",")));
+                    scannedComponent.setActions(Arrays.stream(pluginAnnotation.actions()).map(Class::getName).collect(Collectors.joining(",")));
+                    scannedComponent.setConfigurationDescriptor(mapper.readTree(
+                            Resources.toString(Resources.getResource(descriptorResourceName), Charsets.UTF_8)));
                     break;
                 default:
                     throw new RuntimeException(type + " is not supported yet!");
             }
-            scannedComponent.setConfigurationDescriptor(mapper.readTree(
-                    Resources.toString(Resources.getResource(descriptorResourceName), Charsets.UTF_8)));
             scannedComponent.setClazz(clazzName);
             log.info("Processing scanned component: {}", scannedComponent);
         } catch (Exception e) {
@@ -156,6 +180,23 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe
         return scannedComponent;
     }
 
+    private NodeDefinition prepareNodeDefinition(RuleNode nodeAnnotation) throws Exception {
+        NodeDefinition nodeDefinition = new NodeDefinition();
+        nodeDefinition.setDetails(nodeAnnotation.nodeDetails());
+        nodeDefinition.setDescription(nodeAnnotation.nodeDescription());
+        nodeDefinition.setInEnabled(nodeAnnotation.inEnabled());
+        nodeDefinition.setOutEnabled(nodeAnnotation.outEnabled());
+        nodeDefinition.setRelationTypes(nodeAnnotation.relationTypes());
+        nodeDefinition.setCustomRelations(nodeAnnotation.customRelations());
+        Class<? extends NodeConfiguration> configClazz = nodeAnnotation.configClazz();
+        NodeConfiguration config = configClazz.newInstance();
+        NodeConfiguration defaultConfiguration = config.defaultConfiguration();
+        nodeDefinition.setDefaultConfiguration(mapper.valueToTree(defaultConfiguration));
+        nodeDefinition.setUiResources(nodeAnnotation.uiResources());
+        nodeDefinition.setConfigDirective(nodeAnnotation.configDirective());
+        return nodeDefinition;
+    }
+
     private Set<BeanDefinition> getBeanDefinitions(Class<? extends Annotation> componentType) {
         ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(false);
         scanner.addIncludeFilter(new AnnotationTypeFilter(componentType));
@@ -168,11 +209,10 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe
 
     @Override
     public void discoverComponents() {
-        registerComponents(ComponentType.FILTER, Filter.class);
 
-        registerComponents(ComponentType.PROCESSOR, Processor.class);
+        registerRuleNodeComponents();
 
-        registerComponents(ComponentType.ACTION, Action.class);
+        registerComponents(ComponentType.OLD_ACTION, Action.class);
 
         registerComponents(ComponentType.PLUGIN, Plugin.class);
 
@@ -181,7 +221,20 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe
 
     @Override
     public List<ComponentDescriptor> getComponents(ComponentType type) {
-        return Collections.unmodifiableList(componentsMap.get(type));
+        if (componentsMap.containsKey(type)) {
+            return Collections.unmodifiableList(componentsMap.get(type));
+        } else {
+            return Collections.emptyList();
+        }
+    }
+
+    @Override
+    public List<ComponentDescriptor> getComponents(Set<ComponentType> types) {
+        List<ComponentDescriptor> result = new ArrayList<>();
+        types.stream().filter(type -> componentsMap.containsKey(type)).forEach(type -> {
+            result.addAll(componentsMap.get(type));
+        });
+        return Collections.unmodifiableList(result);
     }
 
     @Override
@@ -199,7 +252,7 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe
             }
             List<ComponentDescriptor> result = new ArrayList<>();
             for (String action : plugin.getActions().split(",")) {
-                getComponent(action).ifPresent(v -> result.add(v));
+                getComponent(action).ifPresent(result::add);
             }
             return result;
         } else {
diff --git a/application/src/main/java/org/thingsboard/server/service/component/ComponentDiscoveryService.java b/application/src/main/java/org/thingsboard/server/service/component/ComponentDiscoveryService.java
index ea27e60..7a15a1b 100644
--- a/application/src/main/java/org/thingsboard/server/service/component/ComponentDiscoveryService.java
+++ b/application/src/main/java/org/thingsboard/server/service/component/ComponentDiscoveryService.java
@@ -20,6 +20,7 @@ import org.thingsboard.server.common.data.plugin.ComponentType;
 
 import java.util.List;
 import java.util.Optional;
+import java.util.Set;
 
 /**
  * @author Andrew Shvayka
@@ -30,6 +31,8 @@ public interface ComponentDiscoveryService {
 
     List<ComponentDescriptor> getComponents(ComponentType type);
 
+    List<ComponentDescriptor> getComponents(Set<ComponentType> types);
+
     Optional<ComponentDescriptor> getComponent(String clazz);
 
     List<ComponentDescriptor> getPluginActions(String pluginClazz);
diff --git a/application/src/main/java/org/thingsboard/server/service/executors/AbstractListeningExecutor.java b/application/src/main/java/org/thingsboard/server/service/executors/AbstractListeningExecutor.java
new file mode 100644
index 0000000..91ef9de
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/executors/AbstractListeningExecutor.java
@@ -0,0 +1,59 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.server.service.executors;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.common.util.concurrent.MoreExecutors;
+import org.thingsboard.rule.engine.api.ListeningExecutor;
+
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+import java.util.concurrent.Callable;
+import java.util.concurrent.Executors;
+
+/**
+ * Created by igor on 4/13/18.
+ */
+public abstract class AbstractListeningExecutor implements ListeningExecutor {
+
+    private ListeningExecutorService service;
+
+    @PostConstruct
+    public void init() {
+        this.service = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(getThreadPollSize()));
+    }
+
+    @PreDestroy
+    public void destroy() {
+        if (this.service != null) {
+            this.service.shutdown();
+        }
+    }
+
+    @Override
+    public <T> ListenableFuture<T> executeAsync(Callable<T> task) {
+        return service.submit(task);
+    }
+
+    @Override
+    public void execute(Runnable command) {
+        service.execute(command);
+    }
+
+    protected abstract int getThreadPollSize();
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/executors/DbCallbackExecutorService.java b/application/src/main/java/org/thingsboard/server/service/executors/DbCallbackExecutorService.java
new file mode 100644
index 0000000..2eec3ed
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/executors/DbCallbackExecutorService.java
@@ -0,0 +1,32 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.server.service.executors;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+@Component
+public class DbCallbackExecutorService extends AbstractListeningExecutor {
+
+    @Value("${actors.rule.db_callback_thread_pool_size}")
+    private int dbCallbackExecutorThreadPoolSize;
+
+    @Override
+    protected int getThreadPollSize() {
+        return dbCallbackExecutorThreadPoolSize;
+    }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java b/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java
index 1ef805f..908faf2 100644
--- a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java
+++ b/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java
@@ -187,7 +187,7 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
 
     @Override
     public void loadSystemRules() throws Exception {
-        loadRules(Paths.get(dataDir, JSON_DIR, SYSTEM_DIR, RULES_DIR), null);
+//        loadRules(Paths.get(dataDir, JSON_DIR, SYSTEM_DIR, RULES_DIR), null);
     }
 
     @Override
@@ -228,7 +228,7 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
                 "Raspberry Pi GPIO control sample application");
 
         loadPlugins(Paths.get(dataDir, JSON_DIR, DEMO_DIR, PLUGINS_DIR), demoTenant.getId());
-        loadRules(Paths.get(dataDir, JSON_DIR, DEMO_DIR, RULES_DIR), demoTenant.getId());
+//        loadRules(Paths.get(dataDir, JSON_DIR, DEMO_DIR, RULES_DIR), demoTenant.getId());
         loadDashboards(Paths.get(dataDir, JSON_DIR, DEMO_DIR, DASHBOARDS_DIR), demoTenant.getId(), null);
     }
 
diff --git a/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java b/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java
index 25b911c..818c935 100644
--- a/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java
+++ b/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java
@@ -27,13 +27,15 @@ import org.springframework.mail.javamail.JavaMailSenderImpl;
 import org.springframework.mail.javamail.MimeMessageHelper;
 import org.springframework.stereotype.Service;
 import org.springframework.ui.velocity.VelocityEngineUtils;
+import org.thingsboard.rule.engine.api.MailService;
 import org.thingsboard.server.common.data.AdminSettings;
+import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
 import org.thingsboard.server.dao.exception.IncorrectParameterException;
 import org.thingsboard.server.dao.settings.AdminSettingsService;
-import org.thingsboard.server.exception.ThingsboardErrorCode;
-import org.thingsboard.server.exception.ThingsboardException;
 
 import javax.annotation.PostConstruct;
+import javax.mail.MessagingException;
 import javax.mail.internet.MimeMessage;
 import java.util.HashMap;
 import java.util.Locale;
@@ -49,18 +51,18 @@ public class DefaultMailService implements MailService {
     public static final String UTF_8 = "UTF-8";
     @Autowired
     private MessageSource messages;
-    
+
     @Autowired
     @Qualifier("velocityEngine")
     private VelocityEngine engine;
-    
+
     private JavaMailSenderImpl mailSender;
-    
+
     private String mailFrom;
-    
+
     @Autowired
-    private AdminSettingsService adminSettingsService; 
-    
+    private AdminSettingsService adminSettingsService;
+
     @PostConstruct
     private void init() {
         updateMailConfiguration();
@@ -77,7 +79,7 @@ public class DefaultMailService implements MailService {
             throw new IncorrectParameterException("Failed to date mail configuration. Settings not found!");
         }
     }
-    
+
     private JavaMailSenderImpl createMailSender(JsonNode jsonConfig) {
         JavaMailSenderImpl mailSender = new JavaMailSenderImpl();
         mailSender.setHost(jsonConfig.get("smtpHost").asText());
@@ -99,7 +101,7 @@ public class DefaultMailService implements MailService {
         javaMailProperties.put(MAIL_PROP + protocol + ".starttls.enable", jsonConfig.has("enableTls") ? jsonConfig.get("enableTls").asText() : "false");
         return javaMailProperties;
     }
-    
+
     private int parsePort(String strPort) {
         try {
             return Integer.valueOf(strPort);
@@ -112,86 +114,102 @@ public class DefaultMailService implements MailService {
     public void sendEmail(String email, String subject, String message) throws ThingsboardException {
         sendMail(mailSender, mailFrom, email, subject, message);
     }
-    
+
     @Override
     public void sendTestMail(JsonNode jsonConfig, String email) throws ThingsboardException {
         JavaMailSenderImpl testMailSender = createMailSender(jsonConfig);
         String mailFrom = jsonConfig.get("mailFrom").asText();
         String subject = messages.getMessage("test.message.subject", null, Locale.US);
-        
+
         Map<String, Object> model = new HashMap<String, Object>();
         model.put(TARGET_EMAIL, email);
-        
+
         String message = VelocityEngineUtils.mergeTemplateIntoString(this.engine,
                 "test.vm", UTF_8, model);
-        
-        sendMail(testMailSender, mailFrom, email, subject, message); 
+
+        sendMail(testMailSender, mailFrom, email, subject, message);
     }
 
     @Override
     public void sendActivationEmail(String activationLink, String email) throws ThingsboardException {
-        
+
         String subject = messages.getMessage("activation.subject", null, Locale.US);
-        
+
         Map<String, Object> model = new HashMap<String, Object>();
         model.put("activationLink", activationLink);
         model.put(TARGET_EMAIL, email);
-        
+
         String message = VelocityEngineUtils.mergeTemplateIntoString(this.engine,
                 "activation.vm", UTF_8, model);
-        
-        sendMail(mailSender, mailFrom, email, subject, message); 
+
+        sendMail(mailSender, mailFrom, email, subject, message);
     }
-    
+
     @Override
     public void sendAccountActivatedEmail(String loginLink, String email) throws ThingsboardException {
-        
+
         String subject = messages.getMessage("account.activated.subject", null, Locale.US);
-        
+
         Map<String, Object> model = new HashMap<String, Object>();
         model.put("loginLink", loginLink);
         model.put(TARGET_EMAIL, email);
-        
+
         String message = VelocityEngineUtils.mergeTemplateIntoString(this.engine,
                 "account.activated.vm", UTF_8, model);
-        
-        sendMail(mailSender, mailFrom, email, subject, message); 
+
+        sendMail(mailSender, mailFrom, email, subject, message);
     }
 
     @Override
     public void sendResetPasswordEmail(String passwordResetLink, String email) throws ThingsboardException {
-        
+
         String subject = messages.getMessage("reset.password.subject", null, Locale.US);
-        
+
         Map<String, Object> model = new HashMap<String, Object>();
         model.put("passwordResetLink", passwordResetLink);
         model.put(TARGET_EMAIL, email);
-        
+
         String message = VelocityEngineUtils.mergeTemplateIntoString(this.engine,
                 "reset.password.vm", UTF_8, model);
-        
-        sendMail(mailSender, mailFrom, email, subject, message); 
+
+        sendMail(mailSender, mailFrom, email, subject, message);
     }
-    
+
     @Override
     public void sendPasswordWasResetEmail(String loginLink, String email) throws ThingsboardException {
-        
+
         String subject = messages.getMessage("password.was.reset.subject", null, Locale.US);
-        
+
         Map<String, Object> model = new HashMap<String, Object>();
         model.put("loginLink", loginLink);
         model.put(TARGET_EMAIL, email);
-        
+
         String message = VelocityEngineUtils.mergeTemplateIntoString(this.engine,
                 "password.was.reset.vm", UTF_8, model);
-        
-        sendMail(mailSender, mailFrom, email, subject, message); 
+
+        sendMail(mailSender, mailFrom, email, subject, message);
     }
 
+    @Override
+    public void send(String from, String to, String cc, String bcc, String subject, String body) throws MessagingException {
+        MimeMessage mailMsg = mailSender.createMimeMessage();
+        MimeMessageHelper helper = new MimeMessageHelper(mailMsg, "UTF-8");
+        helper.setFrom(StringUtils.isBlank(from) ? mailFrom : from);
+        helper.setTo(to.split("\\s*,\\s*"));
+        if (!StringUtils.isBlank(cc)) {
+            helper.setCc(cc.split("\\s*,\\s*"));
+        }
+        if (!StringUtils.isBlank(bcc)) {
+            helper.setBcc(bcc.split("\\s*,\\s*"));
+        }
+        helper.setSubject(subject);
+        helper.setText(body);
+        mailSender.send(helper.getMimeMessage());
+    }
 
-    private void sendMail(JavaMailSenderImpl mailSender, 
-            String mailFrom, String email, 
-            String subject, String message) throws ThingsboardException {
+    private void sendMail(JavaMailSenderImpl mailSender,
+                          String mailFrom, String email,
+                          String subject, String message) throws ThingsboardException {
         try {
             MimeMessage mimeMsg = mailSender.createMimeMessage();
             MimeMessageHelper helper = new MimeMessageHelper(mimeMsg, UTF_8);
@@ -208,7 +226,7 @@ public class DefaultMailService implements MailService {
     protected ThingsboardException handleException(Exception exception) {
         String message;
         if (exception instanceof NestedRuntimeException) {
-            message = ((NestedRuntimeException)exception).getMostSpecificCause().getMessage();
+            message = ((NestedRuntimeException) exception).getMostSpecificCause().getMessage();
         } else {
             message = exception.getMessage();
         }
diff --git a/application/src/main/java/org/thingsboard/server/service/mail/MailExecutorService.java b/application/src/main/java/org/thingsboard/server/service/mail/MailExecutorService.java
new file mode 100644
index 0000000..e8caab6
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/mail/MailExecutorService.java
@@ -0,0 +1,33 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.server.service.mail;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+import org.thingsboard.server.service.executors.AbstractListeningExecutor;
+
+@Component
+public class MailExecutorService extends AbstractListeningExecutor {
+
+    @Value("${actors.rule.mail_thread_pool_size}")
+    private int mailExecutorThreadPoolSize;
+
+    @Override
+    protected int getThreadPollSize() {
+        return mailExecutorThreadPoolSize;
+    }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/script/JsExecutorService.java b/application/src/main/java/org/thingsboard/server/service/script/JsExecutorService.java
new file mode 100644
index 0000000..c60f455
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/script/JsExecutorService.java
@@ -0,0 +1,33 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.server.service.script;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+import org.thingsboard.server.service.executors.AbstractListeningExecutor;
+
+@Component
+public class JsExecutorService extends AbstractListeningExecutor {
+
+    @Value("${actors.rule.js_thread_pool_size}")
+    private int jsExecutorThreadPoolSize;
+
+    @Override
+    protected int getThreadPollSize() {
+        return jsExecutorThreadPoolSize;
+    }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/script/NashornJsEngine.java b/application/src/main/java/org/thingsboard/server/service/script/NashornJsEngine.java
new file mode 100644
index 0000000..d68f6fe
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/script/NashornJsEngine.java
@@ -0,0 +1,211 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.server.service.script;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.collect.Sets;
+import jdk.nashorn.api.scripting.NashornScriptEngineFactory;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.TbMsgMetaData;
+
+import javax.script.Invocable;
+import javax.script.ScriptEngine;
+import javax.script.ScriptException;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Set;
+
+
+@Slf4j
+public class NashornJsEngine implements org.thingsboard.rule.engine.api.ScriptEngine {
+
+    public static final String MSG = "msg";
+    public static final String METADATA = "metadata";
+    public static final String MSG_TYPE = "msgType";
+
+    private static final String JS_WRAPPER_PREFIX_TEMPLATE = "function %s(msgStr, metadataStr, msgType) { " +
+            "    var msg = JSON.parse(msgStr); " +
+            "    var metadata = JSON.parse(metadataStr); " +
+            "    return JSON.stringify(%s(msg, metadata, msgType));" +
+            "    function %s(%s, %s, %s) {";
+    private static final String JS_WRAPPER_SUFFIX = "}" +
+            "\n}";
+
+    private static final ObjectMapper mapper = new ObjectMapper();
+    private static NashornScriptEngineFactory factory = new NashornScriptEngineFactory();
+    private ScriptEngine engine = factory.getScriptEngine(new String[]{"--no-java"});
+
+    private final String invokeFunctionName;
+
+    public NashornJsEngine(String script, String functionName, String... argNames) {
+        this.invokeFunctionName = "invokeInternal" + this.hashCode();
+        String msgArg;
+        String metadataArg;
+        String msgTypeArg;
+        if (argNames != null && argNames.length == 3) {
+            msgArg = argNames[0];
+            metadataArg = argNames[1];
+            msgTypeArg = argNames[2];
+        } else {
+            msgArg = MSG;
+            metadataArg = METADATA;
+            msgTypeArg = MSG_TYPE;
+        }
+        String jsWrapperPrefix = String.format(JS_WRAPPER_PREFIX_TEMPLATE, this.invokeFunctionName,
+                functionName, functionName, msgArg, metadataArg, msgTypeArg);
+        compileScript(jsWrapperPrefix + script + JS_WRAPPER_SUFFIX);
+    }
+
+    private void compileScript(String script) {
+        try {
+            engine.eval(script);
+        } catch (ScriptException e) {
+            log.warn("Failed to compile JS script: {}", e.getMessage(), e);
+            throw new IllegalArgumentException("Can't compile script: " + e.getMessage());
+        }
+    }
+
+    private static String[] prepareArgs(TbMsg msg) {
+        try {
+            String[] args = new String[3];
+            if (msg.getData() != null) {
+                args[0] = msg.getData();
+            } else {
+                args[0] = "";
+            }
+            args[1] = mapper.writeValueAsString(msg.getMetaData().getData());
+            args[2] = msg.getType();
+            return args;
+        } catch (Throwable th) {
+            throw new IllegalArgumentException("Cannot bind js args", th);
+        }
+    }
+
+    private static TbMsg unbindMsg(JsonNode msgData, TbMsg msg) {
+        try {
+            String data = null;
+            Map<String, String> metadata = null;
+            String messageType = null;
+            if (msgData.has(MSG)) {
+                JsonNode msgPayload = msgData.get(MSG);
+                data = mapper.writeValueAsString(msgPayload);
+            }
+            if (msgData.has(METADATA)) {
+                JsonNode msgMetadata = msgData.get(METADATA);
+                metadata = mapper.convertValue(msgMetadata, new TypeReference<Map<String, String>>() {
+                });
+            }
+            if (msgData.has(MSG_TYPE)) {
+                messageType = msgData.get(MSG_TYPE).asText();
+            }
+            String newData = data != null ? data : msg.getData();
+            TbMsgMetaData newMetadata = metadata != null ? new TbMsgMetaData(metadata) : msg.getMetaData();
+            String newMessageType = !StringUtils.isEmpty(messageType) ? messageType : msg.getType();
+            return new TbMsg(msg.getId(), newMessageType, msg.getOriginator(), newMetadata, newData);
+        } catch (Throwable th) {
+            th.printStackTrace();
+            throw new RuntimeException("Failed to unbind message data from javascript result", th);
+        }
+    }
+
+    @Override
+    public TbMsg executeUpdate(TbMsg msg) throws ScriptException {
+        JsonNode result = executeScript(msg);
+        if (!result.isObject()) {
+            log.warn("Wrong result type: {}", result.getNodeType());
+            throw new ScriptException("Wrong result type: " + result.getNodeType());
+        }
+        return unbindMsg(result, msg);
+    }
+
+    @Override
+    public TbMsg executeGenerate(TbMsg prevMsg) throws ScriptException {
+        JsonNode result = executeScript(prevMsg);
+        if (!result.isObject()) {
+            log.warn("Wrong result type: {}", result.getNodeType());
+            throw new ScriptException("Wrong result type: " + result.getNodeType());
+        }
+        return unbindMsg(result, prevMsg);
+    }
+
+    @Override
+    public JsonNode executeJson(TbMsg msg) throws ScriptException {
+        return executeScript(msg);
+    }
+
+    @Override
+    public String executeToString(TbMsg msg) throws ScriptException {
+        JsonNode result = executeScript(msg);
+        if (!result.isTextual()) {
+            log.warn("Wrong result type: {}", result.getNodeType());
+            throw new ScriptException("Wrong result type: " + result.getNodeType());
+        }
+        return result.asText();
+    }
+
+    @Override
+    public boolean executeFilter(TbMsg msg) throws ScriptException {
+        JsonNode result = executeScript(msg);
+        if (!result.isBoolean()) {
+            log.warn("Wrong result type: {}", result.getNodeType());
+            throw new ScriptException("Wrong result type: " + result.getNodeType());
+        }
+        return result.asBoolean();
+    }
+
+    @Override
+    public Set<String> executeSwitch(TbMsg msg) throws ScriptException {
+        JsonNode result = executeScript(msg);
+        if (result.isTextual()) {
+            return Collections.singleton(result.asText());
+        } else if (result.isArray()) {
+            Set<String> nextStates = Sets.newHashSet();
+            for (JsonNode val : result) {
+                if (!val.isTextual()) {
+                    log.warn("Wrong result type: {}", val.getNodeType());
+                    throw new ScriptException("Wrong result type: " + val.getNodeType());
+                } else {
+                    nextStates.add(val.asText());
+                }
+            }
+            return nextStates;
+        } else {
+            log.warn("Wrong result type: {}", result.getNodeType());
+            throw new ScriptException("Wrong result type: " + result.getNodeType());
+        }
+    }
+
+    private JsonNode executeScript(TbMsg msg) throws ScriptException {
+        try {
+            String[] inArgs = prepareArgs(msg);
+            String eval = ((Invocable)engine).invokeFunction(this.invokeFunctionName, inArgs[0], inArgs[1], inArgs[2]).toString();
+            return mapper.readTree(eval);
+        } catch (ScriptException | IllegalArgumentException th) {
+            throw th;
+        } catch (Throwable th) {
+            th.printStackTrace();
+            throw new RuntimeException("Failed to execute js script", th);
+        }
+    }
+
+    public void destroy() {
+        engine = null;
+    }
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/security/AccessValidator.java b/application/src/main/java/org/thingsboard/server/service/security/AccessValidator.java
new file mode 100644
index 0000000..c1f3688
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/security/AccessValidator.java
@@ -0,0 +1,295 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.server.service.security;
+
+import com.google.common.base.Function;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Component;
+import org.springframework.web.context.request.async.DeferredResult;
+import org.thingsboard.server.actors.plugin.ValidationResult;
+import org.thingsboard.server.common.data.Customer;
+import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.Tenant;
+import org.thingsboard.server.common.data.asset.Asset;
+import org.thingsboard.server.common.data.id.AssetId;
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.EntityIdFactory;
+import org.thingsboard.server.common.data.id.RuleChainId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.rule.RuleChain;
+import org.thingsboard.server.controller.HttpValidationCallback;
+import org.thingsboard.server.dao.alarm.AlarmService;
+import org.thingsboard.server.dao.asset.AssetService;
+import org.thingsboard.server.dao.customer.CustomerService;
+import org.thingsboard.server.dao.device.DeviceService;
+import org.thingsboard.server.dao.rule.RuleChainService;
+import org.thingsboard.server.dao.tenant.TenantService;
+import org.thingsboard.server.dao.user.UserService;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
+import org.thingsboard.server.extensions.api.exception.ToErrorResponseEntity;
+import org.thingsboard.server.service.security.model.SecurityUser;
+
+import javax.annotation.Nullable;
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.function.BiConsumer;
+
+/**
+ * Created by ashvayka on 27.03.18.
+ */
+@Component
+public class AccessValidator {
+
+    public static final String CUSTOMER_USER_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION = "Customer user is not allowed to perform this operation!";
+    public static final String SYSTEM_ADMINISTRATOR_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION = "System administrator is not allowed to perform this operation!";
+    public static final String DEVICE_WITH_REQUESTED_ID_NOT_FOUND = "Device with requested id wasn't found!";
+
+    @Autowired
+    protected TenantService tenantService;
+
+    @Autowired
+    protected CustomerService customerService;
+
+    @Autowired
+    protected UserService userService;
+
+    @Autowired
+    protected DeviceService deviceService;
+
+    @Autowired
+    protected AssetService assetService;
+
+    @Autowired
+    protected AlarmService alarmService;
+
+    @Autowired
+    protected RuleChainService ruleChainService;
+
+    private ExecutorService executor;
+
+    @PostConstruct
+    public void initExecutor() {
+        executor = Executors.newSingleThreadExecutor();
+    }
+
+    @PreDestroy
+    public void shutdownExecutor() {
+        if (executor != null) {
+            executor.shutdownNow();
+        }
+    }
+
+    public DeferredResult<ResponseEntity> validateEntityAndCallback(SecurityUser currentUser, String entityType, String entityIdStr,
+                                                                    BiConsumer<DeferredResult<ResponseEntity>, EntityId> onSuccess) throws ThingsboardException {
+        return validateEntityAndCallback(currentUser, entityType, entityIdStr, onSuccess, (result, t) -> handleError(t, result, HttpStatus.INTERNAL_SERVER_ERROR));
+    }
+
+    public DeferredResult<ResponseEntity> validateEntityAndCallback(SecurityUser currentUser, String entityType, String entityIdStr,
+                                                                    BiConsumer<DeferredResult<ResponseEntity>, EntityId> onSuccess,
+                                                                    BiConsumer<DeferredResult<ResponseEntity>, Throwable> onFailure) throws ThingsboardException {
+        return validateEntityAndCallback(currentUser, EntityIdFactory.getByTypeAndId(entityType, entityIdStr),
+                onSuccess, onFailure);
+    }
+
+    public DeferredResult<ResponseEntity> validateEntityAndCallback(SecurityUser currentUser, EntityId entityId,
+                                                                    BiConsumer<DeferredResult<ResponseEntity>, EntityId> onSuccess) throws ThingsboardException {
+        return validateEntityAndCallback(currentUser, entityId, onSuccess, (result, t) -> handleError(t, result, HttpStatus.INTERNAL_SERVER_ERROR));
+    }
+
+    public DeferredResult<ResponseEntity> validateEntityAndCallback(SecurityUser currentUser, EntityId entityId,
+                                                                    BiConsumer<DeferredResult<ResponseEntity>, EntityId> onSuccess,
+                                                                    BiConsumer<DeferredResult<ResponseEntity>, Throwable> onFailure) throws ThingsboardException {
+
+        final DeferredResult<ResponseEntity> response = new DeferredResult<>();
+
+        validate(currentUser, entityId, new HttpValidationCallback(response,
+                new FutureCallback<DeferredResult<ResponseEntity>>() {
+                    @Override
+                    public void onSuccess(@Nullable DeferredResult<ResponseEntity> result) {
+                        onSuccess.accept(response, entityId);
+                    }
+
+                    @Override
+                    public void onFailure(Throwable t) {
+                        onFailure.accept(response, t);
+                    }
+                }));
+
+        return response;
+    }
+
+    public <T> void validate(SecurityUser currentUser, EntityId entityId, FutureCallback<ValidationResult> callback) {
+        switch (entityId.getEntityType()) {
+            case DEVICE:
+                validateDevice(currentUser, entityId, callback);
+                return;
+            case ASSET:
+                validateAsset(currentUser, entityId, callback);
+                return;
+            case RULE_CHAIN:
+                validateRuleChain(currentUser, entityId, callback);
+                return;
+            case CUSTOMER:
+                validateCustomer(currentUser, entityId, callback);
+                return;
+            case TENANT:
+                validateTenant(currentUser, entityId, callback);
+                return;
+            default:
+                //TODO: add support of other entities
+                throw new IllegalStateException("Not Implemented!");
+        }
+    }
+
+    private void validateDevice(final SecurityUser currentUser, EntityId entityId, FutureCallback<ValidationResult> callback) {
+        if (currentUser.isSystemAdmin()) {
+            callback.onSuccess(ValidationResult.accessDenied(SYSTEM_ADMINISTRATOR_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION));
+        } else {
+            ListenableFuture<Device> deviceFuture = deviceService.findDeviceByIdAsync(new DeviceId(entityId.getId()));
+            Futures.addCallback(deviceFuture, getCallback(callback, device -> {
+                if (device == null) {
+                    return ValidationResult.entityNotFound(DEVICE_WITH_REQUESTED_ID_NOT_FOUND);
+                } else {
+                    if (!device.getTenantId().equals(currentUser.getTenantId())) {
+                        return ValidationResult.accessDenied("Device doesn't belong to the current Tenant!");
+                    } else if (currentUser.isCustomerUser() && !device.getCustomerId().equals(currentUser.getCustomerId())) {
+                        return ValidationResult.accessDenied("Device doesn't belong to the current Customer!");
+                    } else {
+                        return ValidationResult.ok();
+                    }
+                }
+            }), executor);
+        }
+    }
+
+    private <T> void validateAsset(final SecurityUser currentUser, EntityId entityId, FutureCallback<ValidationResult> callback) {
+        if (currentUser.isSystemAdmin()) {
+            callback.onSuccess(ValidationResult.accessDenied(SYSTEM_ADMINISTRATOR_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION));
+        } else {
+            ListenableFuture<Asset> assetFuture = assetService.findAssetByIdAsync(new AssetId(entityId.getId()));
+            Futures.addCallback(assetFuture, getCallback(callback, asset -> {
+                if (asset == null) {
+                    return ValidationResult.entityNotFound("Asset with requested id wasn't found!");
+                } else {
+                    if (!asset.getTenantId().equals(currentUser.getTenantId())) {
+                        return ValidationResult.accessDenied("Asset doesn't belong to the current Tenant!");
+                    } else if (currentUser.isCustomerUser() && !asset.getCustomerId().equals(currentUser.getCustomerId())) {
+                        return ValidationResult.accessDenied("Asset doesn't belong to the current Customer!");
+                    } else {
+                        return ValidationResult.ok();
+                    }
+                }
+            }), executor);
+        }
+    }
+
+
+    private <T> void validateRuleChain(final SecurityUser currentUser, EntityId entityId, FutureCallback<ValidationResult> callback) {
+        if (currentUser.isCustomerUser()) {
+            callback.onSuccess(ValidationResult.accessDenied(CUSTOMER_USER_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION));
+        } else {
+            ListenableFuture<RuleChain> ruleChainFuture = ruleChainService.findRuleChainByIdAsync(new RuleChainId(entityId.getId()));
+            Futures.addCallback(ruleChainFuture, getCallback(callback, ruleChain -> {
+                if (ruleChain == null) {
+                    return ValidationResult.entityNotFound("Rule chain with requested id wasn't found!");
+                } else {
+                    if (currentUser.isTenantAdmin() && !ruleChain.getTenantId().equals(currentUser.getTenantId())) {
+                        return ValidationResult.accessDenied("Rule chain doesn't belong to the current Tenant!");
+                    } else if (currentUser.isSystemAdmin() && !ruleChain.getTenantId().isNullUid()) {
+                        return ValidationResult.accessDenied("Rule chain is not in system scope!");
+                    } else {
+                        return ValidationResult.ok();
+                    }
+                }
+            }), executor);
+        }
+    }
+
+    private <T> void validateCustomer(final SecurityUser currentUser, EntityId entityId, FutureCallback<ValidationResult> callback) {
+        if (currentUser.isSystemAdmin()) {
+            callback.onSuccess(ValidationResult.accessDenied(SYSTEM_ADMINISTRATOR_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION));
+        } else {
+            ListenableFuture<Customer> customerFuture = customerService.findCustomerByIdAsync(new CustomerId(entityId.getId()));
+            Futures.addCallback(customerFuture, getCallback(callback, customer -> {
+                if (customer == null) {
+                    return ValidationResult.entityNotFound("Customer with requested id wasn't found!");
+                } else {
+                    if (!customer.getTenantId().equals(currentUser.getTenantId())) {
+                        return ValidationResult.accessDenied("Customer doesn't belong to the current Tenant!");
+                    } else if (currentUser.isCustomerUser() && !customer.getId().equals(currentUser.getCustomerId())) {
+                        return ValidationResult.accessDenied("Customer doesn't relate to the currently authorized customer user!");
+                    } else {
+                        return ValidationResult.ok();
+                    }
+                }
+            }), executor);
+        }
+    }
+
+    private <T> void validateTenant(final SecurityUser currentUser, EntityId entityId, FutureCallback<ValidationResult> callback) {
+        if (currentUser.isCustomerUser()) {
+            callback.onSuccess(ValidationResult.accessDenied(CUSTOMER_USER_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION));
+        } else if (currentUser.isSystemAdmin()) {
+            callback.onSuccess(ValidationResult.ok());
+        } else {
+            ListenableFuture<Tenant> tenantFuture = tenantService.findTenantByIdAsync(new TenantId(entityId.getId()));
+            Futures.addCallback(tenantFuture, getCallback(callback, tenant -> {
+                if (tenant == null) {
+                    return ValidationResult.entityNotFound("Tenant with requested id wasn't found!");
+                } else if (!tenant.getId().equals(currentUser.getTenantId())) {
+                    return ValidationResult.accessDenied("Tenant doesn't relate to the currently authorized user!");
+                } else {
+                    return ValidationResult.ok();
+                }
+            }), executor);
+        }
+    }
+
+    private <T> FutureCallback<T> getCallback(FutureCallback<ValidationResult> callback, Function<T, ValidationResult> transformer) {
+        return new FutureCallback<T>() {
+            @Override
+            public void onSuccess(@Nullable T result) {
+                callback.onSuccess(transformer.apply(result));
+            }
+
+            @Override
+            public void onFailure(Throwable t) {
+                callback.onFailure(t);
+            }
+        };
+    }
+
+    public static void handleError(Throwable e, final DeferredResult<ResponseEntity> response, HttpStatus defaultErrorStatus) {
+        ResponseEntity responseEntity;
+        if (e != null && e instanceof ToErrorResponseEntity) {
+            responseEntity = ((ToErrorResponseEntity) e).toErrorResponseEntity();
+        } else if (e != null && e instanceof IllegalArgumentException) {
+            responseEntity = new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST);
+        } else {
+            responseEntity = new ResponseEntity<>(defaultErrorStatus);
+        }
+        response.setResult(responseEntity);
+    }
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/security/model/SecurityUser.java b/application/src/main/java/org/thingsboard/server/service/security/model/SecurityUser.java
index f4a28a0..91ee7bf 100644
--- a/application/src/main/java/org/thingsboard/server/service/security/model/SecurityUser.java
+++ b/application/src/main/java/org/thingsboard/server/service/security/model/SecurityUser.java
@@ -18,6 +18,7 @@ package org.thingsboard.server.service.security.model;
 import org.springframework.security.core.GrantedAuthority;
 import org.springframework.security.core.authority.SimpleGrantedAuthority;
 import org.thingsboard.server.common.data.User;
+import org.thingsboard.server.common.data.id.EntityId;
 import org.thingsboard.server.common.data.id.UserId;
 
 import java.util.Collection;
diff --git a/application/src/main/java/org/thingsboard/server/service/security/ValidationCallback.java b/application/src/main/java/org/thingsboard/server/service/security/ValidationCallback.java
new file mode 100644
index 0000000..2b91c60
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/security/ValidationCallback.java
@@ -0,0 +1,71 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.server.service.security;
+
+import com.google.common.util.concurrent.FutureCallback;
+import org.thingsboard.server.actors.plugin.ValidationResult;
+import org.thingsboard.server.actors.plugin.ValidationResultCode;
+import org.thingsboard.server.extensions.api.exception.AccessDeniedException;
+import org.thingsboard.server.extensions.api.exception.EntityNotFoundException;
+import org.thingsboard.server.extensions.api.exception.InternalErrorException;
+import org.thingsboard.server.extensions.api.exception.UnauthorizedException;
+
+/**
+ * Created by ashvayka on 31.03.18.
+ */
+public class ValidationCallback<T> implements FutureCallback<ValidationResult> {
+
+    private final T response;
+    private final FutureCallback<T> action;
+
+    public ValidationCallback(T response, FutureCallback<T> action) {
+        this.response = response;
+        this.action = action;
+    }
+
+    @Override
+    public void onSuccess(ValidationResult result) {
+        ValidationResultCode resultCode = result.getResultCode();
+        if (resultCode == ValidationResultCode.OK) {
+            action.onSuccess(response);
+        } else {
+            Exception e;
+            switch (resultCode) {
+                case ENTITY_NOT_FOUND:
+                    e = new EntityNotFoundException(result.getMessage());
+                    break;
+                case UNAUTHORIZED:
+                    e = new UnauthorizedException(result.getMessage());
+                    break;
+                case ACCESS_DENIED:
+                    e = new AccessDeniedException(result.getMessage());
+                    break;
+                case INTERNAL_ERROR:
+                    e = new InternalErrorException(result.getMessage());
+                    break;
+                default:
+                    e = new UnauthorizedException("Permission denied.");
+                    break;
+            }
+            onFailure(e);
+        }
+    }
+
+    @Override
+    public void onFailure(Throwable e) {
+        action.onFailure(e);
+    }
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java
new file mode 100644
index 0000000..58bbec5
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java
@@ -0,0 +1,335 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.server.service.telemetry;
+
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.kv.AttributeKvEntry;
+import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
+import org.thingsboard.server.common.data.kv.TsKvEntry;
+import org.thingsboard.server.common.msg.cluster.ServerAddress;
+import org.thingsboard.server.dao.attributes.AttributesService;
+import org.thingsboard.server.dao.timeseries.TimeseriesService;
+import org.thingsboard.server.extensions.core.plugin.telemetry.handlers.TelemetryFeature;
+import org.thingsboard.server.extensions.core.plugin.telemetry.sub.Subscription;
+import org.thingsboard.server.extensions.core.plugin.telemetry.sub.SubscriptionState;
+import org.thingsboard.server.extensions.core.plugin.telemetry.sub.SubscriptionUpdate;
+import org.thingsboard.server.service.cluster.routing.ClusterRoutingService;
+
+import javax.annotation.Nullable;
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+/**
+ * Created by ashvayka on 27.03.18.
+ */
+@Service
+@Slf4j
+public class DefaultTelemetrySubscriptionService implements TelemetrySubscriptionService {
+
+    @Autowired
+    private TelemetryWebSocketService wsService;
+
+    @Autowired
+    private AttributesService attrService;
+
+    @Autowired
+    private TimeseriesService tsService;
+
+    @Autowired
+    private ClusterRoutingService routingService;
+
+    private ExecutorService tsCallBackExecutor;
+    private ExecutorService wsCallBackExecutor;
+
+    @PostConstruct
+    public void initExecutor() {
+        tsCallBackExecutor = Executors.newSingleThreadExecutor();
+        wsCallBackExecutor = Executors.newSingleThreadExecutor();
+    }
+
+    @PreDestroy
+    public void shutdownExecutor() {
+        if (tsCallBackExecutor != null) {
+            tsCallBackExecutor.shutdownNow();
+        }
+        if (wsCallBackExecutor != null) {
+            wsCallBackExecutor.shutdownNow();
+        }
+    }
+
+    private final Map<EntityId, Set<Subscription>> subscriptionsByEntityId = new HashMap<>();
+
+    private final Map<String, Map<Integer, Subscription>> subscriptionsByWsSessionId = new HashMap<>();
+
+    @Override
+    public void addLocalWsSubscription(String sessionId, EntityId entityId, SubscriptionState sub) {
+        Optional<ServerAddress> server = routingService.resolveById(entityId);
+        Subscription subscription;
+        if (server.isPresent()) {
+            ServerAddress address = server.get();
+            log.trace("[{}] Forwarding subscription [{}] for device [{}] to [{}]", sessionId, sub.getSubscriptionId(), entityId, address);
+            subscription = new Subscription(sub, true, address);
+//            rpcHandler.onNewSubscription(ctx, address, sessionId, subscription);
+        } else {
+            log.trace("[{}] Registering local subscription [{}] for device [{}]", sessionId, sub.getSubscriptionId(), entityId);
+            subscription = new Subscription(sub, true);
+        }
+        registerSubscription(sessionId, entityId, subscription);
+    }
+
+    @Override
+    public void cleanupLocalWsSessionSubscriptions(TelemetryWebSocketSessionRef sessionRef, String sessionId) {
+        cleanupLocalWsSessionSubscriptions(sessionId);
+    }
+
+    @Override
+    public void removeSubscription(String sessionId, int subscriptionId) {
+        log.debug("[{}][{}] Going to remove subscription.", sessionId, subscriptionId);
+        Map<Integer, Subscription> sessionSubscriptions = subscriptionsByWsSessionId.get(sessionId);
+        if (sessionSubscriptions != null) {
+            Subscription subscription = sessionSubscriptions.remove(subscriptionId);
+            if (subscription != null) {
+                processSubscriptionRemoval(sessionId, sessionSubscriptions, subscription);
+            } else {
+                log.debug("[{}][{}] Subscription not found!", sessionId, subscriptionId);
+            }
+        } else {
+            log.debug("[{}] No session subscriptions found!", sessionId);
+        }
+    }
+
+    @Override
+    public void saveAndNotify(EntityId entityId, List<TsKvEntry> ts, FutureCallback<Void> callback) {
+        saveAndNotify(entityId, ts, 0L, callback);
+    }
+
+    @Override
+    public void saveAndNotify(EntityId entityId, List<TsKvEntry> ts, long ttl, FutureCallback<Void> callback) {
+        ListenableFuture<List<Void>> saveFuture = tsService.save(entityId, ts, ttl);
+        addMainCallback(saveFuture, callback);
+        addWsCallback(saveFuture, success -> onTimeseriesUpdate(entityId, ts));
+    }
+
+    @Override
+    public void saveAndNotify(EntityId entityId, String scope, List<AttributeKvEntry> attributes, FutureCallback<Void> callback) {
+        ListenableFuture<List<Void>> saveFuture = attrService.save(entityId, scope, attributes);
+        addMainCallback(saveFuture, callback);
+        addWsCallback(saveFuture, success -> onAttributesUpdate(entityId, scope, attributes));
+    }
+
+    private void onAttributesUpdate(EntityId entityId, String scope, List<AttributeKvEntry> attributes) {
+        Optional<ServerAddress> serverAddress = routingService.resolveById(entityId);
+        if (!serverAddress.isPresent()) {
+            onLocalAttributesUpdate(entityId, scope, attributes);
+        } else {
+//            rpcHandler.onAttributesUpdate(ctx, serverAddress.get(), entityId, entries);
+        }
+    }
+
+    private void onTimeseriesUpdate(EntityId entityId, List<TsKvEntry> ts) {
+        Optional<ServerAddress> serverAddress = routingService.resolveById(entityId);
+        if (!serverAddress.isPresent()) {
+            onLocalTimeseriesUpdate(entityId, ts);
+        } else {
+//            rpcHandler.onTimeseriesUpdate(ctx, serverAddress.get(), entityId, entries);
+        }
+    }
+
+    private void onLocalAttributesUpdate(EntityId entityId, String scope, List<AttributeKvEntry> attributes) {
+        onLocalSubUpdate(entityId, s -> TelemetryFeature.ATTRIBUTES == s.getType() && (StringUtils.isEmpty(s.getScope()) || scope.equals(s.getScope())), s -> {
+            List<TsKvEntry> subscriptionUpdate = null;
+            for (AttributeKvEntry kv : attributes) {
+                if (s.isAllKeys() || s.getKeyStates().containsKey(kv.getKey())) {
+                    if (subscriptionUpdate == null) {
+                        subscriptionUpdate = new ArrayList<>();
+                    }
+                    subscriptionUpdate.add(new BasicTsKvEntry(kv.getLastUpdateTs(), kv));
+                }
+            }
+            return subscriptionUpdate;
+        });
+    }
+
+    private void onLocalTimeseriesUpdate(EntityId entityId, List<TsKvEntry> ts) {
+        onLocalSubUpdate(entityId, s -> TelemetryFeature.TIMESERIES == s.getType(), s -> {
+            List<TsKvEntry> subscriptionUpdate = null;
+            for (TsKvEntry kv : ts) {
+                if (s.isAllKeys() || s.getKeyStates().containsKey((kv.getKey()))) {
+                    if (subscriptionUpdate == null) {
+                        subscriptionUpdate = new ArrayList<>();
+                    }
+                    subscriptionUpdate.add(kv);
+                }
+            }
+            return subscriptionUpdate;
+        });
+    }
+
+    private void onLocalSubUpdate(EntityId entityId, Predicate<Subscription> filter, Function<Subscription, List<TsKvEntry>> f) {
+        Set<Subscription> deviceSubscriptions = subscriptionsByEntityId.get(entityId);
+        if (deviceSubscriptions != null) {
+            deviceSubscriptions.stream().filter(filter).forEach(s -> {
+                String sessionId = s.getWsSessionId();
+                List<TsKvEntry> subscriptionUpdate = f.apply(s);
+                if (subscriptionUpdate == null || !subscriptionUpdate.isEmpty()) {
+                    SubscriptionUpdate update = new SubscriptionUpdate(s.getSubscriptionId(), subscriptionUpdate);
+                    if (s.isLocal()) {
+                        updateSubscriptionState(sessionId, s, update);
+                        wsService.sendWsMsg(sessionId, update);
+                    } else {
+                        //TODO: ashvayka
+//                        rpcHandler.onSubscriptionUpdate(ctx, s.getServer(), sessionId, update);
+                    }
+                }
+            });
+        } else {
+            log.debug("[{}] No device subscriptions to process!", entityId);
+        }
+    }
+
+    private void updateSubscriptionState(String sessionId, Subscription subState, SubscriptionUpdate update) {
+        log.trace("[{}] updating subscription state {} using onUpdate {}", sessionId, subState, update);
+        update.getLatestValues().entrySet().forEach(e -> subState.setKeyState(e.getKey(), e.getValue()));
+    }
+
+    private void registerSubscription(String sessionId, EntityId entityId, Subscription subscription) {
+        Set<Subscription> deviceSubscriptions = subscriptionsByEntityId.computeIfAbsent(entityId, k -> new HashSet<>());
+        deviceSubscriptions.add(subscription);
+        Map<Integer, Subscription> sessionSubscriptions = subscriptionsByWsSessionId.computeIfAbsent(sessionId, k -> new HashMap<>());
+        sessionSubscriptions.put(subscription.getSubscriptionId(), subscription);
+    }
+
+    public void cleanupLocalWsSessionSubscriptions(String sessionId) {
+        cleanupWsSessionSubscriptions(sessionId, true);
+    }
+
+    public void cleanupRemoteWsSessionSubscriptions(String sessionId) {
+        cleanupWsSessionSubscriptions(sessionId, false);
+    }
+
+    private void cleanupWsSessionSubscriptions(String sessionId, boolean localSession) {
+        log.debug("[{}] Removing all subscriptions for particular session.", sessionId);
+        Map<Integer, Subscription> sessionSubscriptions = subscriptionsByWsSessionId.get(sessionId);
+        if (sessionSubscriptions != null) {
+            int sessionSubscriptionSize = sessionSubscriptions.size();
+
+            for (Subscription subscription : sessionSubscriptions.values()) {
+                EntityId entityId = subscription.getEntityId();
+                Set<Subscription> deviceSubscriptions = subscriptionsByEntityId.get(entityId);
+                deviceSubscriptions.remove(subscription);
+                if (deviceSubscriptions.isEmpty()) {
+                    subscriptionsByEntityId.remove(entityId);
+                }
+            }
+            subscriptionsByWsSessionId.remove(sessionId);
+            log.debug("[{}] Removed {} subscriptions for particular session.", sessionId, sessionSubscriptionSize);
+
+            if (localSession) {
+                notifyWsSubscriptionClosed(sessionId, sessionSubscriptions);
+            }
+        } else {
+            log.debug("[{}] No subscriptions found!", sessionId);
+        }
+    }
+
+    private void notifyWsSubscriptionClosed(String sessionId, Map<Integer, Subscription> sessionSubscriptions) {
+        Set<ServerAddress> affectedServers = new HashSet<>();
+        for (Subscription subscription : sessionSubscriptions.values()) {
+            if (subscription.getServer() != null) {
+                affectedServers.add(subscription.getServer());
+            }
+        }
+        for (ServerAddress address : affectedServers) {
+            log.debug("[{}] Going to onSubscriptionUpdate [{}] server about session close event", sessionId, address);
+//            rpcHandler.onSessionClose(ctx, address, sessionId);
+        }
+    }
+
+    private void processSubscriptionRemoval(String sessionId, Map<Integer, Subscription> sessionSubscriptions, Subscription subscription) {
+        EntityId entityId = subscription.getEntityId();
+        if (subscription.isLocal() && subscription.getServer() != null) {
+//            rpcHandler.onSubscriptionClose(ctx, subscription.getServer(), sessionId, subscription.getSubscriptionId());
+        }
+        if (sessionSubscriptions.isEmpty()) {
+            log.debug("[{}] Removed last subscription for particular session.", sessionId);
+            subscriptionsByWsSessionId.remove(sessionId);
+        } else {
+            log.debug("[{}] Removed session subscription.", sessionId);
+        }
+        Set<Subscription> deviceSubscriptions = subscriptionsByEntityId.get(entityId);
+        if (deviceSubscriptions != null) {
+            boolean result = deviceSubscriptions.remove(subscription);
+            if (result) {
+                if (deviceSubscriptions.size() == 0) {
+                    log.debug("[{}] Removed last subscription for particular device.", sessionId);
+                    subscriptionsByEntityId.remove(entityId);
+                } else {
+                    log.debug("[{}] Removed device subscription.", sessionId);
+                }
+            } else {
+                log.debug("[{}] Subscription not found!", sessionId);
+            }
+        } else {
+            log.debug("[{}] No device subscriptions found!", sessionId);
+        }
+    }
+
+    private void addMainCallback(ListenableFuture<List<Void>> saveFuture, final FutureCallback<Void> callback) {
+        Futures.addCallback(saveFuture, new FutureCallback<List<Void>>() {
+            @Override
+            public void onSuccess(@Nullable List<Void> result) {
+                callback.onSuccess(null);
+            }
+
+            @Override
+            public void onFailure(Throwable t) {
+                callback.onFailure(t);
+            }
+        }, tsCallBackExecutor);
+    }
+
+    private void addWsCallback(ListenableFuture<List<Void>> saveFuture, Consumer<Void> callback) {
+        Futures.addCallback(saveFuture, new FutureCallback<List<Void>>() {
+            @Override
+            public void onSuccess(@Nullable List<Void> result) {
+                callback.accept(null);
+            }
+
+            @Override
+            public void onFailure(Throwable t) {
+            }
+        }, wsCallBackExecutor);
+    }
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetryWebSocketService.java b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetryWebSocketService.java
new file mode 100644
index 0000000..57f3876
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetryWebSocketService.java
@@ -0,0 +1,563 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.server.service.telemetry;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.base.Function;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.hazelcast.util.function.Consumer;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
+import org.thingsboard.server.actors.plugin.ValidationResult;
+import org.thingsboard.server.common.data.DataConstants;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.EntityIdFactory;
+import org.thingsboard.server.common.data.kv.Aggregation;
+import org.thingsboard.server.common.data.kv.AttributeKvEntry;
+import org.thingsboard.server.common.data.kv.BaseTsKvQuery;
+import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
+import org.thingsboard.server.common.data.kv.TsKvEntry;
+import org.thingsboard.server.common.data.kv.TsKvQuery;
+import org.thingsboard.server.dao.attributes.AttributesService;
+import org.thingsboard.server.dao.timeseries.TimeseriesService;
+import org.thingsboard.server.extensions.api.exception.UnauthorizedException;
+import org.thingsboard.server.extensions.api.plugins.PluginContext;
+import org.thingsboard.server.extensions.api.plugins.ws.SessionEvent;
+import org.thingsboard.server.extensions.core.plugin.telemetry.cmd.AttributesSubscriptionCmd;
+import org.thingsboard.server.extensions.core.plugin.telemetry.cmd.GetHistoryCmd;
+import org.thingsboard.server.extensions.core.plugin.telemetry.cmd.SubscriptionCmd;
+import org.thingsboard.server.extensions.core.plugin.telemetry.cmd.TelemetryPluginCmd;
+import org.thingsboard.server.extensions.core.plugin.telemetry.cmd.TelemetryPluginCmdsWrapper;
+import org.thingsboard.server.extensions.core.plugin.telemetry.cmd.TimeseriesSubscriptionCmd;
+import org.thingsboard.server.extensions.core.plugin.telemetry.handlers.TelemetryFeature;
+import org.thingsboard.server.extensions.core.plugin.telemetry.sub.SubscriptionErrorCode;
+import org.thingsboard.server.extensions.core.plugin.telemetry.sub.SubscriptionState;
+import org.thingsboard.server.extensions.core.plugin.telemetry.sub.SubscriptionUpdate;
+import org.thingsboard.server.service.security.AccessValidator;
+
+import javax.annotation.Nullable;
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.stream.Collectors;
+
+/**
+ * Created by ashvayka on 27.03.18.
+ */
+@Service
+@Slf4j
+public class DefaultTelemetryWebSocketService implements TelemetryWebSocketService {
+
+    public static final int DEFAULT_LIMIT = 100;
+    public static final Aggregation DEFAULT_AGGREGATION = Aggregation.NONE;
+    private static final int UNKNOWN_SUBSCRIPTION_ID = 0;
+    private static final String PROCESSING_MSG = "[{}] Processing: {}";
+    private static final ObjectMapper jsonMapper = new ObjectMapper();
+    private static final String FAILED_TO_FETCH_DATA = "Failed to fetch data!";
+    private static final String FAILED_TO_FETCH_ATTRIBUTES = "Failed to fetch attributes!";
+    private static final String SESSION_META_DATA_NOT_FOUND = "Session meta-data not found!";
+
+    private final ConcurrentMap<String, WsSessionMetaData> wsSessionsMap = new ConcurrentHashMap<>();
+
+    @Autowired
+    private TelemetrySubscriptionService subscriptionManager;
+
+    @Autowired
+    private TelemetryWebSocketMsgEndpoint msgEndpoint;
+
+    @Autowired
+    private AccessValidator accessValidator;
+
+    @Autowired
+    private AttributesService attributesService;
+
+    @Autowired
+    private TimeseriesService tsService;
+
+    private ExecutorService executor;
+
+    @PostConstruct
+    public void initExecutor() {
+        executor = Executors.newSingleThreadExecutor();
+    }
+
+    @PreDestroy
+    public void shutdownExecutor() {
+        if (executor != null) {
+            executor.shutdownNow();
+        }
+    }
+
+    @Override
+    public void handleWebSocketSessionEvent(TelemetryWebSocketSessionRef sessionRef, SessionEvent event) {
+        String sessionId = sessionRef.getSessionId();
+        log.debug(PROCESSING_MSG, sessionId, event);
+        switch (event.getEventType()) {
+            case ESTABLISHED:
+                wsSessionsMap.put(sessionId, new WsSessionMetaData(sessionRef));
+                break;
+            case ERROR:
+                log.debug("[{}] Unknown websocket session error: {}. ", sessionId, event.getError().orElse(null));
+                break;
+            case CLOSED:
+                wsSessionsMap.remove(sessionId);
+                subscriptionManager.cleanupLocalWsSessionSubscriptions(sessionRef, sessionId);
+                break;
+        }
+    }
+
+    @Override
+    public void handleWebSocketMsg(TelemetryWebSocketSessionRef sessionRef, String msg) {
+        if (log.isTraceEnabled()) {
+            log.trace("[{}] Processing: {}", sessionRef.getSessionId(), msg);
+        }
+
+        try {
+            TelemetryPluginCmdsWrapper cmdsWrapper = jsonMapper.readValue(msg, TelemetryPluginCmdsWrapper.class);
+            if (cmdsWrapper != null) {
+                if (cmdsWrapper.getAttrSubCmds() != null) {
+                    cmdsWrapper.getAttrSubCmds().forEach(cmd -> handleWsAttributesSubscriptionCmd(sessionRef, cmd));
+                }
+                if (cmdsWrapper.getTsSubCmds() != null) {
+                    cmdsWrapper.getTsSubCmds().forEach(cmd -> handleWsTimeseriesSubscriptionCmd(sessionRef, cmd));
+                }
+                if (cmdsWrapper.getHistoryCmds() != null) {
+                    cmdsWrapper.getHistoryCmds().forEach(cmd -> handleWsHistoryCmd(sessionRef, cmd));
+                }
+            }
+        } catch (IOException e) {
+            log.warn("Failed to decode subscription cmd: {}", e.getMessage(), e);
+            SubscriptionUpdate update = new SubscriptionUpdate(UNKNOWN_SUBSCRIPTION_ID, SubscriptionErrorCode.INTERNAL_ERROR, SESSION_META_DATA_NOT_FOUND);
+            sendWsMsg(sessionRef, update);
+        }
+    }
+
+    @Override
+    public void sendWsMsg(String sessionId, SubscriptionUpdate update) {
+        WsSessionMetaData md = wsSessionsMap.get(sessionId);
+        if (md != null) {
+            sendWsMsg(md.getSessionRef(), update);
+        }
+    }
+
+    private void handleWsAttributesSubscriptionCmd(TelemetryWebSocketSessionRef sessionRef, AttributesSubscriptionCmd cmd) {
+        String sessionId = sessionRef.getSessionId();
+        log.debug("[{}] Processing: {}", sessionId, cmd);
+
+        if (validateSessionMetadata(sessionRef, cmd, sessionId)) {
+            if (cmd.isUnsubscribe()) {
+                unsubscribe(sessionRef, cmd, sessionId);
+            } else if (validateSubscriptionCmd(sessionRef, cmd)) {
+                EntityId entityId = EntityIdFactory.getByTypeAndId(cmd.getEntityType(), cmd.getEntityId());
+                log.debug("[{}] fetching latest attributes ({}) values for device: {}", sessionId, cmd.getKeys(), entityId);
+                Optional<Set<String>> keysOptional = getKeys(cmd);
+                if (keysOptional.isPresent()) {
+                    List<String> keys = new ArrayList<>(keysOptional.get());
+                    handleWsAttributesSubscriptionByKeys(sessionRef, cmd, sessionId, entityId, keys);
+                } else {
+                    handleWsAttributesSubscription(sessionRef, cmd, sessionId, entityId);
+                }
+            }
+        }
+    }
+
+    private void handleWsAttributesSubscriptionByKeys(TelemetryWebSocketSessionRef sessionRef,
+                                                      AttributesSubscriptionCmd cmd, String sessionId, EntityId entityId,
+                                                      List<String> keys) {
+        FutureCallback<List<AttributeKvEntry>> callback = new FutureCallback<List<AttributeKvEntry>>() {
+            @Override
+            public void onSuccess(List<AttributeKvEntry> data) {
+                List<TsKvEntry> attributesData = data.stream().map(d -> new BasicTsKvEntry(d.getLastUpdateTs(), d)).collect(Collectors.toList());
+                sendWsMsg(sessionRef, new SubscriptionUpdate(cmd.getCmdId(), attributesData));
+
+                Map<String, Long> subState = new HashMap<>(keys.size());
+                keys.forEach(key -> subState.put(key, 0L));
+                attributesData.forEach(v -> subState.put(v.getKey(), v.getTs()));
+
+                SubscriptionState sub = new SubscriptionState(sessionId, cmd.getCmdId(), entityId, TelemetryFeature.ATTRIBUTES, false, subState, cmd.getScope());
+                subscriptionManager.addLocalWsSubscription(sessionId, entityId, sub);
+            }
+
+            @Override
+            public void onFailure(Throwable e) {
+                log.error(FAILED_TO_FETCH_ATTRIBUTES, e);
+                SubscriptionUpdate update;
+                if (UnauthorizedException.class.isInstance(e)) {
+                    update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.UNAUTHORIZED,
+                            SubscriptionErrorCode.UNAUTHORIZED.getDefaultMsg());
+                } else {
+                    update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR,
+                            FAILED_TO_FETCH_ATTRIBUTES);
+                }
+                sendWsMsg(sessionRef, update);
+            }
+        };
+
+        if (StringUtils.isEmpty(cmd.getScope())) {
+            accessValidator.validate(sessionRef.getSecurityCtx(), entityId, getAttributesFetchCallback(entityId, keys, callback));
+        } else {
+            accessValidator.validate(sessionRef.getSecurityCtx(), entityId, getAttributesFetchCallback(entityId, cmd.getScope(), keys, callback));
+        }
+    }
+
+    private void handleWsHistoryCmd(TelemetryWebSocketSessionRef sessionRef, GetHistoryCmd cmd) {
+        String sessionId = sessionRef.getSessionId();
+        WsSessionMetaData sessionMD = wsSessionsMap.get(sessionId);
+        if (sessionMD == null) {
+            log.warn("[{}] Session meta data not found. ", sessionId);
+            SubscriptionUpdate update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR,
+                    SESSION_META_DATA_NOT_FOUND);
+            sendWsMsg(sessionRef, update);
+            return;
+        }
+        if (cmd.getEntityId() == null || cmd.getEntityId().isEmpty() || cmd.getEntityType() == null || cmd.getEntityType().isEmpty()) {
+            SubscriptionUpdate update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST,
+                    "Device id is empty!");
+            sendWsMsg(sessionRef, update);
+            return;
+        }
+        if (cmd.getKeys() == null || cmd.getKeys().isEmpty()) {
+            SubscriptionUpdate update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST,
+                    "Keys are empty!");
+            sendWsMsg(sessionRef, update);
+            return;
+        }
+        EntityId entityId = EntityIdFactory.getByTypeAndId(cmd.getEntityType(), cmd.getEntityId());
+        List<String> keys = new ArrayList<>(getKeys(cmd).orElse(Collections.emptySet()));
+        List<TsKvQuery> queries = keys.stream().map(key -> new BaseTsKvQuery(key, cmd.getStartTs(), cmd.getEndTs(), cmd.getInterval(), getLimit(cmd.getLimit()), getAggregation(cmd.getAgg())))
+                .collect(Collectors.toList());
+
+        FutureCallback<List<TsKvEntry>> callback = new FutureCallback<List<TsKvEntry>>() {
+            @Override
+            public void onSuccess(List<TsKvEntry> data) {
+                sendWsMsg(sessionRef, new SubscriptionUpdate(cmd.getCmdId(), data));
+            }
+
+            @Override
+            public void onFailure(Throwable e) {
+                SubscriptionUpdate update;
+                if (UnauthorizedException.class.isInstance(e)) {
+                    update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.UNAUTHORIZED,
+                            SubscriptionErrorCode.UNAUTHORIZED.getDefaultMsg());
+                } else {
+                    update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR,
+                            FAILED_TO_FETCH_DATA);
+                }
+                sendWsMsg(sessionRef, update);
+            }
+        };
+        accessValidator.validate(sessionRef.getSecurityCtx(), entityId,
+                on(r -> Futures.addCallback(tsService.findAll(entityId, queries), callback, executor), callback::onFailure));
+    }
+
+    private void handleWsAttributesSubscription(TelemetryWebSocketSessionRef sessionRef,
+                                                AttributesSubscriptionCmd cmd, String sessionId, EntityId entityId) {
+        FutureCallback<List<AttributeKvEntry>> callback = new FutureCallback<List<AttributeKvEntry>>() {
+            @Override
+            public void onSuccess(List<AttributeKvEntry> data) {
+                List<TsKvEntry> attributesData = data.stream().map(d -> new BasicTsKvEntry(d.getLastUpdateTs(), d)).collect(Collectors.toList());
+                sendWsMsg(sessionRef, new SubscriptionUpdate(cmd.getCmdId(), attributesData));
+
+                Map<String, Long> subState = new HashMap<>(attributesData.size());
+                attributesData.forEach(v -> subState.put(v.getKey(), v.getTs()));
+
+                SubscriptionState sub = new SubscriptionState(sessionId, cmd.getCmdId(), entityId, TelemetryFeature.ATTRIBUTES, true, subState, cmd.getScope());
+                subscriptionManager.addLocalWsSubscription(sessionId, entityId, sub);
+            }
+
+            @Override
+            public void onFailure(Throwable e) {
+                log.error(FAILED_TO_FETCH_ATTRIBUTES, e);
+                SubscriptionUpdate update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR,
+                        FAILED_TO_FETCH_ATTRIBUTES);
+                sendWsMsg(sessionRef, update);
+            }
+        };
+
+
+        if (StringUtils.isEmpty(cmd.getScope())) {
+            accessValidator.validate(sessionRef.getSecurityCtx(), entityId, getAttributesFetchCallback(entityId, callback));
+        } else {
+            accessValidator.validate(sessionRef.getSecurityCtx(), entityId, getAttributesFetchCallback(entityId, cmd.getScope(), callback));
+        }
+    }
+
+    private void handleWsTimeseriesSubscriptionCmd(TelemetryWebSocketSessionRef sessionRef, TimeseriesSubscriptionCmd cmd) {
+        String sessionId = sessionRef.getSessionId();
+        log.debug("[{}] Processing: {}", sessionId, cmd);
+
+        if (validateSessionMetadata(sessionRef, cmd, sessionId)) {
+            if (cmd.isUnsubscribe()) {
+                unsubscribe(sessionRef, cmd, sessionId);
+            } else if (validateSubscriptionCmd(sessionRef, cmd)) {
+                EntityId entityId = EntityIdFactory.getByTypeAndId(cmd.getEntityType(), cmd.getEntityId());
+                Optional<Set<String>> keysOptional = getKeys(cmd);
+
+                if (keysOptional.isPresent()) {
+                    handleWsTimeseriesSubscriptionByKeys(sessionRef, cmd, sessionId, entityId);
+                } else {
+                    handleWsTimeseriesSubscription(sessionRef, cmd, sessionId, entityId);
+                }
+            }
+        }
+    }
+
+    private void handleWsTimeseriesSubscriptionByKeys(TelemetryWebSocketSessionRef sessionRef,
+                                                      TimeseriesSubscriptionCmd cmd, String sessionId, EntityId entityId) {
+        long startTs;
+        if (cmd.getTimeWindow() > 0) {
+            List<String> keys = new ArrayList<>(getKeys(cmd).orElse(Collections.emptySet()));
+            log.debug("[{}] fetching timeseries data for last {} ms for keys: ({}) for device : {}", sessionId, cmd.getTimeWindow(), cmd.getKeys(), entityId);
+            startTs = cmd.getStartTs();
+            long endTs = cmd.getStartTs() + cmd.getTimeWindow();
+            List<TsKvQuery> queries = keys.stream().map(key -> new BaseTsKvQuery(key, startTs, endTs, cmd.getInterval(),
+                    getLimit(cmd.getLimit()), getAggregation(cmd.getAgg()))).collect(Collectors.toList());
+
+            final FutureCallback<List<TsKvEntry>> callback = getSubscriptionCallback(sessionRef, cmd, sessionId, entityId, startTs, keys);
+            accessValidator.validate(sessionRef.getSecurityCtx(), entityId,
+                    on(r -> Futures.addCallback(tsService.findAll(entityId, queries), callback, executor), callback::onFailure));
+        } else {
+            List<String> keys = new ArrayList<>(getKeys(cmd).orElse(Collections.emptySet()));
+            startTs = System.currentTimeMillis();
+            log.debug("[{}] fetching latest timeseries data for keys: ({}) for device : {}", sessionId, cmd.getKeys(), entityId);
+            final FutureCallback<List<TsKvEntry>> callback = getSubscriptionCallback(sessionRef, cmd, sessionId, entityId, startTs, keys);
+            accessValidator.validate(sessionRef.getSecurityCtx(), entityId,
+                    on(r -> Futures.addCallback(tsService.findLatest(entityId, keys), callback, executor), callback::onFailure));
+        }
+    }
+
+    private void handleWsTimeseriesSubscription(TelemetryWebSocketSessionRef sessionRef,
+                                                TimeseriesSubscriptionCmd cmd, String sessionId, EntityId entityId) {
+        FutureCallback<List<TsKvEntry>> callback = new FutureCallback<List<TsKvEntry>>() {
+            @Override
+            public void onSuccess(List<TsKvEntry> data) {
+                sendWsMsg(sessionRef, new SubscriptionUpdate(cmd.getCmdId(), data));
+                Map<String, Long> subState = new HashMap<>(data.size());
+                data.forEach(v -> subState.put(v.getKey(), v.getTs()));
+                SubscriptionState sub = new SubscriptionState(sessionId, cmd.getCmdId(), entityId, TelemetryFeature.TIMESERIES, true, subState, cmd.getScope());
+                subscriptionManager.addLocalWsSubscription(sessionId, entityId, sub);
+            }
+
+            @Override
+            public void onFailure(Throwable e) {
+                SubscriptionUpdate update;
+                if (UnauthorizedException.class.isInstance(e)) {
+                    update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.UNAUTHORIZED,
+                            SubscriptionErrorCode.UNAUTHORIZED.getDefaultMsg());
+                } else {
+                    update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR,
+                            FAILED_TO_FETCH_DATA);
+                }
+                sendWsMsg(sessionRef, update);
+            }
+        };
+        accessValidator.validate(sessionRef.getSecurityCtx(), entityId,
+                on(r -> Futures.addCallback(tsService.findAllLatest(entityId), callback, executor), callback::onFailure));
+    }
+
+    private FutureCallback<List<TsKvEntry>> getSubscriptionCallback(final TelemetryWebSocketSessionRef sessionRef, final TimeseriesSubscriptionCmd cmd, final String sessionId, final EntityId entityId, final long startTs, final List<String> keys) {
+        return new FutureCallback<List<TsKvEntry>>() {
+            @Override
+            public void onSuccess(List<TsKvEntry> data) {
+                sendWsMsg(sessionRef, new SubscriptionUpdate(cmd.getCmdId(), data));
+
+                Map<String, Long> subState = new HashMap<>(keys.size());
+                keys.forEach(key -> subState.put(key, startTs));
+                data.forEach(v -> subState.put(v.getKey(), v.getTs()));
+                SubscriptionState sub = new SubscriptionState(sessionId, cmd.getCmdId(), entityId, TelemetryFeature.TIMESERIES, false, subState, cmd.getScope());
+                subscriptionManager.addLocalWsSubscription(sessionId, entityId, sub);
+            }
+
+            @Override
+            public void onFailure(Throwable e) {
+                log.error(FAILED_TO_FETCH_DATA, e);
+                SubscriptionUpdate update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR,
+                        FAILED_TO_FETCH_DATA);
+                sendWsMsg(sessionRef, update);
+            }
+        };
+    }
+
+    private void unsubscribe(TelemetryWebSocketSessionRef sessionRef, SubscriptionCmd cmd, String sessionId) {
+        if (cmd.getEntityId() == null || cmd.getEntityId().isEmpty()) {
+            subscriptionManager.cleanupLocalWsSessionSubscriptions(sessionRef, sessionId);
+        } else {
+            subscriptionManager.removeSubscription(sessionId, cmd.getCmdId());
+        }
+    }
+
+    private boolean validateSubscriptionCmd(TelemetryWebSocketSessionRef sessionRef, SubscriptionCmd cmd) {
+        if (cmd.getEntityId() == null || cmd.getEntityId().isEmpty()) {
+            SubscriptionUpdate update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST,
+                    "Device id is empty!");
+            sendWsMsg(sessionRef, update);
+            return false;
+        }
+        return true;
+    }
+
+    private boolean validateSessionMetadata(TelemetryWebSocketSessionRef sessionRef, SubscriptionCmd cmd, String sessionId) {
+        WsSessionMetaData sessionMD = wsSessionsMap.get(sessionId);
+        if (sessionMD == null) {
+            log.warn("[{}] Session meta data not found. ", sessionId);
+            SubscriptionUpdate update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR,
+                    SESSION_META_DATA_NOT_FOUND);
+            sendWsMsg(sessionRef, update);
+            return false;
+        } else {
+            return true;
+        }
+    }
+
+    private void sendWsMsg(TelemetryWebSocketSessionRef sessionRef, SubscriptionUpdate update) {
+        try {
+            msgEndpoint.send(sessionRef, jsonMapper.writeValueAsString(update));
+        } catch (JsonProcessingException e) {
+            log.warn("[{}] Failed to encode reply: {}", sessionRef.getSessionId(), update, e);
+        } catch (IOException e) {
+            log.warn("[{}] Failed to send reply: {}", sessionRef.getSessionId(), update, e);
+        }
+    }
+
+    private static Optional<Set<String>> getKeys(TelemetryPluginCmd cmd) {
+        if (!StringUtils.isEmpty(cmd.getKeys())) {
+            Set<String> keys = new HashSet<>();
+            Collections.addAll(keys, cmd.getKeys().split(","));
+            return Optional.of(keys);
+        } else {
+            return Optional.empty();
+        }
+    }
+
+    private ListenableFuture<List<AttributeKvEntry>> mergeAllAttributesFutures(List<ListenableFuture<List<AttributeKvEntry>>> futures) {
+        return Futures.transform(Futures.successfulAsList(futures),
+                (Function<? super List<List<AttributeKvEntry>>, ? extends List<AttributeKvEntry>>) input -> {
+                    List<AttributeKvEntry> tmp = new ArrayList<>();
+                    if (input != null) {
+                        input.forEach(tmp::addAll);
+                    }
+                    return tmp;
+                }, executor);
+    }
+
+    private <T> FutureCallback<ValidationResult> getAttributesFetchCallback(final EntityId entityId, final List<String> keys, final FutureCallback<List<AttributeKvEntry>> callback) {
+        return new FutureCallback<ValidationResult>() {
+            @Override
+            public void onSuccess(@Nullable ValidationResult result) {
+                List<ListenableFuture<List<AttributeKvEntry>>> futures = new ArrayList<>();
+                for (String scope : DataConstants.allScopes()) {
+                    futures.add(attributesService.find(entityId, scope, keys));
+                }
+
+                ListenableFuture<List<AttributeKvEntry>> future = mergeAllAttributesFutures(futures);
+                Futures.addCallback(future, callback);
+            }
+
+            @Override
+            public void onFailure(Throwable t) {
+                callback.onFailure(t);
+            }
+        };
+    }
+
+    private <T> FutureCallback<ValidationResult> getAttributesFetchCallback(final EntityId entityId, final String scope, final List<String> keys, final FutureCallback<List<AttributeKvEntry>> callback) {
+        return new FutureCallback<ValidationResult>() {
+            @Override
+            public void onSuccess(@Nullable ValidationResult result) {
+                Futures.addCallback(attributesService.find(entityId, scope, keys), callback);
+            }
+
+            @Override
+            public void onFailure(Throwable t) {
+                callback.onFailure(t);
+            }
+        };
+    }
+
+    private <T> FutureCallback<ValidationResult> getAttributesFetchCallback(final EntityId entityId, final FutureCallback<List<AttributeKvEntry>> callback) {
+        return new FutureCallback<ValidationResult>() {
+            @Override
+            public void onSuccess(@Nullable ValidationResult result) {
+                List<ListenableFuture<List<AttributeKvEntry>>> futures = new ArrayList<>();
+                for (String scope : DataConstants.allScopes()) {
+                    futures.add(attributesService.findAll(entityId, scope));
+                }
+
+                ListenableFuture<List<AttributeKvEntry>> future = mergeAllAttributesFutures(futures);
+                Futures.addCallback(future, callback);
+            }
+
+            @Override
+            public void onFailure(Throwable t) {
+                callback.onFailure(t);
+            }
+        };
+    }
+
+    private <T> FutureCallback<ValidationResult> getAttributesFetchCallback(final EntityId entityId, final String scope, final FutureCallback<List<AttributeKvEntry>> callback) {
+        return new FutureCallback<ValidationResult>() {
+            @Override
+            public void onSuccess(@Nullable ValidationResult result) {
+                Futures.addCallback(attributesService.findAll(entityId, scope), callback);
+            }
+
+            @Override
+            public void onFailure(Throwable t) {
+                callback.onFailure(t);
+            }
+        };
+    }
+
+    private FutureCallback<ValidationResult> on(Consumer<ValidationResult> success, Consumer<Throwable> failure) {
+        return new FutureCallback<ValidationResult>() {
+            @Override
+            public void onSuccess(@Nullable ValidationResult result) {
+                success.accept(result);
+            }
+
+            @Override
+            public void onFailure(Throwable t) {
+                failure.accept(t);
+            }
+        };
+    }
+
+
+    private static Aggregation getAggregation(String agg) {
+        return StringUtils.isEmpty(agg) ? DEFAULT_AGGREGATION : Aggregation.valueOf(agg);
+    }
+
+    private int getLimit(int limit) {
+        return limit == 0 ? DEFAULT_LIMIT : limit;
+    }
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/TelemetrySubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/telemetry/TelemetrySubscriptionService.java
new file mode 100644
index 0000000..923d06b
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/telemetry/TelemetrySubscriptionService.java
@@ -0,0 +1,33 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.server.service.telemetry;
+
+import org.thingsboard.rule.engine.api.RuleEngineTelemetryService;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.extensions.core.plugin.telemetry.sub.SubscriptionState;
+
+/**
+ * Created by ashvayka on 27.03.18.
+ */
+public interface TelemetrySubscriptionService extends RuleEngineTelemetryService {
+
+    void addLocalWsSubscription(String sessionId, EntityId entityId, SubscriptionState sub);
+
+    void cleanupLocalWsSessionSubscriptions(TelemetryWebSocketSessionRef sessionRef, String sessionId);
+
+    void removeSubscription(String sessionId, int cmdId);
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/TelemetryWebSocketService.java b/application/src/main/java/org/thingsboard/server/service/telemetry/TelemetryWebSocketService.java
new file mode 100644
index 0000000..be6fc56
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/telemetry/TelemetryWebSocketService.java
@@ -0,0 +1,31 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.server.service.telemetry;
+
+import org.thingsboard.server.extensions.api.plugins.ws.SessionEvent;
+import org.thingsboard.server.extensions.core.plugin.telemetry.sub.SubscriptionUpdate;
+
+/**
+ * Created by ashvayka on 27.03.18.
+ */
+public interface TelemetryWebSocketService {
+
+    void handleWebSocketSessionEvent(TelemetryWebSocketSessionRef sessionRef, SessionEvent sessionEvent);
+
+    void handleWebSocketMsg(TelemetryWebSocketSessionRef sessionRef, String msg);
+
+    void sendWsMsg(String sessionId, SubscriptionUpdate update);
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/TelemetryWebSocketSessionRef.java b/application/src/main/java/org/thingsboard/server/service/telemetry/TelemetryWebSocketSessionRef.java
new file mode 100644
index 0000000..53438c5
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/telemetry/TelemetryWebSocketSessionRef.java
@@ -0,0 +1,68 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.server.service.telemetry;
+
+import lombok.Getter;
+import org.thingsboard.server.service.security.model.SecurityUser;
+
+import java.net.InetSocketAddress;
+import java.util.Objects;
+
+/**
+ * Created by ashvayka on 27.03.18.
+ */
+public class TelemetryWebSocketSessionRef {
+
+    private static final long serialVersionUID = 1L;
+
+    @Getter
+    private final String sessionId;
+    @Getter
+    private final SecurityUser securityCtx;
+    @Getter
+    private final InetSocketAddress localAddress;
+    @Getter
+    private final InetSocketAddress remoteAddress;
+
+    public TelemetryWebSocketSessionRef(String sessionId, SecurityUser securityCtx, InetSocketAddress localAddress, InetSocketAddress remoteAddress) {
+        this.sessionId = sessionId;
+        this.securityCtx = securityCtx;
+        this.localAddress = localAddress;
+        this.remoteAddress = remoteAddress;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        TelemetryWebSocketSessionRef that = (TelemetryWebSocketSessionRef) o;
+        return Objects.equals(sessionId, that.sessionId);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(sessionId);
+    }
+
+    @Override
+    public String toString() {
+        return "TelemetryWebSocketSessionRef{" +
+                "sessionId='" + sessionId + '\'' +
+                ", localAddress=" + localAddress +
+                ", remoteAddress=" + remoteAddress +
+                '}';
+    }
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/TelemetryWebSocketTextMsg.java b/application/src/main/java/org/thingsboard/server/service/telemetry/TelemetryWebSocketTextMsg.java
new file mode 100644
index 0000000..5d4630c
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/telemetry/TelemetryWebSocketTextMsg.java
@@ -0,0 +1,34 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.server.service.telemetry;
+
+import lombok.Data;
+import lombok.Getter;
+import org.thingsboard.server.service.security.model.SecurityUser;
+
+import java.net.InetSocketAddress;
+import java.util.Objects;
+
+/**
+ * Created by ashvayka on 27.03.18.
+ */
+@Data
+public class TelemetryWebSocketTextMsg {
+
+    private final TelemetryWebSocketSessionRef sessionRef;
+    private final String payload;
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/WsSessionMetaData.java b/application/src/main/java/org/thingsboard/server/service/telemetry/WsSessionMetaData.java
new file mode 100644
index 0000000..dd15ed3
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/telemetry/WsSessionMetaData.java
@@ -0,0 +1,53 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.server.service.telemetry;
+
+import org.thingsboard.server.extensions.api.plugins.ws.PluginWebsocketSessionRef;
+
+/**
+ * Created by ashvayka on 27.03.18.
+ */
+public class WsSessionMetaData {
+    private TelemetryWebSocketSessionRef sessionRef;
+    private long lastActivityTime;
+
+    public WsSessionMetaData(TelemetryWebSocketSessionRef sessionRef) {
+        super();
+        this.sessionRef = sessionRef;
+        this.lastActivityTime = System.currentTimeMillis();
+    }
+
+    public TelemetryWebSocketSessionRef getSessionRef() {
+        return sessionRef;
+    }
+
+    public void setSessionRef(TelemetryWebSocketSessionRef sessionRef) {
+        this.sessionRef = sessionRef;
+    }
+
+    public long getLastActivityTime() {
+        return lastActivityTime;
+    }
+
+    public void setLastActivityTime(long lastActivityTime) {
+        this.lastActivityTime = lastActivityTime;
+    }
+
+    @Override
+    public String toString() {
+        return "WsSessionMetaData [sessionRef=" + sessionRef + ", lastActivityTime=" + lastActivityTime + "]";
+    }
+}
diff --git a/application/src/main/java/org/thingsboard/server/ThingsboardServerApplication.java b/application/src/main/java/org/thingsboard/server/ThingsboardServerApplication.java
index c3444d4..751bde6 100644
--- a/application/src/main/java/org/thingsboard/server/ThingsboardServerApplication.java
+++ b/application/src/main/java/org/thingsboard/server/ThingsboardServerApplication.java
@@ -19,6 +19,7 @@ import org.springframework.boot.SpringApplication;
 import org.springframework.boot.SpringBootConfiguration;
 import org.springframework.context.annotation.ComponentScan;
 import org.springframework.scheduling.annotation.EnableAsync;
+import org.springframework.scheduling.annotation.EnableScheduling;
 import springfox.documentation.swagger2.annotations.EnableSwagger2;
 
 import java.util.Arrays;
@@ -26,6 +27,7 @@ import java.util.Arrays;
 @SpringBootConfiguration
 @EnableAsync
 @EnableSwagger2
+@EnableScheduling
 @ComponentScan({"org.thingsboard.server"})
 public class ThingsboardServerApplication {
 
diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml
index 1c842c8..a9e47de 100644
--- a/application/src/main/resources/thingsboard.yml
+++ b/application/src/main/resources/thingsboard.yml
@@ -62,7 +62,7 @@ cluster:
 # Plugins configuration parameters
 plugins:
   # Comma seperated package list used during classpath scanning for plugins
-  scan_packages: "${PLUGINS_SCAN_PACKAGES:org.thingsboard.server.extensions}"
+  scan_packages: "${PLUGINS_SCAN_PACKAGES:org.thingsboard.server.extensions,org.thingsboard.rule.engine}"
 
 # JWT Token parameters
 security.jwt:
@@ -181,6 +181,10 @@ cassandra:
     default_fetch_size: "${CASSANDRA_DEFAULT_FETCH_SIZE:2000}"
     # Specify partitioning size for timestamp key-value storage. Example MINUTES, HOURS, DAYS, MONTHS
     ts_key_value_partitioning: "${TS_KV_PARTITIONING:MONTHS}"
+    buffer_size: "${CASSANDRA_QUERY_BUFFER_SIZE:200000}"
+    concurrent_limit: "${CASSANDRA_QUERY_CONCURRENT_LIMIT:1000}"
+    permit_max_wait_time: "${PERMIT_MAX_WAIT_TIME:120000}"
+    rate_limit_print_interval_ms: "${CASSANDRA_QUERY_RATE_LIMIT_PRINT_MS:30000}"
 
   queue:
     msg.ttl: 604800 # 7 days
@@ -215,6 +219,18 @@ actors:
     termination.delay: "${ACTORS_RULE_TERMINATION_DELAY:30000}"
     # Errors for particular actor are persisted once per specified amount of milliseconds
     error_persist_frequency: "${ACTORS_RULE_ERROR_FREQUENCY:3000}"
+    # Specify thread pool size for database request callbacks executor service
+    db_callback_thread_pool_size: "${ACTORS_RULE_DB_CALLBACK_THREAD_POOL_SIZE:1}"
+    # Specify thread pool size for javascript executor service
+    js_thread_pool_size: "${ACTORS_RULE_JS_THREAD_POOL_SIZE:10}"
+    # Specify thread pool size for mail sender executor service
+    mail_thread_pool_size: "${ACTORS_RULE_MAIL_THREAD_POOL_SIZE:10}"
+    chain:
+      # Errors for particular actor are persisted once per specified amount of milliseconds
+      error_persist_frequency: "${ACTORS_RULE_CHAIN_ERROR_FREQUENCY:3000}"
+    node:
+      # Errors for particular actor are persisted once per specified amount of milliseconds
+      error_persist_frequency: "${ACTORS_RULE_NODE_ERROR_FREQUENCY:3000}"
   statistics:
     # Enable/disable actor statistics
     enabled: "${ACTORS_STATISTICS_ENABLED:true}"
diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractControllerTest.java
index b92e464..3ec4dc8 100644
--- a/application/src/test/java/org/thingsboard/server/controller/AbstractControllerTest.java
+++ b/application/src/test/java/org/thingsboard/server/controller/AbstractControllerTest.java
@@ -96,6 +96,8 @@ import static org.springframework.test.web.servlet.setup.MockMvcBuilders.webAppC
 @Slf4j
 public abstract class AbstractControllerTest {
 
+    protected ObjectMapper mapper = new ObjectMapper();
+
     protected static final String TEST_TENANT_NAME = "TEST TENANT";
 
     protected static final String SYS_ADMIN_EMAIL = "sysadmin@thingsboard.org";
diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractRuleEngineControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractRuleEngineControllerTest.java
new file mode 100644
index 0000000..93fe767
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/controller/AbstractRuleEngineControllerTest.java
@@ -0,0 +1,56 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.server.controller;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import org.thingsboard.server.common.data.DataConstants;
+import org.thingsboard.server.common.data.Event;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.RuleChainId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.page.TimePageData;
+import org.thingsboard.server.common.data.page.TimePageLink;
+import org.thingsboard.server.common.data.rule.RuleChain;
+import org.thingsboard.server.common.data.rule.RuleChainMetaData;
+
+/**
+ * Created by ashvayka on 20.03.18.
+ */
+public class AbstractRuleEngineControllerTest extends AbstractControllerTest {
+
+    protected RuleChain saveRuleChain(RuleChain ruleChain) throws Exception {
+        return doPost("/api/ruleChain", ruleChain, RuleChain.class);
+    }
+
+    protected RuleChain getRuleChain(RuleChainId ruleChainId) throws Exception {
+        return doGet("/api/ruleChain/" + ruleChainId.getId().toString(), RuleChain.class);
+    }
+
+    protected RuleChainMetaData saveRuleChainMetaData(RuleChainMetaData ruleChainMD) throws Exception {
+        return doPost("/api/ruleChain/metadata", ruleChainMD, RuleChainMetaData.class);
+    }
+
+    protected RuleChainMetaData getRuleChainMetaData(RuleChainId ruleChainId) throws Exception {
+        return doGet("/api/ruleChain/metadata/" + ruleChainId.getId().toString(), RuleChainMetaData.class);
+    }
+
+    protected TimePageData<Event> getDebugEvents(TenantId tenantId, EntityId entityId, int limit) throws Exception {
+        TimePageLink pageLink = new TimePageLink(limit);
+        return doGetTypedWithTimePageLink("/api/events/{entityType}/{entityId}/{eventType}?tenantId={tenantId}&",
+                new TypeReference<TimePageData<Event>>() {
+                }, pageLink, entityId.getEntityType(), entityId.getId(), DataConstants.DEBUG_RULE_NODE, tenantId.getId());
+    }
+}
diff --git a/application/src/test/java/org/thingsboard/server/rules/flow/AbstractRuleEngineFlowIntegrationTest.java b/application/src/test/java/org/thingsboard/server/rules/flow/AbstractRuleEngineFlowIntegrationTest.java
new file mode 100644
index 0000000..f88eb24
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/rules/flow/AbstractRuleEngineFlowIntegrationTest.java
@@ -0,0 +1,190 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.server.rules.flow;
+
+import com.datastax.driver.core.utils.UUIDs;
+import lombok.Data;
+import lombok.extern.slf4j.Slf4j;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.thingsboard.rule.engine.metadata.TbGetAttributesNodeConfiguration;
+import org.thingsboard.server.actors.service.ActorService;
+import org.thingsboard.server.common.data.*;
+import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry;
+import org.thingsboard.server.common.data.kv.StringDataEntry;
+import org.thingsboard.server.common.data.page.TimePageData;
+import org.thingsboard.server.common.data.rule.RuleChain;
+import org.thingsboard.server.common.data.rule.RuleChainMetaData;
+import org.thingsboard.server.common.data.rule.RuleNode;
+import org.thingsboard.server.common.data.security.Authority;
+import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.TbMsgMetaData;
+import org.thingsboard.server.common.msg.system.ServiceToRuleEngineMsg;
+import org.thingsboard.server.controller.AbstractRuleEngineControllerTest;
+import org.thingsboard.server.dao.attributes.AttributesService;
+import org.thingsboard.server.dao.rule.RuleChainService;
+
+import java.util.Arrays;
+import java.util.Collections;
+
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+/**
+ * @author Valerii Sosliuk
+ */
+@Slf4j
+public abstract class AbstractRuleEngineFlowIntegrationTest extends AbstractRuleEngineControllerTest {
+
+    protected Tenant savedTenant;
+    protected User tenantAdmin;
+
+    @Autowired
+    protected ActorService actorService;
+
+    @Autowired
+    protected AttributesService attributesService;
+
+    @Autowired
+    protected RuleChainService ruleChainService;
+
+    @Before
+    public void beforeTest() throws Exception {
+        loginSysAdmin();
+
+        Tenant tenant = new Tenant();
+        tenant.setTitle("My tenant");
+        savedTenant = doPost("/api/tenant", tenant, Tenant.class);
+        Assert.assertNotNull(savedTenant);
+
+        tenantAdmin = new User();
+        tenantAdmin.setAuthority(Authority.TENANT_ADMIN);
+        tenantAdmin.setTenantId(savedTenant.getId());
+        tenantAdmin.setEmail("tenant2@thingsboard.org");
+        tenantAdmin.setFirstName("Joe");
+        tenantAdmin.setLastName("Downs");
+
+        createUserAndLogin(tenantAdmin, "testPassword1");
+    }
+
+    @After
+    public void afterTest() throws Exception {
+        loginSysAdmin();
+        if (savedTenant != null) {
+            doDelete("/api/tenant/" + savedTenant.getId().getId().toString()).andExpect(status().isOk());
+        }
+    }
+
+    @Test
+    public void testRuleChainWithTwoRules() throws Exception {
+        // Creating Rule Chain
+        RuleChain ruleChain = new RuleChain();
+        ruleChain.setName("Simple Rule Chain");
+        ruleChain.setTenantId(savedTenant.getId());
+        ruleChain.setRoot(true);
+        ruleChain.setDebugMode(true);
+        ruleChain = saveRuleChain(ruleChain);
+        Assert.assertNull(ruleChain.getFirstRuleNodeId());
+
+        RuleChainMetaData metaData = new RuleChainMetaData();
+        metaData.setRuleChainId(ruleChain.getId());
+
+        RuleNode ruleNode1 = new RuleNode();
+        ruleNode1.setName("Simple Rule Node 1");
+        ruleNode1.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName());
+        ruleNode1.setDebugMode(true);
+        TbGetAttributesNodeConfiguration configuration1 = new TbGetAttributesNodeConfiguration();
+        configuration1.setServerAttributeNames(Collections.singletonList("serverAttributeKey1"));
+        ruleNode1.setConfiguration(mapper.valueToTree(configuration1));
+
+        RuleNode ruleNode2 = new RuleNode();
+        ruleNode2.setName("Simple Rule Node 2");
+        ruleNode2.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName());
+        ruleNode2.setDebugMode(true);
+        TbGetAttributesNodeConfiguration configuration2 = new TbGetAttributesNodeConfiguration();
+        configuration2.setServerAttributeNames(Collections.singletonList("serverAttributeKey2"));
+        ruleNode2.setConfiguration(mapper.valueToTree(configuration2));
+
+
+        metaData.setNodes(Arrays.asList(ruleNode1, ruleNode2));
+        metaData.setFirstNodeIndex(0);
+        metaData.addConnectionInfo(0, 1, "Success");
+        metaData = saveRuleChainMetaData(metaData);
+        Assert.assertNotNull(metaData);
+
+        ruleChain = getRuleChain(ruleChain.getId());
+        Assert.assertNotNull(ruleChain.getFirstRuleNodeId());
+
+        // Saving the device
+        Device device = new Device();
+        device.setName("My device");
+        device.setType("default");
+        device = doPost("/api/device", device, Device.class);
+
+        attributesService.save(device.getId(), DataConstants.SERVER_SCOPE,
+                Collections.singletonList(new BaseAttributeKvEntry(new StringDataEntry("serverAttributeKey1", "serverAttributeValue1"), System.currentTimeMillis())));
+        attributesService.save(device.getId(), DataConstants.SERVER_SCOPE,
+                Collections.singletonList(new BaseAttributeKvEntry(new StringDataEntry("serverAttributeKey2", "serverAttributeValue2"), System.currentTimeMillis())));
+
+
+        Thread.sleep(1000);
+
+        // Pushing Message to the system
+        TbMsg tbMsg = new TbMsg(UUIDs.timeBased(),
+                "CUSTOM",
+                device.getId(),
+                new TbMsgMetaData(),
+                "{}");
+        actorService.onMsg(new ServiceToRuleEngineMsg(savedTenant.getId(), tbMsg));
+
+        Thread.sleep(3000);
+
+        TimePageData<Event> events = getDebugEvents(savedTenant.getId(), ruleChain.getFirstRuleNodeId(), 1000);
+
+        Assert.assertEquals(2, events.getData().size());
+
+        Event inEvent = events.getData().stream().filter(e -> e.getBody().get("type").asText().equals(DataConstants.IN)).findFirst().get();
+        Assert.assertEquals(ruleChain.getFirstRuleNodeId(), inEvent.getEntityId());
+        Assert.assertEquals(device.getId().getId().toString(), inEvent.getBody().get("entityId").asText());
+
+        Event outEvent = events.getData().stream().filter(e -> e.getBody().get("type").asText().equals(DataConstants.OUT)).findFirst().get();
+        Assert.assertEquals(ruleChain.getFirstRuleNodeId(), outEvent.getEntityId());
+        Assert.assertEquals(device.getId().getId().toString(), outEvent.getBody().get("entityId").asText());
+
+        Assert.assertEquals("serverAttributeValue1", outEvent.getBody().get("metadata").get("ss.serverAttributeKey1").asText());
+
+        RuleChain finalRuleChain = ruleChain;
+        RuleNode lastRuleNode = metaData.getNodes().stream().filter(node -> !node.getId().equals(finalRuleChain.getFirstRuleNodeId())).findFirst().get();
+
+        events = getDebugEvents(savedTenant.getId(), lastRuleNode.getId(), 1000);
+
+        Assert.assertEquals(2, events.getData().size());
+
+        inEvent = events.getData().stream().filter(e -> e.getBody().get("type").asText().equals(DataConstants.IN)).findFirst().get();
+        Assert.assertEquals(lastRuleNode.getId(), inEvent.getEntityId());
+        Assert.assertEquals(device.getId().getId().toString(), inEvent.getBody().get("entityId").asText());
+
+        outEvent = events.getData().stream().filter(e -> e.getBody().get("type").asText().equals(DataConstants.OUT)).findFirst().get();
+        Assert.assertEquals(lastRuleNode.getId(), outEvent.getEntityId());
+        Assert.assertEquals(device.getId().getId().toString(), outEvent.getBody().get("entityId").asText());
+
+        Assert.assertEquals("serverAttributeValue1", outEvent.getBody().get("metadata").get("ss.serverAttributeKey1").asText());
+        Assert.assertEquals("serverAttributeValue2", outEvent.getBody().get("metadata").get("ss.serverAttributeKey2").asText());
+    }
+
+}
diff --git a/application/src/test/java/org/thingsboard/server/rules/lifecycle/AbstractRuleEngineLifecycleIntegrationTest.java b/application/src/test/java/org/thingsboard/server/rules/lifecycle/AbstractRuleEngineLifecycleIntegrationTest.java
new file mode 100644
index 0000000..22d79f0
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/rules/lifecycle/AbstractRuleEngineLifecycleIntegrationTest.java
@@ -0,0 +1,158 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.server.rules.lifecycle;
+
+import com.datastax.driver.core.utils.UUIDs;
+import lombok.extern.slf4j.Slf4j;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.thingsboard.rule.engine.metadata.TbGetAttributesNodeConfiguration;
+import org.thingsboard.server.actors.service.ActorService;
+import org.thingsboard.server.common.data.DataConstants;
+import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.Event;
+import org.thingsboard.server.common.data.Tenant;
+import org.thingsboard.server.common.data.User;
+import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry;
+import org.thingsboard.server.common.data.kv.StringDataEntry;
+import org.thingsboard.server.common.data.page.TimePageData;
+import org.thingsboard.server.common.data.rule.RuleChain;
+import org.thingsboard.server.common.data.rule.RuleChainMetaData;
+import org.thingsboard.server.common.data.rule.RuleNode;
+import org.thingsboard.server.common.data.security.Authority;
+import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.TbMsgMetaData;
+import org.thingsboard.server.common.msg.system.ServiceToRuleEngineMsg;
+import org.thingsboard.server.controller.AbstractRuleEngineControllerTest;
+import org.thingsboard.server.dao.attributes.AttributesService;
+
+import java.util.Collections;
+
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+/**
+ * @author Valerii Sosliuk
+ */
+@Slf4j
+public abstract class AbstractRuleEngineLifecycleIntegrationTest extends AbstractRuleEngineControllerTest {
+
+    protected Tenant savedTenant;
+    protected User tenantAdmin;
+
+    @Autowired
+    protected ActorService actorService;
+
+    @Autowired
+    protected AttributesService attributesService;
+
+    @Before
+    public void beforeTest() throws Exception {
+        loginSysAdmin();
+
+        Tenant tenant = new Tenant();
+        tenant.setTitle("My tenant");
+        savedTenant = doPost("/api/tenant", tenant, Tenant.class);
+        Assert.assertNotNull(savedTenant);
+
+        tenantAdmin = new User();
+        tenantAdmin.setAuthority(Authority.TENANT_ADMIN);
+        tenantAdmin.setTenantId(savedTenant.getId());
+        tenantAdmin.setEmail("tenant2@thingsboard.org");
+        tenantAdmin.setFirstName("Joe");
+        tenantAdmin.setLastName("Downs");
+
+        createUserAndLogin(tenantAdmin, "testPassword1");
+    }
+
+    @After
+    public void afterTest() throws Exception {
+        loginSysAdmin();
+        if (savedTenant != null) {
+            doDelete("/api/tenant/" + savedTenant.getId().getId().toString()).andExpect(status().isOk());
+        }
+    }
+
+    @Test
+    public void testRuleChainWithOneRule() throws Exception {
+        // Creating Rule Chain
+        RuleChain ruleChain = new RuleChain();
+        ruleChain.setName("Simple Rule Chain");
+        ruleChain.setTenantId(savedTenant.getId());
+        ruleChain.setRoot(true);
+        ruleChain.setDebugMode(true);
+        ruleChain = saveRuleChain(ruleChain);
+        Assert.assertNull(ruleChain.getFirstRuleNodeId());
+
+        RuleChainMetaData metaData = new RuleChainMetaData();
+        metaData.setRuleChainId(ruleChain.getId());
+
+        RuleNode ruleNode = new RuleNode();
+        ruleNode.setName("Simple Rule Node");
+        ruleNode.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName());
+        ruleNode.setDebugMode(true);
+        TbGetAttributesNodeConfiguration configuration = new TbGetAttributesNodeConfiguration();
+        configuration.setServerAttributeNames(Collections.singletonList("serverAttributeKey"));
+        ruleNode.setConfiguration(mapper.valueToTree(configuration));
+
+        metaData.setNodes(Collections.singletonList(ruleNode));
+        metaData.setFirstNodeIndex(0);
+
+        metaData = saveRuleChainMetaData(metaData);
+        Assert.assertNotNull(metaData);
+
+        ruleChain = getRuleChain(ruleChain.getId());
+        Assert.assertNotNull(ruleChain.getFirstRuleNodeId());
+
+        // Saving the device
+        Device device = new Device();
+        device.setName("My device");
+        device.setType("default");
+        device = doPost("/api/device", device, Device.class);
+
+        attributesService.save(device.getId(), DataConstants.SERVER_SCOPE,
+                Collections.singletonList(new BaseAttributeKvEntry(new StringDataEntry("serverAttributeKey", "serverAttributeValue"), System.currentTimeMillis())));
+
+        Thread.sleep(1000);
+
+        // Pushing Message to the system
+        TbMsg tbMsg = new TbMsg(UUIDs.timeBased(),
+                "CUSTOM",
+                device.getId(),
+                new TbMsgMetaData(),
+                "{}");
+        actorService.onMsg(new ServiceToRuleEngineMsg(savedTenant.getId(), tbMsg));
+
+        Thread.sleep(3000);
+
+        TimePageData<Event> events = getDebugEvents(savedTenant.getId(), ruleChain.getFirstRuleNodeId(), 1000);
+
+        Assert.assertEquals(2, events.getData().size());
+
+        Event inEvent = events.getData().stream().filter(e -> e.getBody().get("type").asText().equals(DataConstants.IN)).findFirst().get();
+        Assert.assertEquals(ruleChain.getFirstRuleNodeId(), inEvent.getEntityId());
+        Assert.assertEquals(device.getId().getId().toString(), inEvent.getBody().get("entityId").asText());
+
+        Event outEvent = events.getData().stream().filter(e -> e.getBody().get("type").asText().equals(DataConstants.OUT)).findFirst().get();
+        Assert.assertEquals(ruleChain.getFirstRuleNodeId(), outEvent.getEntityId());
+        Assert.assertEquals(device.getId().getId().toString(), outEvent.getBody().get("entityId").asText());
+
+        Assert.assertEquals("serverAttributeValue", outEvent.getBody().get("metadata").get("ss.serverAttributeKey").asText());
+    }
+
+}
diff --git a/application/src/test/java/org/thingsboard/server/rules/lifecycle/RuleEngineLifecycleSqlIntegrationTest.java b/application/src/test/java/org/thingsboard/server/rules/lifecycle/RuleEngineLifecycleSqlIntegrationTest.java
new file mode 100644
index 0000000..004958b
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/rules/lifecycle/RuleEngineLifecycleSqlIntegrationTest.java
@@ -0,0 +1,26 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.server.rules.lifecycle;
+
+import org.thingsboard.server.dao.service.DaoSqlTest;
+import org.thingsboard.server.rules.flow.AbstractRuleEngineFlowIntegrationTest;
+
+/**
+ * Created by Valerii Sosliuk on 8/22/2017.
+ */
+@DaoSqlTest
+public class RuleEngineLifecycleSqlIntegrationTest extends AbstractRuleEngineLifecycleIntegrationTest {
+}
diff --git a/application/src/test/java/org/thingsboard/server/rules/RuleEngineSqlTestSuite.java b/application/src/test/java/org/thingsboard/server/rules/RuleEngineSqlTestSuite.java
new file mode 100644
index 0000000..65b4293
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/rules/RuleEngineSqlTestSuite.java
@@ -0,0 +1,35 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.server.rules;
+
+import org.junit.ClassRule;
+import org.junit.extensions.cpsuite.ClasspathSuite;
+import org.junit.runner.RunWith;
+import org.thingsboard.server.dao.CustomSqlUnit;
+
+import java.util.Arrays;
+
+@RunWith(ClasspathSuite.class)
+@ClasspathSuite.ClassnameFilters({
+        "org.thingsboard.server.rules.flow.*Test"})
+public class RuleEngineSqlTestSuite {
+
+    @ClassRule
+    public static CustomSqlUnit sqlUnit = new CustomSqlUnit(
+            Arrays.asList("sql/schema.sql", "sql/system-data.sql"),
+            "sql/drop-all-tables.sql",
+            "sql-test.properties");
+}
diff --git a/application/src/test/java/org/thingsboard/server/service/mail/TestMailService.java b/application/src/test/java/org/thingsboard/server/service/mail/TestMailService.java
index ed3750d..ba2bb65 100644
--- a/application/src/test/java/org/thingsboard/server/service/mail/TestMailService.java
+++ b/application/src/test/java/org/thingsboard/server/service/mail/TestMailService.java
@@ -22,7 +22,8 @@ import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.context.annotation.Primary;
 import org.springframework.context.annotation.Profile;
-import org.thingsboard.server.exception.ThingsboardException;
+import org.thingsboard.rule.engine.api.MailService;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
 
 @Profile("test")
 @Configuration
diff --git a/application/src/test/java/org/thingsboard/server/service/script/NashornJsEngineTest.java b/application/src/test/java/org/thingsboard/server/service/script/NashornJsEngineTest.java
new file mode 100644
index 0000000..e6a48e2
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/service/script/NashornJsEngineTest.java
@@ -0,0 +1,151 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.server.service.script;
+
+import com.datastax.driver.core.utils.UUIDs;
+import com.google.common.collect.Sets;
+import org.junit.Test;
+import org.thingsboard.rule.engine.api.ScriptEngine;
+import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.TbMsgMetaData;
+
+import javax.script.ScriptException;
+
+import java.util.Set;
+
+import static org.junit.Assert.*;
+
+public class NashornJsEngineTest {
+
+    private ScriptEngine scriptEngine;
+
+    @Test
+    public void msgCanBeUpdated() throws ScriptException {
+        String function = "metadata.temp = metadata.temp * 10; return {metadata: metadata};";
+        scriptEngine = new NashornJsEngine(function, "Transform");
+
+        TbMsgMetaData metaData = new TbMsgMetaData();
+        metaData.putValue("temp", "7");
+        metaData.putValue("humidity", "99");
+        String rawJson = "{\"name\": \"Vit\", \"passed\": 5, \"bigObj\": {\"prop\":42}}";
+
+        TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, metaData, rawJson);
+
+        TbMsg actual = scriptEngine.executeUpdate(msg);
+        assertEquals("70", actual.getMetaData().getValue("temp"));
+    }
+
+    @Test
+    public void newAttributesCanBeAddedInMsg() throws ScriptException {
+        String function = "metadata.newAttr = metadata.humidity - msg.passed; return {metadata: metadata};";
+        scriptEngine = new NashornJsEngine(function, "Transform");
+        TbMsgMetaData metaData = new TbMsgMetaData();
+        metaData.putValue("temp", "7");
+        metaData.putValue("humidity", "99");
+        String rawJson = "{\"name\": \"Vit\", \"passed\": 5, \"bigObj\": {\"prop\":42}}";
+
+        TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, metaData, rawJson);
+
+        TbMsg actual = scriptEngine.executeUpdate(msg);
+        assertEquals("94", actual.getMetaData().getValue("newAttr"));
+    }
+
+    @Test
+    public void payloadCanBeUpdated() throws ScriptException {
+        String function = "msg.passed = msg.passed * metadata.temp; msg.bigObj.newProp = 'Ukraine'; return {msg: msg};";
+        scriptEngine = new NashornJsEngine(function, "Transform");
+        TbMsgMetaData metaData = new TbMsgMetaData();
+        metaData.putValue("temp", "7");
+        metaData.putValue("humidity", "99");
+        String rawJson = "{\"name\":\"Vit\",\"passed\": 5,\"bigObj\":{\"prop\":42}}";
+
+        TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, metaData, rawJson);
+
+        TbMsg actual = scriptEngine.executeUpdate(msg);
+
+        String expectedJson = "{\"name\":\"Vit\",\"passed\":35,\"bigObj\":{\"prop\":42,\"newProp\":\"Ukraine\"}}";
+        assertEquals(expectedJson, actual.getData());
+    }
+
+    @Test
+    public void metadataAccessibleForFilter() throws ScriptException {
+        String function = "return metadata.humidity < 15;";
+        scriptEngine = new NashornJsEngine(function, "Filter");
+        TbMsgMetaData metaData = new TbMsgMetaData();
+        metaData.putValue("temp", "7");
+        metaData.putValue("humidity", "99");
+        String rawJson = "{\"name\": \"Vit\", \"passed\": 5, \"bigObj\": {\"prop\":42}}";
+
+        TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, metaData, rawJson);
+        assertFalse(scriptEngine.executeFilter(msg));
+    }
+
+    @Test
+    public void dataAccessibleForFilter() throws ScriptException {
+        String function = "return msg.passed < 15 && msg.name === 'Vit' && metadata.temp == 7 && msg.bigObj.prop == 42;";
+        scriptEngine = new NashornJsEngine(function, "Filter");
+        TbMsgMetaData metaData = new TbMsgMetaData();
+        metaData.putValue("temp", "7");
+        metaData.putValue("humidity", "99");
+        String rawJson = "{\"name\": \"Vit\", \"passed\": 5, \"bigObj\": {\"prop\":42}}";
+
+        TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, metaData, rawJson);
+        assertTrue(scriptEngine.executeFilter(msg));
+    }
+
+    @Test
+    public void dataAccessibleForSwitch() throws ScriptException {
+        String jsCode = "function nextRelation(metadata, msg) {\n" +
+                "    if(msg.passed == 5 && metadata.temp == 10)\n" +
+                "        return 'one'\n" +
+                "    else\n" +
+                "        return 'two';\n" +
+                "};\n" +
+                "\n" +
+                "return nextRelation(metadata, msg);";
+        scriptEngine = new NashornJsEngine(jsCode, "Switch");
+        TbMsgMetaData metaData = new TbMsgMetaData();
+        metaData.putValue("temp", "10");
+        metaData.putValue("humidity", "99");
+        String rawJson = "{\"name\": \"Vit\", \"passed\": 5, \"bigObj\": {\"prop\":42}}";
+
+        TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, metaData, rawJson);
+        Set<String> actual = scriptEngine.executeSwitch(msg);
+        assertEquals(Sets.newHashSet("one"), actual);
+    }
+
+    @Test
+    public void multipleRelationsReturnedFromSwitch() throws ScriptException {
+        String jsCode = "function nextRelation(metadata, msg) {\n" +
+                "    if(msg.passed == 5 && metadata.temp == 10)\n" +
+                "        return ['three', 'one']\n" +
+                "    else\n" +
+                "        return 'two';\n" +
+                "};\n" +
+                "\n" +
+                "return nextRelation(metadata, msg);";
+        scriptEngine = new NashornJsEngine(jsCode, "Switch");
+        TbMsgMetaData metaData = new TbMsgMetaData();
+        metaData.putValue("temp", "10");
+        metaData.putValue("humidity", "99");
+        String rawJson = "{\"name\": \"Vit\", \"passed\": 5, \"bigObj\": {\"prop\":42}}";
+
+        TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, metaData, rawJson);
+        Set<String> actual = scriptEngine.executeSwitch(msg);
+        assertEquals(Sets.newHashSet("one", "three"), actual);
+    }
+
+}
\ No newline at end of file
diff --git a/application/src/test/java/org/thingsboard/server/system/BaseDeviceOfflineTest.java b/application/src/test/java/org/thingsboard/server/system/BaseDeviceOfflineTest.java
new file mode 100644
index 0000000..5a09a68
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/system/BaseDeviceOfflineTest.java
@@ -0,0 +1,123 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.server.system;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.google.common.collect.ImmutableMap;
+import org.junit.Before;
+import org.junit.Test;
+import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.security.DeviceCredentials;
+import org.thingsboard.server.controller.AbstractControllerTest;
+
+import java.util.List;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+
+import static org.junit.Assert.assertEquals;
+
+public class BaseDeviceOfflineTest  extends AbstractControllerTest {
+
+    private Device deviceA;
+    private Device deviceB;
+    private DeviceCredentials credA;
+    private DeviceCredentials credB;
+
+    @Before
+    public void before() throws Exception {
+        loginTenantAdmin();
+        deviceA = createDevice("DevA", "VMS");
+        credA = getCredentials(deviceA.getUuidId());
+        deviceB = createDevice("DevB", "SOLAR");
+        credB = getCredentials(deviceB.getUuidId());
+    }
+
+    @Test
+    public void offlineDevicesCanBeFoundByLastConnectField() throws Exception {
+        makeDeviceContact(credA);
+        Thread.sleep(1000);
+        makeDeviceContact(credB);
+        Thread.sleep(100);
+        List<Device> devices = doGetTyped("/api/device/offline?contactType=CONNECT&threshold=700", new TypeReference<List<Device>>() {
+        });
+
+        assertEquals(devices.toString(),1, devices.size());
+        assertEquals("DevA", devices.get(0).getName());
+    }
+
+    @Test
+    public void offlineDevicesCanBeFoundByLastUpdateField() throws Exception {
+        makeDeviceUpdate(credA);
+        Thread.sleep(1000);
+        makeDeviceUpdate(credB);
+        makeDeviceContact(credA);
+        Thread.sleep(100);
+        List<Device> devices = doGetTyped("/api/device/offline?contactType=UPLOAD&threshold=700", new TypeReference<List<Device>>() {
+        });
+
+        assertEquals(devices.toString(),1, devices.size());
+        assertEquals("DevA", devices.get(0).getName());
+    }
+
+    @Test
+    public void onlineDevicesCanBeFoundByLastConnectField() throws Exception {
+        makeDeviceContact(credB);
+        Thread.sleep(1000);
+        makeDeviceContact(credA);
+        Thread.sleep(100);
+        List<Device> devices = doGetTyped("/api/device/online?contactType=CONNECT&threshold=700", new TypeReference<List<Device>>() {
+        });
+
+        assertEquals(devices.toString(),1, devices.size());
+        assertEquals("DevA", devices.get(0).getName());
+    }
+
+    @Test
+    public void onlineDevicesCanBeFoundByLastUpdateField() throws Exception {
+        makeDeviceUpdate(credB);
+        Thread.sleep(1000);
+        makeDeviceUpdate(credA);
+        makeDeviceContact(credB);
+        Thread.sleep(100);
+        List<Device> devices = doGetTyped("/api/device/online?contactType=UPLOAD&threshold=700", new TypeReference<List<Device>>() {
+        });
+
+        assertEquals(devices.toString(),1, devices.size());
+        assertEquals("DevA", devices.get(0).getName());
+    }
+
+    private Device createDevice(String name, String type) throws Exception {
+        Device device = new Device();
+        device.setName(name);
+        device.setType(type);
+        long currentTime = System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(10);
+        device.setLastConnectTs(currentTime);
+        device.setLastUpdateTs(currentTime);
+        return doPost("/api/device", device, Device.class);
+    }
+
+    private DeviceCredentials getCredentials(UUID deviceId) throws Exception {
+        return doGet("/api/device/" + deviceId.toString() + "/credentials", DeviceCredentials.class);
+    }
+
+    private void makeDeviceUpdate(DeviceCredentials credentials) throws Exception {
+        doPost("/api/v1/" + credentials.getCredentialsId() + "/attributes", ImmutableMap.of("keyA", "valueA"), new String[]{});
+    }
+
+    private void makeDeviceContact(DeviceCredentials credentials) throws Exception {
+        doGet("/api/v1/" + credentials.getCredentialsId() + "/attributes?clientKeys=keyA,keyB,keyC");
+    }
+}
diff --git a/application/src/test/java/org/thingsboard/server/system/BaseHttpDeviceApiTest.java b/application/src/test/java/org/thingsboard/server/system/BaseHttpDeviceApiTest.java
index 4fa6162..c3e87c2 100644
--- a/application/src/test/java/org/thingsboard/server/system/BaseHttpDeviceApiTest.java
+++ b/application/src/test/java/org/thingsboard/server/system/BaseHttpDeviceApiTest.java
@@ -15,6 +15,7 @@
  */
 package org.thingsboard.server.system;
 
+import com.google.common.collect.ImmutableMap;
 import org.junit.Before;
 import org.junit.Test;
 import org.springframework.test.web.servlet.ResultActions;
@@ -28,6 +29,9 @@ import java.util.Map;
 import java.util.Random;
 import java.util.concurrent.atomic.AtomicInteger;
 
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
 import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.request;
 import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@@ -48,6 +52,9 @@ public abstract class BaseHttpDeviceApiTest extends AbstractControllerTest {
         device = new Device();
         device.setName("My device");
         device.setType("default");
+        long currentTime = System.currentTimeMillis();
+        device.setLastConnectTs(currentTime);
+        device.setLastUpdateTs(currentTime);
         device = doPost("/api/device", device, Device.class);
 
         deviceCredentials =
@@ -67,6 +74,34 @@ public abstract class BaseHttpDeviceApiTest extends AbstractControllerTest {
         doGetAsync("/api/v1/" + deviceCredentials.getCredentialsId() + "/attributes?clientKeys=keyA,keyB,keyC").andExpect(status().isOk());
     }
 
+    @Test
+    public void deviceLastContactAndUpdateFieldsAreUpdated() throws Exception {
+        Device actualDevice = doGet("/api/device/" + this.device.getId(), Device.class);
+        Long initConnectTs = actualDevice.getLastConnectTs();
+        Long initUpdateTs = actualDevice.getLastUpdateTs();
+        assertNotNull(initConnectTs);
+        assertNotNull(initUpdateTs);
+        Thread.sleep(50);
+
+        doPost("/api/v1/" + deviceCredentials.getCredentialsId() + "/attributes", ImmutableMap.of("keyA", "valueA"), new String[]{});
+        actualDevice = doGet("/api/device/" + this.device.getId(), Device.class);
+        Long postConnectTs = actualDevice.getLastConnectTs();
+        Long postUpdateTs = actualDevice.getLastUpdateTs();
+        System.out.println(postConnectTs + "  -   " + postUpdateTs + " -> " + (postConnectTs - initConnectTs) + " : " + (postUpdateTs - initUpdateTs));
+        assertTrue(postConnectTs > initConnectTs);
+        assertEquals(postConnectTs, postUpdateTs);
+        Thread.sleep(50);
+
+        doGet("/api/v1/" + deviceCredentials.getCredentialsId() + "/attributes?clientKeys=keyA,keyB,keyC");
+        Thread.sleep(50);
+        actualDevice = doGet("/api/device/" + this.device.getId(), Device.class);
+        Long getConnectTs = actualDevice.getLastConnectTs();
+        Long getUpdateTs = actualDevice.getLastUpdateTs();
+        assertTrue(getConnectTs > postConnectTs);
+        assertEquals(getUpdateTs, postUpdateTs);
+
+    }
+
     protected ResultActions doGetAsync(String urlTemplate, Object... urlVariables) throws Exception {
         MockHttpServletRequestBuilder getRequest;
         getRequest = get(urlTemplate, urlVariables);
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java
index 70f5042..125406c 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java
@@ -22,6 +22,7 @@ import lombok.Builder;
 import lombok.Data;
 import org.thingsboard.server.common.data.BaseData;
 import org.thingsboard.server.common.data.HasName;
+import org.thingsboard.server.common.data.HasTenantId;
 import org.thingsboard.server.common.data.id.EntityId;
 import org.thingsboard.server.common.data.id.TenantId;
 
@@ -31,7 +32,7 @@ import org.thingsboard.server.common.data.id.TenantId;
 @Data
 @Builder
 @AllArgsConstructor
-public class Alarm extends BaseData<AlarmId> implements HasName {
+public class Alarm extends BaseData<AlarmId> implements HasName, HasTenantId {
 
     private TenantId tenantId;
     private String type;
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/asset/Asset.java b/common/data/src/main/java/org/thingsboard/server/common/data/asset/Asset.java
index cc3c111..c7b246c 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/asset/Asset.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/asset/Asset.java
@@ -17,16 +17,13 @@ package org.thingsboard.server.common.data.asset;
 
 import com.fasterxml.jackson.databind.JsonNode;
 import lombok.EqualsAndHashCode;
-import org.thingsboard.server.common.data.HasAdditionalInfo;
-import org.thingsboard.server.common.data.HasName;
-import org.thingsboard.server.common.data.SearchTextBased;
-import org.thingsboard.server.common.data.SearchTextBasedWithAdditionalInfo;
+import org.thingsboard.server.common.data.*;
 import org.thingsboard.server.common.data.id.AssetId;
 import org.thingsboard.server.common.data.id.CustomerId;
 import org.thingsboard.server.common.data.id.TenantId;
 
 @EqualsAndHashCode(callSuper = true)
-public class Asset extends SearchTextBasedWithAdditionalInfo<AssetId> implements HasName {
+public class Asset extends SearchTextBasedWithAdditionalInfo<AssetId> implements HasName, HasTenantId, HasCustomerId {
 
     private static final long serialVersionUID = 2807343040519543363L;
 
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/Customer.java b/common/data/src/main/java/org/thingsboard/server/common/data/Customer.java
index 03115a9..078c97b 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/Customer.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/Customer.java
@@ -23,7 +23,7 @@ import org.thingsboard.server.common.data.id.TenantId;
 
 import com.fasterxml.jackson.databind.JsonNode;
 
-public class Customer extends ContactBased<CustomerId> implements HasName {
+public class Customer extends ContactBased<CustomerId> implements HasName, HasTenantId {
     
     private static final long serialVersionUID = -1599722990298929275L;
     
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java b/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java
index a776d7b..7d4e480 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java
@@ -37,7 +37,12 @@ public class DataConstants {
     public static final String ERROR = "ERROR";
     public static final String LC_EVENT = "LC_EVENT";
     public static final String STATS = "STATS";
+    public static final String DEBUG_RULE_NODE = "DEBUG_RULE_NODE";
 
     public static final String ONEWAY = "ONEWAY";
     public static final String TWOWAY = "TWOWAY";
+
+    public static final String IN = "IN";
+    public static final String OUT = "OUT";
+
 }
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/Device.java b/common/data/src/main/java/org/thingsboard/server/common/data/Device.java
index 13fa011..6d257fc 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/Device.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/Device.java
@@ -23,7 +23,7 @@ import org.thingsboard.server.common.data.id.TenantId;
 import com.fasterxml.jackson.databind.JsonNode;
 
 @EqualsAndHashCode(callSuper = true)
-public class Device extends SearchTextBasedWithAdditionalInfo<DeviceId> implements HasName {
+public class Device extends SearchTextBasedWithAdditionalInfo<DeviceId> implements HasName, HasTenantId, HasCustomerId {
 
     private static final long serialVersionUID = 2807343040519543363L;
 
@@ -31,6 +31,8 @@ public class Device extends SearchTextBasedWithAdditionalInfo<DeviceId> implemen
     private CustomerId customerId;
     private String name;
     private String type;
+    private Long lastConnectTs;
+    private Long lastUpdateTs;
 
     public Device() {
         super();
@@ -81,6 +83,22 @@ public class Device extends SearchTextBasedWithAdditionalInfo<DeviceId> implemen
         this.type = type;
     }
 
+    public Long getLastConnectTs() {
+        return lastConnectTs;
+    }
+
+    public void setLastConnectTs(Long lastConnectTs) {
+        this.lastConnectTs = lastConnectTs;
+    }
+
+    public Long getLastUpdateTs() {
+        return lastUpdateTs;
+    }
+
+    public void setLastUpdateTs(Long lastUpdateTs) {
+        this.lastUpdateTs = lastUpdateTs;
+    }
+
     @Override
     public String getSearchText() {
         return getName();
@@ -101,6 +119,10 @@ public class Device extends SearchTextBasedWithAdditionalInfo<DeviceId> implemen
         builder.append(getAdditionalInfo());
         builder.append(", createdTime=");
         builder.append(createdTime);
+        builder.append(", lastUpdateTs=");
+        builder.append(lastUpdateTs);
+        builder.append(", lastConnectTs=");
+        builder.append(lastConnectTs);
         builder.append(", id=");
         builder.append(id);
         builder.append("]");
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/DeviceStatusQuery.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/DeviceStatusQuery.java
new file mode 100644
index 0000000..0d0dad1
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/DeviceStatusQuery.java
@@ -0,0 +1,40 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.server.common.data.device;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.ToString;
+
+@Data
+@AllArgsConstructor
+@ToString
+public class DeviceStatusQuery {
+
+    private Status status;
+    private ContactType contactType;
+    private long threshold;
+
+
+    public enum Status {
+        ONLINE, OFFLINE
+    }
+
+    public enum ContactType {
+        CONNECT, UPLOAD
+    }
+
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java
index 76b3e33..31c1cda 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java
@@ -33,6 +33,10 @@ public class EntityIdFactory {
         return getByTypeAndUuid(EntityType.valueOf(type), uuid);
     }
 
+    public static EntityId getByTypeAndUuid(EntityType type, String uuid) {
+        return getByTypeAndUuid(type, UUID.fromString(uuid));
+    }
+
     public static EntityId getByTypeAndUuid(EntityType type, UUID uuid) {
         switch (type) {
             case TENANT:
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/page/PageDataIterable.java b/common/data/src/main/java/org/thingsboard/server/common/data/page/PageDataIterable.java
index 34f8c3a..ffd7822 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/page/PageDataIterable.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/page/PageDataIterable.java
@@ -20,6 +20,7 @@ import java.util.List;
 import java.util.NoSuchElementException;
 
 import org.thingsboard.server.common.data.SearchTextBased;
+import org.thingsboard.server.common.data.id.EntityId;
 import org.thingsboard.server.common.data.id.UUIDBased;
 
 public class PageDataIterable<T extends SearchTextBased<? extends UUIDBased>> implements Iterable<T>, Iterator<T> {
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/plugin/ComponentType.java b/common/data/src/main/java/org/thingsboard/server/common/data/plugin/ComponentType.java
index 45fb590..a103064 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/plugin/ComponentType.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/plugin/ComponentType.java
@@ -20,6 +20,6 @@ package org.thingsboard.server.common.data.plugin;
  */
 public enum ComponentType {
 
-    FILTER, PROCESSOR, ACTION, PLUGIN
+    ENRICHMENT, FILTER, TRANSFORMATION, ACTION, OLD_ACTION, PLUGIN
 
 }
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/plugin/PluginMetaData.java b/common/data/src/main/java/org/thingsboard/server/common/data/plugin/PluginMetaData.java
index 8576264..4c33ffe 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/plugin/PluginMetaData.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/plugin/PluginMetaData.java
@@ -21,6 +21,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
 import lombok.EqualsAndHashCode;
 import lombok.extern.slf4j.Slf4j;
 import org.thingsboard.server.common.data.HasName;
+import org.thingsboard.server.common.data.HasTenantId;
 import org.thingsboard.server.common.data.SearchTextBasedWithAdditionalInfo;
 import org.thingsboard.server.common.data.id.PluginId;
 import org.thingsboard.server.common.data.id.TenantId;
@@ -32,7 +33,7 @@ import java.io.IOException;
 
 @EqualsAndHashCode(callSuper = true)
 @Slf4j
-public class PluginMetaData extends SearchTextBasedWithAdditionalInfo<PluginId> implements HasName {
+public class PluginMetaData extends SearchTextBasedWithAdditionalInfo<PluginId> implements HasName, HasTenantId {
 
     private static final long serialVersionUID = 1L;
 
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/rule/NodeConnectionInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/rule/NodeConnectionInfo.java
new file mode 100644
index 0000000..0c9fd5f
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/rule/NodeConnectionInfo.java
@@ -0,0 +1,28 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.server.common.data.rule;
+
+import lombok.Data;
+
+/**
+ * Created by ashvayka on 21.03.18.
+ */
+@Data
+public class NodeConnectionInfo {
+    private int fromIndex;
+    private int toIndex;
+    private String type;
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChain.java b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChain.java
index e82c850..218061a 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChain.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChain.java
@@ -21,6 +21,7 @@ import lombok.Data;
 import lombok.EqualsAndHashCode;
 import lombok.extern.slf4j.Slf4j;
 import org.thingsboard.server.common.data.HasName;
+import org.thingsboard.server.common.data.HasTenantId;
 import org.thingsboard.server.common.data.SearchTextBasedWithAdditionalInfo;
 import org.thingsboard.server.common.data.id.RuleChainId;
 import org.thingsboard.server.common.data.id.RuleNodeId;
@@ -29,7 +30,7 @@ import org.thingsboard.server.common.data.id.TenantId;
 @Data
 @EqualsAndHashCode(callSuper = true)
 @Slf4j
-public class RuleChain extends SearchTextBasedWithAdditionalInfo<RuleChainId> implements HasName {
+public class RuleChain extends SearchTextBasedWithAdditionalInfo<RuleChainId> implements HasName, HasTenantId {
 
     private static final long serialVersionUID = -5656679015121935465L;
 
@@ -37,6 +38,7 @@ public class RuleChain extends SearchTextBasedWithAdditionalInfo<RuleChainId> im
     private String name;
     private RuleNodeId firstRuleNodeId;
     private boolean root;
+    private boolean debugMode;
     private transient JsonNode configuration;
     @JsonIgnore
     private byte[] configurationBytes;
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainConnectionInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainConnectionInfo.java
new file mode 100644
index 0000000..a537fe4
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainConnectionInfo.java
@@ -0,0 +1,31 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.server.common.data.rule;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import lombok.Data;
+import org.thingsboard.server.common.data.id.RuleChainId;
+
+/**
+ * Created by ashvayka on 21.03.18.
+ */
+@Data
+public class RuleChainConnectionInfo {
+    private int fromIndex;
+    private RuleChainId targetRuleChainId;
+    private JsonNode additionalInfo;
+    private String type;
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainMetaData.java b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainMetaData.java
index af141d6..7ecd6df 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainMetaData.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainMetaData.java
@@ -15,6 +15,7 @@
  */
 package org.thingsboard.server.common.data.rule;
 
+import com.fasterxml.jackson.databind.JsonNode;
 import lombok.Data;
 import org.thingsboard.server.common.data.id.RuleChainId;
 
@@ -47,29 +48,15 @@ public class RuleChainMetaData {
         }
         connections.add(connectionInfo);
     }
-    public void addRuleChainConnectionInfo(int fromIndex, RuleChainId targetRuleChainId, String type) {
+    public void addRuleChainConnectionInfo(int fromIndex, RuleChainId targetRuleChainId, String type, JsonNode additionalInfo) {
         RuleChainConnectionInfo connectionInfo = new RuleChainConnectionInfo();
         connectionInfo.setFromIndex(fromIndex);
         connectionInfo.setTargetRuleChainId(targetRuleChainId);
         connectionInfo.setType(type);
+        connectionInfo.setAdditionalInfo(additionalInfo);
         if (ruleChainConnections == null) {
             ruleChainConnections = new ArrayList<>();
         }
         ruleChainConnections.add(connectionInfo);
     }
-
-    @Data
-    public class NodeConnectionInfo {
-        private int fromIndex;
-        private int toIndex;
-        private String type;
-    }
-
-    @Data
-    public class RuleChainConnectionInfo {
-        private int fromIndex;
-        private RuleChainId targetRuleChainId;
-        private String type;
-    }
-
 }
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleMetaData.java b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleMetaData.java
index 98adeb7..953e5eb 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleMetaData.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleMetaData.java
@@ -23,6 +23,7 @@ import lombok.Data;
 import lombok.EqualsAndHashCode;
 import lombok.extern.slf4j.Slf4j;
 import org.thingsboard.server.common.data.HasName;
+import org.thingsboard.server.common.data.HasTenantId;
 import org.thingsboard.server.common.data.SearchTextBasedWithAdditionalInfo;
 import org.thingsboard.server.common.data.id.RuleId;
 import org.thingsboard.server.common.data.id.TenantId;
@@ -31,7 +32,7 @@ import org.thingsboard.server.common.data.plugin.ComponentLifecycleState;
 @Data
 @EqualsAndHashCode(callSuper = true)
 @Slf4j
-public class RuleMetaData extends SearchTextBasedWithAdditionalInfo<RuleId> implements HasName {
+public class RuleMetaData extends SearchTextBasedWithAdditionalInfo<RuleId> implements HasName, HasTenantId {
 
     private static final long serialVersionUID = -5656679015122935465L;
 
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleNode.java b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleNode.java
index d044000..fbc1103 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleNode.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleNode.java
@@ -34,6 +34,7 @@ public class RuleNode extends SearchTextBasedWithAdditionalInfo<RuleNodeId> impl
 
     private String type;
     private String name;
+    private boolean debugMode;
     private transient JsonNode configuration;
     @JsonIgnore
     private byte[] configurationBytes;
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/User.java b/common/data/src/main/java/org/thingsboard/server/common/data/User.java
index c893d64..15c52dc 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/User.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/User.java
@@ -15,9 +15,11 @@
  */
 package org.thingsboard.server.common.data;
 
+import com.fasterxml.jackson.annotation.JsonIgnore;
 import com.fasterxml.jackson.annotation.JsonProperty;
 import lombok.EqualsAndHashCode;
 import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.EntityId;
 import org.thingsboard.server.common.data.id.TenantId;
 import org.thingsboard.server.common.data.id.UserId;
 import org.thingsboard.server.common.data.security.Authority;
@@ -25,7 +27,7 @@ import org.thingsboard.server.common.data.security.Authority;
 import com.fasterxml.jackson.databind.JsonNode;
 
 @EqualsAndHashCode(callSuper = true)
-public class User extends SearchTextBasedWithAdditionalInfo<UserId> implements HasName {
+public class User extends SearchTextBasedWithAdditionalInfo<UserId> implements HasName, HasTenantId, HasCustomerId {
 
     private static final long serialVersionUID = 8250339805336035966L;
 
@@ -138,4 +140,18 @@ public class User extends SearchTextBasedWithAdditionalInfo<UserId> implements H
         return builder.toString();
     }
 
+    @JsonIgnore
+    public boolean isSystemAdmin() {
+        return tenantId == null || EntityId.NULL_UUID.equals(tenantId.getId());
+    }
+
+    @JsonIgnore
+    public boolean isTenantAdmin() {
+        return !isSystemAdmin() && (customerId == null || EntityId.NULL_UUID.equals(customerId.getId()));
+    }
+
+    @JsonIgnore
+    public boolean isCustomerUser() {
+        return !isSystemAdmin() && !isTenantAdmin();
+    }
 }
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/core/ToServerRpcRequestMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/core/ToServerRpcRequestMsg.java
index ace51c0..87708a7 100644
--- a/common/message/src/main/java/org/thingsboard/server/common/msg/core/ToServerRpcRequestMsg.java
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/ToServerRpcRequestMsg.java
@@ -16,16 +16,16 @@
 package org.thingsboard.server.common.msg.core;
 
 import lombok.Data;
-import org.thingsboard.server.common.msg.session.FromDeviceMsg;
+import org.thingsboard.server.common.msg.session.FromDeviceRequestMsg;
 import org.thingsboard.server.common.msg.session.MsgType;
 
 /**
  * @author Andrew Shvayka
  */
 @Data
-public class ToServerRpcRequestMsg implements FromDeviceMsg {
+public class ToServerRpcRequestMsg implements FromDeviceRequestMsg {
 
-    private final int requestId;
+    private final Integer requestId;
     private final String method;
     private final String params;
 
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/MsgType.java b/common/message/src/main/java/org/thingsboard/server/common/msg/MsgType.java
new file mode 100644
index 0000000..f8f2044
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/MsgType.java
@@ -0,0 +1,62 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.server.common.msg;
+
+/**
+ * Created by ashvayka on 15.03.18.
+ */
+public enum MsgType {
+
+    /**
+     * ADDED/UPDATED/DELETED events for main entities.
+     *
+     * @See {@link org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg}
+     */
+    COMPONENT_LIFE_CYCLE_MSG,
+
+    /**
+     * Misc messages from the REST API/SERVICE layer to the new rule engine.
+     *
+     * @See {@link org.thingsboard.server.common.msg.system.ServiceToRuleEngineMsg}
+     */
+    SERVICE_TO_RULE_ENGINE_MSG,
+
+
+    SESSION_TO_DEVICE_ACTOR_MSG,
+    DEVICE_ACTOR_TO_SESSION_MSG,
+
+
+    /**
+     * Message that is sent by RuleChainActor to RuleActor with command to process TbMsg.
+     */
+    RULE_CHAIN_TO_RULE_MSG,
+
+    /**
+     * Message that is sent by RuleActor to RuleChainActor with command to process TbMsg by next nodes in chain.
+     */
+    RULE_TO_RULE_CHAIN_TELL_NEXT_MSG,
+
+    /**
+     * Message that is sent by RuleActor implementation to RuleActor itself to log the error.
+     */
+    RULE_TO_SELF_ERROR_MSG,
+
+    /**
+     * Message that is sent by RuleActor implementation to RuleActor itself to process the message.
+     */
+    RULE_TO_SELF_MSG,
+
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/plugin/ComponentLifecycleMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/plugin/ComponentLifecycleMsg.java
index d48c3fe..c104281 100644
--- a/common/message/src/main/java/org/thingsboard/server/common/msg/plugin/ComponentLifecycleMsg.java
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/plugin/ComponentLifecycleMsg.java
@@ -15,14 +15,14 @@
  */
 package org.thingsboard.server.common.msg.plugin;
 
-import lombok.Data;
 import lombok.Getter;
 import lombok.ToString;
-import org.thingsboard.server.common.data.id.PluginId;
-import org.thingsboard.server.common.data.id.RuleId;
-import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.id.*;
 import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
-import org.thingsboard.server.common.data.plugin.ComponentLifecycleState;
+import org.thingsboard.server.common.data.rule.RuleChain;
+import org.thingsboard.server.common.msg.MsgType;
+import org.thingsboard.server.common.msg.TbActorMsg;
 import org.thingsboard.server.common.msg.aware.TenantAwareMsg;
 import org.thingsboard.server.common.msg.cluster.ToAllNodesMsg;
 
@@ -32,34 +32,34 @@ import java.util.Optional;
  * @author Andrew Shvayka
  */
 @ToString
-public class ComponentLifecycleMsg implements TenantAwareMsg, ToAllNodesMsg {
+public class ComponentLifecycleMsg implements TbActorMsg, TenantAwareMsg, ToAllNodesMsg {
     @Getter
     private final TenantId tenantId;
-    private final PluginId pluginId;
-    private final RuleId ruleId;
+    @Getter
+    private final EntityId entityId;
     @Getter
     private final ComponentLifecycleEvent event;
 
-    public static ComponentLifecycleMsg forPlugin(TenantId tenantId, PluginId pluginId, ComponentLifecycleEvent event) {
-        return new ComponentLifecycleMsg(tenantId, pluginId, null, event);
-    }
-
-    public static ComponentLifecycleMsg forRule(TenantId tenantId, RuleId ruleId, ComponentLifecycleEvent event) {
-        return new ComponentLifecycleMsg(tenantId, null, ruleId, event);
-    }
-
-    private ComponentLifecycleMsg(TenantId tenantId, PluginId pluginId, RuleId ruleId, ComponentLifecycleEvent event) {
+    public ComponentLifecycleMsg(TenantId tenantId, EntityId entityId, ComponentLifecycleEvent event) {
         this.tenantId = tenantId;
-        this.pluginId = pluginId;
-        this.ruleId = ruleId;
+        this.entityId = entityId;
         this.event = event;
     }
 
     public Optional<PluginId> getPluginId() {
-        return Optional.ofNullable(pluginId);
+        return entityId.getEntityType() == EntityType.PLUGIN ? Optional.of((PluginId) entityId) : Optional.empty();
     }
 
     public Optional<RuleId> getRuleId() {
-        return Optional.ofNullable(ruleId);
+        return entityId.getEntityType() == EntityType.RULE ? Optional.of((RuleId) entityId) : Optional.empty();
+    }
+
+    public Optional<RuleChainId> getRuleChainId() {
+        return entityId.getEntityType() == EntityType.RULE_CHAIN ? Optional.of((RuleChainId) entityId) : Optional.empty();
+    }
+
+    @Override
+    public MsgType getMsgType() {
+        return MsgType.COMPONENT_LIFE_CYCLE_MSG;
     }
 }
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/system/ServiceToRuleEngineMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/system/ServiceToRuleEngineMsg.java
new file mode 100644
index 0000000..0792b63
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/system/ServiceToRuleEngineMsg.java
@@ -0,0 +1,37 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.server.common.msg.system;
+
+import lombok.Data;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.msg.MsgType;
+import org.thingsboard.server.common.msg.TbActorMsg;
+import org.thingsboard.server.common.msg.TbMsg;
+
+/**
+ * Created by ashvayka on 15.03.18.
+ */
+@Data
+public final class ServiceToRuleEngineMsg implements TbActorMsg {
+
+    private final TenantId tenantId;
+    private final TbMsg tbMsg;
+
+    @Override
+    public MsgType getMsgType() {
+        return MsgType.SERVICE_TO_RULE_ENGINE_MSG;
+    }
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java
index 5163b6c..1c7de3b 100644
--- a/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java
@@ -17,6 +17,7 @@ package org.thingsboard.server.common.msg;
 
 import com.google.protobuf.ByteString;
 import com.google.protobuf.InvalidProtocolBufferException;
+import lombok.AllArgsConstructor;
 import lombok.Data;
 import org.thingsboard.server.common.data.id.EntityId;
 import org.thingsboard.server.common.data.id.EntityIdFactory;
@@ -30,14 +31,24 @@ import java.util.UUID;
  * Created by ashvayka on 13.01.18.
  */
 @Data
+@AllArgsConstructor
 public final class TbMsg implements Serializable {
 
     private final UUID id;
     private final String type;
     private final EntityId originator;
     private final TbMsgMetaData metaData;
+    private final TbMsgDataType dataType;
+    private final String data;
 
-    private final byte[] data;
+    public TbMsg(UUID id, String type, EntityId originator, TbMsgMetaData metaData, String data) {
+        this.id = id;
+        this.type = type;
+        this.originator = originator;
+        this.metaData = metaData;
+        this.dataType = TbMsgDataType.JSON;
+        this.data = data;
+    }
 
     public static ByteBuffer toBytes(TbMsg msg) {
         MsgProtos.TbMsgProto.Builder builder = MsgProtos.TbMsgProto.newBuilder();
@@ -49,12 +60,11 @@ public final class TbMsg implements Serializable {
         }
 
         if (msg.getMetaData() != null) {
-            MsgProtos.TbMsgProto.TbMsgMetaDataProto.Builder metadataBuilder = MsgProtos.TbMsgProto.TbMsgMetaDataProto.newBuilder();
-            metadataBuilder.putAllData(msg.getMetaData().getData());
-            builder.addMetaData(metadataBuilder.build());
+            builder.setMetaData(MsgProtos.TbMsgMetaDataProto.newBuilder().putAllData(msg.getMetaData().getData()).build());
         }
 
-        builder.setData(ByteString.copyFrom(msg.getData()));
+        builder.setDataType(msg.getDataType().ordinal());
+        builder.setData(msg.getData());
         byte[] bytes = builder.build().toByteArray();
         return ByteBuffer.wrap(bytes);
     }
@@ -62,19 +72,16 @@ public final class TbMsg implements Serializable {
     public static TbMsg fromBytes(ByteBuffer buffer) {
         try {
             MsgProtos.TbMsgProto proto = MsgProtos.TbMsgProto.parseFrom(buffer.array());
-            TbMsgMetaData metaData = new TbMsgMetaData();
-            if (proto.getMetaDataCount() > 0) {
-                metaData.setData(proto.getMetaData(0).getDataMap());
-            }
-
-            EntityId entityId = null;
-            if (proto.getEntityId() != null) {
-                entityId = EntityIdFactory.getByTypeAndId(proto.getEntityType(), proto.getEntityId());
-            }
-
-            return new TbMsg(UUID.fromString(proto.getId()), proto.getType(), entityId, metaData, proto.getData().toByteArray());
+            TbMsgMetaData metaData = new TbMsgMetaData(proto.getMetaData().getDataMap());
+            EntityId entityId = EntityIdFactory.getByTypeAndId(proto.getEntityType(), proto.getEntityId());
+            TbMsgDataType dataType = TbMsgDataType.values()[proto.getDataType()];
+            return new TbMsg(UUID.fromString(proto.getId()), proto.getType(), entityId, metaData, dataType, proto.getData());
         } catch (InvalidProtocolBufferException e) {
             throw new IllegalStateException("Could not parse protobuf for TbMsg", e);
         }
     }
+
+    public TbMsg copy() {
+        return new TbMsg(id, type, originator, metaData.copy(), dataType, data);
+    }
 }
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsgMetaData.java b/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsgMetaData.java
index 1bbc792..4b7314c 100644
--- a/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsgMetaData.java
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsgMetaData.java
@@ -15,9 +15,12 @@
  */
 package org.thingsboard.server.common.msg;
 
+import lombok.AllArgsConstructor;
 import lombok.Data;
+import lombok.NoArgsConstructor;
 
 import java.io.Serializable;
+import java.util.HashMap;
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
 
@@ -25,10 +28,15 @@ import java.util.concurrent.ConcurrentHashMap;
  * Created by ashvayka on 13.01.18.
  */
 @Data
+@NoArgsConstructor
 public final class TbMsgMetaData implements Serializable {
 
     private Map<String, String> data = new ConcurrentHashMap<>();
 
+    public TbMsgMetaData(Map<String, String> data) {
+        this.data = data;
+    }
+
     public String getValue(String key) {
         return data.get(key);
     }
@@ -37,4 +45,11 @@ public final class TbMsgMetaData implements Serializable {
         data.put(key, value);
     }
 
+    public Map<String, String> values() {
+        return new HashMap<>(data);
+    }
+
+    public TbMsgMetaData copy() {
+        return new TbMsgMetaData(new ConcurrentHashMap<>(data));
+    }
 }
diff --git a/common/message/src/main/proto/tbmsg.proto b/common/message/src/main/proto/tbmsg.proto
index 90fa2bd..4ce1fb6 100644
--- a/common/message/src/main/proto/tbmsg.proto
+++ b/common/message/src/main/proto/tbmsg.proto
@@ -19,6 +19,9 @@ package msgqueue;
 option java_package = "org.thingsboard.server.common.msg.gen";
 option java_outer_classname = "MsgProtos";
 
+message TbMsgMetaDataProto {
+    map<string, string> data = 1;
+}
 
 message TbMsgProto {
     string id = 1;
@@ -26,11 +29,8 @@ message TbMsgProto {
     string entityType = 3;
     string entityId = 4;
 
-    message TbMsgMetaDataProto {
-        map<string, string> data = 1;
-    }
+    TbMsgMetaDataProto metaData = 5;
 
-    repeated TbMsgMetaDataProto metaData = 5;
-
-    bytes data = 6;
+    int32 dataType = 6;
+    string data = 7;
 }
\ No newline at end of file
diff --git a/common/transport/src/main/java/org/thingsboard/server/common/transport/quota/inmemory/HostRequestIntervalRegistry.java b/common/transport/src/main/java/org/thingsboard/server/common/transport/quota/inmemory/HostRequestIntervalRegistry.java
index 8d254a0..3782ed2 100644
--- a/common/transport/src/main/java/org/thingsboard/server/common/transport/quota/inmemory/HostRequestIntervalRegistry.java
+++ b/common/transport/src/main/java/org/thingsboard/server/common/transport/quota/inmemory/HostRequestIntervalRegistry.java
@@ -61,13 +61,14 @@ public class HostRequestIntervalRegistry {
     }
 
     public long tick(String clientHostId) {
+        IntervalCount intervalCount = hostCounts.computeIfAbsent(clientHostId, s -> new IntervalCount(intervalDurationMs));
+        long currentCount = intervalCount.resetIfExpiredAndTick();
         if (whiteList.contains(clientHostId)) {
             return 0;
         } else if (blackList.contains(clientHostId)) {
             return Long.MAX_VALUE;
         }
-        IntervalCount intervalCount = hostCounts.computeIfAbsent(clientHostId, s -> new IntervalCount(intervalDurationMs));
-        return intervalCount.resetIfExpiredAndTick();
+        return currentCount;
     }
 
     public void clean() {
diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/CassandraAssetDao.java b/dao/src/main/java/org/thingsboard/server/dao/asset/CassandraAssetDao.java
index 4f923fe..64ec718 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/asset/CassandraAssetDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/asset/CassandraAssetDao.java
@@ -148,7 +148,7 @@ public class CassandraAssetDao extends CassandraAbstractSearchTextDao<AssetEntit
         query.and(eq(ENTITY_SUBTYPE_TENANT_ID_PROPERTY, tenantId));
         query.and(eq(ENTITY_SUBTYPE_ENTITY_TYPE_PROPERTY, EntityType.ASSET));
         query.setConsistencyLevel(cluster.getDefaultReadConsistencyLevel());
-        ResultSetFuture resultSetFuture = getSession().executeAsync(query);
+        ResultSetFuture resultSetFuture = executeAsyncRead(query);
         return Futures.transform(resultSetFuture, new Function<ResultSet, List<EntitySubtype>>() {
             @Nullable
             @Override
diff --git a/dao/src/main/java/org/thingsboard/server/dao/attributes/CassandraBaseAttributesDao.java b/dao/src/main/java/org/thingsboard/server/dao/attributes/CassandraBaseAttributesDao.java
index 932d6b9..8ae9dc8 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/attributes/CassandraBaseAttributesDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/attributes/CassandraBaseAttributesDao.java
@@ -147,12 +147,12 @@ public class CassandraBaseAttributesDao extends CassandraAbstractAsyncDao implem
                 .and(eq(ATTRIBUTE_TYPE_COLUMN, attributeType))
                 .and(eq(ATTRIBUTE_KEY_COLUMN, key));
         log.debug("Remove request: {}", delete.toString());
-        return getFuture(getSession().executeAsync(delete), rs -> null);
+        return getFuture(executeAsyncWrite(delete), rs -> null);
     }
 
     private PreparedStatement getSaveStmt() {
         if (saveStmt == null) {
-            saveStmt = getSession().prepare("INSERT INTO " + ModelConstants.ATTRIBUTES_KV_CF +
+            saveStmt = prepare("INSERT INTO " + ModelConstants.ATTRIBUTES_KV_CF +
                     "(" + ENTITY_TYPE_COLUMN +
                     "," + ENTITY_ID_COLUMN +
                     "," + ATTRIBUTE_TYPE_COLUMN +
diff --git a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java
index 23fadeb..2a49130 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java
@@ -38,6 +38,7 @@ import org.thingsboard.server.common.data.id.*;
 import org.thingsboard.server.common.data.kv.AttributeKvEntry;
 import org.thingsboard.server.common.data.page.TimePageData;
 import org.thingsboard.server.common.data.page.TimePageLink;
+import org.thingsboard.server.common.data.rule.RuleChainMetaData;
 import org.thingsboard.server.common.data.security.DeviceCredentials;
 import org.thingsboard.server.dao.audit.sink.AuditLogSink;
 import org.thingsboard.server.dao.entity.EntityService;
@@ -158,11 +159,20 @@ public class AuditLogServiceImpl implements AuditLogService {
         switch(actionType) {
             case ADDED:
             case UPDATED:
-                ObjectNode entityNode = objectMapper.valueToTree(entity);
-                if (entityId.getEntityType() == EntityType.DASHBOARD) {
-                    entityNode.put("configuration", "");
+                if (entity != null) {
+                    ObjectNode entityNode = objectMapper.valueToTree(entity);
+                    if (entityId.getEntityType() == EntityType.DASHBOARD) {
+                        entityNode.put("configuration", "");
+                    }
+                    actionData.set("entity", entityNode);
+                }
+                if (entityId.getEntityType() == EntityType.RULE_CHAIN) {
+                    RuleChainMetaData ruleChainMetaData = extractParameter(RuleChainMetaData.class, additionalInfo);
+                    if (ruleChainMetaData != null) {
+                        ObjectNode ruleChainMetaDataNode = objectMapper.valueToTree(ruleChainMetaData);
+                        actionData.set("metadata", ruleChainMetaDataNode);
+                    }
                 }
-                actionData.set("entity", entityNode);
                 break;
             case DELETED:
             case ACTIVATED:
diff --git a/dao/src/main/java/org/thingsboard/server/dao/audit/CassandraAuditLogDao.java b/dao/src/main/java/org/thingsboard/server/dao/audit/CassandraAuditLogDao.java
index 27f7adc..fd02b5f 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/audit/CassandraAuditLogDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/audit/CassandraAuditLogDao.java
@@ -244,12 +244,12 @@ public class CassandraAuditLogDao extends CassandraAbstractSearchTimeDao<AuditLo
             values.add("?");
         }
         String statementString = INSERT_INTO + cfName + " (" + String.join(",", columnsList) + ") VALUES (" + values.toString() + ")";
-        return getSession().prepare(statementString);
+        return prepare(statementString);
     }
 
     private PreparedStatement getPartitionInsertStmt() {
         if (partitionInsertStmt == null) {
-            partitionInsertStmt = getSession().prepare(INSERT_INTO + ModelConstants.AUDIT_LOG_BY_TENANT_ID_PARTITIONS_CF +
+            partitionInsertStmt = prepare(INSERT_INTO + ModelConstants.AUDIT_LOG_BY_TENANT_ID_PARTITIONS_CF +
                     "(" + ModelConstants.AUDIT_LOG_TENANT_ID_PROPERTY +
                     "," + ModelConstants.AUDIT_LOG_PARTITION_PROPERTY + ")" +
                     " VALUES(?, ?)");
@@ -343,7 +343,7 @@ public class CassandraAuditLogDao extends CassandraAbstractSearchTimeDao<AuditLo
                 .where(eq(ModelConstants.AUDIT_LOG_TENANT_ID_PROPERTY, tenantId));
         select.and(QueryBuilder.gte(ModelConstants.PARTITION_COLUMN, minPartition));
         select.and(QueryBuilder.lte(ModelConstants.PARTITION_COLUMN, maxPartition));
-        return getSession().execute(select);
+        return executeRead(select);
     }
 
 }
diff --git a/dao/src/main/java/org/thingsboard/server/dao/component/CassandraBaseComponentDescriptorDao.java b/dao/src/main/java/org/thingsboard/server/dao/component/CassandraBaseComponentDescriptorDao.java
index 5e03545..b5b9f15 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/component/CassandraBaseComponentDescriptorDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/component/CassandraBaseComponentDescriptorDao.java
@@ -130,7 +130,7 @@ public class CassandraBaseComponentDescriptorDao extends CassandraAbstractSearch
     public boolean removeById(UUID key) {
         Statement delete = QueryBuilder.delete().all().from(ModelConstants.COMPONENT_DESCRIPTOR_BY_ID).where(eq(ModelConstants.ID_PROPERTY, key));
         log.debug("Remove request: {}", delete.toString());
-        return getSession().execute(delete).wasApplied();
+        return executeWrite(delete).wasApplied();
     }
 
     @Override
@@ -145,7 +145,7 @@ public class CassandraBaseComponentDescriptorDao extends CassandraAbstractSearch
         log.debug("Delete plugin meta-data entity by id [{}]", clazz);
         Statement delete = QueryBuilder.delete().all().from(getColumnFamilyName()).where(eq(ModelConstants.COMPONENT_DESCRIPTOR_CLASS_PROPERTY, clazz));
         log.debug("Remove request: {}", delete.toString());
-        ResultSet resultSet = getSession().execute(delete);
+        ResultSet resultSet = executeWrite(delete);
         log.debug("Delete result: [{}]", resultSet.wasApplied());
     }
 
diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/CassandraDeviceDao.java b/dao/src/main/java/org/thingsboard/server/dao/device/CassandraDeviceDao.java
index ac72ae8..0246da5 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/device/CassandraDeviceDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/device/CassandraDeviceDao.java
@@ -15,9 +15,9 @@
  */
 package org.thingsboard.server.dao.device;
 
-import com.datastax.driver.core.ResultSet;
-import com.datastax.driver.core.ResultSetFuture;
-import com.datastax.driver.core.Statement;
+import com.datastax.driver.core.*;
+import com.datastax.driver.core.querybuilder.Clause;
+import com.datastax.driver.core.querybuilder.QueryBuilder;
 import com.datastax.driver.core.querybuilder.Select;
 import com.datastax.driver.mapping.Result;
 import com.google.common.base.Function;
@@ -28,9 +28,11 @@ import org.springframework.stereotype.Component;
 import org.thingsboard.server.common.data.Device;
 import org.thingsboard.server.common.data.EntitySubtype;
 import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.device.DeviceStatusQuery;
 import org.thingsboard.server.common.data.page.TextPageLink;
 import org.thingsboard.server.dao.DaoUtil;
 import org.thingsboard.server.dao.model.EntitySubtypeEntity;
+import org.thingsboard.server.dao.model.ModelConstants;
 import org.thingsboard.server.dao.model.nosql.DeviceEntity;
 import org.thingsboard.server.dao.nosql.CassandraAbstractSearchTextDao;
 import org.thingsboard.server.dao.util.NoSqlDao;
@@ -148,7 +150,7 @@ public class CassandraDeviceDao extends CassandraAbstractSearchTextDao<DeviceEnt
         query.and(eq(ENTITY_SUBTYPE_TENANT_ID_PROPERTY, tenantId));
         query.and(eq(ENTITY_SUBTYPE_ENTITY_TYPE_PROPERTY, EntityType.DEVICE));
         query.setConsistencyLevel(cluster.getDefaultReadConsistencyLevel());
-        ResultSetFuture resultSetFuture = getSession().executeAsync(query);
+        ResultSetFuture resultSetFuture = executeAsyncRead(query);
         return Futures.transform(resultSetFuture, new Function<ResultSet, List<EntitySubtype>>() {
             @Nullable
             @Override
@@ -157,7 +159,7 @@ public class CassandraDeviceDao extends CassandraAbstractSearchTextDao<DeviceEnt
                 if (result != null) {
                     List<EntitySubtype> entitySubtypes = new ArrayList<>();
                     result.all().forEach((entitySubtypeEntity) ->
-                        entitySubtypes.add(entitySubtypeEntity.toEntitySubtype())
+                            entitySubtypes.add(entitySubtypeEntity.toEntitySubtype())
                     );
                     return entitySubtypes;
                 } else {
@@ -167,4 +169,68 @@ public class CassandraDeviceDao extends CassandraAbstractSearchTextDao<DeviceEnt
         });
     }
 
+    @Override
+    public ListenableFuture<List<Device>> findDevicesByTenantIdAndStatus(UUID tenantId, DeviceStatusQuery statusQuery) {
+        log.debug("Try to find [{}] devices by tenantId [{}]", statusQuery.getStatus(), tenantId);
+
+        Select select = select().from(DEVICE_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME).allowFiltering();
+        Select.Where query = select.where();
+        query.and(eq(DEVICE_TENANT_ID_PROPERTY, tenantId));
+        Clause clause = statusClause(statusQuery);
+        query.and(clause);
+        return findListByStatementAsync(query);
+    }
+
+    @Override
+    public ListenableFuture<List<Device>> findDevicesByTenantIdTypeAndStatus(UUID tenantId, String type, DeviceStatusQuery statusQuery) {
+        log.debug("Try to find [{}] devices by tenantId [{}] and type [{}]", statusQuery.getStatus(), tenantId, type);
+
+        Select select = select().from(DEVICE_BY_TENANT_BY_TYPE_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME).allowFiltering();
+        Select.Where query = select.where()
+                .and(eq(DEVICE_TENANT_ID_PROPERTY, tenantId))
+                .and(eq(DEVICE_TYPE_PROPERTY, type));
+
+        query.and(statusClause(statusQuery));
+        return findListByStatementAsync(query);
+    }
+
+
+    @Override
+    public void saveDeviceStatus(Device device) {
+        PreparedStatement statement = prepare("insert into " +
+                "device (id, tenant_id, customer_id, type, last_connect, last_update) values (?, ?, ?, ?, ?, ?)");
+        BoundStatement boundStatement = statement.bind(device.getUuidId(), device.getTenantId().getId(), device.getCustomerId().getId(),
+                device.getType(), device.getLastConnectTs(), device.getLastUpdateTs());
+        ResultSetFuture resultSetFuture = executeAsyncWrite(boundStatement);
+        Futures.withFallback(resultSetFuture, t -> {
+            log.error("Can't update device status for [{}]", device, t);
+            throw new IllegalArgumentException("Can't update device status for {" + device + "}", t);
+        });
+    }
+
+    private String getStatusProperty(DeviceStatusQuery statusQuery) {
+        switch (statusQuery.getContactType()) {
+            case UPLOAD:
+                return DEVICE_LAST_UPDATE_PROPERTY;
+            case CONNECT:
+                return DEVICE_LAST_CONNECT_PROPERTY;
+        }
+        return null;
+    }
+
+    private Clause statusClause(DeviceStatusQuery statusQuery) {
+        long minTime = System.currentTimeMillis() - statusQuery.getThreshold();
+        String statusProperty = getStatusProperty(statusQuery);
+        if (statusProperty != null) {
+            switch (statusQuery.getStatus()) {
+                case ONLINE:
+                    return gt(statusProperty, minTime);
+                case OFFLINE:
+                    return lt(statusProperty, minTime);
+            }
+        }
+        log.error("Could not build status query from [{}]", statusQuery);
+        throw new IllegalStateException("Could not build status query for device []");
+    }
+
 }
diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceDao.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceDao.java
index dbc098e..2b9e522 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceDao.java
@@ -18,6 +18,7 @@ package org.thingsboard.server.dao.device;
 import com.google.common.util.concurrent.ListenableFuture;
 import org.thingsboard.server.common.data.Device;
 import org.thingsboard.server.common.data.EntitySubtype;
+import org.thingsboard.server.common.data.device.DeviceStatusQuery;
 import org.thingsboard.server.common.data.page.TextPageLink;
 import org.thingsboard.server.dao.Dao;
 
@@ -27,7 +28,6 @@ import java.util.UUID;
 
 /**
  * The Interface DeviceDao.
- *
  */
 public interface DeviceDao extends Dao<Device> {
 
@@ -52,7 +52,7 @@ public interface DeviceDao extends Dao<Device> {
      * Find devices by tenantId, type and page link.
      *
      * @param tenantId the tenantId
-     * @param type the type
+     * @param type     the type
      * @param pageLink the page link
      * @return the list of device objects
      */
@@ -61,7 +61,7 @@ public interface DeviceDao extends Dao<Device> {
     /**
      * Find devices by tenantId and devices Ids.
      *
-     * @param tenantId the tenantId
+     * @param tenantId  the tenantId
      * @param deviceIds the device Ids
      * @return the list of device objects
      */
@@ -70,9 +70,9 @@ public interface DeviceDao extends Dao<Device> {
     /**
      * Find devices by tenantId, customerId and page link.
      *
-     * @param tenantId the tenantId
+     * @param tenantId   the tenantId
      * @param customerId the customerId
-     * @param pageLink the page link
+     * @param pageLink   the page link
      * @return the list of device objects
      */
     List<Device> findDevicesByTenantIdAndCustomerId(UUID tenantId, UUID customerId, TextPageLink pageLink);
@@ -80,10 +80,10 @@ public interface DeviceDao extends Dao<Device> {
     /**
      * Find devices by tenantId, customerId, type and page link.
      *
-     * @param tenantId the tenantId
+     * @param tenantId   the tenantId
      * @param customerId the customerId
-     * @param type the type
-     * @param pageLink the page link
+     * @param type       the type
+     * @param pageLink   the page link
      * @return the list of device objects
      */
     List<Device> findDevicesByTenantIdAndCustomerIdAndType(UUID tenantId, UUID customerId, String type, TextPageLink pageLink);
@@ -92,9 +92,9 @@ public interface DeviceDao extends Dao<Device> {
     /**
      * Find devices by tenantId, customerId and devices Ids.
      *
-     * @param tenantId the tenantId
+     * @param tenantId   the tenantId
      * @param customerId the customerId
-     * @param deviceIds the device Ids
+     * @param deviceIds  the device Ids
      * @return the list of device objects
      */
     ListenableFuture<List<Device>> findDevicesByTenantIdCustomerIdAndIdsAsync(UUID tenantId, UUID customerId, List<UUID> deviceIds);
@@ -103,7 +103,7 @@ public interface DeviceDao extends Dao<Device> {
      * Find devices by tenantId and device name.
      *
      * @param tenantId the tenantId
-     * @param name the device name
+     * @param name     the device name
      * @return the optional device object
      */
     Optional<Device> findDeviceByTenantIdAndName(UUID tenantId, String name);
@@ -114,4 +114,31 @@ public interface DeviceDao extends Dao<Device> {
      * @return the list of tenant device type objects
      */
     ListenableFuture<List<EntitySubtype>> findTenantDeviceTypesAsync(UUID tenantId);
+
+    /**
+     * Find devices by tenantId, statusQuery and page link.
+     *
+     * @param tenantId    the tenantId
+     * @param statusQuery the page link
+     * @return the list of device objects
+     */
+    ListenableFuture<List<Device>> findDevicesByTenantIdAndStatus(UUID tenantId, DeviceStatusQuery statusQuery);
+
+    /**
+     * Find devices by tenantId, type, statusQuery and page link.
+     *
+     * @param tenantId    the tenantId
+     * @param type        the type
+     * @param statusQuery the page link
+     * @return the list of device objects
+     */
+    ListenableFuture<List<Device>> findDevicesByTenantIdTypeAndStatus(UUID tenantId, String type, DeviceStatusQuery statusQuery);
+
+
+    /**
+     * Update device last contact and update timestamp async
+     *
+     * @param device the device object
+     */
+    void saveDeviceStatus(Device device);
 }
diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceOfflineService.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceOfflineService.java
new file mode 100644
index 0000000..3bf3662
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceOfflineService.java
@@ -0,0 +1,34 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.server.dao.device;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.device.DeviceStatusQuery;
+
+import java.util.List;
+import java.util.UUID;
+
+public interface DeviceOfflineService {
+
+    void online(Device device, boolean isUpdate);
+
+    void offline(Device device);
+
+    ListenableFuture<List<Device>> findOfflineDevices(UUID tenantId, DeviceStatusQuery.ContactType contactType, long offlineThreshold);
+
+    ListenableFuture<List<Device>> findOnlineDevices(UUID tenantId, DeviceStatusQuery.ContactType contactType, long offlineThreshold);
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceOfflineServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceOfflineServiceImpl.java
new file mode 100644
index 0000000..f4d8e61
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceOfflineServiceImpl.java
@@ -0,0 +1,62 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.server.dao.device;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.device.DeviceStatusQuery;
+
+import java.util.List;
+import java.util.UUID;
+
+import static org.thingsboard.server.common.data.device.DeviceStatusQuery.Status.OFFLINE;
+import static org.thingsboard.server.common.data.device.DeviceStatusQuery.Status.ONLINE;
+
+@Service
+public class DeviceOfflineServiceImpl implements DeviceOfflineService {
+
+    @Autowired
+    private DeviceDao deviceDao;
+
+    @Override
+    public void online(Device device, boolean isUpdate) {
+        long current = System.currentTimeMillis();
+        device.setLastConnectTs(current);
+        if(isUpdate) {
+            device.setLastUpdateTs(current);
+        }
+        deviceDao.saveDeviceStatus(device);
+    }
+
+    @Override
+    public void offline(Device device) {
+        online(device, false);
+    }
+
+    @Override
+    public ListenableFuture<List<Device>> findOfflineDevices(UUID tenantId, DeviceStatusQuery.ContactType contactType, long offlineThreshold) {
+        DeviceStatusQuery statusQuery = new DeviceStatusQuery(OFFLINE, contactType, offlineThreshold);
+        return deviceDao.findDevicesByTenantIdAndStatus(tenantId, statusQuery);
+    }
+
+    @Override
+    public ListenableFuture<List<Device>> findOnlineDevices(UUID tenantId, DeviceStatusQuery.ContactType contactType, long offlineThreshold) {
+        DeviceStatusQuery statusQuery = new DeviceStatusQuery(ONLINE, contactType, offlineThreshold);
+        return deviceDao.findDevicesByTenantIdAndStatus(tenantId, statusQuery);
+    }
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java
index a159b9e..52b15ef 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java
@@ -133,6 +133,8 @@ public class ModelConstants {
     public static final String DEVICE_NAME_PROPERTY = "name";
     public static final String DEVICE_TYPE_PROPERTY = "type";
     public static final String DEVICE_ADDITIONAL_INFO_PROPERTY = ADDITIONAL_INFO_PROPERTY;
+    public static final String DEVICE_LAST_CONNECT_PROPERTY = "last_connect";
+    public static final String DEVICE_LAST_UPDATE_PROPERTY = "last_update";
 
     public static final String DEVICE_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "device_by_tenant_and_search_text";
     public static final String DEVICE_BY_TENANT_BY_TYPE_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "device_by_tenant_by_type_and_search_text";
@@ -332,6 +334,8 @@ public class ModelConstants {
     public static final String EVENT_BY_TYPE_AND_ID_VIEW_NAME = "event_by_type_and_id";
     public static final String EVENT_BY_ID_VIEW_NAME = "event_by_id";
 
+    public static final String DEBUG_MODE = "debug_mode";
+
     /**
      * Cassandra rule chain constants.
      */
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/AuditLogEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/AuditLogEntity.java
index ac90cb7..ab2e3bc 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/AuditLogEntity.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/AuditLogEntity.java
@@ -36,7 +36,7 @@ import java.util.UUID;
 
 import static org.thingsboard.server.dao.model.ModelConstants.*;
 
-@Table(name = AUDIT_LOG_COLUMN_FAMILY_NAME)
+@Table(name = AUDIT_LOG_BY_ENTITY_ID_CF)
 @Data
 @NoArgsConstructor
 public class AuditLogEntity implements BaseEntity<AuditLog> {
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/DeviceEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/DeviceEntity.java
index ef0c5fe..7458e56 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/DeviceEntity.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/DeviceEntity.java
@@ -63,6 +63,12 @@ public final class DeviceEntity implements SearchTextEntity<Device> {
     @Column(name = DEVICE_ADDITIONAL_INFO_PROPERTY, codec = JsonCodec.class)
     private JsonNode additionalInfo;
 
+    @Column(name = DEVICE_LAST_CONNECT_PROPERTY)
+    private Long lastConnectTs;
+
+    @Column(name = DEVICE_LAST_UPDATE_PROPERTY)
+    private Long lastUpdateTs;
+
     public DeviceEntity() {
         super();
     }
@@ -80,6 +86,8 @@ public final class DeviceEntity implements SearchTextEntity<Device> {
         this.name = device.getName();
         this.type = device.getType();
         this.additionalInfo = device.getAdditionalInfo();
+        this.lastConnectTs = device.getLastConnectTs();
+        this.lastUpdateTs = device.getLastUpdateTs();
     }
     
     public UUID getId() {
@@ -129,7 +137,23 @@ public final class DeviceEntity implements SearchTextEntity<Device> {
     public void setAdditionalInfo(JsonNode additionalInfo) {
         this.additionalInfo = additionalInfo;
     }
-    
+
+    public Long getLastConnectTs() {
+        return lastConnectTs;
+    }
+
+    public void setLastConnectTs(Long lastConnectTs) {
+        this.lastConnectTs = lastConnectTs;
+    }
+
+    public Long getLastUpdateTs() {
+        return lastUpdateTs;
+    }
+
+    public void setLastUpdateTs(Long lastUpdateTs) {
+        this.lastUpdateTs = lastUpdateTs;
+    }
+
     @Override
     public String getSearchTextSource() {
         return getName();
@@ -157,6 +181,8 @@ public final class DeviceEntity implements SearchTextEntity<Device> {
         device.setName(name);
         device.setType(type);
         device.setAdditionalInfo(additionalInfo);
+        device.setLastConnectTs(lastConnectTs);
+        device.setLastUpdateTs(lastUpdateTs);
         return device;
     }
 
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/RuleChainEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/RuleChainEntity.java
index 34659a8..251a689 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/RuleChainEntity.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/RuleChainEntity.java
@@ -22,6 +22,8 @@ import com.datastax.driver.mapping.annotations.PartitionKey;
 import com.datastax.driver.mapping.annotations.Table;
 import com.fasterxml.jackson.databind.JsonNode;
 import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.Setter;
 import lombok.ToString;
 import org.thingsboard.server.common.data.id.RuleChainId;
 import org.thingsboard.server.common.data.id.RuleNodeId;
@@ -54,6 +56,10 @@ public class RuleChainEntity implements SearchTextEntity<RuleChain> {
     private UUID firstRuleNodeId;
     @Column(name = RULE_CHAIN_ROOT_PROPERTY)
     private boolean root;
+    @Getter
+    @Setter
+    @Column(name = DEBUG_MODE)
+    private boolean debugMode;
     @Column(name = RULE_CHAIN_CONFIGURATION_PROPERTY, codec = JsonCodec.class)
     private JsonNode configuration;
     @Column(name = ADDITIONAL_INFO_PROPERTY, codec = JsonCodec.class)
@@ -71,6 +77,7 @@ public class RuleChainEntity implements SearchTextEntity<RuleChain> {
         this.searchText = ruleChain.getName();
         this.firstRuleNodeId = DaoUtil.getId(ruleChain.getFirstRuleNodeId());
         this.root = ruleChain.isRoot();
+        this.debugMode = ruleChain.isDebugMode();
         this.configuration = ruleChain.getConfiguration();
         this.additionalInfo = ruleChain.getAdditionalInfo();
     }
@@ -157,6 +164,7 @@ public class RuleChainEntity implements SearchTextEntity<RuleChain> {
             ruleChain.setFirstRuleNodeId(new RuleNodeId(this.firstRuleNodeId));
         }
         ruleChain.setRoot(this.root);
+        ruleChain.setDebugMode(this.debugMode);
         ruleChain.setConfiguration(this.configuration);
         ruleChain.setAdditionalInfo(this.additionalInfo);
         return ruleChain;
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/RuleNodeEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/RuleNodeEntity.java
index ba96e4b..8d3f3c3 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/RuleNodeEntity.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/RuleNodeEntity.java
@@ -21,6 +21,8 @@ import com.datastax.driver.mapping.annotations.PartitionKey;
 import com.datastax.driver.mapping.annotations.Table;
 import com.fasterxml.jackson.databind.JsonNode;
 import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.Setter;
 import lombok.ToString;
 import org.thingsboard.server.common.data.id.RuleNodeId;
 import org.thingsboard.server.common.data.rule.RuleNode;
@@ -49,6 +51,11 @@ public class RuleNodeEntity implements SearchTextEntity<RuleNode> {
     private JsonNode configuration;
     @Column(name = ADDITIONAL_INFO_PROPERTY, codec = JsonCodec.class)
     private JsonNode additionalInfo;
+    @Getter
+    @Setter
+    @Column(name = DEBUG_MODE)
+    private boolean debugMode;
+
 
     public RuleNodeEntity() {
     }
@@ -59,6 +66,7 @@ public class RuleNodeEntity implements SearchTextEntity<RuleNode> {
         }
         this.type = ruleNode.getType();
         this.name = ruleNode.getName();
+        this.debugMode = ruleNode.isDebugMode();
         this.searchText = ruleNode.getName();
         this.configuration = ruleNode.getConfiguration();
         this.additionalInfo = ruleNode.getAdditionalInfo();
@@ -126,6 +134,7 @@ public class RuleNodeEntity implements SearchTextEntity<RuleNode> {
         ruleNode.setCreatedTime(UUIDs.unixTimestamp(id));
         ruleNode.setType(this.type);
         ruleNode.setName(this.name);
+        ruleNode.setDebugMode(this.debugMode);
         ruleNode.setConfiguration(this.configuration);
         ruleNode.setAdditionalInfo(this.additionalInfo);
         return ruleNode;
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/DeviceEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/DeviceEntity.java
index 7aaf0ae..e831c6e 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/DeviceEntity.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/DeviceEntity.java
@@ -34,6 +34,9 @@ import javax.persistence.Column;
 import javax.persistence.Entity;
 import javax.persistence.Table;
 
+import static org.thingsboard.server.dao.model.ModelConstants.DEVICE_LAST_CONNECT_PROPERTY;
+import static org.thingsboard.server.dao.model.ModelConstants.DEVICE_LAST_UPDATE_PROPERTY;
+
 @Data
 @EqualsAndHashCode(callSuper = true)
 @Entity
@@ -60,6 +63,12 @@ public final class DeviceEntity extends BaseSqlEntity<Device> implements SearchT
     @Column(name = ModelConstants.DEVICE_ADDITIONAL_INFO_PROPERTY)
     private JsonNode additionalInfo;
 
+    @Column(name = DEVICE_LAST_CONNECT_PROPERTY)
+    private Long lastConnectTs;
+
+    @Column(name = DEVICE_LAST_UPDATE_PROPERTY)
+    private Long lastUpdateTs;
+
     public DeviceEntity() {
         super();
     }
@@ -77,6 +86,8 @@ public final class DeviceEntity extends BaseSqlEntity<Device> implements SearchT
         this.name = device.getName();
         this.type = device.getType();
         this.additionalInfo = device.getAdditionalInfo();
+        this.lastConnectTs = device.getLastConnectTs();
+        this.lastUpdateTs = device.getLastUpdateTs();
     }
 
     @Override
@@ -102,6 +113,8 @@ public final class DeviceEntity extends BaseSqlEntity<Device> implements SearchT
         device.setName(name);
         device.setType(type);
         device.setAdditionalInfo(additionalInfo);
+        device.setLastConnectTs(lastConnectTs);
+        device.setLastUpdateTs(lastUpdateTs);
         return device;
     }
 }
\ No newline at end of file
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/RuleChainEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/RuleChainEntity.java
index 471ec7b..a48421a 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/RuleChainEntity.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/RuleChainEntity.java
@@ -58,6 +58,9 @@ public class RuleChainEntity extends BaseSqlEntity<RuleChain> implements SearchT
     @Column(name = ModelConstants.RULE_CHAIN_ROOT_PROPERTY)
     private boolean root;
 
+    @Column(name = ModelConstants.DEBUG_MODE)
+    private boolean debugMode;
+
     @Type(type = "json")
     @Column(name = ModelConstants.RULE_CHAIN_CONFIGURATION_PROPERTY)
     private JsonNode configuration;
@@ -80,6 +83,7 @@ public class RuleChainEntity extends BaseSqlEntity<RuleChain> implements SearchT
             this.firstRuleNodeId = UUIDConverter.fromTimeUUID(ruleChain.getFirstRuleNodeId().getId());
         }
         this.root = ruleChain.isRoot();
+        this.debugMode = ruleChain.isDebugMode();
         this.configuration = ruleChain.getConfiguration();
         this.additionalInfo = ruleChain.getAdditionalInfo();
     }
@@ -104,6 +108,7 @@ public class RuleChainEntity extends BaseSqlEntity<RuleChain> implements SearchT
             ruleChain.setFirstRuleNodeId(new RuleNodeId(UUIDConverter.fromString(firstRuleNodeId)));
         }
         ruleChain.setRoot(root);
+        ruleChain.setDebugMode(debugMode);
         ruleChain.setConfiguration(configuration);
         ruleChain.setAdditionalInfo(additionalInfo);
         return ruleChain;
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/RuleNodeEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/RuleNodeEntity.java
index d960487..6a888c2 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/RuleNodeEntity.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/RuleNodeEntity.java
@@ -56,6 +56,9 @@ public class RuleNodeEntity extends BaseSqlEntity<RuleNode> implements SearchTex
     @Column(name = ModelConstants.ADDITIONAL_INFO_PROPERTY)
     private JsonNode additionalInfo;
 
+    @Column(name = ModelConstants.DEBUG_MODE)
+    private boolean debugMode;
+
     public RuleNodeEntity() {
     }
 
@@ -65,6 +68,7 @@ public class RuleNodeEntity extends BaseSqlEntity<RuleNode> implements SearchTex
         }
         this.type = ruleNode.getType();
         this.name = ruleNode.getName();
+        this.debugMode = ruleNode.isDebugMode();
         this.searchText = ruleNode.getName();
         this.configuration = ruleNode.getConfiguration();
         this.additionalInfo = ruleNode.getAdditionalInfo();
@@ -86,6 +90,7 @@ public class RuleNodeEntity extends BaseSqlEntity<RuleNode> implements SearchTex
         ruleNode.setCreatedTime(UUIDs.unixTimestamp(getId()));
         ruleNode.setType(type);
         ruleNode.setName(name);
+        ruleNode.setDebugMode(debugMode);
         ruleNode.setConfiguration(configuration);
         ruleNode.setAdditionalInfo(additionalInfo);
         return ruleNode;
diff --git a/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraAbstractDao.java b/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraAbstractDao.java
index c2f709f..ba186cc 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraAbstractDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraAbstractDao.java
@@ -21,6 +21,7 @@ import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.thingsboard.server.dao.cassandra.CassandraCluster;
 import org.thingsboard.server.dao.model.type.*;
+import org.thingsboard.server.dao.util.BufferedRateLimiter;
 
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentMap;
@@ -33,16 +34,15 @@ public abstract class CassandraAbstractDao {
 
     private ConcurrentMap<String, PreparedStatement> preparedStatementMap = new ConcurrentHashMap<>();
 
-    protected PreparedStatement prepare(String query) {
-        return preparedStatementMap.computeIfAbsent(query, i -> getSession().prepare(i));
-    }
+    @Autowired
+    private BufferedRateLimiter rateLimiter;
 
     private Session session;
 
     private ConsistencyLevel defaultReadLevel;
     private ConsistencyLevel defaultWriteLevel;
 
-    protected Session getSession() {
+    private Session getSession() {
         if (session == null) {
             session = cluster.getSession();
             defaultReadLevel = cluster.getDefaultReadConsistencyLevel();
@@ -59,6 +59,10 @@ public abstract class CassandraAbstractDao {
         return session;
     }
 
+    protected PreparedStatement prepare(String query) {
+        return preparedStatementMap.computeIfAbsent(query, i -> getSession().prepare(i));
+    }
+
     private void registerCodecIfNotFound(CodecRegistry registry, TypeCodec<?> codec) {
         try {
             registry.codecFor(codec.getCqlType(), codec.getJavaType());
@@ -85,10 +89,7 @@ public abstract class CassandraAbstractDao {
 
     private ResultSet execute(Statement statement, ConsistencyLevel level) {
         log.debug("Execute cassandra statement {}", statement);
-        if (statement.getConsistencyLevel() == null) {
-            statement.setConsistencyLevel(level);
-        }
-        return getSession().execute(statement);
+        return executeAsync(statement, level).getUninterruptibly();
     }
 
     private ResultSetFuture executeAsync(Statement statement, ConsistencyLevel level) {
@@ -96,6 +97,6 @@ public abstract class CassandraAbstractDao {
         if (statement.getConsistencyLevel() == null) {
             statement.setConsistencyLevel(level);
         }
-        return getSession().executeAsync(statement);
+        return new RateLimitedResultSetFuture(getSession(), rateLimiter, statement);
     }
 }
\ No newline at end of file
diff --git a/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraAbstractModelDao.java b/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraAbstractModelDao.java
index 7e87fa8..47d43ba 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraAbstractModelDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraAbstractModelDao.java
@@ -63,7 +63,7 @@ public abstract class CassandraAbstractModelDao<E extends BaseEntity<D>, D> exte
         List<E> list = Collections.emptyList();
         if (statement != null) {
             statement.setConsistencyLevel(cluster.getDefaultReadConsistencyLevel());
-            ResultSet resultSet = getSession().execute(statement);
+            ResultSet resultSet = executeRead(statement);
             Result<E> result = getMapper().map(resultSet);
             if (result != null) {
                 list = result.all();
@@ -75,7 +75,7 @@ public abstract class CassandraAbstractModelDao<E extends BaseEntity<D>, D> exte
     protected ListenableFuture<List<D>> findListByStatementAsync(Statement statement) {
         if (statement != null) {
             statement.setConsistencyLevel(cluster.getDefaultReadConsistencyLevel());
-            ResultSetFuture resultSetFuture = getSession().executeAsync(statement);
+            ResultSetFuture resultSetFuture = executeAsyncRead(statement);
             return Futures.transform(resultSetFuture, new Function<ResultSet, List<D>>() {
                 @Nullable
                 @Override
@@ -97,7 +97,7 @@ public abstract class CassandraAbstractModelDao<E extends BaseEntity<D>, D> exte
         E object = null;
         if (statement != null) {
             statement.setConsistencyLevel(cluster.getDefaultReadConsistencyLevel());
-            ResultSet resultSet = getSession().execute(statement);
+            ResultSet resultSet = executeRead(statement);
             Result<E> result = getMapper().map(resultSet);
             if (result != null) {
                 object = result.one();
@@ -109,7 +109,7 @@ public abstract class CassandraAbstractModelDao<E extends BaseEntity<D>, D> exte
     protected ListenableFuture<D> findOneByStatementAsync(Statement statement) {
         if (statement != null) {
             statement.setConsistencyLevel(cluster.getDefaultReadConsistencyLevel());
-            ResultSetFuture resultSetFuture = getSession().executeAsync(statement);
+            ResultSetFuture resultSetFuture = executeAsyncRead(statement);
             return Futures.transform(resultSetFuture, new Function<ResultSet, D>() {
                 @Nullable
                 @Override
@@ -184,7 +184,7 @@ public abstract class CassandraAbstractModelDao<E extends BaseEntity<D>, D> exte
     public boolean removeById(UUID key) {
         Statement delete = QueryBuilder.delete().all().from(getColumnFamilyName()).where(eq(ModelConstants.ID_PROPERTY, key));
         log.debug("Remove request: {}", delete.toString());
-        return getSession().execute(delete).wasApplied();
+        return executeWrite(delete).wasApplied();
     }
 
     @Override
diff --git a/dao/src/main/java/org/thingsboard/server/dao/nosql/RateLimitedResultSetFuture.java b/dao/src/main/java/org/thingsboard/server/dao/nosql/RateLimitedResultSetFuture.java
new file mode 100644
index 0000000..d250563
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/nosql/RateLimitedResultSetFuture.java
@@ -0,0 +1,152 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.server.dao.nosql;
+
+import com.datastax.driver.core.ResultSet;
+import com.datastax.driver.core.ResultSetFuture;
+import com.datastax.driver.core.Session;
+import com.datastax.driver.core.Statement;
+import com.google.common.base.Function;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.Uninterruptibles;
+import org.thingsboard.server.dao.exception.BufferLimitException;
+import org.thingsboard.server.dao.util.AsyncRateLimiter;
+
+import javax.annotation.Nullable;
+import java.util.concurrent.*;
+
+public class RateLimitedResultSetFuture implements ResultSetFuture {
+
+    private final ListenableFuture<ResultSetFuture> originalFuture;
+    private final ListenableFuture<Void> rateLimitFuture;
+
+    public RateLimitedResultSetFuture(Session session, AsyncRateLimiter rateLimiter, Statement statement) {
+        this.rateLimitFuture = Futures.withFallback(rateLimiter.acquireAsync(), t -> {
+            if (!(t instanceof BufferLimitException)) {
+                rateLimiter.release();
+            }
+            return Futures.immediateFailedFuture(t);
+        });
+        this.originalFuture = Futures.transform(rateLimitFuture,
+                (Function<Void, ResultSetFuture>) i -> executeAsyncWithRelease(rateLimiter, session, statement));
+
+    }
+
+    @Override
+    public ResultSet getUninterruptibly() {
+        return safeGet().getUninterruptibly();
+    }
+
+    @Override
+    public ResultSet getUninterruptibly(long timeout, TimeUnit unit) throws TimeoutException {
+        long rateLimitStart = System.nanoTime();
+        ResultSetFuture resultSetFuture = null;
+        try {
+            resultSetFuture = originalFuture.get(timeout, unit);
+        } catch (InterruptedException | ExecutionException e) {
+            throw new IllegalStateException(e);
+        }
+        long rateLimitDurationNano = System.nanoTime() - rateLimitStart;
+        long innerTimeoutNano = unit.toNanos(timeout) - rateLimitDurationNano;
+        if (innerTimeoutNano > 0) {
+            return resultSetFuture.getUninterruptibly(innerTimeoutNano, TimeUnit.NANOSECONDS);
+        }
+        throw new TimeoutException("Timeout waiting for task.");
+    }
+
+    @Override
+    public boolean cancel(boolean mayInterruptIfRunning) {
+        if (originalFuture.isDone()) {
+            return safeGet().cancel(mayInterruptIfRunning);
+        } else {
+            return originalFuture.cancel(mayInterruptIfRunning);
+        }
+    }
+
+    @Override
+    public boolean isCancelled() {
+        if (originalFuture.isDone()) {
+            return safeGet().isCancelled();
+        }
+
+        return originalFuture.isCancelled();
+    }
+
+    @Override
+    public boolean isDone() {
+        return originalFuture.isDone() && safeGet().isDone();
+    }
+
+    @Override
+    public ResultSet get() throws InterruptedException, ExecutionException {
+        return safeGet().get();
+    }
+
+    @Override
+    public ResultSet get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
+        long rateLimitStart = System.nanoTime();
+        ResultSetFuture resultSetFuture = originalFuture.get(timeout, unit);
+        long rateLimitDurationNano = System.nanoTime() - rateLimitStart;
+        long innerTimeoutNano = unit.toNanos(timeout) - rateLimitDurationNano;
+        if (innerTimeoutNano > 0) {
+            return resultSetFuture.get(innerTimeoutNano, TimeUnit.NANOSECONDS);
+        }
+        throw new TimeoutException("Timeout waiting for task.");
+    }
+
+    @Override
+    public void addListener(Runnable listener, Executor executor) {
+        originalFuture.addListener(() -> {
+            try {
+                ResultSetFuture resultSetFuture = Uninterruptibles.getUninterruptibly(originalFuture);
+                resultSetFuture.addListener(listener, executor);
+            } catch (CancellationException | ExecutionException e) {
+                Futures.immediateFailedFuture(e).addListener(listener, executor);
+            }
+        }, executor);
+    }
+
+    private ResultSetFuture safeGet() {
+        try {
+            return originalFuture.get();
+        } catch (InterruptedException | ExecutionException e) {
+            throw new IllegalStateException(e);
+        }
+    }
+
+    private ResultSetFuture executeAsyncWithRelease(AsyncRateLimiter rateLimiter, Session session, Statement statement) {
+        try {
+            ResultSetFuture resultSetFuture = session.executeAsync(statement);
+            Futures.addCallback(resultSetFuture, new FutureCallback<ResultSet>() {
+                @Override
+                public void onSuccess(@Nullable ResultSet result) {
+                    rateLimiter.release();
+                }
+
+                @Override
+                public void onFailure(Throwable t) {
+                    rateLimiter.release();
+                }
+            });
+            return resultSetFuture;
+        } catch (RuntimeException re) {
+            rateLimiter.release();
+            throw re;
+        }
+    }
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/queue/QueueBenchmark.java b/dao/src/main/java/org/thingsboard/server/dao/queue/QueueBenchmark.java
index da991fa..85f42ae 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/queue/QueueBenchmark.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/queue/QueueBenchmark.java
@@ -32,6 +32,7 @@ import org.springframework.boot.autoconfigure.SpringBootApplication;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.ComponentScan;
 import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.TbMsgDataType;
 import org.thingsboard.server.common.msg.TbMsgMetaData;
 
 import javax.annotation.Nullable;
@@ -125,7 +126,7 @@ public class QueueBenchmark implements CommandLineRunner {
         TbMsgMetaData metaData = new TbMsgMetaData();
         metaData.putValue("key", "value");
         String dataStr = "someContent";
-        return new TbMsg(UUIDs.timeBased(), "type", null, metaData, dataStr.getBytes());
+        return new TbMsg(UUIDs.timeBased(), "type", null, metaData, TbMsgDataType.JSON, dataStr);
     }
 
     @Bean
diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationDao.java b/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationDao.java
index 9e25241..55838d6 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationDao.java
@@ -242,7 +242,7 @@ public class BaseRelationDao extends CassandraAbstractAsyncDao implements Relati
 
     private PreparedStatement getSaveStmt() {
         if (saveStmt == null) {
-            saveStmt = getSession().prepare("INSERT INTO " + ModelConstants.RELATION_COLUMN_FAMILY_NAME + " " +
+            saveStmt = prepare("INSERT INTO " + ModelConstants.RELATION_COLUMN_FAMILY_NAME + " " +
                     "(" + ModelConstants.RELATION_FROM_ID_PROPERTY +
                     "," + ModelConstants.RELATION_FROM_TYPE_PROPERTY +
                     "," + ModelConstants.RELATION_TO_ID_PROPERTY +
@@ -257,7 +257,7 @@ public class BaseRelationDao extends CassandraAbstractAsyncDao implements Relati
 
     private PreparedStatement getDeleteStmt() {
         if (deleteStmt == null) {
-            deleteStmt = getSession().prepare("DELETE FROM " + ModelConstants.RELATION_COLUMN_FAMILY_NAME +
+            deleteStmt = prepare("DELETE FROM " + ModelConstants.RELATION_COLUMN_FAMILY_NAME +
                     WHERE + ModelConstants.RELATION_FROM_ID_PROPERTY + " = ?" +
                     AND + ModelConstants.RELATION_FROM_TYPE_PROPERTY + " = ?" +
                     AND + ModelConstants.RELATION_TO_ID_PROPERTY + " = ?" +
@@ -270,7 +270,7 @@ public class BaseRelationDao extends CassandraAbstractAsyncDao implements Relati
 
     private PreparedStatement getDeleteAllByEntityStmt() {
         if (deleteAllByEntityStmt == null) {
-            deleteAllByEntityStmt = getSession().prepare("DELETE FROM " + ModelConstants.RELATION_COLUMN_FAMILY_NAME +
+            deleteAllByEntityStmt = prepare("DELETE FROM " + ModelConstants.RELATION_COLUMN_FAMILY_NAME +
                     WHERE + ModelConstants.RELATION_FROM_ID_PROPERTY + " = ?" +
                     AND + ModelConstants.RELATION_FROM_TYPE_PROPERTY + " = ?");
         }
@@ -279,7 +279,7 @@ public class BaseRelationDao extends CassandraAbstractAsyncDao implements Relati
 
     private PreparedStatement getFindAllByFromStmt() {
         if (findAllByFromStmt == null) {
-            findAllByFromStmt = getSession().prepare(SELECT_COLUMNS + " " +
+            findAllByFromStmt = prepare(SELECT_COLUMNS + " " +
                     FROM + ModelConstants.RELATION_COLUMN_FAMILY_NAME + " " +
                     WHERE + ModelConstants.RELATION_FROM_ID_PROPERTY + EQUAL_TO_PARAM +
                     AND + ModelConstants.RELATION_FROM_TYPE_PROPERTY + EQUAL_TO_PARAM +
@@ -290,7 +290,7 @@ public class BaseRelationDao extends CassandraAbstractAsyncDao implements Relati
 
     private PreparedStatement getFindAllByFromAndTypeStmt() {
         if (findAllByFromAndTypeStmt == null) {
-            findAllByFromAndTypeStmt = getSession().prepare(SELECT_COLUMNS + " " +
+            findAllByFromAndTypeStmt = prepare(SELECT_COLUMNS + " " +
                     FROM + ModelConstants.RELATION_COLUMN_FAMILY_NAME + " " +
                     WHERE + ModelConstants.RELATION_FROM_ID_PROPERTY + EQUAL_TO_PARAM +
                     AND + ModelConstants.RELATION_FROM_TYPE_PROPERTY + EQUAL_TO_PARAM +
@@ -303,7 +303,7 @@ public class BaseRelationDao extends CassandraAbstractAsyncDao implements Relati
 
     private PreparedStatement getFindAllByToStmt() {
         if (findAllByToStmt == null) {
-            findAllByToStmt = getSession().prepare(SELECT_COLUMNS + " " +
+            findAllByToStmt = prepare(SELECT_COLUMNS + " " +
                     FROM + ModelConstants.RELATION_REVERSE_VIEW_NAME + " " +
                     WHERE + ModelConstants.RELATION_TO_ID_PROPERTY + EQUAL_TO_PARAM +
                     AND + ModelConstants.RELATION_TO_TYPE_PROPERTY + EQUAL_TO_PARAM +
@@ -314,7 +314,7 @@ public class BaseRelationDao extends CassandraAbstractAsyncDao implements Relati
 
     private PreparedStatement getFindAllByToAndTypeStmt() {
         if (findAllByToAndTypeStmt == null) {
-            findAllByToAndTypeStmt = getSession().prepare(SELECT_COLUMNS + " " +
+            findAllByToAndTypeStmt = prepare(SELECT_COLUMNS + " " +
                     FROM + ModelConstants.RELATION_REVERSE_VIEW_NAME + " " +
                     WHERE + ModelConstants.RELATION_TO_ID_PROPERTY + EQUAL_TO_PARAM +
                     AND + ModelConstants.RELATION_TO_TYPE_PROPERTY + EQUAL_TO_PARAM +
@@ -327,7 +327,7 @@ public class BaseRelationDao extends CassandraAbstractAsyncDao implements Relati
 
     private PreparedStatement getCheckRelationStmt() {
         if (checkRelationStmt == null) {
-            checkRelationStmt = getSession().prepare(SELECT_COLUMNS + " " +
+            checkRelationStmt = prepare(SELECT_COLUMNS + " " +
                     FROM + ModelConstants.RELATION_COLUMN_FAMILY_NAME + " " +
                     WHERE + ModelConstants.RELATION_FROM_ID_PROPERTY + EQUAL_TO_PARAM +
                     AND + ModelConstants.RELATION_FROM_TYPE_PROPERTY + EQUAL_TO_PARAM +
diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java b/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java
index 01f60f8..836bd3d 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java
@@ -82,8 +82,9 @@ public class BaseRelationService implements RelationService {
     }
 
     @Caching(evict = {
-            @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.typeGroup}"),
+            @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.to, #relation.type, #relation.typeGroup}"),
             @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.type, #relation.typeGroup}"),
+            @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.typeGroup}"),
             @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.to, #relation.typeGroup}"),
             @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.to, #relation.type, #relation.typeGroup}")
     })
@@ -95,8 +96,9 @@ public class BaseRelationService implements RelationService {
     }
 
     @Caching(evict = {
-            @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.typeGroup}"),
+            @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.to, #relation.type, #relation.typeGroup}"),
             @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.type, #relation.typeGroup}"),
+            @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.typeGroup}"),
             @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.to, #relation.typeGroup}"),
             @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.to, #relation.type, #relation.typeGroup}")
     })
@@ -108,11 +110,11 @@ public class BaseRelationService implements RelationService {
     }
 
     @Caching(evict = {
-            @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.typeGroup}"),
+            @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.to, #relation.type, #relation.typeGroup}"),
             @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.type, #relation.typeGroup}"),
+            @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.typeGroup}"),
             @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.to, #relation.typeGroup}"),
-            @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.to, #relation.type, #relation.typeGroup}"),
-            @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.to, #relation.type, #relation.typeGroup}")
+            @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.to, #relation.type, #relation.typeGroup}")
     })
     @Override
     public boolean deleteRelation(EntityRelation relation) {
@@ -122,11 +124,11 @@ public class BaseRelationService implements RelationService {
     }
 
     @Caching(evict = {
-            @CacheEvict(cacheNames = RELATIONS_CACHE, key = "#relation.from"),
-            @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.type}"),
-            @CacheEvict(cacheNames = RELATIONS_CACHE, key = "#relation.to"),
-            @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.to, #relation.type}"),
-            @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.to, #relation.type, #relation.typeGroup}")
+            @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.to, #relation.type, #relation.typeGroup}"),
+            @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.type, #relation.typeGroup}"),
+            @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.typeGroup}"),
+            @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.to, #relation.typeGroup}"),
+            @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.to, #relation.type, #relation.typeGroup}")
     })
     @Override
     public ListenableFuture<Boolean> deleteRelationAsync(EntityRelation relation) {
@@ -136,11 +138,11 @@ public class BaseRelationService implements RelationService {
     }
 
     @Caching(evict = {
-            @CacheEvict(cacheNames = RELATIONS_CACHE, key = "#from"),
-            @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#from, #relationType}"),
-            @CacheEvict(cacheNames = RELATIONS_CACHE, key = "#to"),
-            @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#to, #relationType}"),
-            @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#from, #to, #relationType}")
+            @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#from, #to, #relationType, #typeGroup}"),
+            @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#from, #relationType, #typeGroup}"),
+            @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#from, #typeGroup}"),
+            @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#to, #typeGroup}"),
+            @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#to, #relationType, #typeGroup}")
     })
     @Override
     public boolean deleteRelation(EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup) {
@@ -150,11 +152,11 @@ public class BaseRelationService implements RelationService {
     }
 
     @Caching(evict = {
-            @CacheEvict(cacheNames = RELATIONS_CACHE, key = "#from"),
-            @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#from, #relationType}"),
-            @CacheEvict(cacheNames = RELATIONS_CACHE, key = "#to"),
-            @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#to, #relationType}"),
-            @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#from, #to, #relationType}")
+            @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#from, #to, #relationType, #typeGroup}"),
+            @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#from, #relationType, #typeGroup}"),
+            @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#from, #typeGroup}"),
+            @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#to, #typeGroup}"),
+            @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#to, #relationType, #typeGroup}")
     })
     @Override
     public ListenableFuture<Boolean> deleteRelationAsync(EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup) {
@@ -209,9 +211,9 @@ public class BaseRelationService implements RelationService {
     private void checkFromDeleteSync(Cache cache, List<Boolean> results, EntityRelation relation, boolean isRemove) {
         if (isRemove) {
             results.add(relationDao.deleteRelation(relation));
-            cacheEviction(relation, relation.getTo(), cache);
+            cacheEviction(relation, false, cache);
         } else {
-            cacheEviction(relation, relation.getFrom(), cache);
+            cacheEviction(relation, true, cache);
         }
     }
 
@@ -260,25 +262,43 @@ public class BaseRelationService implements RelationService {
     private void checkFromDeleteAsync(Cache cache, List<ListenableFuture<Boolean>> results, EntityRelation relation, boolean isRemove) {
         if (isRemove) {
             results.add(relationDao.deleteRelationAsync(relation));
-            cacheEviction(relation, relation.getTo(), cache);
+            cacheEviction(relation, false, cache);
         } else {
-            cacheEviction(relation, relation.getFrom(), cache);
+            cacheEviction(relation, true, cache);
         }
     }
 
-    private void cacheEviction(EntityRelation relation, EntityId entityId, Cache cache) {
-        cache.evict(entityId);
-
-        List<Object> toAndType = new ArrayList<>();
-        toAndType.add(entityId);
-        toAndType.add(relation.getType());
-        cache.evict(toAndType);
-
-        List<Object> fromToAndType = new ArrayList<>();
-        fromToAndType.add(relation.getFrom());
-        fromToAndType.add(relation.getTo());
-        fromToAndType.add(relation.getType());
-        cache.evict(fromToAndType);
+    private void cacheEviction(EntityRelation relation, boolean outboundOnly, Cache cache) {
+        List<Object> fromToTypeAndTypeGroup = new ArrayList<>();
+        fromToTypeAndTypeGroup.add(relation.getFrom());
+        fromToTypeAndTypeGroup.add(relation.getTo());
+        fromToTypeAndTypeGroup.add(relation.getType());
+        fromToTypeAndTypeGroup.add(relation.getTypeGroup());
+        cache.evict(fromToTypeAndTypeGroup);
+
+        List<Object> fromTypeAndTypeGroup = new ArrayList<>();
+        fromTypeAndTypeGroup.add(relation.getFrom());
+        fromTypeAndTypeGroup.add(relation.getType());
+        fromTypeAndTypeGroup.add(relation.getTypeGroup());
+        cache.evict(fromTypeAndTypeGroup);
+
+        List<Object> fromAndTypeGroup = new ArrayList<>();
+        fromAndTypeGroup.add(relation.getFrom());
+        fromAndTypeGroup.add(relation.getTypeGroup());
+        cache.evict(fromAndTypeGroup);
+
+        if (!outboundOnly) {
+            List<Object> toAndTypeGroup = new ArrayList<>();
+            toAndTypeGroup.add(relation.getTo());
+            toAndTypeGroup.add(relation.getTypeGroup());
+            cache.evict(toAndTypeGroup);
+
+            List<Object> toTypeAndTypeGroup = new ArrayList<>();
+            fromTypeAndTypeGroup.add(relation.getTo());
+            fromTypeAndTypeGroup.add(relation.getType());
+            fromTypeAndTypeGroup.add(relation.getTypeGroup());
+            cache.evict(toTypeAndTypeGroup);
+        }
     }
 
     @Cacheable(cacheNames = RELATIONS_CACHE, key = "{#from, #typeGroup}")
diff --git a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java
index 1e79163..cdb9a80 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java
@@ -16,6 +16,7 @@
 
 package org.thingsboard.server.dao.rule;
 
+import com.google.common.util.concurrent.ListenableFuture;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.StringUtils;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -30,7 +31,9 @@ import org.thingsboard.server.common.data.page.TextPageData;
 import org.thingsboard.server.common.data.page.TextPageLink;
 import org.thingsboard.server.common.data.relation.EntityRelation;
 import org.thingsboard.server.common.data.relation.RelationTypeGroup;
+import org.thingsboard.server.common.data.rule.NodeConnectionInfo;
 import org.thingsboard.server.common.data.rule.RuleChain;
+import org.thingsboard.server.common.data.rule.RuleChainConnectionInfo;
 import org.thingsboard.server.common.data.rule.RuleChainMetaData;
 import org.thingsboard.server.common.data.rule.RuleNode;
 import org.thingsboard.server.dao.entity.AbstractEntityService;
@@ -147,7 +150,7 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC
             ruleChainDao.save(ruleChain);
         }
         if (ruleChainMetaData.getConnections() != null) {
-            for (RuleChainMetaData.NodeConnectionInfo nodeConnection : ruleChainMetaData.getConnections()) {
+            for (NodeConnectionInfo nodeConnection : ruleChainMetaData.getConnections()) {
                 EntityId from = nodes.get(nodeConnection.getFromIndex()).getId();
                 EntityId to = nodes.get(nodeConnection.getToIndex()).getId();
                 String type = nodeConnection.getType();
@@ -160,12 +163,12 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC
             }
         }
         if (ruleChainMetaData.getRuleChainConnections() != null) {
-            for (RuleChainMetaData.RuleChainConnectionInfo nodeToRuleChainConnection : ruleChainMetaData.getRuleChainConnections()) {
+            for (RuleChainConnectionInfo nodeToRuleChainConnection : ruleChainMetaData.getRuleChainConnections()) {
                 EntityId from = nodes.get(nodeToRuleChainConnection.getFromIndex()).getId();
                 EntityId to = nodeToRuleChainConnection.getTargetRuleChainId();
                 String type = nodeToRuleChainConnection.getType();
                 try {
-                    createRelation(new EntityRelation(from, to, type, RelationTypeGroup.RULE_NODE));
+                    createRelation(new EntityRelation(from, to, type, RelationTypeGroup.RULE_NODE, nodeToRuleChainConnection.getAdditionalInfo()));
                 } catch (ExecutionException | InterruptedException e) {
                     log.warn("[{}] Failed to create rule node to rule chain relation. from: [{}], to: [{}]", from, to);
                     throw new RuntimeException(e);
@@ -205,7 +208,7 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC
                     ruleChainMetaData.addConnectionInfo(fromIndex, toIndex, type);
                 } else if (nodeRelation.getTo().getEntityType() == EntityType.RULE_CHAIN) {
                     RuleChainId targetRuleChainId = new RuleChainId(nodeRelation.getTo().getId());
-                    ruleChainMetaData.addRuleChainConnectionInfo(fromIndex, targetRuleChainId, type);
+                    ruleChainMetaData.addRuleChainConnectionInfo(fromIndex, targetRuleChainId, type, nodeRelation.getAdditionalInfo());
                 }
             }
         }
@@ -219,6 +222,18 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC
     }
 
     @Override
+    public RuleNode findRuleNodeById(RuleNodeId ruleNodeId) {
+        Validator.validateId(ruleNodeId, "Incorrect rule node id for search request.");
+        return ruleNodeDao.findById(ruleNodeId.getId());
+    }
+
+    @Override
+    public ListenableFuture<RuleChain> findRuleChainByIdAsync(RuleChainId ruleChainId) {
+        Validator.validateId(ruleChainId, "Incorrect rule chain id for search request.");
+        return ruleChainDao.findByIdAsync(ruleChainId.getId());
+    }
+
+    @Override
     public RuleChain getRootTenantRuleChain(TenantId tenantId) {
         Validator.validateId(tenantId, "Incorrect tenant id for search request.");
         List<EntityRelation> relations = relationService.findByFrom(tenantId, RelationTypeGroup.RULE_CHAIN);
@@ -301,7 +316,7 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC
 
     private void createRelation(EntityRelation relation) throws ExecutionException, InterruptedException {
         log.debug("Creating relation: {}", relation);
-        relationService.saveRelationAsync(relation).get();
+        relationService.saveRelation(relation);
     }
 
     private DataValidator<RuleChain> ruleChainValidator =
@@ -318,7 +333,7 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC
                         }
                         if (ruleChain.isRoot()) {
                             RuleChain rootRuleChain = getRootTenantRuleChain(ruleChain.getTenantId());
-                            if (ruleChain.getId() == null || !ruleChain.getId().equals(rootRuleChain.getId())) {
+                            if (rootRuleChain != null && !rootRuleChain.getId().equals(ruleChain.getId())) {
                                 throw new DataValidationException("Another root rule chain is present in scope of current tenant!");
                             }
                         }
diff --git a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleService.java b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleService.java
index f1df09e..fff3f6d 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleService.java
@@ -16,7 +16,6 @@
 package org.thingsboard.server.dao.rule;
 
 import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.node.ArrayNode;
 import com.google.common.util.concurrent.ListenableFuture;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.StringUtils;
@@ -67,67 +66,7 @@ public class BaseRuleService extends AbstractEntityService implements RuleServic
 
     @Override
     public RuleMetaData saveRule(RuleMetaData rule) {
-        ruleValidator.validate(rule);
-        if (rule.getTenantId() == null) {
-            log.trace("Save system rule metadata with predefined id {}", systemTenantId);
-            rule.setTenantId(systemTenantId);
-        }
-        if (rule.getId() != null) {
-            RuleMetaData oldVersion = ruleDao.findById(rule.getId());
-            if (rule.getState() == null) {
-                rule.setState(oldVersion.getState());
-            } else if (rule.getState() != oldVersion.getState()) {
-                throw new IncorrectParameterException("Use Activate/Suspend method to control state of the rule!");
-            }
-        } else {
-            if (rule.getState() == null) {
-                rule.setState(ComponentLifecycleState.SUSPENDED);
-            } else if (rule.getState() != ComponentLifecycleState.SUSPENDED) {
-                throw new IncorrectParameterException("Use Activate/Suspend method to control state of the rule!");
-            }
-        }
-
-        validateFilters(rule.getFilters());
-        if (rule.getProcessor() != null && !rule.getProcessor().isNull()) {
-            validateComponentJson(rule.getProcessor(), ComponentType.PROCESSOR);
-        }
-        if (rule.getAction() != null && !rule.getAction().isNull()) {
-            validateComponentJson(rule.getAction(), ComponentType.ACTION);
-        }
-        validateRuleAndPluginState(rule);
-        return ruleDao.save(rule);
-    }
-
-    private void validateFilters(JsonNode filtersJson) {
-        if (filtersJson == null || filtersJson.isNull()) {
-            throw new IncorrectParameterException("Rule filters are required!");
-        }
-        if (!filtersJson.isArray()) {
-            throw new IncorrectParameterException("Filters json is not an array!");
-        }
-        ArrayNode filtersArray = (ArrayNode) filtersJson;
-        for (int i = 0; i < filtersArray.size(); i++) {
-            validateComponentJson(filtersArray.get(i), ComponentType.FILTER);
-        }
-    }
-
-    private void validateComponentJson(JsonNode json, ComponentType type) {
-        if (json == null || json.isNull()) {
-            throw new IncorrectParameterException(type.name() + " is required!");
-        }
-        String clazz = getIfValid(type.name(), json, "clazz", JsonNode::isTextual, JsonNode::asText);
-        String name = getIfValid(type.name(), json, "name", JsonNode::isTextual, JsonNode::asText);
-        JsonNode configuration = getIfValid(type.name(), json, "configuration", JsonNode::isObject, node -> node);
-        ComponentDescriptor descriptor = componentDescriptorService.findByClazz(clazz);
-        if (descriptor == null) {
-            throw new IncorrectParameterException(type.name() + " clazz " + clazz + " is not a valid component!");
-        }
-        if (descriptor.getType() != type) {
-            throw new IncorrectParameterException("Clazz " + clazz + " is not a valid " + type.name() + " component!");
-        }
-        if (!componentDescriptorService.validate(descriptor, configuration)) {
-            throw new IncorrectParameterException(type.name() + " configuration is not valid!");
-        }
+        throw new RuntimeException("Not supported since v1.5!");
     }
 
     private void validateRuleAndPluginState(RuleMetaData rule) {
diff --git a/dao/src/main/java/org/thingsboard/server/dao/rule/RuleChainService.java b/dao/src/main/java/org/thingsboard/server/dao/rule/RuleChainService.java
index 6c44090..da7833d 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/rule/RuleChainService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/rule/RuleChainService.java
@@ -16,6 +16,7 @@
 
 package org.thingsboard.server.dao.rule;
 
+import com.google.common.util.concurrent.ListenableFuture;
 import org.thingsboard.server.common.data.id.RuleChainId;
 import org.thingsboard.server.common.data.id.RuleNodeId;
 import org.thingsboard.server.common.data.id.TenantId;
@@ -41,6 +42,10 @@ public interface RuleChainService {
 
     RuleChain findRuleChainById(RuleChainId ruleChainId);
 
+    RuleNode findRuleNodeById(RuleNodeId ruleNodeId);
+
+    ListenableFuture<RuleChain> findRuleChainByIdAsync(RuleChainId ruleChainId);
+
     RuleChain getRootTenantRuleChain(TenantId tenantId);
 
     List<RuleNode> getRuleChainNodes(RuleChainId ruleChainId);
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceRepository.java
index 3bab1c5..a48805b 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceRepository.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceRepository.java
@@ -79,4 +79,28 @@ public interface DeviceRepository extends CrudRepository<DeviceEntity, String> {
     List<DeviceEntity> findDevicesByTenantIdAndCustomerIdAndIdIn(String tenantId, String customerId, List<String> deviceIds);
 
     List<DeviceEntity> findDevicesByTenantIdAndIdIn(String tenantId, List<String> deviceIds);
+
+    @Query("SELECT d FROM DeviceEntity d WHERE d.tenantId = :tenantId AND d.lastConnectTs > :time")
+    List<DeviceEntity> findConnectOnlineByTenantId(@Param("tenantId") String tenantId, @Param("time") long time);
+
+    @Query("SELECT d FROM DeviceEntity d WHERE d.tenantId = :tenantId AND d.lastConnectTs < :time")
+    List<DeviceEntity> findConnectOfflineByTenantId(@Param("tenantId") String tenantId, @Param("time") long time);
+
+    @Query("SELECT d FROM DeviceEntity d WHERE d.tenantId = :tenantId AND d.lastUpdateTs > :time")
+    List<DeviceEntity> findUpdateOnlineByTenantId(@Param("tenantId") String tenantId, @Param("time") long time);
+
+    @Query("SELECT d FROM DeviceEntity d WHERE d.tenantId = :tenantId AND d.lastUpdateTs < :time")
+    List<DeviceEntity> findUpdateOfflineByTenantId(@Param("tenantId") String tenantId, @Param("time") long time);
+
+    @Query("SELECT d FROM DeviceEntity d WHERE d.tenantId = :tenantId AND d.lastConnectTs > :time AND d.type = :type")
+    List<DeviceEntity> findConnectOnlineByTenantIdAndType(@Param("tenantId") String tenantId, @Param("time") long time, @Param("type") String type);
+
+    @Query("SELECT d FROM DeviceEntity d WHERE d.tenantId = :tenantId AND d.lastConnectTs < :time AND d.type = :type")
+    List<DeviceEntity> findConnectOfflineByTenantIdAndType(@Param("tenantId") String tenantId, @Param("time") long time, @Param("type") String type);
+
+    @Query("SELECT d FROM DeviceEntity d WHERE d.tenantId = :tenantId AND d.lastUpdateTs > :time AND d.type = :type")
+    List<DeviceEntity> findUpdateOnlineByTenantIdAndType(@Param("tenantId") String tenantId, @Param("time") long time, @Param("type") String type);
+
+    @Query("SELECT d FROM DeviceEntity d WHERE d.tenantId = :tenantId AND d.lastUpdateTs < :time AND d.type = :type")
+    List<DeviceEntity> findUpdateOfflineByTenantIdAndType(@Param("tenantId") String tenantId, @Param("time") long time, @Param("type") String type);
 }
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceDao.java
index 4f3cd7d..baba659 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceDao.java
@@ -15,7 +15,9 @@
  */
 package org.thingsboard.server.dao.sql.device;
 
+import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.ListenableFuture;
+import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.data.domain.PageRequest;
 import org.springframework.data.repository.CrudRepository;
@@ -24,6 +26,7 @@ import org.thingsboard.server.common.data.Device;
 import org.thingsboard.server.common.data.EntitySubtype;
 import org.thingsboard.server.common.data.EntityType;
 import org.thingsboard.server.common.data.UUIDConverter;
+import org.thingsboard.server.common.data.device.DeviceStatusQuery;
 import org.thingsboard.server.common.data.id.TenantId;
 import org.thingsboard.server.common.data.page.TextPageLink;
 import org.thingsboard.server.dao.DaoUtil;
@@ -43,6 +46,7 @@ import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID_STR;
  */
 @Component
 @SqlDao
+@Slf4j
 public class JpaDeviceDao extends JpaAbstractSearchTextDao<DeviceEntity, Device> implements DeviceDao {
 
     @Autowired
@@ -124,6 +128,73 @@ public class JpaDeviceDao extends JpaAbstractSearchTextDao<DeviceEntity, Device>
         return service.submit(() -> convertTenantDeviceTypesToDto(tenantId, deviceRepository.findTenantDeviceTypes(fromTimeUUID(tenantId))));
     }
 
+    @Override
+    public ListenableFuture<List<Device>> findDevicesByTenantIdAndStatus(UUID tenantId, DeviceStatusQuery statusQuery) {
+        String strTenantId = fromTimeUUID(tenantId);
+        long minTime = System.currentTimeMillis() - statusQuery.getThreshold();
+        switch (statusQuery.getStatus()) {
+            case OFFLINE: {
+                switch (statusQuery.getContactType()) {
+                    case UPLOAD:
+                        return service.submit(() -> DaoUtil.convertDataList(deviceRepository.findUpdateOfflineByTenantId(strTenantId, minTime)));
+                    case CONNECT:
+                        return service.submit(() -> DaoUtil.convertDataList(deviceRepository.findConnectOfflineByTenantId(strTenantId, minTime)));
+                }
+                break;
+            }
+            case ONLINE: {
+                switch (statusQuery.getContactType()) {
+                    case UPLOAD:
+                        return service.submit(() -> DaoUtil.convertDataList(deviceRepository.findUpdateOnlineByTenantId(strTenantId, minTime)));
+                    case CONNECT:
+                        return service.submit(() -> DaoUtil.convertDataList(deviceRepository.findConnectOnlineByTenantId(strTenantId, minTime)));
+                }
+                break;
+            }
+        }
+
+        log.error("Could not build status query from [{}]", statusQuery);
+        throw new IllegalStateException("Could not build status query for device []");
+    }
+
+    @Override
+    public ListenableFuture<List<Device>> findDevicesByTenantIdTypeAndStatus(UUID tenantId, String type, DeviceStatusQuery statusQuery) {
+        String strTenantId = fromTimeUUID(tenantId);
+        long minTime = System.currentTimeMillis() - statusQuery.getThreshold();
+        switch (statusQuery.getStatus()) {
+            case OFFLINE: {
+                switch (statusQuery.getContactType()) {
+                    case UPLOAD:
+                        return service.submit(() -> DaoUtil.convertDataList(deviceRepository.findUpdateOfflineByTenantIdAndType(strTenantId, minTime, type)));
+                    case CONNECT:
+                        return service.submit(() -> DaoUtil.convertDataList(deviceRepository.findConnectOfflineByTenantIdAndType(strTenantId, minTime, type)));
+                }
+                break;
+            }
+            case ONLINE: {
+                switch (statusQuery.getContactType()) {
+                    case UPLOAD:
+                        return service.submit(() -> DaoUtil.convertDataList(deviceRepository.findUpdateOnlineByTenantIdAndType(strTenantId, minTime, type)));
+                    case CONNECT:
+                        return service.submit(() -> DaoUtil.convertDataList(deviceRepository.findConnectOnlineByTenantIdAndType(strTenantId, minTime, type)));
+                }
+                break;
+            }
+        }
+
+        log.error("Could not build status query from [{}]", statusQuery);
+        throw new IllegalStateException("Could not build status query for device []");
+    }
+
+    @Override
+    public void saveDeviceStatus(Device device) {
+        ListenableFuture<Device> future = service.submit(() -> save(device));
+        Futures.withFallback(future, t -> {
+            log.error("Can't update device status for [{}]", device, t);
+            throw new IllegalArgumentException("Can't update device status for {" + device + "}", t);
+        });
+    }
+
     private List<EntitySubtype> convertTenantDeviceTypesToDto(UUID tenantId, List<String> types) {
         List<EntitySubtype> list = Collections.emptyList();
         if (types != null && !types.isEmpty()) {
diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java
index d620e11..cda4b16 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java
@@ -73,7 +73,7 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem
 
     private PreparedStatement partitionInsertStmt;
     private PreparedStatement partitionInsertTtlStmt;
-    private PreparedStatement[] latestInsertStmts;
+    private PreparedStatement latestInsertStmt;
     private PreparedStatement[] saveStmts;
     private PreparedStatement[] saveTtlStmts;
     private PreparedStatement[] fetchStmts;
@@ -306,13 +306,15 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem
 
     @Override
     public ListenableFuture<Void> saveLatest(EntityId entityId, TsKvEntry tsKvEntry) {
-        DataType type = tsKvEntry.getDataType();
-        BoundStatement stmt = getLatestStmt(type).bind()
+        BoundStatement stmt = getLatestStmt().bind()
                 .setString(0, entityId.getEntityType().name())
                 .setUUID(1, entityId.getId())
                 .setString(2, tsKvEntry.getKey())
-                .setLong(3, tsKvEntry.getTs());
-        addValue(tsKvEntry, stmt, 4);
+                .setLong(3, tsKvEntry.getTs())
+                .set(4, tsKvEntry.getBooleanValue().orElse(null), Boolean.class)
+                .set(5, tsKvEntry.getStrValue().orElse(null), String.class)
+                .set(6, tsKvEntry.getLongValue().orElse(null), Long.class)
+                .set(7, tsKvEntry.getDoubleValue().orElse(null), Double.class);
         return getFuture(executeAsyncWrite(stmt), rs -> null);
     }
 
@@ -381,7 +383,7 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem
         if (saveStmts == null) {
             saveStmts = new PreparedStatement[DataType.values().length];
             for (DataType type : DataType.values()) {
-                saveStmts[type.ordinal()] = getSession().prepare(INSERT_INTO + ModelConstants.TS_KV_CF +
+                saveStmts[type.ordinal()] = prepare(INSERT_INTO + ModelConstants.TS_KV_CF +
                         "(" + ModelConstants.ENTITY_TYPE_COLUMN +
                         "," + ModelConstants.ENTITY_ID_COLUMN +
                         "," + ModelConstants.KEY_COLUMN +
@@ -398,7 +400,7 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem
         if (saveTtlStmts == null) {
             saveTtlStmts = new PreparedStatement[DataType.values().length];
             for (DataType type : DataType.values()) {
-                saveTtlStmts[type.ordinal()] = getSession().prepare(INSERT_INTO + ModelConstants.TS_KV_CF +
+                saveTtlStmts[type.ordinal()] = prepare(INSERT_INTO + ModelConstants.TS_KV_CF +
                         "(" + ModelConstants.ENTITY_TYPE_COLUMN +
                         "," + ModelConstants.ENTITY_ID_COLUMN +
                         "," + ModelConstants.KEY_COLUMN +
@@ -420,7 +422,7 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem
                 } else if (type == Aggregation.AVG && fetchStmts[Aggregation.SUM.ordinal()] != null) {
                     fetchStmts[type.ordinal()] = fetchStmts[Aggregation.SUM.ordinal()];
                 } else {
-                    fetchStmts[type.ordinal()] = getSession().prepare(SELECT_PREFIX +
+                    fetchStmts[type.ordinal()] = prepare(SELECT_PREFIX +
                             String.join(", ", ModelConstants.getFetchColumnNames(type)) + " FROM " + ModelConstants.TS_KV_CF
                             + " WHERE " + ModelConstants.ENTITY_TYPE_COLUMN + EQUALS_PARAM
                             + "AND " + ModelConstants.ENTITY_ID_COLUMN + EQUALS_PARAM
@@ -435,26 +437,26 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem
         return fetchStmts[aggType.ordinal()];
     }
 
-    private PreparedStatement getLatestStmt(DataType dataType) {
-        if (latestInsertStmts == null) {
-            latestInsertStmts = new PreparedStatement[DataType.values().length];
-            for (DataType type : DataType.values()) {
-                latestInsertStmts[type.ordinal()] = getSession().prepare(INSERT_INTO + ModelConstants.TS_KV_LATEST_CF +
-                        "(" + ModelConstants.ENTITY_TYPE_COLUMN +
-                        "," + ModelConstants.ENTITY_ID_COLUMN +
-                        "," + ModelConstants.KEY_COLUMN +
-                        "," + ModelConstants.TS_COLUMN +
-                        "," + getColumnName(type) + ")" +
-                        " VALUES(?, ?, ?, ?, ?)");
-            }
+    private PreparedStatement getLatestStmt() {
+        if (latestInsertStmt == null) {
+            latestInsertStmt = prepare(INSERT_INTO + ModelConstants.TS_KV_LATEST_CF +
+                    "(" + ModelConstants.ENTITY_TYPE_COLUMN +
+                    "," + ModelConstants.ENTITY_ID_COLUMN +
+                    "," + ModelConstants.KEY_COLUMN +
+                    "," + ModelConstants.TS_COLUMN +
+                    "," + ModelConstants.BOOLEAN_VALUE_COLUMN +
+                    "," + ModelConstants.STRING_VALUE_COLUMN +
+                    "," + ModelConstants.LONG_VALUE_COLUMN +
+                    "," + ModelConstants.DOUBLE_VALUE_COLUMN + ")" +
+                    " VALUES(?, ?, ?, ?, ?, ?, ?, ?)");
         }
-        return latestInsertStmts[dataType.ordinal()];
+        return latestInsertStmt;
     }
 
 
     private PreparedStatement getPartitionInsertStmt() {
         if (partitionInsertStmt == null) {
-            partitionInsertStmt = getSession().prepare(INSERT_INTO + ModelConstants.TS_KV_PARTITIONS_CF +
+            partitionInsertStmt = prepare(INSERT_INTO + ModelConstants.TS_KV_PARTITIONS_CF +
                     "(" + ModelConstants.ENTITY_TYPE_COLUMN +
                     "," + ModelConstants.ENTITY_ID_COLUMN +
                     "," + ModelConstants.PARTITION_COLUMN +
@@ -466,7 +468,7 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem
 
     private PreparedStatement getPartitionInsertTtlStmt() {
         if (partitionInsertTtlStmt == null) {
-            partitionInsertTtlStmt = getSession().prepare(INSERT_INTO + ModelConstants.TS_KV_PARTITIONS_CF +
+            partitionInsertTtlStmt = prepare(INSERT_INTO + ModelConstants.TS_KV_PARTITIONS_CF +
                     "(" + ModelConstants.ENTITY_TYPE_COLUMN +
                     "," + ModelConstants.ENTITY_ID_COLUMN +
                     "," + ModelConstants.PARTITION_COLUMN +
@@ -479,7 +481,7 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem
 
     private PreparedStatement getFindLatestStmt() {
         if (findLatestStmt == null) {
-            findLatestStmt = getSession().prepare(SELECT_PREFIX +
+            findLatestStmt = prepare(SELECT_PREFIX +
                     ModelConstants.KEY_COLUMN + "," +
                     ModelConstants.TS_COLUMN + "," +
                     ModelConstants.STRING_VALUE_COLUMN + "," +
@@ -496,7 +498,7 @@ public class CassandraBaseTimeseriesDao extends CassandraAbstractAsyncDao implem
 
     private PreparedStatement getFindAllLatestStmt() {
         if (findAllLatestStmt == null) {
-            findAllLatestStmt = getSession().prepare(SELECT_PREFIX +
+            findAllLatestStmt = prepare(SELECT_PREFIX +
                     ModelConstants.KEY_COLUMN + "," +
                     ModelConstants.TS_COLUMN + "," +
                     ModelConstants.STRING_VALUE_COLUMN + "," +
diff --git a/dao/src/main/java/org/thingsboard/server/dao/util/BufferedRateLimiter.java b/dao/src/main/java/org/thingsboard/server/dao/util/BufferedRateLimiter.java
new file mode 100644
index 0000000..0419668
--- /dev/null
+++ b/dao/src/main/java/org/thingsboard/server/dao/util/BufferedRateLimiter.java
@@ -0,0 +1,178 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.server.dao.util;
+
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.common.util.concurrent.MoreExecutors;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+import org.thingsboard.server.dao.exception.BufferLimitException;
+
+import java.util.concurrent.*;
+import java.util.concurrent.atomic.AtomicInteger;
+
+@Component
+@Slf4j
+@NoSqlDao
+public class BufferedRateLimiter implements AsyncRateLimiter {
+
+    private final ListeningExecutorService pool = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(10));
+
+    private final int permitsLimit;
+    private final int maxPermitWaitTime;
+    private final AtomicInteger permits;
+    private final BlockingQueue<LockedFuture> queue;
+
+    private final AtomicInteger maxQueueSize = new AtomicInteger();
+    private final AtomicInteger maxGrantedPermissions = new AtomicInteger();
+    private final AtomicInteger totalGranted = new AtomicInteger();
+    private final AtomicInteger totalReleased = new AtomicInteger();
+    private final AtomicInteger totalRequested = new AtomicInteger();
+
+    public BufferedRateLimiter(@Value("${cassandra.query.buffer_size}") int queueLimit,
+                               @Value("${cassandra.query.concurrent_limit}") int permitsLimit,
+                               @Value("${cassandra.query.permit_max_wait_time}") int maxPermitWaitTime) {
+        this.permitsLimit = permitsLimit;
+        this.maxPermitWaitTime = maxPermitWaitTime;
+        this.permits = new AtomicInteger();
+        this.queue = new LinkedBlockingQueue<>(queueLimit);
+    }
+
+    @Override
+    public ListenableFuture<Void> acquireAsync() {
+        totalRequested.incrementAndGet();
+        if (queue.isEmpty()) {
+            if (permits.incrementAndGet() <= permitsLimit) {
+                if (permits.get() > maxGrantedPermissions.get()) {
+                    maxGrantedPermissions.set(permits.get());
+                }
+                totalGranted.incrementAndGet();
+                return Futures.immediateFuture(null);
+            }
+            permits.decrementAndGet();
+        }
+
+        return putInQueue();
+    }
+
+    @Override
+    public void release() {
+        permits.decrementAndGet();
+        totalReleased.incrementAndGet();
+        reprocessQueue();
+    }
+
+    private void reprocessQueue() {
+        while (permits.get() < permitsLimit) {
+            if (permits.incrementAndGet() <= permitsLimit) {
+                if (permits.get() > maxGrantedPermissions.get()) {
+                    maxGrantedPermissions.set(permits.get());
+                }
+                LockedFuture lockedFuture = queue.poll();
+                if (lockedFuture != null) {
+                    totalGranted.incrementAndGet();
+                    lockedFuture.latch.countDown();
+                } else {
+                    permits.decrementAndGet();
+                    break;
+                }
+            } else {
+                permits.decrementAndGet();
+            }
+        }
+    }
+
+    private LockedFuture createLockedFuture() {
+        CountDownLatch latch = new CountDownLatch(1);
+        ListenableFuture<Void> future = pool.submit(() -> {
+            latch.await();
+            return null;
+        });
+        return new LockedFuture(latch, future, System.currentTimeMillis());
+    }
+
+    private ListenableFuture<Void> putInQueue() {
+
+        int size = queue.size();
+        if (size > maxQueueSize.get()) {
+            maxQueueSize.set(size);
+        }
+
+        if (queue.remainingCapacity() > 0) {
+            try {
+                LockedFuture lockedFuture = createLockedFuture();
+                if (!queue.offer(lockedFuture, 1, TimeUnit.SECONDS)) {
+                    lockedFuture.cancelFuture();
+                    return Futures.immediateFailedFuture(new BufferLimitException());
+                }
+                if(permits.get() < permitsLimit) {
+                    reprocessQueue();
+                }
+                if(permits.get() < permitsLimit) {
+                    reprocessQueue();
+                }
+                return lockedFuture.future;
+            } catch (InterruptedException e) {
+                return Futures.immediateFailedFuture(new BufferLimitException());
+            }
+        }
+        return Futures.immediateFailedFuture(new BufferLimitException());
+    }
+
+    @Scheduled(fixedDelayString = "${cassandra.query.rate_limit_print_interval_ms}")
+    public void printStats() {
+        int expiredCount = 0;
+        for (LockedFuture lockedFuture : queue) {
+            if (lockedFuture.isExpired()) {
+                lockedFuture.cancelFuture();
+                expiredCount++;
+            }
+        }
+        log.info("Permits maxBuffer [{}] maxPermits [{}] expired [{}] currPermits [{}] currBuffer [{}] " +
+                        "totalPermits [{}] totalRequests [{}] totalReleased [{}]",
+                maxQueueSize.getAndSet(0), maxGrantedPermissions.getAndSet(0), expiredCount,
+                permits.get(), queue.size(),
+                totalGranted.getAndSet(0), totalRequested.getAndSet(0), totalReleased.getAndSet(0));
+    }
+
+    private class LockedFuture {
+        final CountDownLatch latch;
+        final ListenableFuture<Void> future;
+        final long createTime;
+
+        public LockedFuture(CountDownLatch latch, ListenableFuture<Void> future, long createTime) {
+            this.latch = latch;
+            this.future = future;
+            this.createTime = createTime;
+        }
+
+        void cancelFuture() {
+            future.cancel(false);
+            latch.countDown();
+        }
+
+        boolean isExpired() {
+            return (System.currentTimeMillis() - createTime) > maxPermitWaitTime;
+        }
+
+    }
+
+
+}
diff --git a/dao/src/main/resources/cassandra/schema.cql b/dao/src/main/resources/cassandra/schema.cql
index c221f68..79e9655 100644
--- a/dao/src/main/resources/cassandra/schema.cql
+++ b/dao/src/main/resources/cassandra/schema.cql
@@ -159,6 +159,8 @@ CREATE TABLE IF NOT EXISTS thingsboard.device (
     type text,
     search_text text,
     additional_info text,
+    last_connect bigint,
+    last_update bigint,
     PRIMARY KEY (id, tenant_id, customer_id, type)
 );
 
@@ -669,6 +671,7 @@ CREATE TABLE IF NOT EXISTS  thingsboard.rule_chain (
     search_text text,
     first_rule_node_id uuid,
     root boolean,
+    debug_mode boolean,
     configuration text,
     additional_info text,
     PRIMARY KEY (id, tenant_id)
@@ -685,6 +688,7 @@ CREATE TABLE IF NOT EXISTS  thingsboard.rule_node (
     id uuid,
     type text,
     name text,
+    debug_mode boolean,
     search_text text,
     configuration text,
     additional_info text,
diff --git a/dao/src/main/resources/sql/schema.sql b/dao/src/main/resources/sql/schema.sql
index 106204a..08fe7fa 100644
--- a/dao/src/main/resources/sql/schema.sql
+++ b/dao/src/main/resources/sql/schema.sql
@@ -118,7 +118,9 @@ CREATE TABLE IF NOT EXISTS device (
     type varchar(255),
     name varchar(255),
     search_text varchar(255),
-    tenant_id varchar(31)
+    tenant_id varchar(31),
+    last_connect bigint,
+    last_update bigint
 );
 
 CREATE TABLE IF NOT EXISTS device_credentials (
@@ -263,6 +265,7 @@ CREATE TABLE IF NOT EXISTS rule_chain (
     name varchar(255),
     first_rule_node_id varchar(31),
     root boolean,
+    debug_mode boolean,
     search_text varchar(255),
     tenant_id varchar(31)
 );
@@ -273,5 +276,6 @@ CREATE TABLE IF NOT EXISTS rule_node (
     configuration varchar(10000000),
     type varchar(255),
     name varchar(255),
+    debug_mode boolean,
     search_text varchar(255)
 );
diff --git a/dao/src/test/java/org/thingsboard/server/dao/nosql/RateLimitedResultSetFutureTest.java b/dao/src/test/java/org/thingsboard/server/dao/nosql/RateLimitedResultSetFutureTest.java
new file mode 100644
index 0000000..f49668d
--- /dev/null
+++ b/dao/src/test/java/org/thingsboard/server/dao/nosql/RateLimitedResultSetFutureTest.java
@@ -0,0 +1,182 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.server.dao.nosql;
+
+import com.datastax.driver.core.*;
+import com.datastax.driver.core.exceptions.UnsupportedFeatureException;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.MoreExecutors;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.runners.MockitoJUnitRunner;
+import org.mockito.stubbing.Answer;
+import org.thingsboard.server.dao.exception.BufferLimitException;
+import org.thingsboard.server.dao.util.AsyncRateLimiter;
+
+import java.util.concurrent.*;
+
+import static org.junit.Assert.*;
+import static org.mockito.Mockito.*;
+
+@RunWith(MockitoJUnitRunner.class)
+public class RateLimitedResultSetFutureTest {
+
+    private RateLimitedResultSetFuture resultSetFuture;
+
+    @Mock
+    private AsyncRateLimiter rateLimiter;
+    @Mock
+    private Session session;
+    @Mock
+    private Statement statement;
+    @Mock
+    private ResultSetFuture realFuture;
+    @Mock
+    private ResultSet rows;
+    @Mock
+    private Row row;
+
+    @Test
+    public void doNotReleasePermissionIfRateLimitFutureFailed() throws InterruptedException {
+        when(rateLimiter.acquireAsync()).thenReturn(Futures.immediateFailedFuture(new BufferLimitException()));
+        resultSetFuture = new RateLimitedResultSetFuture(session, rateLimiter, statement);
+        Thread.sleep(1000L);
+        verify(rateLimiter).acquireAsync();
+        try {
+            assertTrue(resultSetFuture.isDone());
+            fail();
+        } catch (Exception e) {
+            assertTrue(e instanceof IllegalStateException);
+            Throwable actualCause = e.getCause();
+            assertTrue(actualCause instanceof ExecutionException);
+        }
+        verifyNoMoreInteractions(session, rateLimiter, statement);
+
+    }
+
+    @Test
+    public void getUninterruptiblyDelegateToCassandra() throws InterruptedException, ExecutionException {
+        when(rateLimiter.acquireAsync()).thenReturn(Futures.immediateFuture(null));
+        when(session.executeAsync(statement)).thenReturn(realFuture);
+        Mockito.doAnswer((Answer<Void>) invocation -> {
+            Object[] args = invocation.getArguments();
+            Runnable task = (Runnable) args[0];
+            task.run();
+            return null;
+        }).when(realFuture).addListener(Mockito.any(), Mockito.any());
+
+        when(realFuture.getUninterruptibly()).thenReturn(rows);
+
+        resultSetFuture = new RateLimitedResultSetFuture(session, rateLimiter, statement);
+        ResultSet actual = resultSetFuture.getUninterruptibly();
+        assertSame(rows, actual);
+        verify(rateLimiter, times(1)).acquireAsync();
+        verify(rateLimiter, times(1)).release();
+    }
+
+    @Test
+    public void addListenerAllowsFutureTransformation() throws InterruptedException, ExecutionException {
+        when(rateLimiter.acquireAsync()).thenReturn(Futures.immediateFuture(null));
+        when(session.executeAsync(statement)).thenReturn(realFuture);
+        Mockito.doAnswer((Answer<Void>) invocation -> {
+            Object[] args = invocation.getArguments();
+            Runnable task = (Runnable) args[0];
+            task.run();
+            return null;
+        }).when(realFuture).addListener(Mockito.any(), Mockito.any());
+
+        when(realFuture.get()).thenReturn(rows);
+        when(rows.one()).thenReturn(row);
+
+        resultSetFuture = new RateLimitedResultSetFuture(session, rateLimiter, statement);
+
+        ListenableFuture<Row> transform = Futures.transform(resultSetFuture, ResultSet::one);
+        Row actualRow = transform.get();
+
+        assertSame(row, actualRow);
+        verify(rateLimiter, times(1)).acquireAsync();
+        verify(rateLimiter, times(1)).release();
+    }
+
+    @Test
+    public void immidiateCassandraExceptionReturnsPermit() throws InterruptedException, ExecutionException {
+        when(rateLimiter.acquireAsync()).thenReturn(Futures.immediateFuture(null));
+        when(session.executeAsync(statement)).thenThrow(new UnsupportedFeatureException(ProtocolVersion.V3, "hjg"));
+        resultSetFuture = new RateLimitedResultSetFuture(session, rateLimiter, statement);
+        ListenableFuture<Row> transform = Futures.transform(resultSetFuture, ResultSet::one);
+        try {
+            transform.get();
+            fail();
+        } catch (Exception e) {
+            assertTrue(e instanceof ExecutionException);
+        }
+        verify(rateLimiter, times(1)).acquireAsync();
+        verify(rateLimiter, times(1)).release();
+    }
+
+    @Test
+    public void queryTimeoutReturnsPermit() throws InterruptedException, ExecutionException {
+        when(rateLimiter.acquireAsync()).thenReturn(Futures.immediateFuture(null));
+        when(session.executeAsync(statement)).thenReturn(realFuture);
+        Mockito.doAnswer((Answer<Void>) invocation -> {
+            Object[] args = invocation.getArguments();
+            Runnable task = (Runnable) args[0];
+            task.run();
+            return null;
+        }).when(realFuture).addListener(Mockito.any(), Mockito.any());
+
+        when(realFuture.get()).thenThrow(new ExecutionException("Fail", new TimeoutException("timeout")));
+        resultSetFuture = new RateLimitedResultSetFuture(session, rateLimiter, statement);
+        ListenableFuture<Row> transform = Futures.transform(resultSetFuture, ResultSet::one);
+        try {
+            transform.get();
+            fail();
+        } catch (Exception e) {
+            assertTrue(e instanceof ExecutionException);
+        }
+        verify(rateLimiter, times(1)).acquireAsync();
+        verify(rateLimiter, times(1)).release();
+    }
+
+    @Test
+    public void expiredQueryReturnPermit() throws InterruptedException, ExecutionException {
+        CountDownLatch latch = new CountDownLatch(1);
+        ListenableFuture<Void> future = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(1)).submit(() -> {
+            latch.await();
+            return null;
+        });
+        when(rateLimiter.acquireAsync()).thenReturn(future);
+        resultSetFuture = new RateLimitedResultSetFuture(session, rateLimiter, statement);
+
+        ListenableFuture<Row> transform = Futures.transform(resultSetFuture, ResultSet::one);
+//        TimeUnit.MILLISECONDS.sleep(200);
+        future.cancel(false);
+        latch.countDown();
+
+        try {
+            transform.get();
+            fail();
+        } catch (Exception e) {
+            assertTrue(e instanceof ExecutionException);
+        }
+        verify(rateLimiter, times(1)).acquireAsync();
+        verify(rateLimiter, times(1)).release();
+    }
+
+}
\ No newline at end of file
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java
index d083a90..44a1a09 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java
@@ -217,10 +217,10 @@ public abstract class AbstractServiceTest {
         ruleMetaData.setWeight(weight);
         ruleMetaData.setPluginToken(pluginToken);
 
-        ruleMetaData.setAction(createNode(ComponentScope.TENANT, ComponentType.ACTION,
+        ruleMetaData.setAction(createNode(ComponentScope.TENANT, ComponentType.OLD_ACTION,
                 "org.thingsboard.component.ActionTest", "TestJsonDescriptor.json", "TestJsonData.json"));
-        ruleMetaData.setProcessor(createNode(ComponentScope.TENANT, ComponentType.PROCESSOR,
-                "org.thingsboard.component.ProcessorTest", "TestJsonDescriptor.json", "TestJsonData.json"));
+//        ruleMetaData.setProcessor(createNode(ComponentScope.TENANT, ComponentType.PROCESSOR,
+//                "org.thingsboard.component.ProcessorTest", "TestJsonDescriptor.json", "TestJsonData.json"));
         ruleMetaData.setFilters(mapper.createArrayNode().add(
                 createNode(ComponentScope.TENANT, ComponentType.FILTER,
                         "org.thingsboard.component.FilterTest", "TestJsonDescriptor.json", "TestJsonData.json")
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/queue/cassandra/repository/impl/CassandraMsgRepositoryTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/queue/cassandra/repository/impl/CassandraMsgRepositoryTest.java
index d17e1f2..a766aa4 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/queue/cassandra/repository/impl/CassandraMsgRepositoryTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/queue/cassandra/repository/impl/CassandraMsgRepositoryTest.java
@@ -24,6 +24,7 @@ import org.junit.Test;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.thingsboard.server.common.data.id.DeviceId;
 import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.TbMsgDataType;
 import org.thingsboard.server.common.msg.TbMsgMetaData;
 import org.thingsboard.server.dao.service.AbstractServiceTest;
 import org.thingsboard.server.dao.service.DaoNoSqlTest;
@@ -44,7 +45,7 @@ public class CassandraMsgRepositoryTest extends AbstractServiceTest {
 
     @Test
     public void msgCanBeSavedAndRead() throws ExecutionException, InterruptedException {
-        TbMsg msg = new TbMsg(UUIDs.timeBased(), "type", new DeviceId(UUIDs.timeBased()), null, new byte[4]);
+        TbMsg msg = new TbMsg(UUIDs.timeBased(), "type", new DeviceId(UUIDs.timeBased()), null, TbMsgDataType.JSON, "0000");
         UUID nodeId = UUIDs.timeBased();
         ListenableFuture<Void> future = msgRepository.save(msg, nodeId, 1L, 1L, 1L);
         future.get();
@@ -54,7 +55,7 @@ public class CassandraMsgRepositoryTest extends AbstractServiceTest {
 
     @Test
     public void expiredMsgsAreNotReturned() throws ExecutionException, InterruptedException {
-        TbMsg msg = new TbMsg(UUIDs.timeBased(), "type", new DeviceId(UUIDs.timeBased()), null, new byte[4]);
+        TbMsg msg = new TbMsg(UUIDs.timeBased(), "type", new DeviceId(UUIDs.timeBased()), null, TbMsgDataType.JSON, "0000");
         UUID nodeId = UUIDs.timeBased();
         ListenableFuture<Void> future = msgRepository.save(msg, nodeId, 2L, 2L, 2L);
         future.get();
@@ -67,7 +68,7 @@ public class CassandraMsgRepositoryTest extends AbstractServiceTest {
         TbMsgMetaData metaData = new TbMsgMetaData();
         metaData.putValue("key", "value");
         String dataStr = "someContent";
-        TbMsg msg = new TbMsg(UUIDs.timeBased(), "type", new DeviceId(UUIDs.timeBased()), metaData, dataStr.getBytes());
+        TbMsg msg = new TbMsg(UUIDs.timeBased(), "type", new DeviceId(UUIDs.timeBased()), metaData, TbMsgDataType.JSON, dataStr);
         UUID nodeId = UUIDs.timeBased();
         ListenableFuture<Void> future = msgRepository.save(msg, nodeId, 1L, 1L, 1L);
         future.get();
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/queue/cassandra/UnprocessedMsgFilterTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/queue/cassandra/UnprocessedMsgFilterTest.java
index 6302e63..3935c9b 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/queue/cassandra/UnprocessedMsgFilterTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/queue/cassandra/UnprocessedMsgFilterTest.java
@@ -33,8 +33,8 @@ public class UnprocessedMsgFilterTest {
     public void acknowledgedMsgsAreFilteredOut() {
         UUID id1 = UUID.randomUUID();
         UUID id2 = UUID.randomUUID();
-        TbMsg msg1 = new TbMsg(id1, "T", null, null, null);
-        TbMsg msg2 = new TbMsg(id2, "T", null, null, null);
+        TbMsg msg1 = new TbMsg(id1, "T", null, null, null, null);
+        TbMsg msg2 = new TbMsg(id2, "T", null, null, null, null);
         List<TbMsg> msgs = Lists.newArrayList(msg1, msg2);
         List<MsgAck> acks = Lists.newArrayList(new MsgAck(id2, UUID.randomUUID(), 1L, 1L));
         Collection<TbMsg> actual = msgFilter.filter(msgs, acks);
diff --git a/dao/src/test/java/org/thingsboard/server/dao/util/BufferedRateLimiterTest.java b/dao/src/test/java/org/thingsboard/server/dao/util/BufferedRateLimiterTest.java
new file mode 100644
index 0000000..67c3ce8
--- /dev/null
+++ b/dao/src/test/java/org/thingsboard/server/dao/util/BufferedRateLimiterTest.java
@@ -0,0 +1,135 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.server.dao.util;
+
+import com.google.common.util.concurrent.*;
+import org.junit.Test;
+import org.thingsboard.server.dao.exception.BufferLimitException;
+
+import javax.annotation.Nullable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.junit.Assert.*;
+
+
+public class BufferedRateLimiterTest {
+
+    @Test
+    public void finishedFutureReturnedIfPermitsAreGranted() {
+        BufferedRateLimiter limiter = new BufferedRateLimiter(10, 10, 100);
+        ListenableFuture<Void> actual = limiter.acquireAsync();
+        assertTrue(actual.isDone());
+    }
+
+    @Test
+    public void notFinishedFutureReturnedIfPermitsAreNotGranted() {
+        BufferedRateLimiter limiter = new BufferedRateLimiter(10, 1, 100);
+        ListenableFuture<Void> actual1 = limiter.acquireAsync();
+        ListenableFuture<Void> actual2 = limiter.acquireAsync();
+        assertTrue(actual1.isDone());
+        assertFalse(actual2.isDone());
+    }
+
+    @Test
+    public void failedFutureReturnedIfQueueIsfull() {
+        BufferedRateLimiter limiter = new BufferedRateLimiter(1, 1, 100);
+        ListenableFuture<Void> actual1 = limiter.acquireAsync();
+        ListenableFuture<Void> actual2 = limiter.acquireAsync();
+        ListenableFuture<Void> actual3 = limiter.acquireAsync();
+
+        assertTrue(actual1.isDone());
+        assertFalse(actual2.isDone());
+        assertTrue(actual3.isDone());
+        try {
+            actual3.get();
+            fail();
+        } catch (Exception e) {
+            assertTrue(e instanceof ExecutionException);
+            Throwable actualCause = e.getCause();
+            assertTrue(actualCause instanceof BufferLimitException);
+            assertEquals("Rate Limit Buffer is full", actualCause.getMessage());
+        }
+    }
+
+    @Test
+    public void releasedPermitTriggerTasksFromQueue() throws InterruptedException {
+        BufferedRateLimiter limiter = new BufferedRateLimiter(10, 2, 100);
+        ListenableFuture<Void> actual1 = limiter.acquireAsync();
+        ListenableFuture<Void> actual2 = limiter.acquireAsync();
+        ListenableFuture<Void> actual3 = limiter.acquireAsync();
+        ListenableFuture<Void> actual4 = limiter.acquireAsync();
+        assertTrue(actual1.isDone());
+        assertTrue(actual2.isDone());
+        assertFalse(actual3.isDone());
+        assertFalse(actual4.isDone());
+        limiter.release();
+        TimeUnit.MILLISECONDS.sleep(100L);
+        assertTrue(actual3.isDone());
+        assertFalse(actual4.isDone());
+        limiter.release();
+        TimeUnit.MILLISECONDS.sleep(100L);
+        assertTrue(actual4.isDone());
+    }
+
+    @Test
+    public void permitsReleasedInConcurrentMode() throws InterruptedException {
+        BufferedRateLimiter limiter = new BufferedRateLimiter(10, 2, 100);
+        AtomicInteger actualReleased = new AtomicInteger();
+        AtomicInteger actualRejected = new AtomicInteger();
+        ListeningExecutorService pool = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(5));
+        for (int i = 0; i < 100; i++) {
+            ListenableFuture<ListenableFuture<Void>> submit = pool.submit(limiter::acquireAsync);
+            Futures.addCallback(submit, new FutureCallback<ListenableFuture<Void>>() {
+                @Override
+                public void onSuccess(@Nullable ListenableFuture<Void> result) {
+                    Futures.addCallback(result, new FutureCallback<Void>() {
+                        @Override
+                        public void onSuccess(@Nullable Void result) {
+                            try {
+                                TimeUnit.MILLISECONDS.sleep(100);
+                            } catch (InterruptedException e) {
+                                e.printStackTrace();
+                            }
+                            limiter.release();
+                            actualReleased.incrementAndGet();
+                        }
+
+                        @Override
+                        public void onFailure(Throwable t) {
+                            actualRejected.incrementAndGet();
+                        }
+                    });
+                }
+
+                @Override
+                public void onFailure(Throwable t) {
+                }
+            });
+        }
+
+        TimeUnit.SECONDS.sleep(2);
+        assertTrue("Unexpected released count " + actualReleased.get(),
+                actualReleased.get() > 10 && actualReleased.get() < 20);
+        assertTrue("Unexpected rejected count " + actualRejected.get(),
+                actualRejected.get() > 80 && actualRejected.get() < 90);
+
+    }
+
+
+}
\ No newline at end of file
diff --git a/dao/src/test/resources/cassandra-test.properties b/dao/src/test/resources/cassandra-test.properties
index 82fcbe1..737687f 100644
--- a/dao/src/test/resources/cassandra-test.properties
+++ b/dao/src/test/resources/cassandra-test.properties
@@ -47,3 +47,8 @@ cassandra.query.default_fetch_size=2000
 cassandra.query.ts_key_value_partitioning=HOURS
 
 cassandra.query.max_limit_per_request=1000
+cassandra.query.buffer_size=100000
+cassandra.query.concurrent_limit=1000
+cassandra.query.permit_max_wait_time=20000
+cassandra.query.rate_limit_print_interval_ms=30000
+
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/PluginConstants.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/PluginConstants.java
index ac9600a..d7438f4 100644
--- a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/PluginConstants.java
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/PluginConstants.java
@@ -19,5 +19,7 @@ package org.thingsboard.server.extensions.api.plugins;
  * @author Andrew Shvayka
  */
 public class PluginConstants {
+    public static final String TELEMETRY_URL_PREFIX = "/api/plugins/telemetry";
+    public static final String RPC_URL_PREFIX = "/api/plugins/rpc";
     public static final String PLUGIN_URL_PREFIX = "/api/plugins";
 }
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/AttributesSubscriptionCmd.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/AttributesSubscriptionCmd.java
index d116b2e..f30f1a5 100644
--- a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/AttributesSubscriptionCmd.java
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/AttributesSubscriptionCmd.java
@@ -16,7 +16,7 @@
 package org.thingsboard.server.extensions.core.plugin.telemetry.cmd;
 
 import lombok.NoArgsConstructor;
-import org.thingsboard.server.extensions.core.plugin.telemetry.sub.SubscriptionType;
+import org.thingsboard.server.extensions.core.plugin.telemetry.handlers.TelemetryFeature;
 
 /**
  * @author Andrew Shvayka
@@ -25,8 +25,8 @@ import org.thingsboard.server.extensions.core.plugin.telemetry.sub.SubscriptionT
 public class AttributesSubscriptionCmd extends SubscriptionCmd {
 
     @Override
-    public SubscriptionType getType() {
-        return SubscriptionType.ATTRIBUTES;
+    public TelemetryFeature getType() {
+        return TelemetryFeature.ATTRIBUTES;
     }
 
 }
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/SubscriptionCmd.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/SubscriptionCmd.java
index b06476a..7f78abd 100644
--- a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/SubscriptionCmd.java
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/SubscriptionCmd.java
@@ -18,7 +18,7 @@ package org.thingsboard.server.extensions.core.plugin.telemetry.cmd;
 import lombok.AllArgsConstructor;
 import lombok.Data;
 import lombok.NoArgsConstructor;
-import org.thingsboard.server.extensions.core.plugin.telemetry.sub.SubscriptionType;
+import org.thingsboard.server.extensions.core.plugin.telemetry.handlers.TelemetryFeature;
 
 @NoArgsConstructor
 @AllArgsConstructor
@@ -32,7 +32,7 @@ public abstract class SubscriptionCmd implements TelemetryPluginCmd {
     private String scope;
     private boolean unsubscribe;
 
-    public abstract SubscriptionType getType();
+    public abstract TelemetryFeature getType();
 
     @Override
     public String toString() {
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/TimeseriesSubscriptionCmd.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/TimeseriesSubscriptionCmd.java
index 4d64ca7..88ecb2d 100644
--- a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/TimeseriesSubscriptionCmd.java
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/TimeseriesSubscriptionCmd.java
@@ -18,7 +18,7 @@ package org.thingsboard.server.extensions.core.plugin.telemetry.cmd;
 import lombok.AllArgsConstructor;
 import lombok.Data;
 import lombok.NoArgsConstructor;
-import org.thingsboard.server.extensions.core.plugin.telemetry.sub.SubscriptionType;
+import org.thingsboard.server.extensions.core.plugin.telemetry.handlers.TelemetryFeature;
 
 /**
  * @author Andrew Shvayka
@@ -35,7 +35,7 @@ public class TimeseriesSubscriptionCmd extends SubscriptionCmd {
     private String agg;
 
     @Override
-    public SubscriptionType getType() {
-        return SubscriptionType.TIMESERIES;
+    public TelemetryFeature getType() {
+        return TelemetryFeature.TIMESERIES;
     }
 }
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRpcMsgHandler.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRpcMsgHandler.java
index c6e7a54..1acc29d 100644
--- a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRpcMsgHandler.java
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRpcMsgHandler.java
@@ -114,7 +114,7 @@ public class TelemetryRpcMsgHandler implements RpcMsgHandler {
         }
         Map<String, Long> statesMap = proto.getKeyStatesList().stream().collect(Collectors.toMap(SubscriptionKetStateProto::getKey, SubscriptionKetStateProto::getTs));
         Subscription subscription = new Subscription(
-                new SubscriptionState(proto.getSessionId(), proto.getSubscriptionId(), EntityIdFactory.getByTypeAndId(proto.getEntityType(), proto.getEntityId()), SubscriptionType.valueOf(proto.getType()), proto.getAllKeys(), statesMap, proto.getScope()),
+                new SubscriptionState(proto.getSessionId(), proto.getSubscriptionId(), EntityIdFactory.getByTypeAndId(proto.getEntityType(), proto.getEntityId()), TelemetryFeature.valueOf(proto.getType()), proto.getAllKeys(), statesMap, proto.getScope()),
                 false, msg.getServerAddress());
         subscriptionManager.addRemoteWsSubscription(ctx, msg.getServerAddress(), proto.getSessionId(), subscription);
     }
@@ -243,27 +243,19 @@ public class TelemetryRpcMsgHandler implements RpcMsgHandler {
         switch (attr.getDataType()) {
             case BOOLEAN:
                 Optional<Boolean> booleanValue = attr.getBooleanValue();
-                if (booleanValue.isPresent()) {
-                    dataBuilder.setBoolValue(booleanValue.get());
-                }
+                booleanValue.ifPresent(dataBuilder::setBoolValue);
                 break;
             case LONG:
                 Optional<Long> longValue = attr.getLongValue();
-                if (longValue.isPresent()) {
-                    dataBuilder.setLongValue(longValue.get());
-                }
+                longValue.ifPresent(dataBuilder::setLongValue);
                 break;
             case DOUBLE:
                 Optional<Double> doubleValue = attr.getDoubleValue();
-                if (doubleValue.isPresent()) {
-                    dataBuilder.setDoubleValue(doubleValue.get());
-                }
+                doubleValue.ifPresent(dataBuilder::setDoubleValue);
                 break;
             case STRING:
                 Optional<String> stringValue = attr.getStrValue();
-                if (stringValue.isPresent()) {
-                    dataBuilder.setStrValue(stringValue.get());
-                }
+                stringValue.ifPresent(dataBuilder::setStrValue);
                 break;
         }
         return dataBuilder;
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRuleMsgHandler.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRuleMsgHandler.java
index 4fdfe4a..9cb67fd 100644
--- a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRuleMsgHandler.java
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRuleMsgHandler.java
@@ -24,7 +24,11 @@ import org.thingsboard.server.common.data.kv.AttributeKvEntry;
 import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
 import org.thingsboard.server.common.data.kv.KvEntry;
 import org.thingsboard.server.common.data.kv.TsKvEntry;
-import org.thingsboard.server.common.msg.core.*;
+import org.thingsboard.server.common.msg.core.BasicGetAttributesResponse;
+import org.thingsboard.server.common.msg.core.BasicStatusCodeResponse;
+import org.thingsboard.server.common.msg.core.GetAttributesRequest;
+import org.thingsboard.server.common.msg.core.TelemetryUploadRequest;
+import org.thingsboard.server.common.msg.core.UpdateAttributesRequest;
 import org.thingsboard.server.common.msg.kv.BasicAttributeKVMsg;
 import org.thingsboard.server.extensions.api.plugins.PluginCallback;
 import org.thingsboard.server.extensions.api.plugins.PluginContext;
@@ -35,9 +39,13 @@ import org.thingsboard.server.extensions.api.plugins.msg.TelemetryUploadRequestR
 import org.thingsboard.server.extensions.api.plugins.msg.UpdateAttributesRequestRuleToPluginMsg;
 import org.thingsboard.server.extensions.core.plugin.telemetry.SubscriptionManager;
 import org.thingsboard.server.extensions.core.plugin.telemetry.sub.Subscription;
-import org.thingsboard.server.extensions.core.plugin.telemetry.sub.SubscriptionType;
 
-import java.util.*;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
 import java.util.stream.Collectors;
 
 @Slf4j
@@ -97,7 +105,7 @@ public class TelemetryRuleMsgHandler extends DefaultRuleMsgHandler {
             @Override
             public void onSuccess(PluginContext ctx, Void data) {
                 ctx.reply(new ResponsePluginToRuleMsg(msg.getUid(), tenantId, ruleId, BasicStatusCodeResponse.onSuccess(request.getMsgType(), request.getRequestId())));
-                subscriptionManager.onLocalSubscriptionUpdate(ctx, msg.getDeviceId(), SubscriptionType.TIMESERIES, s ->
+                subscriptionManager.onLocalSubscriptionUpdate(ctx, msg.getDeviceId(), TelemetryFeature.TIMESERIES, s ->
                     prepareSubscriptionUpdate(request, s)
                 );
             }
@@ -131,7 +139,7 @@ public class TelemetryRuleMsgHandler extends DefaultRuleMsgHandler {
                     public void onSuccess(PluginContext ctx, Void value) {
                         ctx.reply(new ResponsePluginToRuleMsg(msg.getUid(), tenantId, ruleId, BasicStatusCodeResponse.onSuccess(request.getMsgType(), request.getRequestId())));
 
-                        subscriptionManager.onLocalSubscriptionUpdate(ctx, msg.getDeviceId(), SubscriptionType.ATTRIBUTES, s -> {
+                        subscriptionManager.onLocalSubscriptionUpdate(ctx, msg.getDeviceId(), TelemetryFeature.ATTRIBUTES, s -> {
                             List<TsKvEntry> subscriptionUpdate = new ArrayList<>();
                             for (AttributeKvEntry kv : request.getAttributes()) {
                                 if (s.isAllKeys() || s.getKeyStates().containsKey(kv.getKey())) {
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryWebsocketMsgHandler.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryWebsocketMsgHandler.java
index 1374ef6..8c80e78 100644
--- a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryWebsocketMsgHandler.java
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryWebsocketMsgHandler.java
@@ -21,7 +21,12 @@ import org.springframework.util.StringUtils;
 import org.thingsboard.server.common.data.DataConstants;
 import org.thingsboard.server.common.data.id.EntityId;
 import org.thingsboard.server.common.data.id.EntityIdFactory;
-import org.thingsboard.server.common.data.kv.*;
+import org.thingsboard.server.common.data.kv.Aggregation;
+import org.thingsboard.server.common.data.kv.AttributeKvEntry;
+import org.thingsboard.server.common.data.kv.BaseTsKvQuery;
+import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
+import org.thingsboard.server.common.data.kv.TsKvEntry;
+import org.thingsboard.server.common.data.kv.TsKvQuery;
 import org.thingsboard.server.extensions.api.exception.UnauthorizedException;
 import org.thingsboard.server.extensions.api.plugins.PluginCallback;
 import org.thingsboard.server.extensions.api.plugins.PluginContext;
@@ -32,14 +37,26 @@ import org.thingsboard.server.extensions.api.plugins.ws.msg.BinaryPluginWebSocke
 import org.thingsboard.server.extensions.api.plugins.ws.msg.PluginWebsocketMsg;
 import org.thingsboard.server.extensions.api.plugins.ws.msg.TextPluginWebSocketMsg;
 import org.thingsboard.server.extensions.core.plugin.telemetry.SubscriptionManager;
-import org.thingsboard.server.extensions.core.plugin.telemetry.cmd.*;
+import org.thingsboard.server.extensions.core.plugin.telemetry.cmd.AttributesSubscriptionCmd;
+import org.thingsboard.server.extensions.core.plugin.telemetry.cmd.GetHistoryCmd;
+import org.thingsboard.server.extensions.core.plugin.telemetry.cmd.SubscriptionCmd;
+import org.thingsboard.server.extensions.core.plugin.telemetry.cmd.TelemetryPluginCmd;
+import org.thingsboard.server.extensions.core.plugin.telemetry.cmd.TelemetryPluginCmdsWrapper;
+import org.thingsboard.server.extensions.core.plugin.telemetry.cmd.TimeseriesSubscriptionCmd;
 import org.thingsboard.server.extensions.core.plugin.telemetry.sub.SubscriptionErrorCode;
 import org.thingsboard.server.extensions.core.plugin.telemetry.sub.SubscriptionState;
-import org.thingsboard.server.extensions.core.plugin.telemetry.sub.SubscriptionType;
 import org.thingsboard.server.extensions.core.plugin.telemetry.sub.SubscriptionUpdate;
 
 import java.io.IOException;
-import java.util.*;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
 import java.util.stream.Collectors;
 
 /**
@@ -131,7 +148,7 @@ public class TelemetryWebsocketMsgHandler extends DefaultWebsocketMsgHandler {
                 keys.forEach(key -> subState.put(key, 0L));
                 attributesData.forEach(v -> subState.put(v.getKey(), v.getTs()));
 
-                SubscriptionState sub = new SubscriptionState(sessionId, cmd.getCmdId(), entityId, SubscriptionType.ATTRIBUTES, false, subState, cmd.getScope());
+                SubscriptionState sub = new SubscriptionState(sessionId, cmd.getCmdId(), entityId, TelemetryFeature.ATTRIBUTES, false, subState, cmd.getScope());
                 subscriptionManager.addLocalWsSubscription(ctx, sessionId, entityId, sub);
             }
 
@@ -168,7 +185,7 @@ public class TelemetryWebsocketMsgHandler extends DefaultWebsocketMsgHandler {
                 Map<String, Long> subState = new HashMap<>(attributesData.size());
                 attributesData.forEach(v -> subState.put(v.getKey(), v.getTs()));
 
-                SubscriptionState sub = new SubscriptionState(sessionId, cmd.getCmdId(), entityId, SubscriptionType.ATTRIBUTES, true, subState, cmd.getScope());
+                SubscriptionState sub = new SubscriptionState(sessionId, cmd.getCmdId(), entityId, TelemetryFeature.ATTRIBUTES, true, subState, cmd.getScope());
                 subscriptionManager.addLocalWsSubscription(ctx, sessionId, entityId, sub);
             }
 
@@ -234,7 +251,7 @@ public class TelemetryWebsocketMsgHandler extends DefaultWebsocketMsgHandler {
                 sendWsMsg(ctx, sessionRef, new SubscriptionUpdate(cmd.getCmdId(), data));
                 Map<String, Long> subState = new HashMap<>(data.size());
                 data.forEach(v -> subState.put(v.getKey(), v.getTs()));
-                SubscriptionState sub = new SubscriptionState(sessionId, cmd.getCmdId(), entityId, SubscriptionType.TIMESERIES, true, subState, cmd.getScope());
+                SubscriptionState sub = new SubscriptionState(sessionId, cmd.getCmdId(), entityId, TelemetryFeature.TIMESERIES, true, subState, cmd.getScope());
                 subscriptionManager.addLocalWsSubscription(ctx, sessionId, entityId, sub);
             }
 
@@ -262,7 +279,7 @@ public class TelemetryWebsocketMsgHandler extends DefaultWebsocketMsgHandler {
                 Map<String, Long> subState = new HashMap<>(keys.size());
                 keys.forEach(key -> subState.put(key, startTs));
                 data.forEach(v -> subState.put(v.getKey(), v.getTs()));
-                SubscriptionState sub = new SubscriptionState(sessionId, cmd.getCmdId(), entityId, SubscriptionType.TIMESERIES, false, subState, cmd.getScope());
+                SubscriptionState sub = new SubscriptionState(sessionId, cmd.getCmdId(), entityId, TelemetryFeature.TIMESERIES, false, subState, cmd.getScope());
                 subscriptionManager.addLocalWsSubscription(ctx, sessionId, entityId, sub);
             }
 
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/Subscription.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/Subscription.java
index fc04713..98c7632 100644
--- a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/Subscription.java
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/Subscription.java
@@ -20,6 +20,7 @@ import lombok.Data;
 import org.thingsboard.server.common.data.id.DeviceId;
 import org.thingsboard.server.common.data.id.EntityId;
 import org.thingsboard.server.common.msg.cluster.ServerAddress;
+import org.thingsboard.server.extensions.core.plugin.telemetry.handlers.TelemetryFeature;
 
 import java.util.Map;
 
@@ -47,7 +48,7 @@ public class Subscription {
         return getSub().getEntityId();
     }
 
-    public SubscriptionType getType() {
+    public TelemetryFeature getType() {
         return getSub().getType();
     }
 
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/SubscriptionState.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/SubscriptionState.java
index c9598ef..e4a0d26 100644
--- a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/SubscriptionState.java
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/SubscriptionState.java
@@ -18,6 +18,7 @@ package org.thingsboard.server.extensions.core.plugin.telemetry.sub;
 import lombok.AllArgsConstructor;
 import lombok.Getter;
 import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.extensions.core.plugin.telemetry.handlers.TelemetryFeature;
 
 import java.util.Map;
 
@@ -30,7 +31,7 @@ public class SubscriptionState {
     @Getter private final String wsSessionId;
     @Getter private final int subscriptionId;
     @Getter private final EntityId entityId;
-    @Getter private final SubscriptionType type;
+    @Getter private final TelemetryFeature type;
     @Getter private final boolean allKeys;
     @Getter private final Map<String, Long> keyStates;
     @Getter private final String scope;
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/SubscriptionManager.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/SubscriptionManager.java
index ec00677..0c3b174 100644
--- a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/SubscriptionManager.java
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/SubscriptionManager.java
@@ -19,20 +19,30 @@ import lombok.Setter;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.util.StringUtils;
 import org.thingsboard.server.common.data.DataConstants;
-import org.thingsboard.server.common.data.id.DeviceId;
 import org.thingsboard.server.common.data.id.EntityId;
-import org.thingsboard.server.common.data.kv.*;
+import org.thingsboard.server.common.data.kv.AttributeKvEntry;
+import org.thingsboard.server.common.data.kv.BaseTsKvQuery;
+import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
+import org.thingsboard.server.common.data.kv.TsKvEntry;
+import org.thingsboard.server.common.data.kv.TsKvQuery;
 import org.thingsboard.server.common.msg.cluster.ServerAddress;
 import org.thingsboard.server.extensions.api.plugins.PluginCallback;
 import org.thingsboard.server.extensions.api.plugins.PluginContext;
+import org.thingsboard.server.extensions.core.plugin.telemetry.handlers.TelemetryFeature;
 import org.thingsboard.server.extensions.core.plugin.telemetry.handlers.TelemetryRpcMsgHandler;
 import org.thingsboard.server.extensions.core.plugin.telemetry.handlers.TelemetryWebsocketMsgHandler;
 import org.thingsboard.server.extensions.core.plugin.telemetry.sub.Subscription;
 import org.thingsboard.server.extensions.core.plugin.telemetry.sub.SubscriptionState;
-import org.thingsboard.server.extensions.core.plugin.telemetry.sub.SubscriptionType;
 import org.thingsboard.server.extensions.core.plugin.telemetry.sub.SubscriptionUpdate;
 
-import java.util.*;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
 import java.util.function.Function;
 import java.util.function.Predicate;
 
@@ -70,7 +80,7 @@ public class SubscriptionManager {
         EntityId entityId = subscription.getEntityId();
         log.trace("[{}] Registering remote subscription [{}] for device [{}] to [{}]", sessionId, subscription.getSubscriptionId(), entityId, address);
         registerSubscription(sessionId, entityId, subscription);
-        if (subscription.getType() == SubscriptionType.ATTRIBUTES) {
+        if (subscription.getType() == TelemetryFeature.ATTRIBUTES) {
             final Map<String, Long> keyStates = subscription.getKeyStates();
             ctx.loadAttributes(entityId, DataConstants.CLIENT_SCOPE, keyStates.keySet(), new PluginCallback<List<AttributeKvEntry>>() {
                 @Override
@@ -91,7 +101,7 @@ public class SubscriptionManager {
                     log.error("Failed to fetch missed updates.", e);
                 }
             });
-        } else if (subscription.getType() == SubscriptionType.TIMESERIES) {
+        } else if (subscription.getType() == TelemetryFeature.TIMESERIES) {
             long curTs = System.currentTimeMillis();
             List<TsKvQuery> queries = new ArrayList<>();
             subscription.getKeyStates().entrySet().forEach(e -> {
@@ -175,7 +185,7 @@ public class SubscriptionManager {
         }
     }
 
-    public void onLocalSubscriptionUpdate(PluginContext ctx, EntityId entityId, SubscriptionType type, Function<Subscription, List<TsKvEntry>> f) {
+    public void onLocalSubscriptionUpdate(PluginContext ctx, EntityId entityId, TelemetryFeature type, Function<Subscription, List<TsKvEntry>> f) {
         onLocalSubscriptionUpdate(ctx, entityId, s -> type == s.getType(), f);
     }
 
@@ -212,7 +222,7 @@ public class SubscriptionManager {
     public void onAttributesUpdateFromServer(PluginContext ctx, EntityId entityId, String scope, List<AttributeKvEntry> attributes) {
         Optional<ServerAddress> serverAddress = ctx.resolve(entityId);
         if (!serverAddress.isPresent()) {
-            onLocalSubscriptionUpdate(ctx, entityId, s -> SubscriptionType.ATTRIBUTES == s.getType() && (StringUtils.isEmpty(s.getScope()) || scope.equals(s.getScope())), s -> {
+            onLocalSubscriptionUpdate(ctx, entityId, s -> TelemetryFeature.ATTRIBUTES == s.getType() && (StringUtils.isEmpty(s.getScope()) || scope.equals(s.getScope())), s -> {
                 List<TsKvEntry> subscriptionUpdate = new ArrayList<TsKvEntry>();
                 for (AttributeKvEntry kv : attributes) {
                     if (s.isAllKeys() || s.getKeyStates().containsKey(kv.getKey())) {
@@ -229,7 +239,7 @@ public class SubscriptionManager {
     public void onTimeseriesUpdateFromServer(PluginContext ctx, EntityId entityId, List<TsKvEntry> entries) {
         Optional<ServerAddress> serverAddress = ctx.resolve(entityId);
         if (!serverAddress.isPresent()) {
-            onLocalSubscriptionUpdate(ctx, entityId, SubscriptionType.TIMESERIES, s -> {
+            onLocalSubscriptionUpdate(ctx, entityId, TelemetryFeature.TIMESERIES, s -> {
                 List<TsKvEntry> subscriptionUpdate = new ArrayList<TsKvEntry>();
                 for (TsKvEntry kv : entries) {
                     if (s.isAllKeys() || s.getKeyStates().containsKey((kv.getKey()))) {
@@ -344,9 +354,7 @@ public class SubscriptionManager {
     }
 
     private void checkSubsciptionsPrevAddress(Set<Subscription> subscriptions) {
-        Iterator<Subscription> subscriptionIterator = subscriptions.iterator();
-        while (subscriptionIterator.hasNext()) {
-            Subscription s = subscriptionIterator.next();
+        for (Subscription s : subscriptions) {
             if (s.isLocal()) {
                 if (s.getServer() != null) {
                     log.trace("[{}] Local subscription is no longer handled on remote server address [{}]", s.getWsSessionId(), s.getServer());

pom.xml 8(+7 -1)

diff --git a/pom.xml b/pom.xml
index f331e32..dc58627 100755
--- a/pom.xml
+++ b/pom.xml
@@ -41,7 +41,7 @@
         <logback.version>1.2.3</logback.version>
         <mockito.version>1.9.5</mockito.version>
         <rat.version>0.10</rat.version>
-        <cassandra.version>3.0.0</cassandra.version>
+        <cassandra.version>3.0.7</cassandra.version>
         <cassandra-unit.version>3.0.0.1</cassandra-unit.version>
         <takari-cpsuite.version>1.2.7</takari-cpsuite.version>
         <guava.version>18.0</guava.version>
@@ -284,6 +284,7 @@
                             <exclude>src/sh/**</exclude>
                             <exclude>src/main/scripts/control/**</exclude>
                             <exclude>src/main/scripts/windows/**</exclude>
+                            <exclude>src/main/resources/public/static/rulenode/**</exclude>
                         </excludes>
                         <mapping>
                             <proto>JAVADOC_STYLE</proto>
@@ -379,6 +380,11 @@
                 <version>${project.version}</version>
             </dependency>
             <dependency>
+                <groupId>org.thingsboard.rule-engine</groupId>
+                <artifactId>rule-engine-components</artifactId>
+                <version>${project.version}</version>
+            </dependency>
+            <dependency>
                 <groupId>org.thingsboard.common</groupId>
                 <artifactId>message</artifactId>
                 <version>${project.version}</version>
diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/ListeningExecutor.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/ListeningExecutor.java
new file mode 100644
index 0000000..9356be9
--- /dev/null
+++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/ListeningExecutor.java
@@ -0,0 +1,27 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.rule.engine.api;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.util.concurrent.Callable;
+import java.util.concurrent.Executor;
+
+public interface ListeningExecutor extends Executor {
+
+    <T> ListenableFuture<T> executeAsync(Callable<T> task);
+
+}
diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/NodeDefinition.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/NodeDefinition.java
new file mode 100644
index 0000000..18b2b94
--- /dev/null
+++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/NodeDefinition.java
@@ -0,0 +1,35 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.rule.engine.api;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import lombok.Data;
+
+@Data
+public class NodeDefinition {
+
+    private String details;
+    private String description;
+    private boolean inEnabled;
+    private boolean outEnabled;
+    String[] relationTypes;
+    boolean customRelations;
+    JsonNode defaultConfiguration;
+    String[] uiResources;
+    String configDirective;
+
+}
diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineTelemetryService.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineTelemetryService.java
new file mode 100644
index 0000000..1ba18cd
--- /dev/null
+++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineTelemetryService.java
@@ -0,0 +1,36 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.rule.engine.api;
+
+import com.google.common.util.concurrent.FutureCallback;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.kv.AttributeKvEntry;
+import org.thingsboard.server.common.data.kv.TsKvEntry;
+
+import java.util.List;
+
+/**
+ * Created by ashvayka on 02.04.18.
+ */
+public interface RuleEngineTelemetryService {
+
+    void saveAndNotify(EntityId entityId, List<TsKvEntry> ts, FutureCallback<Void> callback);
+
+    void saveAndNotify(EntityId entityId, List<TsKvEntry> ts, long ttl, FutureCallback<Void> callback);
+
+    void saveAndNotify(EntityId entityId, String scope, List<AttributeKvEntry> attributes, FutureCallback<Void> callback);
+
+}
diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleNode.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleNode.java
new file mode 100644
index 0000000..eea92ed
--- /dev/null
+++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleNode.java
@@ -0,0 +1,54 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.rule.engine.api;
+
+import org.thingsboard.server.common.data.plugin.ComponentScope;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+public @interface RuleNode {
+
+    ComponentType type();
+
+    String name();
+
+    String nodeDescription();
+
+    String nodeDetails();
+
+    Class<? extends NodeConfiguration> configClazz();
+
+    boolean inEnabled() default true;
+
+    boolean outEnabled() default true;
+
+    ComponentScope scope() default ComponentScope.TENANT;
+
+    String[] relationTypes() default {"Success", "Failure"};
+
+    String[] uiResources() default {};
+
+    String configDirective() default "";
+
+    boolean customRelations() default false;
+
+}
diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/ScriptEngine.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/ScriptEngine.java
new file mode 100644
index 0000000..1db046a
--- /dev/null
+++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/ScriptEngine.java
@@ -0,0 +1,40 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.rule.engine.api;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import org.thingsboard.server.common.msg.TbMsg;
+
+import javax.script.ScriptException;
+import java.util.Set;
+
+public interface ScriptEngine {
+
+    TbMsg executeUpdate(TbMsg msg) throws ScriptException;
+
+    TbMsg executeGenerate(TbMsg prevMsg) throws ScriptException;
+
+    boolean executeFilter(TbMsg msg) throws ScriptException;
+
+    Set<String> executeSwitch(TbMsg msg) throws ScriptException;
+
+    JsonNode executeJson(TbMsg msg) throws ScriptException;
+
+    String executeToString(TbMsg msg) throws ScriptException;
+
+    void destroy();
+
+}
diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java
index 07cd72c..6038e6d 100644
--- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java
+++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java
@@ -15,11 +15,23 @@
  */
 package org.thingsboard.rule.engine.api;
 
+import org.thingsboard.server.common.data.id.RuleNodeId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.rule.RuleNode;
 import org.thingsboard.server.common.msg.TbMsg;
 import org.thingsboard.server.common.msg.cluster.ServerAddress;
+import org.thingsboard.server.dao.alarm.AlarmService;
+import org.thingsboard.server.dao.asset.AssetService;
 import org.thingsboard.server.dao.attributes.AttributesService;
+import org.thingsboard.server.dao.customer.CustomerService;
+import org.thingsboard.server.dao.device.DeviceService;
+import org.thingsboard.server.dao.plugin.PluginService;
+import org.thingsboard.server.dao.relation.RelationService;
+import org.thingsboard.server.dao.rule.RuleChainService;
+import org.thingsboard.server.dao.timeseries.TimeseriesService;
+import org.thingsboard.server.dao.user.UserService;
 
-import java.util.UUID;
+import java.util.Set;
 
 /**
  * Created by ashvayka on 13.01.18.
@@ -30,6 +42,8 @@ public interface TbContext {
 
     void tellNext(TbMsg msg, String relationType);
 
+    void tellNext(TbMsg msg, Set<String> relationTypes);
+
     void tellSelf(TbMsg msg, long delayMs);
 
     void tellOthers(TbMsg msg);
@@ -38,8 +52,46 @@ public interface TbContext {
 
     void spawn(TbMsg msg);
 
-    void ack(UUID msg);
+    void ack(TbMsg msg);
+
+    void tellError(TbMsg msg, Throwable th);
+
+    void updateSelf(RuleNode self);
+
+    RuleNodeId getSelfId();
+
+    TenantId getTenantId();
 
     AttributesService getAttributesService();
 
+    CustomerService getCustomerService();
+
+    UserService getUserService();
+
+    PluginService getPluginService();
+
+    AssetService getAssetService();
+
+    DeviceService getDeviceService();
+
+    AlarmService getAlarmService();
+
+    RuleChainService getRuleChainService();
+
+    RuleEngineTelemetryService getTelemetryService();
+
+    TimeseriesService getTimeseriesService();
+
+    RelationService getRelationService();
+
+    ListeningExecutor getJsExecutor();
+
+    ListeningExecutor getMailExecutor();
+
+    ListeningExecutor getDbCallbackExecutor();
+
+    MailService getMailService();
+
+    ScriptEngine createJsScriptEngine(String script, String functionName, String... argNames);
+
 }
diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbNode.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbNode.java
index 89442bb..2555c99 100644
--- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbNode.java
+++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbNode.java
@@ -24,7 +24,7 @@ import java.util.concurrent.ExecutionException;
  */
 public interface TbNode {
 
-    void init(TbNodeConfiguration configuration, TbNodeState state) throws TbNodeException;
+    void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException;
 
     void onMsg(TbContext ctx, TbMsg msg) throws ExecutionException, InterruptedException, TbNodeException;
 
diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbNodeConfiguration.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbNodeConfiguration.java
index d06c0d2..64053cd 100644
--- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbNodeConfiguration.java
+++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbNodeConfiguration.java
@@ -22,8 +22,8 @@ import lombok.Data;
  * Created by ashvayka on 19.01.18.
  */
 @Data
-public class TbNodeConfiguration {
+public final class TbNodeConfiguration {
 
-    private JsonNode data;
+    private final JsonNode data;
 
 }
diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbNodeException.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbNodeException.java
index 6766999..b42ec8e 100644
--- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbNodeException.java
+++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbNodeException.java
@@ -22,6 +22,10 @@ import com.fasterxml.jackson.core.JsonProcessingException;
  */
 public class TbNodeException extends Exception {
 
+    public TbNodeException(String message) {
+        super(message);
+    }
+
     public TbNodeException(Exception e) {
         super(e);
     }
diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbNodeState.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbNodeState.java
index c48b11d..2c77a69 100644
--- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbNodeState.java
+++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbNodeState.java
@@ -18,5 +18,5 @@ package org.thingsboard.rule.engine.api;
 /**
  * Created by ashvayka on 19.01.18.
  */
-public class TbNodeState {
+public final class TbNodeState {
 }
diff --git a/rule-engine/rule-engine-components/pom.xml b/rule-engine/rule-engine-components/pom.xml
index 9b903b1..419b5f6 100644
--- a/rule-engine/rule-engine-components/pom.xml
+++ b/rule-engine/rule-engine-components/pom.xml
@@ -44,6 +44,11 @@
             <scope>provided</scope>
         </dependency>
         <dependency>
+            <groupId>org.thingsboard.common</groupId>
+            <artifactId>transport</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
             <groupId>ch.qos.logback</groupId>
             <artifactId>logback-core</artifactId>
             <scope>provided</scope>
@@ -67,6 +72,16 @@
             <artifactId>guava</artifactId>
         </dependency>
         <dependency>
+            <groupId>org.apache.velocity</groupId>
+            <artifactId>velocity</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.velocity</groupId>
+            <artifactId>velocity-tools</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
             <groupId>junit</groupId>
             <artifactId>junit</artifactId>
             <version>${junit.version}</version>
@@ -89,9 +104,8 @@
             <scope>test</scope>
         </dependency>
         <dependency>
-            <groupId>org.junit.jupiter</groupId>
-            <artifactId>junit-jupiter-api</artifactId>
-            <version>RELEASE</version>
+            <groupId>org.thingsboard.common</groupId>
+            <artifactId>transport</artifactId>
         </dependency>
 
         <!--<dependency>-->
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAlarmNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAlarmNode.java
new file mode 100644
index 0000000..0549e6a
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAlarmNode.java
@@ -0,0 +1,220 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ * <p>
+ * Licensed 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
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * 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.thingsboard.rule.engine.action;
+
+import com.datastax.driver.core.utils.UUIDs;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.base.Function;
+import com.google.common.util.concurrent.AsyncFunction;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.rule.engine.TbNodeUtils;
+import org.thingsboard.rule.engine.api.*;
+import org.thingsboard.server.common.data.alarm.Alarm;
+import org.thingsboard.server.common.data.alarm.AlarmStatus;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.TbMsgMetaData;
+
+import java.util.concurrent.ExecutorService;
+
+import static org.thingsboard.rule.engine.DonAsynchron.withCallback;
+
+@Slf4j
+@RuleNode(
+        type = ComponentType.ACTION,
+        name = "alarm", relationTypes = {"Created", "Updated", "Cleared", "False"},
+        configClazz = TbAlarmNodeConfiguration.class,
+        nodeDescription = "Create/Update/Clear Alarm",
+        nodeDetails = "isAlarm - JS function that verifies if Alarm should be CREATED for incoming message.\n" +
+                "isCleared - JS function that verifies if Alarm should be CLEARED for incoming message.\n" +
+                "Details - JS function that creates JSON object based on incoming message. This object will be added into Alarm.details field.\n" +
+                "Node output:\n" +
+                "If alarm was not created, original message is returned. Otherwise new Message returned with type 'ALARM', Alarm object in 'msg' property and 'matadata' will contains one of those properties 'isNewAlarm/isExistingAlarm/isClearedAlarm' " +
+                "Message payload can be accessed via <code>msg</code> property. For example <code>'temperature = ' + msg.temperature ;</code>" +
+                "Message metadata can be accessed via <code>metadata</code> property. For example <code>'name = ' + metadata.customerName;</code>",
+        uiResources = {"static/rulenode/rulenode-core-config.js"},
+        configDirective = "tbActionNodeAlarmConfig")
+
+public class TbAlarmNode implements TbNode {
+
+    static final String IS_NEW_ALARM = "isNewAlarm";
+    static final String IS_EXISTING_ALARM = "isExistingAlarm";
+    static final String IS_CLEARED_ALARM = "isClearedAlarm";
+
+    private final ObjectMapper mapper = new ObjectMapper();
+
+    private TbAlarmNodeConfiguration config;
+    private ScriptEngine createJsEngine;
+    private ScriptEngine clearJsEngine;
+    private ScriptEngine buildDetailsJsEngine;
+
+    @Override
+    public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
+        this.config = TbNodeUtils.convert(configuration, TbAlarmNodeConfiguration.class);
+        this.createJsEngine = ctx.createJsScriptEngine(config.getCreateConditionJs(), "isAlarm");
+        this.clearJsEngine = ctx.createJsScriptEngine(config.getClearConditionJs(), "isCleared");
+        this.buildDetailsJsEngine = ctx.createJsScriptEngine(config.getAlarmDetailsBuildJs(), "Details");
+    }
+
+    @Override
+    public void onMsg(TbContext ctx, TbMsg msg) {
+        ListeningExecutor jsExecutor = ctx.getJsExecutor();
+
+        ListenableFuture<Boolean> shouldCreate = jsExecutor.executeAsync(() -> createJsEngine.executeFilter(msg));
+        ListenableFuture<AlarmResult> transform = Futures.transform(shouldCreate, (AsyncFunction<Boolean, AlarmResult>) create -> {
+            if (create) {
+                return createOrUpdate(ctx, msg);
+            } else {
+                return checkForClearIfExist(ctx, msg);
+            }
+        }, ctx.getDbCallbackExecutor());
+
+        withCallback(transform,
+                alarmResult -> {
+                    if (alarmResult.alarm == null) {
+                        ctx.tellNext(msg, "False");
+                    } else if (alarmResult.isCreated) {
+                        ctx.tellNext(toAlarmMsg(alarmResult, msg), "Created");
+                    } else if (alarmResult.isUpdated) {
+                        ctx.tellNext(toAlarmMsg(alarmResult, msg), "Updated");
+                    } else if (alarmResult.isCleared) {
+                        ctx.tellNext(toAlarmMsg(alarmResult, msg), "Cleared");
+                    }
+                },
+                t -> ctx.tellError(msg, t));
+
+    }
+
+    private ListenableFuture<AlarmResult> createOrUpdate(TbContext ctx, TbMsg msg) {
+        ListenableFuture<Alarm> latest = ctx.getAlarmService().findLatestByOriginatorAndType(ctx.getTenantId(), msg.getOriginator(), config.getAlarmType());
+        return Futures.transform(latest, (AsyncFunction<Alarm, AlarmResult>) a -> {
+            if (a == null || a.getStatus().isCleared()) {
+                return createNewAlarm(ctx, msg);
+            } else {
+                return updateAlarm(ctx, msg, a);
+            }
+        }, ctx.getDbCallbackExecutor());
+    }
+
+    private ListenableFuture<AlarmResult> checkForClearIfExist(TbContext ctx, TbMsg msg) {
+        ListenableFuture<Alarm> latest = ctx.getAlarmService().findLatestByOriginatorAndType(ctx.getTenantId(), msg.getOriginator(), config.getAlarmType());
+        return Futures.transform(latest, (AsyncFunction<Alarm, AlarmResult>) a -> {
+            if (a != null && !a.getStatus().isCleared()) {
+                return clearAlarm(ctx, msg, a);
+            }
+            return Futures.immediateFuture(new AlarmResult(false, false, false, null));
+        }, ctx.getDbCallbackExecutor());
+    }
+
+    private ListenableFuture<AlarmResult> createNewAlarm(TbContext ctx, TbMsg msg) {
+        ListenableFuture<Alarm> asyncAlarm = Futures.transform(buildAlarmDetails(ctx, msg),
+                (Function<JsonNode, Alarm>) details -> buildAlarm(msg, details, ctx.getTenantId()));
+        ListenableFuture<Alarm> asyncCreated = Futures.transform(asyncAlarm,
+                (Function<Alarm, Alarm>) alarm -> ctx.getAlarmService().createOrUpdateAlarm(alarm), ctx.getDbCallbackExecutor());
+        return Futures.transform(asyncCreated, (Function<Alarm, AlarmResult>) alarm -> new AlarmResult(true, false, false, alarm));
+    }
+
+    private ListenableFuture<AlarmResult> updateAlarm(TbContext ctx, TbMsg msg, Alarm alarm) {
+        ListenableFuture<Alarm> asyncUpdated = Futures.transform(buildAlarmDetails(ctx, msg), (Function<JsonNode, Alarm>) details -> {
+            alarm.setSeverity(config.getSeverity());
+            alarm.setPropagate(config.isPropagate());
+            alarm.setDetails(details);
+            alarm.setEndTs(System.currentTimeMillis());
+            return ctx.getAlarmService().createOrUpdateAlarm(alarm);
+        }, ctx.getDbCallbackExecutor());
+
+        return Futures.transform(asyncUpdated, (Function<Alarm, AlarmResult>) a -> new AlarmResult(false, true, false, a));
+    }
+
+    private ListenableFuture<AlarmResult> clearAlarm(TbContext ctx, TbMsg msg, Alarm alarm) {
+        ListenableFuture<Boolean> shouldClear = ctx.getJsExecutor().executeAsync(() -> clearJsEngine.executeFilter(msg));
+        return Futures.transform(shouldClear, (AsyncFunction<Boolean, AlarmResult>) clear -> {
+            if (clear) {
+                ListenableFuture<Boolean> clearFuture = ctx.getAlarmService().clearAlarm(alarm.getId(), System.currentTimeMillis());
+                return Futures.transform(clearFuture, (Function<Boolean, AlarmResult>) cleared -> {
+                    alarm.setStatus(alarm.getStatus().isAck() ? AlarmStatus.CLEARED_ACK : AlarmStatus.CLEARED_UNACK);
+                    return new AlarmResult(false, false, true, alarm);
+                });
+            }
+            return Futures.immediateFuture(new AlarmResult(false, false, false, null));
+        });
+    }
+
+    private Alarm buildAlarm(TbMsg msg, JsonNode details, TenantId tenantId) {
+        return Alarm.builder()
+                .tenantId(tenantId)
+                .originator(msg.getOriginator())
+                .status(AlarmStatus.ACTIVE_UNACK)
+                .severity(config.getSeverity())
+                .propagate(config.isPropagate())
+                .type(config.getAlarmType())
+                //todo-vp: alarm date should be taken from Message or current Time should be used?
+//                .startTs(System.currentTimeMillis())
+//                .endTs(System.currentTimeMillis())
+                .details(details)
+                .build();
+    }
+
+    private ListenableFuture<JsonNode> buildAlarmDetails(TbContext ctx, TbMsg msg) {
+        return ctx.getJsExecutor().executeAsync(() -> buildDetailsJsEngine.executeJson(msg));
+    }
+
+    private TbMsg toAlarmMsg(AlarmResult alarmResult, TbMsg originalMsg) {
+        JsonNode jsonNodes = mapper.valueToTree(alarmResult.alarm);
+        String data = jsonNodes.toString();
+        TbMsgMetaData metaData = originalMsg.getMetaData().copy();
+        if (alarmResult.isCreated) {
+            metaData.putValue(IS_NEW_ALARM, Boolean.TRUE.toString());
+        } else if (alarmResult.isUpdated) {
+            metaData.putValue(IS_EXISTING_ALARM, Boolean.TRUE.toString());
+        } else if (alarmResult.isCleared) {
+            metaData.putValue(IS_CLEARED_ALARM, Boolean.TRUE.toString());
+        }
+        return new TbMsg(UUIDs.timeBased(), "ALARM", originalMsg.getOriginator(), metaData, data);
+    }
+
+
+    @Override
+    public void destroy() {
+        if (createJsEngine != null) {
+            createJsEngine.destroy();
+        }
+        if (clearJsEngine != null) {
+            clearJsEngine.destroy();
+        }
+        if (buildDetailsJsEngine != null) {
+            buildDetailsJsEngine.destroy();
+        }
+    }
+
+    private static class AlarmResult {
+        boolean isCreated;
+        boolean isUpdated;
+        boolean isCleared;
+        Alarm alarm;
+
+        AlarmResult(boolean isCreated, boolean isUpdated, boolean isCleared, Alarm alarm) {
+            this.isCreated = isCreated;
+            this.isUpdated = isUpdated;
+            this.isCleared = isCleared;
+            this.alarm = alarm;
+        }
+    }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAlarmNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAlarmNodeConfiguration.java
new file mode 100644
index 0000000..3575459
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAlarmNodeConfiguration.java
@@ -0,0 +1,44 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.rule.engine.action;
+
+import lombok.Data;
+import org.thingsboard.rule.engine.api.NodeConfiguration;
+import org.thingsboard.server.common.data.alarm.AlarmSeverity;
+
+@Data
+public class TbAlarmNodeConfiguration implements NodeConfiguration {
+
+    private String createConditionJs;
+    private String clearConditionJs;
+    private String alarmDetailsBuildJs;
+    private String alarmType;
+    private AlarmSeverity severity;
+    private boolean propagate;
+
+
+    @Override
+    public TbAlarmNodeConfiguration defaultConfiguration() {
+        TbAlarmNodeConfiguration configuration = new TbAlarmNodeConfiguration();
+        configuration.setCreateConditionJs("return 'incoming message = ' + msg + meta;");
+        configuration.setClearConditionJs("return 'incoming message = ' + msg + meta;");
+        configuration.setAlarmDetailsBuildJs("return 'incoming message = ' + msg + meta;");
+        configuration.setAlarmType("General Alarm");
+        configuration.setSeverity(AlarmSeverity.CRITICAL);
+        configuration.setPropagate(false);
+        return configuration;
+    }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbLogNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbLogNode.java
new file mode 100644
index 0000000..3cd299d
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbLogNode.java
@@ -0,0 +1,66 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.rule.engine.action;
+
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.rule.engine.TbNodeUtils;
+import org.thingsboard.rule.engine.api.*;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+import org.thingsboard.server.common.msg.TbMsg;
+
+import static org.thingsboard.rule.engine.DonAsynchron.withCallback;
+
+@Slf4j
+@RuleNode(
+        type = ComponentType.ACTION,
+        name = "log",
+        configClazz = TbLogNodeConfiguration.class,
+        nodeDescription = "Log incoming messages using JS script for transformation Message into String",
+        nodeDetails = "Transform incoming Message with configured JS condition to String and log final value. " +
+                "Message payload can be accessed via <code>msg</code> property. For example <code>'temperature = ' + msg.temperature ;</code>" +
+                "Message metadata can be accessed via <code>metadata</code> property. For example <code>'name = ' + metadata.customerName;</code>",
+        uiResources = {"static/rulenode/rulenode-core-config.js"},
+        configDirective = "tbActionNodeLogConfig")
+
+public class TbLogNode implements TbNode {
+
+    private TbLogNodeConfiguration config;
+    private ScriptEngine jsEngine;
+
+    @Override
+    public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
+        this.config = TbNodeUtils.convert(configuration, TbLogNodeConfiguration.class);
+        this.jsEngine = ctx.createJsScriptEngine(config.getJsScript(), "ToString");
+    }
+
+    @Override
+    public void onMsg(TbContext ctx, TbMsg msg) {
+        ListeningExecutor jsExecutor = ctx.getJsExecutor();
+        withCallback(jsExecutor.executeAsync(() -> jsEngine.executeToString(msg)),
+                toString -> {
+                    log.info(toString);
+                    ctx.tellNext(msg);
+                },
+                t -> ctx.tellError(msg, t));
+    }
+
+    @Override
+    public void destroy() {
+        if (jsEngine != null) {
+            jsEngine.destroy();
+        }
+    }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbLogNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbLogNodeConfiguration.java
new file mode 100644
index 0000000..aafb7f1
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbLogNodeConfiguration.java
@@ -0,0 +1,32 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.rule.engine.action;
+
+import lombok.Data;
+import org.thingsboard.rule.engine.api.NodeConfiguration;
+
+@Data
+public class TbLogNodeConfiguration implements NodeConfiguration {
+
+    private String jsScript;
+
+    @Override
+    public TbLogNodeConfiguration defaultConfiguration() {
+        TbLogNodeConfiguration configuration = new TbLogNodeConfiguration();
+        configuration.setJsScript("return 'incoming message = ' + msg + meta;");
+        return configuration;
+    }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/data/RelationsQuery.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/data/RelationsQuery.java
new file mode 100644
index 0000000..1d944ec
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/data/RelationsQuery.java
@@ -0,0 +1,31 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.rule.engine.data;
+
+import lombok.Data;
+import org.thingsboard.server.common.data.relation.EntitySearchDirection;
+import org.thingsboard.server.common.data.relation.EntityTypeFilter;
+
+import java.util.List;
+
+@Data
+public class RelationsQuery {
+
+    private EntitySearchDirection direction;
+    private int maxLevel = 1;
+    private List<EntityTypeFilter> filters;
+
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java
new file mode 100644
index 0000000..65b1371
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java
@@ -0,0 +1,104 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.rule.engine.debug;
+
+import com.datastax.driver.core.utils.UUIDs;
+import com.google.common.util.concurrent.ListenableFuture;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.util.StringUtils;
+import org.thingsboard.rule.engine.TbNodeUtils;
+import org.thingsboard.rule.engine.api.*;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.EntityIdFactory;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.TbMsgMetaData;
+
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+
+import static org.thingsboard.rule.engine.DonAsynchron.withCallback;
+
+@Slf4j
+@RuleNode(
+        type = ComponentType.ACTION,
+        name = "generator",
+        configClazz = TbMsgGeneratorNodeConfiguration.class,
+        nodeDescription = "Periodically generates messages",
+        nodeDetails = "Generates messages with configurable period. ",
+        inEnabled = false,
+        uiResources = {"static/rulenode/rulenode-core-config.js", "static/rulenode/rulenode-core-config.css"},
+        configDirective = "tbActionNodeGeneratorConfig"
+)
+
+public class TbMsgGeneratorNode implements TbNode {
+
+    public static final String TB_MSG_GENERATOR_NODE_MSG = "TbMsgGeneratorNodeMsg";
+
+    private TbMsgGeneratorNodeConfiguration config;
+    private ScriptEngine jsEngine;
+    private long delay;
+    private EntityId originatorId;
+    private UUID nextTickId;
+    private TbMsg prevMsg;
+
+    @Override
+    public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
+        this.config = TbNodeUtils.convert(configuration, TbMsgGeneratorNodeConfiguration.class);
+        this.delay = TimeUnit.SECONDS.toMillis(config.getPeriodInSeconds());
+        if (!StringUtils.isEmpty(config.getOriginatorId())) {
+            originatorId = EntityIdFactory.getByTypeAndUuid(config.getOriginatorType(), config.getOriginatorId());
+        } else {
+            originatorId = ctx.getSelfId();
+        }
+        this.jsEngine = ctx.createJsScriptEngine(config.getJsScript(), "Generate", "prevMsg", "prevMetadata", "prevMsgType");
+        sentTickMsg(ctx);
+    }
+
+    @Override
+    public void onMsg(TbContext ctx, TbMsg msg) {
+        if (msg.getType().equals(TB_MSG_GENERATOR_NODE_MSG) && msg.getId().equals(nextTickId)) {
+            withCallback(generate(ctx),
+                    m -> {ctx.tellNext(m); sentTickMsg(ctx);},
+                    t -> {ctx.tellError(msg, t); sentTickMsg(ctx);});
+        }
+    }
+
+    private void sentTickMsg(TbContext ctx) {
+        TbMsg tickMsg = new TbMsg(UUIDs.timeBased(), TB_MSG_GENERATOR_NODE_MSG, ctx.getSelfId(), new TbMsgMetaData(), "");
+        nextTickId = tickMsg.getId();
+        ctx.tellSelf(tickMsg, delay);
+    }
+
+    protected ListenableFuture<TbMsg> generate(TbContext ctx) {
+        return ctx.getJsExecutor().executeAsync(() -> {
+            if (prevMsg == null) {
+                prevMsg = new TbMsg(UUIDs.timeBased(), "", originatorId, new TbMsgMetaData(), "{}");
+            }
+            TbMsg generated = jsEngine.executeGenerate(prevMsg);
+            prevMsg = new TbMsg(UUIDs.timeBased(), generated.getType(), originatorId, generated.getMetaData(), generated.getData());
+            return prevMsg;
+        });
+    }
+
+    @Override
+    public void destroy() {
+        prevMsg = null;
+        if (jsEngine != null) {
+            jsEngine.destroy();
+        }
+    }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNodeConfiguration.java
new file mode 100644
index 0000000..c568e3d
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNodeConfiguration.java
@@ -0,0 +1,44 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.rule.engine.debug;
+
+import lombok.Data;
+import org.thingsboard.rule.engine.api.NodeConfiguration;
+import org.thingsboard.server.common.data.EntityType;
+
+import java.util.Map;
+
+@Data
+public class TbMsgGeneratorNodeConfiguration implements NodeConfiguration<TbMsgGeneratorNodeConfiguration> {
+
+    private int msgCount;
+    private int periodInSeconds;
+    private String originatorId;
+    private EntityType originatorType;
+    private String jsScript;
+
+    @Override
+    public TbMsgGeneratorNodeConfiguration defaultConfiguration() {
+        TbMsgGeneratorNodeConfiguration configuration = new TbMsgGeneratorNodeConfiguration();
+        configuration.setMsgCount(0);
+        configuration.setPeriodInSeconds(1);
+        configuration.setJsScript("var msg = { temp: 42, humidity: 77 };\n" +
+                "var metadata = { data: 40 };\n" +
+                "var msgType = \"DebugMsg\";\n\n" +
+                "return { msg: msg, metadata: metadata, msgType: msgType };");
+        return configuration;
+    }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNode.java
new file mode 100644
index 0000000..8ad344f
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNode.java
@@ -0,0 +1,65 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.rule.engine.filter;
+
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.rule.engine.TbNodeUtils;
+import org.thingsboard.rule.engine.api.*;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+import org.thingsboard.server.common.msg.TbMsg;
+
+import static org.thingsboard.rule.engine.DonAsynchron.withCallback;
+
+@Slf4j
+@RuleNode(
+        type = ComponentType.FILTER,
+        name = "script", relationTypes = {"True", "False"},
+        configClazz = TbJsFilterNodeConfiguration.class,
+        nodeDescription = "Filter incoming messages using JS script",
+        nodeDetails = "Evaluate incoming Message with configured JS condition. " +
+                "If <b>True</b> - send Message via <b>True</b> chain, otherwise <b>False</b> chain is used." +
+                "Message payload can be accessed via <code>msg</code> property. For example <code>msg.temperature < 10;</code><br/>" +
+                "Message metadata can be accessed via <code>metadata</code> property. For example <code>metadata.customerName === 'John';</code><br/>" +
+                "Message type can be accessed via <code>msgType</code> property.",
+        uiResources = {"static/rulenode/rulenode-core-config.js"},
+        configDirective = "tbFilterNodeScriptConfig")
+
+public class TbJsFilterNode implements TbNode {
+
+    private TbJsFilterNodeConfiguration config;
+    private ScriptEngine jsEngine;
+
+    @Override
+    public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
+        this.config = TbNodeUtils.convert(configuration, TbJsFilterNodeConfiguration.class);
+        this.jsEngine = ctx.createJsScriptEngine(config.getJsScript(), "Filter");
+    }
+
+    @Override
+    public void onMsg(TbContext ctx, TbMsg msg) {
+        ListeningExecutor jsExecutor = ctx.getJsExecutor();
+        withCallback(jsExecutor.executeAsync(() -> jsEngine.executeFilter(msg)),
+                filterResult -> ctx.tellNext(msg, Boolean.toString(filterResult)),
+                t -> ctx.tellError(msg, t));
+    }
+
+    @Override
+    public void destroy() {
+        if (jsEngine != null) {
+            jsEngine.destroy();
+        }
+    }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNodeConfiguration.java
new file mode 100644
index 0000000..9ab74e8
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNodeConfiguration.java
@@ -0,0 +1,32 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.rule.engine.filter;
+
+import lombok.Data;
+import org.thingsboard.rule.engine.api.NodeConfiguration;
+
+@Data
+public class TbJsFilterNodeConfiguration implements NodeConfiguration<TbJsFilterNodeConfiguration> {
+
+    private String jsScript;
+
+    @Override
+    public TbJsFilterNodeConfiguration defaultConfiguration() {
+        TbJsFilterNodeConfiguration configuration = new TbJsFilterNodeConfiguration();
+        configuration.setJsScript("return msg.passed < 15 && msg.name === 'Vit' && metadata.temp == 10 && msg.bigObj.prop == 42;");
+        return configuration;
+    }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNode.java
new file mode 100644
index 0000000..3c6704b
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNode.java
@@ -0,0 +1,70 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.rule.engine.filter;
+
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.rule.engine.TbNodeUtils;
+import org.thingsboard.rule.engine.api.*;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+import org.thingsboard.server.common.msg.TbMsg;
+
+import java.util.Set;
+
+import static org.thingsboard.rule.engine.DonAsynchron.withCallback;
+
+@Slf4j
+@RuleNode(
+        type = ComponentType.FILTER,
+        name = "switch", customRelations = true,
+        configClazz = TbJsSwitchNodeConfiguration.class,
+        nodeDescription = "Route incoming Message to one or multiple output chains",
+        nodeDetails = "Node executes configured JS script. Script should return array of next Chain names where Message should be routed. " +
+                "If Array is empty - message not routed to next Node. " +
+                "Message payload can be accessed via <code>msg</code> property. For example <code>msg.temperature < 10;</code><br/>" +
+                "Message metadata can be accessed via <code>metadata</code> property. For example <code>metadata.customerName === 'John';</code><br/>" +
+                "Message type can be accessed via <code>msgType</code> property.",
+        uiResources = {"static/rulenode/rulenode-core-config.js"},
+        configDirective = "tbFilterNodeSwitchConfig")
+public class TbJsSwitchNode implements TbNode {
+
+    private TbJsSwitchNodeConfiguration config;
+    private ScriptEngine jsEngine;
+
+    @Override
+    public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
+        this.config = TbNodeUtils.convert(configuration, TbJsSwitchNodeConfiguration.class);
+        this.jsEngine = ctx.createJsScriptEngine(config.getJsScript(), "Switch");
+    }
+
+    @Override
+    public void onMsg(TbContext ctx, TbMsg msg) {
+        ListeningExecutor jsExecutor = ctx.getJsExecutor();
+        withCallback(jsExecutor.executeAsync(() -> jsEngine.executeSwitch(msg)),
+                result -> processSwitch(ctx, msg, result),
+                t -> ctx.tellError(msg, t));
+    }
+
+    private void processSwitch(TbContext ctx, TbMsg msg, Set<String> nextRelations) {
+        ctx.tellNext(msg, nextRelations);
+    }
+
+   @Override
+    public void destroy() {
+        if (jsEngine != null) {
+            jsEngine.destroy();
+        }
+    }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNodeConfiguration.java
new file mode 100644
index 0000000..79b0912
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNodeConfiguration.java
@@ -0,0 +1,39 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.rule.engine.filter;
+
+import com.google.common.collect.Sets;
+import lombok.Data;
+import org.thingsboard.rule.engine.api.NodeConfiguration;
+
+import java.util.Set;
+
+@Data
+public class TbJsSwitchNodeConfiguration implements NodeConfiguration<TbJsSwitchNodeConfiguration> {
+
+    private String jsScript;
+
+    @Override
+    public TbJsSwitchNodeConfiguration defaultConfiguration() {
+        TbJsSwitchNodeConfiguration configuration = new TbJsSwitchNodeConfiguration();
+        configuration.setJsScript("function nextRelation(metadata, msg) {\n" +
+                "    return ['one','nine'];" +
+                "};\n" +
+                "\n" +
+                "return nextRelation(metadata, msg);");
+        return configuration;
+    }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNode.java
index 026da1b..225cd99 100644
--- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNode.java
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNode.java
@@ -18,18 +18,29 @@ package org.thingsboard.rule.engine.filter;
 import lombok.extern.slf4j.Slf4j;
 import org.thingsboard.rule.engine.TbNodeUtils;
 import org.thingsboard.rule.engine.api.*;
+import org.thingsboard.server.common.data.plugin.ComponentType;
 import org.thingsboard.server.common.msg.TbMsg;
 
 /**
  * Created by ashvayka on 19.01.18.
  */
 @Slf4j
+@RuleNode(
+        type = ComponentType.FILTER,
+        name = "message type",
+        configClazz = TbMsgTypeFilterNodeConfiguration.class,
+        relationTypes = {"True", "False"},
+        nodeDescription = "Filter incoming messages by Message Type",
+        nodeDetails = "Evaluate incoming Message with configured JS condition. " +
+                "If incoming MessageType is expected - send Message via <b>Success</b> chain, otherwise <b>Failure</b> chain is used.",
+        uiResources = {"static/rulenode/rulenode-core-config.js", "static/rulenode/rulenode-core-config.css"},
+        configDirective = "tbFilterNodeMessageTypeConfig")
 public class TbMsgTypeFilterNode implements TbNode {
 
     TbMsgTypeFilterNodeConfiguration config;
 
     @Override
-    public void init(TbNodeConfiguration configuration, TbNodeState state) throws TbNodeException {
+    public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
         this.config = TbNodeUtils.convert(configuration, TbMsgTypeFilterNodeConfiguration.class);
     }
 
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNodeConfiguration.java
index 3b7ba90..ae88aa8 100644
--- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNodeConfiguration.java
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNodeConfiguration.java
@@ -16,15 +16,24 @@
 package org.thingsboard.rule.engine.filter;
 
 import lombok.Data;
+import org.thingsboard.rule.engine.api.NodeConfiguration;
 
+import java.util.Arrays;
+import java.util.Collections;
 import java.util.List;
 
 /**
  * Created by ashvayka on 19.01.18.
  */
 @Data
-public class TbMsgTypeFilterNodeConfiguration {
+public class TbMsgTypeFilterNodeConfiguration implements NodeConfiguration<TbMsgTypeFilterNodeConfiguration> {
 
     private List<String> messageTypes;
 
+    @Override
+    public TbMsgTypeFilterNodeConfiguration defaultConfiguration() {
+        TbMsgTypeFilterNodeConfiguration configuration = new TbMsgTypeFilterNodeConfiguration();
+        configuration.setMessageTypes(Arrays.asList("POST_ATTRIBUTES","POST_TELEMETRY","RPC_REQUEST"));
+        return configuration;
+    }
 }
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/EmailPojo.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/EmailPojo.java
new file mode 100644
index 0000000..35eaa3b
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/EmailPojo.java
@@ -0,0 +1,32 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.rule.engine.mail;
+
+import lombok.Builder;
+import lombok.Data;
+
+@Data
+@Builder
+class EmailPojo {
+
+    private final String from;
+    private final String to;
+    private final String cc;
+    private final String bcc;
+    private final String subject;
+    private final String body;
+
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/RuleVelocityUtils.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/RuleVelocityUtils.java
new file mode 100644
index 0000000..7413cad
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/RuleVelocityUtils.java
@@ -0,0 +1,68 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.rule.engine.mail;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.velocity.Template;
+import org.apache.velocity.VelocityContext;
+import org.apache.velocity.runtime.RuntimeServices;
+import org.apache.velocity.runtime.RuntimeSingleton;
+import org.apache.velocity.runtime.parser.ParseException;
+import org.apache.velocity.runtime.parser.node.SimpleNode;
+import org.thingsboard.server.common.msg.TbMsg;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.util.Map;
+
+import static org.thingsboard.server.common.msg.TbMsgDataType.JSON;
+
+public class RuleVelocityUtils {
+
+    public static VelocityContext createContext(TbMsg msg) throws IOException {
+        VelocityContext context = new VelocityContext();
+        context.put("originator", msg.getOriginator());
+        context.put("type", msg.getType());
+        context.put("metadata", msg.getMetaData().values());
+        if (msg.getDataType() == JSON) {
+            Map map = new ObjectMapper().readValue(msg.getData(), Map.class);
+            context.put("msg", map);
+        } else {
+            context.put("msg", msg.getData());
+        }
+        return context;
+    }
+
+    public static String merge(Template template, VelocityContext context) {
+        StringWriter writer = new StringWriter();
+        template.merge(context, writer);
+        return writer.toString();
+    }
+
+    public static Template create(String source, String templateName) throws ParseException {
+        RuntimeServices runtimeServices = RuntimeSingleton.getRuntimeServices();
+        StringReader reader = new StringReader(source);
+        SimpleNode node = runtimeServices.parse(reader, templateName);
+        Template template = new Template();
+        template.setRuntimeServices(runtimeServices);
+        template.setData(node);
+        template.initDocument();
+        return template;
+    }
+
+
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNode.java
new file mode 100644
index 0000000..cae2058
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNode.java
@@ -0,0 +1,116 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.rule.engine.mail;
+
+import com.datastax.driver.core.utils.UUIDs;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.velocity.Template;
+import org.apache.velocity.VelocityContext;
+import org.apache.velocity.runtime.parser.ParseException;
+import org.springframework.util.StringUtils;
+import org.thingsboard.rule.engine.TbNodeUtils;
+import org.thingsboard.rule.engine.api.*;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+import org.thingsboard.server.common.msg.TbMsg;
+
+import java.io.IOException;
+import java.util.Optional;
+
+import static org.thingsboard.rule.engine.mail.TbSendEmailNode.SEND_EMAIL_TYPE;
+
+@Slf4j
+@RuleNode(
+        type = ComponentType.TRANSFORMATION,
+        name = "to email",
+        configClazz = TbMsgToEmailNodeConfiguration.class,
+        nodeDescription = "Change Message Originator To Tenant/Customer/Related Entity",
+        nodeDetails = "Related Entity found using configured relation direction and Relation Type. " +
+                "If multiple Related Entities are found, only first Entity is used as new Originator, other entities are discarded. ",
+        uiResources = {"static/rulenode/rulenode-core-config.js"},
+        configDirective = "tbTransformationNodeToEmailConfig")
+public class TbMsgToEmailNode implements TbNode {
+
+    private static final ObjectMapper MAPPER = new ObjectMapper();
+
+    private TbMsgToEmailNodeConfiguration config;
+
+    private Optional<Template> fromTemplate;
+    private Optional<Template> toTemplate;
+    private Optional<Template> ccTemplate;
+    private Optional<Template> bccTemplate;
+    private Optional<Template> subjectTemplate;
+    private Optional<Template> bodyTemplate;
+
+    @Override
+    public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
+        this.config = TbNodeUtils.convert(configuration, TbMsgToEmailNodeConfiguration.class);
+        try {
+            fromTemplate = toTemplate(config.getFromTemplate(), "From Template");
+            toTemplate = toTemplate(config.getToTemplate(), "To Template");
+            ccTemplate = toTemplate(config.getCcTemplate(), "Cc Template");
+            bccTemplate = toTemplate(config.getBccTemplate(), "Bcc Template");
+            subjectTemplate = toTemplate(config.getSubjectTemplate(), "Subject Template");
+            bodyTemplate = toTemplate(config.getBodyTemplate(), "Body Template");
+        } catch (ParseException e) {
+            log.error("Failed to create templates based on provided configuration!", e);
+            throw new TbNodeException(e);
+        }
+    }
+
+    @Override
+    public void onMsg(TbContext ctx, TbMsg msg) {
+        try {
+            EmailPojo email = convert(msg);
+            TbMsg emailMsg = buildEmailMsg(msg, email);
+            ctx.tellNext(emailMsg);
+        } catch (Exception ex) {
+            log.warn("Can not convert message to email " + ex.getMessage());
+            ctx.tellError(msg, ex);
+        }
+    }
+
+    private TbMsg buildEmailMsg(TbMsg msg, EmailPojo email) throws JsonProcessingException {
+        String emailJson = MAPPER.writeValueAsString(email);
+        return new TbMsg(UUIDs.timeBased(), SEND_EMAIL_TYPE, msg.getOriginator(), msg.getMetaData().copy(), emailJson);
+    }
+
+    private EmailPojo convert(TbMsg msg) throws IOException {
+        EmailPojo.EmailPojoBuilder builder = EmailPojo.builder();
+        VelocityContext context = RuleVelocityUtils.createContext(msg);
+        fromTemplate.ifPresent(t -> builder.from(RuleVelocityUtils.merge(t, context)));
+        toTemplate.ifPresent(t -> builder.to(RuleVelocityUtils.merge(t, context)));
+        ccTemplate.ifPresent(t -> builder.cc(RuleVelocityUtils.merge(t, context)));
+        bccTemplate.ifPresent(t -> builder.bcc(RuleVelocityUtils.merge(t, context)));
+        subjectTemplate.ifPresent(t -> builder.subject(RuleVelocityUtils.merge(t, context)));
+        bodyTemplate.ifPresent(t -> builder.body(RuleVelocityUtils.merge(t, context)));
+        return builder.build();
+    }
+
+    private Optional<Template> toTemplate(String source, String name) throws ParseException {
+        if (!StringUtils.isEmpty(source)) {
+            return Optional.of(RuleVelocityUtils.create(source, name));
+        } else {
+            return Optional.empty();
+        }
+    }
+
+    @Override
+    public void destroy() {
+
+    }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNodeConfiguration.java
new file mode 100644
index 0000000..6b0aa58
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNodeConfiguration.java
@@ -0,0 +1,41 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.rule.engine.mail;
+
+import lombok.Data;
+import org.thingsboard.rule.engine.api.NodeConfiguration;
+
+@Data
+public class TbMsgToEmailNodeConfiguration implements NodeConfiguration {
+
+    private String fromTemplate;
+    private String toTemplate;
+    private String ccTemplate;
+    private String bccTemplate;
+    private String subjectTemplate;
+    private String bodyTemplate;
+
+    @Override
+    public TbMsgToEmailNodeConfiguration defaultConfiguration() {
+        TbMsgToEmailNodeConfiguration configuration = new TbMsgToEmailNodeConfiguration();
+        configuration.fromTemplate = "info@testmail.org";
+        configuration.toTemplate = "$metadata.userEmail";
+        configuration.subjectTemplate = "Device $deviceType temperature high";
+        configuration.bodyTemplate = "Device $metadata.deviceName has high temperature $msg.temp";
+
+        return configuration;
+    }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbSendEmailNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbSendEmailNode.java
new file mode 100644
index 0000000..407476b
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbSendEmailNode.java
@@ -0,0 +1,89 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.rule.engine.mail;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.thingsboard.rule.engine.TbNodeUtils;
+import org.thingsboard.rule.engine.api.*;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+import org.thingsboard.server.common.msg.TbMsg;
+
+import java.io.IOException;
+
+import static org.thingsboard.rule.engine.DonAsynchron.withCallback;
+
+@Slf4j
+@RuleNode(
+        type = ComponentType.ACTION,
+        name = "send email",
+        configClazz = TbSendEmailNodeConfiguration.class,
+        nodeDescription = "Log incoming messages using JS script for transformation Message into String",
+        nodeDetails = "Transform incoming Message with configured JS condition to String and log final value. " +
+                "Message payload can be accessed via <code>msg</code> property. For example <code>'temperature = ' + msg.temperature ;</code>" +
+                "Message metadata can be accessed via <code>metadata</code> property. For example <code>'name = ' + metadata.customerName;</code>")
+public class TbSendEmailNode implements TbNode {
+
+    static final String SEND_EMAIL_TYPE = "SEND_EMAIL";
+    private static final ObjectMapper MAPPER = new ObjectMapper();
+
+    private TbSendEmailNodeConfiguration config;
+
+    @Override
+    public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
+        this.config = TbNodeUtils.convert(configuration, TbSendEmailNodeConfiguration.class);
+    }
+
+    @Override
+    public void onMsg(TbContext ctx, TbMsg msg) {
+        try {
+            validateType(msg.getType());
+            EmailPojo email = getEmail(msg);
+            withCallback(ctx.getMailExecutor().executeAsync(() -> {
+                        ctx.getMailService().send(email.getFrom(), email.getTo(), email.getCc(),
+                                email.getBcc(), email.getSubject(), email.getBody());
+                        return null;
+                    }),
+                    ok -> ctx.tellNext(msg),
+                    fail -> ctx.tellError(msg, fail));
+        } catch (Exception ex) {
+            ctx.tellError(msg, ex);
+        }
+
+
+    }
+
+    private EmailPojo getEmail(TbMsg msg) throws IOException {
+        EmailPojo email = MAPPER.readValue(msg.getData(), EmailPojo.class);
+        if (StringUtils.isBlank(email.getTo())) {
+            throw new IllegalStateException("Email destination can not be blank [" + email.getTo() + "]");
+        }
+        return email;
+    }
+
+    private void validateType(String type) {
+        if (!SEND_EMAIL_TYPE.equals(type)) {
+            log.warn("Not expected msg type [{}] for SendEmail Node", type);
+            throw new IllegalStateException("Not expected msg type " + type + " for SendEmail Node");
+        }
+    }
+
+    @Override
+    public void destroy() {
+
+    }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbSendEmailNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbSendEmailNodeConfiguration.java
new file mode 100644
index 0000000..4768b7d
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbSendEmailNodeConfiguration.java
@@ -0,0 +1,32 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.rule.engine.mail;
+
+import lombok.Data;
+import org.thingsboard.rule.engine.api.NodeConfiguration;
+
+@Data
+public class TbSendEmailNodeConfiguration implements NodeConfiguration {
+
+    private String tmp;
+
+    @Override
+    public TbSendEmailNodeConfiguration defaultConfiguration() {
+        TbSendEmailNodeConfiguration configuration = new TbSendEmailNodeConfiguration();
+        configuration.tmp = "";
+        return configuration;
+    }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbEntityGetAttrNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbEntityGetAttrNode.java
new file mode 100644
index 0000000..3bb1cff
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbEntityGetAttrNode.java
@@ -0,0 +1,92 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.rule.engine.metadata;
+
+import com.google.common.base.Function;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import org.thingsboard.rule.engine.TbNodeUtils;
+import org.thingsboard.rule.engine.api.*;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.kv.AttributeKvEntry;
+import org.thingsboard.server.common.data.kv.KvEntry;
+import org.thingsboard.server.common.data.kv.TsKvEntry;
+import org.thingsboard.server.common.msg.TbMsg;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static org.thingsboard.rule.engine.DonAsynchron.withCallback;
+import static org.thingsboard.server.common.data.DataConstants.SERVER_SCOPE;
+
+public abstract class TbEntityGetAttrNode<T extends EntityId> implements TbNode {
+
+    private TbGetEntityAttrNodeConfiguration config;
+
+    @Override
+    public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
+        this.config = TbNodeUtils.convert(configuration, TbGetEntityAttrNodeConfiguration.class);
+    }
+
+    @Override
+    public void onMsg(TbContext ctx, TbMsg msg) {
+        try {
+            withCallback(
+                    findEntityAsync(ctx, msg.getOriginator()),
+                    entityId -> withCallback(
+                            config.isTelemetry() ? getLatestTelemetry(ctx, entityId) : getAttributesAsync(ctx, entityId),
+                            attributes -> putAttributesAndTell(ctx, msg, attributes),
+                            t -> ctx.tellError(msg, t)
+                    ),
+                    t -> ctx.tellError(msg, t));
+        } catch (Throwable th) {
+            ctx.tellError(msg, th);
+        }
+    }
+
+    private ListenableFuture<List<KvEntry>> getAttributesAsync(TbContext ctx, EntityId entityId) {
+        ListenableFuture<List<AttributeKvEntry>> latest = ctx.getAttributesService().find(entityId, SERVER_SCOPE, config.getAttrMapping().keySet());
+        return Futures.transform(latest, (Function<? super List<AttributeKvEntry>, ? extends List<KvEntry>>) l ->
+                l.stream().map(i -> (KvEntry) i).collect(Collectors.toList()));
+    }
+
+    private ListenableFuture<List<KvEntry>> getLatestTelemetry(TbContext ctx, EntityId entityId) {
+        ListenableFuture<List<TsKvEntry>> latest = ctx.getTimeseriesService().findLatest(entityId, config.getAttrMapping().keySet());
+        return Futures.transform(latest, (Function<? super List<TsKvEntry>, ? extends List<KvEntry>>) l ->
+                l.stream().map(i -> (KvEntry) i).collect(Collectors.toList()));
+    }
+
+
+    private void putAttributesAndTell(TbContext ctx, TbMsg msg, List<? extends KvEntry> attributes) {
+        attributes.forEach(r -> {
+            String attrName = config.getAttrMapping().get(r.getKey());
+            msg.getMetaData().putValue(attrName, r.getValueAsString());
+        });
+        ctx.tellNext(msg);
+    }
+
+    @Override
+    public void destroy() {
+
+    }
+
+    protected abstract ListenableFuture<T> findEntityAsync(TbContext ctx, EntityId originator);
+
+    public void setConfig(TbGetEntityAttrNodeConfiguration config) {
+        this.config = config;
+    }
+
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNode.java
index 11c644c..84cff22 100644
--- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNode.java
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNode.java
@@ -15,49 +15,82 @@
  */
 package org.thingsboard.rule.engine.metadata;
 
+import com.google.common.base.Function;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
 import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.collections.CollectionUtils;
 import org.thingsboard.rule.engine.TbNodeUtils;
 import org.thingsboard.rule.engine.api.*;
-import org.thingsboard.server.common.data.DataConstants;
 import org.thingsboard.server.common.data.kv.AttributeKvEntry;
+import org.thingsboard.server.common.data.kv.TsKvEntry;
+import org.thingsboard.server.common.data.plugin.ComponentType;
 import org.thingsboard.server.common.msg.TbMsg;
-import org.thingsboard.server.dao.attributes.AttributesService;
 
 import java.util.List;
 
+import static org.thingsboard.rule.engine.DonAsynchron.withCallback;
+import static org.thingsboard.server.common.data.DataConstants.*;
+
 /**
  * Created by ashvayka on 19.01.18.
  */
 @Slf4j
+@RuleNode(type = ComponentType.ENRICHMENT,
+          name = "originator attributes",
+          configClazz = TbGetAttributesNodeConfiguration.class,
+          nodeDescription = "Add Message Originator Attributes or Latest Telemetry into Message Metadata",
+          nodeDetails = "If Attributes enrichment configured, <b>CLIENT/SHARED/SERVER</b> attributes are added into Message metadata " +
+                "with specific prefix: <i>cs/shared/ss</i>. To access those attributes in other nodes this template can be used " +
+                "<code>metadata.cs_temperature</code> or <code>metadata.shared_limit</code> " +
+                "If Latest Telemetry enrichment configured, latest telemetry added into metadata without prefix.",
+        uiResources = {"static/rulenode/rulenode-core-config.js"},
+        configDirective = "tbEnrichmentNodeOriginatorAttributesConfig")
 public class TbGetAttributesNode implements TbNode {
 
-    TbGetAttributesNodeConfiguration config;
+    private TbGetAttributesNodeConfiguration config;
 
     @Override
-    public void init(TbNodeConfiguration configuration, TbNodeState state) throws TbNodeException {
+    public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
         this.config = TbNodeUtils.convert(configuration, TbGetAttributesNodeConfiguration.class);
     }
 
     @Override
     public void onMsg(TbContext ctx, TbMsg msg) throws TbNodeException {
-        try {
-            //TODO: refactor this to work async and fetch attributes from cache.
-            AttributesService service = ctx.getAttributesService();
-            fetchAttributes(msg, service, config.getClientAttributeNames(), DataConstants.CLIENT_SCOPE, "cs.");
-            fetchAttributes(msg, service, config.getServerAttributeNames(), DataConstants.SERVER_SCOPE, "ss.");
-            fetchAttributes(msg, service, config.getSharedAttributeNames(), DataConstants.SHARED_SCOPE, "shared.");
-            ctx.tellNext(msg);
-        } catch (Exception e) {
-            log.warn("[{}][{}] Failed to fetch attributes", msg.getOriginator(), msg.getId(), e);
-            throw new TbNodeException(e);
+        if (CollectionUtils.isNotEmpty(config.getLatestTsKeyNames())) {
+            withCallback(getLatestTelemetry(ctx, msg, config.getLatestTsKeyNames()),
+                    i -> ctx.tellNext(msg),
+                    t -> ctx.tellError(msg, t));
+        } else {
+            ListenableFuture<List<Void>> future = Futures.allAsList(
+                    putAttrAsync(ctx, msg, CLIENT_SCOPE, config.getClientAttributeNames(), "cs_"),
+                    putAttrAsync(ctx, msg, SHARED_SCOPE, config.getSharedAttributeNames(), "shared_"),
+                    putAttrAsync(ctx, msg, SERVER_SCOPE, config.getServerAttributeNames(), "ss_"));
+
+            withCallback(future, i -> ctx.tellNext(msg), t -> ctx.tellError(msg, t));
+        }
+    }
+
+    private ListenableFuture<Void> putAttrAsync(TbContext ctx, TbMsg msg, String scope, List<String> keys, String prefix) {
+        if (keys == null) {
+            return Futures.immediateFuture(null);
         }
+        ListenableFuture<List<AttributeKvEntry>> latest = ctx.getAttributesService().find(msg.getOriginator(), scope, keys);
+        return Futures.transform(latest, (Function<? super List<AttributeKvEntry>, Void>) l -> {
+            l.forEach(r -> msg.getMetaData().putValue(prefix + r.getKey(), r.getValueAsString()));
+            return null;
+        });
     }
 
-    private void fetchAttributes(TbMsg msg, AttributesService service, List<String> attributeNames, String scope, String prefix) throws InterruptedException, java.util.concurrent.ExecutionException {
-        if (attributeNames != null && attributeNames.isEmpty()) {
-            List<AttributeKvEntry> attributes = service.find(msg.getOriginator(), scope, attributeNames).get();
-            attributes.forEach(attr -> msg.getMetaData().putValue(prefix + attr.getKey(), attr.getValueAsString()));
+    private ListenableFuture<Void> getLatestTelemetry(TbContext ctx, TbMsg msg, List<String> keys) {
+        if (keys == null) {
+            return Futures.immediateFuture(null);
         }
+        ListenableFuture<List<TsKvEntry>> latest = ctx.getTimeseriesService().findLatest(msg.getOriginator(), keys);
+        return Futures.transform(latest, (Function<? super List<TsKvEntry>, Void>) l -> {
+            l.forEach(r -> msg.getMetaData().putValue(r.getKey(), r.getValueAsString()));
+            return null;
+        });
     }
 
     @Override
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNodeConfiguration.java
index b54edef..6cd2247 100644
--- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNodeConfiguration.java
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNodeConfiguration.java
@@ -16,17 +16,30 @@
 package org.thingsboard.rule.engine.metadata;
 
 import lombok.Data;
+import org.thingsboard.rule.engine.api.NodeConfiguration;
 
+import java.util.Collections;
 import java.util.List;
 
 /**
  * Created by ashvayka on 19.01.18.
  */
 @Data
-public class TbGetAttributesNodeConfiguration {
+public class TbGetAttributesNodeConfiguration implements NodeConfiguration<TbGetAttributesNodeConfiguration> {
 
     private List<String> clientAttributeNames;
     private List<String> sharedAttributeNames;
     private List<String> serverAttributeNames;
 
+    private List<String> latestTsKeyNames;
+
+    @Override
+    public TbGetAttributesNodeConfiguration defaultConfiguration() {
+        TbGetAttributesNodeConfiguration configuration = new TbGetAttributesNodeConfiguration();
+        configuration.setClientAttributeNames(Collections.emptyList());
+        configuration.setSharedAttributeNames(Collections.emptyList());
+        configuration.setServerAttributeNames(Collections.emptyList());
+        configuration.setLatestTsKeyNames(Collections.emptyList());
+        return configuration;
+    }
 }
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNode.java
new file mode 100644
index 0000000..b092bad
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNode.java
@@ -0,0 +1,43 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.rule.engine.metadata;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import org.thingsboard.rule.engine.api.RuleNode;
+import org.thingsboard.rule.engine.api.TbContext;
+import org.thingsboard.rule.engine.util.EntitiesCustomerIdAsyncLoader;
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+
+@RuleNode(
+        type = ComponentType.ENRICHMENT,
+        name="customer attributes",
+        configClazz = TbGetEntityAttrNodeConfiguration.class,
+        nodeDescription = "Add Originators Customer Attributes or Latest Telemetry into Message Metadata",
+        nodeDetails = "If Attributes enrichment configured, server scope attributes are added into Message metadata. " +
+                "To access those attributes in other nodes this template can be used " +
+                "<code>metadata.temperature</code>. If Latest Telemetry enrichment configured, latest telemetry added into metadata",
+        uiResources = {"static/rulenode/rulenode-core-config.js", "static/rulenode/rulenode-core-config.css"},
+        configDirective = "tbEnrichmentNodeCustomerAttributesConfig")
+public class TbGetCustomerAttributeNode extends TbEntityGetAttrNode<CustomerId> {
+
+    @Override
+    protected ListenableFuture<CustomerId> findEntityAsync(TbContext ctx, EntityId originator) {
+        return EntitiesCustomerIdAsyncLoader.findEntityIdAsync(ctx, originator);
+    }
+
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetEntityAttrNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetEntityAttrNodeConfiguration.java
new file mode 100644
index 0000000..0bcefae
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetEntityAttrNodeConfiguration.java
@@ -0,0 +1,40 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.rule.engine.metadata;
+
+import lombok.Data;
+import org.thingsboard.rule.engine.api.NodeConfiguration;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+
+@Data
+public class TbGetEntityAttrNodeConfiguration implements NodeConfiguration<TbGetEntityAttrNodeConfiguration> {
+
+    private Map<String, String> attrMapping;
+    private boolean isTelemetry = false;
+
+    @Override
+    public TbGetEntityAttrNodeConfiguration defaultConfiguration() {
+        TbGetEntityAttrNodeConfiguration configuration = new TbGetEntityAttrNodeConfiguration();
+        Map<String, String> attrMapping = new HashMap<>();
+        attrMapping.putIfAbsent("temperature", "tempo");
+        configuration.setAttrMapping(attrMapping);
+        configuration.setTelemetry(true);
+        return configuration;
+    }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttributeNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttributeNode.java
new file mode 100644
index 0000000..8f65c31
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttributeNode.java
@@ -0,0 +1,53 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.rule.engine.metadata;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import org.thingsboard.rule.engine.TbNodeUtils;
+import org.thingsboard.rule.engine.api.*;
+import org.thingsboard.rule.engine.util.EntitiesRelatedEntityIdAsyncLoader;
+
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+
+@RuleNode(
+        type = ComponentType.ENRICHMENT,
+        name="related attributes",
+        configClazz = TbGetRelatedAttrNodeConfiguration.class,
+        nodeDescription = "Add Originators Related Entity Attributes or Latest Telemetry into Message Metadata",
+        nodeDetails = "Related Entity found using configured relation direction and Relation Type. " +
+                "If multiple Related Entities are found, only first Entity is used for attributes enrichment, other entities are discarded. " +
+                "If Attributes enrichment configured, server scope attributes are added into Message metadata. " +
+                "To access those attributes in other nodes this template can be used " +
+                "<code>metadata.temperature</code>. If Latest Telemetry enrichment configured, latest telemetry added into metadata",
+        uiResources = {"static/rulenode/rulenode-core-config.js", "static/rulenode/rulenode-core-config.css"},
+        configDirective = "tbEnrichmentNodeRelatedAttributesConfig")
+
+public class TbGetRelatedAttributeNode extends TbEntityGetAttrNode<EntityId> {
+
+    private TbGetRelatedAttrNodeConfiguration config;
+
+    @Override
+    public void init(TbContext context, TbNodeConfiguration configuration) throws TbNodeException {
+        this.config = TbNodeUtils.convert(configuration, TbGetRelatedAttrNodeConfiguration.class);
+        setConfig(config);
+    }
+
+    @Override
+    protected ListenableFuture<EntityId> findEntityAsync(TbContext ctx, EntityId originator) {
+        return EntitiesRelatedEntityIdAsyncLoader.findEntityAsync(ctx, originator, config.getRelationsQuery());
+    }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttrNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttrNodeConfiguration.java
new file mode 100644
index 0000000..dccd878
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttrNodeConfiguration.java
@@ -0,0 +1,50 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.rule.engine.metadata;
+
+import lombok.Data;
+import org.thingsboard.rule.engine.data.RelationsQuery;
+import org.thingsboard.server.common.data.relation.EntityRelation;
+import org.thingsboard.server.common.data.relation.EntitySearchDirection;
+import org.thingsboard.server.common.data.relation.EntityTypeFilter;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+@Data
+public class TbGetRelatedAttrNodeConfiguration extends TbGetEntityAttrNodeConfiguration {
+
+    private RelationsQuery relationsQuery;
+
+    @Override
+    public TbGetRelatedAttrNodeConfiguration defaultConfiguration() {
+        TbGetRelatedAttrNodeConfiguration configuration = new TbGetRelatedAttrNodeConfiguration();
+        Map<String, String> attrMapping = new HashMap<>();
+        attrMapping.putIfAbsent("temperature", "tempo");
+        configuration.setAttrMapping(attrMapping);
+        configuration.setTelemetry(true);
+
+        RelationsQuery relationsQuery = new RelationsQuery();
+        relationsQuery.setDirection(EntitySearchDirection.FROM);
+        relationsQuery.setMaxLevel(1);
+        EntityTypeFilter entityTypeFilter = new EntityTypeFilter(EntityRelation.CONTAINS_TYPE, Collections.emptyList());
+        relationsQuery.setFilters(Collections.singletonList(entityTypeFilter));
+        configuration.setRelationsQuery(relationsQuery);
+
+        return configuration;
+    }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTenantAttributeNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTenantAttributeNode.java
new file mode 100644
index 0000000..f0d28d3
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTenantAttributeNode.java
@@ -0,0 +1,45 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.rule.engine.metadata;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.rule.engine.api.RuleNode;
+import org.thingsboard.rule.engine.api.TbContext;
+import org.thingsboard.rule.engine.util.EntitiesTenantIdAsyncLoader;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+
+@Slf4j
+@RuleNode(
+        type = ComponentType.ENRICHMENT,
+        name="tenant attributes",
+        configClazz = TbGetEntityAttrNodeConfiguration.class,
+        nodeDescription = "Add Originators Tenant Attributes or Latest Telemetry into Message Metadata",
+        nodeDetails = "If Attributes enrichment configured, server scope attributes are added into Message metadata. " +
+                "To access those attributes in other nodes this template can be used " +
+                "<code>metadata.temperature</code>. If Latest Telemetry enrichment configured, latest telemetry added into metadata",
+        uiResources = {"static/rulenode/rulenode-core-config.js", "static/rulenode/rulenode-core-config.css"},
+        configDirective = "tbEnrichmentNodeTenantAttributesConfig")
+public class TbGetTenantAttributeNode extends TbEntityGetAttrNode<TenantId> {
+
+    @Override
+    protected ListenableFuture<TenantId> findEntityAsync(TbContext ctx, EntityId originator) {
+        return EntitiesTenantIdAsyncLoader.findEntityIdAsync(ctx, originator);
+    }
+
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTelemetryNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTelemetryNode.java
new file mode 100644
index 0000000..0e6687f
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTelemetryNode.java
@@ -0,0 +1,89 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.rule.engine.telemetry;
+
+import com.google.gson.JsonParser;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.util.StringUtils;
+import org.thingsboard.rule.engine.TbNodeUtils;
+import org.thingsboard.rule.engine.api.RuleNode;
+import org.thingsboard.rule.engine.api.TbContext;
+import org.thingsboard.rule.engine.api.TbNode;
+import org.thingsboard.rule.engine.api.TbNodeConfiguration;
+import org.thingsboard.rule.engine.api.TbNodeException;
+import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
+import org.thingsboard.server.common.data.kv.KvEntry;
+import org.thingsboard.server.common.data.kv.TsKvEntry;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.core.TelemetryUploadRequest;
+import org.thingsboard.server.common.transport.adaptor.JsonConverter;
+
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+@Slf4j
+@RuleNode(
+        type = ComponentType.ACTION,
+        name = "save timeseries data",
+        configClazz = TbMsgTelemetryNodeConfiguration.class,
+        nodeDescription = "Saves timeseries data",
+        nodeDetails = "Saves timeseries telemetry data based on configurable TTL parameter. Expects messages with 'POST_TELEMETRY' message type",
+        uiResources = {"static/rulenode/rulenode-core-config.js", "static/rulenode/rulenode-core-config.css"},
+        configDirective = "tbActionNodeTelemetryConfig"
+)
+
+public class TbMsgTelemetryNode implements TbNode {
+
+    private TbMsgTelemetryNodeConfiguration config;
+
+    @Override
+    public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
+        this.config = TbNodeUtils.convert(configuration, TbMsgTelemetryNodeConfiguration.class);
+    }
+
+    @Override
+    public void onMsg(TbContext ctx, TbMsg msg) {
+        if (!msg.getType().equals("POST_TELEMETRY")) {
+            ctx.tellError(msg, new IllegalArgumentException("Unsupported msg type: " + msg.getType()));
+            return;
+        }
+
+        String src = msg.getData();
+        TelemetryUploadRequest telemetryUploadRequest = JsonConverter.convertToTelemetry(new JsonParser().parse(src));
+        Map<Long, List<KvEntry>> tsKvMap = telemetryUploadRequest.getData();
+        if (tsKvMap == null) {
+            ctx.tellError(msg, new IllegalArgumentException("Msg body us empty: " + src));
+            return;
+        }
+        List<TsKvEntry> tsKvEntryList = new ArrayList<>();
+        for (Map.Entry<Long, List<KvEntry>> tsKvEntry : tsKvMap.entrySet()) {
+            for (KvEntry kvEntry : tsKvEntry.getValue()) {
+                tsKvEntryList.add(new BasicTsKvEntry(tsKvEntry.getKey(), kvEntry));
+            }
+        }
+        String ttlValue = msg.getMetaData().getValue("TTL");
+        long ttl = !StringUtils.isEmpty(ttlValue) ? Long.valueOf(ttlValue) : config.getDefaultTTL();
+        ctx.getTelemetryService().saveAndNotify(msg.getOriginator(), tsKvEntryList, ttl, new TelemetryNodeCallback(ctx, msg));
+    }
+
+    @Override
+    public void destroy() {
+    }
+
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTelemetryNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTelemetryNodeConfiguration.java
new file mode 100644
index 0000000..5523926
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTelemetryNodeConfiguration.java
@@ -0,0 +1,34 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.rule.engine.telemetry;
+
+import lombok.Data;
+import org.thingsboard.rule.engine.api.NodeConfiguration;
+
+import java.util.Map;
+
+@Data
+public class TbMsgTelemetryNodeConfiguration implements NodeConfiguration<TbMsgTelemetryNodeConfiguration> {
+
+    private long defaultTTL;
+
+    @Override
+    public TbMsgTelemetryNodeConfiguration defaultConfiguration() {
+        TbMsgTelemetryNodeConfiguration configuration = new TbMsgTelemetryNodeConfiguration();
+        configuration.setDefaultTTL(0L);
+        return configuration;
+    }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TelemetryNodeCallback.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TelemetryNodeCallback.java
new file mode 100644
index 0000000..fab4942
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TelemetryNodeCallback.java
@@ -0,0 +1,42 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.rule.engine.telemetry;
+
+import com.google.common.util.concurrent.FutureCallback;
+import lombok.Data;
+import org.thingsboard.rule.engine.api.TbContext;
+import org.thingsboard.server.common.msg.TbMsg;
+
+import javax.annotation.Nullable;
+
+/**
+ * Created by ashvayka on 02.04.18.
+ */
+@Data
+class TelemetryNodeCallback implements FutureCallback<Void> {
+    private final TbContext ctx;
+    private final TbMsg msg;
+
+    @Override
+    public void onSuccess(@Nullable Void result) {
+        ctx.tellNext(msg);
+    }
+
+    @Override
+    public void onFailure(Throwable t) {
+        ctx.tellError(msg, t);
+    }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNode.java
new file mode 100644
index 0000000..05ad49c
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNode.java
@@ -0,0 +1,100 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.rule.engine.transform;
+
+import com.google.common.base.Function;
+import com.google.common.collect.Sets;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.thingsboard.rule.engine.TbNodeUtils;
+import org.thingsboard.rule.engine.api.*;
+import org.thingsboard.rule.engine.util.EntitiesCustomerIdAsyncLoader;
+import org.thingsboard.rule.engine.util.EntitiesRelatedEntityIdAsyncLoader;
+import org.thingsboard.rule.engine.util.EntitiesTenantIdAsyncLoader;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+import org.thingsboard.server.common.msg.TbMsg;
+
+import java.util.HashSet;
+
+@Slf4j
+@RuleNode(
+        type = ComponentType.TRANSFORMATION,
+        name="change originator",
+        configClazz = TbChangeOriginatorNodeConfiguration.class,
+        nodeDescription = "Change Message Originator To Tenant/Customer/Related Entity",
+        nodeDetails = "Related Entity found using configured relation direction and Relation Type. " +
+                "If multiple Related Entities are found, only first Entity is used as new Originator, other entities are discarded. ",
+        uiResources = {"static/rulenode/rulenode-core-config.js", "static/rulenode/rulenode-core-config.css"},
+        configDirective = "tbTransformationNodeChangeOriginatorConfig")
+public class TbChangeOriginatorNode extends TbAbstractTransformNode {
+
+    protected static final String CUSTOMER_SOURCE = "CUSTOMER";
+    protected static final String TENANT_SOURCE = "TENANT";
+    protected static final String RELATED_SOURCE = "RELATED";
+
+    private TbChangeOriginatorNodeConfiguration config;
+
+    @Override
+    public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
+        this.config = TbNodeUtils.convert(configuration, TbChangeOriginatorNodeConfiguration.class);
+        validateConfig(config);
+        setConfig(config);
+    }
+
+    @Override
+    protected ListenableFuture<TbMsg> transform(TbContext ctx, TbMsg msg) {
+        ListenableFuture<? extends EntityId> newOriginator = getNewOriginator(ctx, msg.getOriginator());
+        return Futures.transform(newOriginator, (Function<EntityId, TbMsg>) n -> new TbMsg(msg.getId(), msg.getType(), n, msg.getMetaData(), msg.getData()));
+    }
+
+    private ListenableFuture<? extends EntityId> getNewOriginator(TbContext ctx, EntityId original) {
+        switch (config.getOriginatorSource()) {
+            case CUSTOMER_SOURCE:
+                return EntitiesCustomerIdAsyncLoader.findEntityIdAsync(ctx, original);
+            case TENANT_SOURCE:
+                return EntitiesTenantIdAsyncLoader.findEntityIdAsync(ctx, original);
+            case RELATED_SOURCE:
+                return EntitiesRelatedEntityIdAsyncLoader.findEntityAsync(ctx, original, config.getRelationsQuery());
+            default:
+                return Futures.immediateFailedFuture(new IllegalStateException("Unexpected originator source " + config.getOriginatorSource()));
+        }
+    }
+
+    private void validateConfig(TbChangeOriginatorNodeConfiguration conf) {
+        HashSet<String> knownSources = Sets.newHashSet(CUSTOMER_SOURCE, TENANT_SOURCE, RELATED_SOURCE);
+        if (!knownSources.contains(conf.getOriginatorSource())) {
+            log.error("Unsupported source [{}] for TbChangeOriginatorNode", conf.getOriginatorSource());
+            throw new IllegalArgumentException("Unsupported source TbChangeOriginatorNode" + conf.getOriginatorSource());
+        }
+
+        if (conf.getOriginatorSource().equals(RELATED_SOURCE)) {
+            if (conf.getRelationsQuery() == null) {
+                log.error("Related source for TbChangeOriginatorNode should have relations query. Actual [{}]",
+                        conf.getRelationsQuery());
+                throw new IllegalArgumentException("Wrong config for RElated Source in TbChangeOriginatorNode" + conf.getOriginatorSource());
+            }
+        }
+
+    }
+
+    @Override
+    public void destroy() {
+
+    }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNodeConfiguration.java
new file mode 100644
index 0000000..7cd77bf
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNodeConfiguration.java
@@ -0,0 +1,49 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.rule.engine.transform;
+
+import lombok.Data;
+import org.thingsboard.rule.engine.api.NodeConfiguration;
+import org.thingsboard.rule.engine.data.RelationsQuery;
+import org.thingsboard.server.common.data.relation.EntityRelation;
+import org.thingsboard.server.common.data.relation.EntitySearchDirection;
+import org.thingsboard.server.common.data.relation.EntityTypeFilter;
+
+import java.util.Collections;
+
+@Data
+public class TbChangeOriginatorNodeConfiguration extends TbTransformNodeConfiguration implements NodeConfiguration {
+
+    private String originatorSource;
+
+    private RelationsQuery relationsQuery;
+
+    @Override
+    public TbChangeOriginatorNodeConfiguration defaultConfiguration() {
+        TbChangeOriginatorNodeConfiguration configuration = new TbChangeOriginatorNodeConfiguration();
+        configuration.setOriginatorSource(TbChangeOriginatorNode.CUSTOMER_SOURCE);
+
+        RelationsQuery relationsQuery = new RelationsQuery();
+        relationsQuery.setDirection(EntitySearchDirection.FROM);
+        relationsQuery.setMaxLevel(1);
+        EntityTypeFilter entityTypeFilter = new EntityTypeFilter(EntityRelation.CONTAINS_TYPE, Collections.emptyList());
+        relationsQuery.setFilters(Collections.singletonList(entityTypeFilter));
+        configuration.setRelationsQuery(relationsQuery);
+
+        configuration.setStartNewChain(false);
+        return configuration;
+    }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNode.java
new file mode 100644
index 0000000..bf0c9fe
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNode.java
@@ -0,0 +1,61 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.rule.engine.transform;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import org.thingsboard.rule.engine.TbNodeUtils;
+import org.thingsboard.rule.engine.api.*;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+import org.thingsboard.server.common.msg.TbMsg;
+
+@RuleNode(
+        type = ComponentType.TRANSFORMATION,
+        name = "script",
+        configClazz = TbTransformMsgNodeConfiguration.class,
+        nodeDescription = "Change Message payload, Metadata or Message type using JavaScript",
+        nodeDetails = "JavaScript function receive 3 input parameters.<br/> " +
+                "<code>metadata</code> - is a Message metadata.<br/>" +
+                "<code>msg</code> - is a Message payload.<br/>" +
+                "<code>msgType</code> - is a Message type.<br/>" +
+                "Should return the following structure:<br/>" +
+                "<code>{ msg: <i style=\"color: #666;\">new payload</i>,<br/>&nbsp&nbsp&nbspmetadata: <i style=\"color: #666;\">new metadata</i>,<br/>&nbsp&nbsp&nbspmsgType: <i style=\"color: #666;\">new msgType</i> }</code><br/>" +
+                "All fields in resulting object are optional and will be taken from original message if not specified.",
+        uiResources = {"static/rulenode/rulenode-core-config.js", "static/rulenode/rulenode-core-config.css"},
+        configDirective = "tbTransformationNodeScriptConfig")
+public class TbTransformMsgNode extends TbAbstractTransformNode {
+
+    private TbTransformMsgNodeConfiguration config;
+    private ScriptEngine jsEngine;
+
+    @Override
+    public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
+        this.config = TbNodeUtils.convert(configuration, TbTransformMsgNodeConfiguration.class);
+        this.jsEngine = ctx.createJsScriptEngine(config.getJsScript(), "Transform");
+        setConfig(config);
+    }
+
+    @Override
+    protected ListenableFuture<TbMsg> transform(TbContext ctx, TbMsg msg) {
+        return ctx.getJsExecutor().executeAsync(() -> jsEngine.executeUpdate(msg));
+    }
+
+    @Override
+    public void destroy() {
+        if (jsEngine != null) {
+            jsEngine.destroy();
+        }
+    }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNodeConfiguration.java
new file mode 100644
index 0000000..a710cf8
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNodeConfiguration.java
@@ -0,0 +1,33 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.rule.engine.transform;
+
+import lombok.Data;
+import org.thingsboard.rule.engine.api.NodeConfiguration;
+
+@Data
+public class TbTransformMsgNodeConfiguration extends TbTransformNodeConfiguration implements NodeConfiguration {
+
+    private String jsScript;
+
+    @Override
+    public TbTransformMsgNodeConfiguration defaultConfiguration() {
+        TbTransformMsgNodeConfiguration configuration = new TbTransformMsgNodeConfiguration();
+        configuration.setStartNewChain(false);
+        configuration.setJsScript("return {msg: msg, metadata: metadata, msgType: msgType};");
+        return configuration;
+    }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesCustomerIdAsyncLoader.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesCustomerIdAsyncLoader.java
new file mode 100644
index 0000000..67eb808
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesCustomerIdAsyncLoader.java
@@ -0,0 +1,50 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.rule.engine.util;
+
+import com.google.common.util.concurrent.AsyncFunction;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import org.thingsboard.rule.engine.api.TbContext;
+import org.thingsboard.rule.engine.api.TbNodeException;
+import org.thingsboard.server.common.data.HasCustomerId;
+import org.thingsboard.server.common.data.id.*;
+
+public class EntitiesCustomerIdAsyncLoader {
+
+
+    public static ListenableFuture<CustomerId> findEntityIdAsync(TbContext ctx, EntityId original) {
+
+        switch (original.getEntityType()) {
+            case CUSTOMER:
+                return Futures.immediateFuture((CustomerId) original);
+            case USER:
+                return getCustomerAsync(ctx.getUserService().findUserByIdAsync((UserId) original));
+            case ASSET:
+                return getCustomerAsync(ctx.getAssetService().findAssetByIdAsync((AssetId) original));
+            case DEVICE:
+                return getCustomerAsync(ctx.getDeviceService().findDeviceByIdAsync((DeviceId) original));
+            default:
+                return Futures.immediateFailedFuture(new TbNodeException("Unexpected original EntityType " + original));
+        }
+    }
+
+    private static <T extends HasCustomerId> ListenableFuture<CustomerId> getCustomerAsync(ListenableFuture<T> future) {
+        return Futures.transform(future, (AsyncFunction<HasCustomerId, CustomerId>) in -> {
+            return in != null ? Futures.immediateFuture(in.getCustomerId())
+                    : Futures.immediateFailedFuture(new IllegalStateException("Customer not found"));});
+    }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesRelatedEntityIdAsyncLoader.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesRelatedEntityIdAsyncLoader.java
new file mode 100644
index 0000000..08ce38e
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesRelatedEntityIdAsyncLoader.java
@@ -0,0 +1,60 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.rule.engine.util;
+
+import com.google.common.util.concurrent.AsyncFunction;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import org.apache.commons.collections.CollectionUtils;
+import org.thingsboard.rule.engine.api.TbContext;
+import org.thingsboard.rule.engine.data.RelationsQuery;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.relation.EntityRelation;
+import org.thingsboard.server.common.data.relation.EntityRelationsQuery;
+import org.thingsboard.server.common.data.relation.EntitySearchDirection;
+import org.thingsboard.server.common.data.relation.RelationsSearchParameters;
+import org.thingsboard.server.dao.relation.RelationService;
+
+import java.util.List;
+
+public class EntitiesRelatedEntityIdAsyncLoader {
+
+    public static ListenableFuture<EntityId> findEntityAsync(TbContext ctx, EntityId originator,
+                                                             RelationsQuery relationsQuery) {
+        RelationService relationService = ctx.getRelationService();
+        EntityRelationsQuery query = buildQuery(originator, relationsQuery);
+        ListenableFuture<List<EntityRelation>> asyncRelation = relationService.findByQuery(query);
+        if (relationsQuery.getDirection() == EntitySearchDirection.FROM) {
+            return Futures.transform(asyncRelation, (AsyncFunction<? super List<EntityRelation>, EntityId>)
+                    r -> CollectionUtils.isNotEmpty(r) ? Futures.immediateFuture(r.get(0).getTo())
+                            : Futures.immediateFailedFuture(new IllegalStateException("Relation not found")));
+        } else if (relationsQuery.getDirection() == EntitySearchDirection.TO) {
+            return Futures.transform(asyncRelation, (AsyncFunction<? super List<EntityRelation>, EntityId>)
+                    r -> CollectionUtils.isNotEmpty(r) ? Futures.immediateFuture(r.get(0).getFrom())
+                            : Futures.immediateFailedFuture(new IllegalStateException("Relation not found")));
+        }
+        return Futures.immediateFailedFuture(new IllegalStateException("Unknown direction"));
+    }
+
+    private static EntityRelationsQuery buildQuery(EntityId originator, RelationsQuery relationsQuery) {
+        EntityRelationsQuery query = new EntityRelationsQuery();
+        RelationsSearchParameters parameters = new RelationsSearchParameters(originator,
+                relationsQuery.getDirection(), relationsQuery.getMaxLevel());
+        query.setParameters(parameters);
+        query.setFilters(relationsQuery.getFilters());
+        return query;
+    }
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesTenantIdAsyncLoader.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesTenantIdAsyncLoader.java
new file mode 100644
index 0000000..5d2aaa8
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesTenantIdAsyncLoader.java
@@ -0,0 +1,58 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.rule.engine.util;
+
+import com.google.common.util.concurrent.AsyncFunction;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import org.thingsboard.rule.engine.api.TbContext;
+import org.thingsboard.rule.engine.api.TbNodeException;
+import org.thingsboard.server.common.data.HasTenantId;
+import org.thingsboard.server.common.data.alarm.AlarmId;
+import org.thingsboard.server.common.data.id.*;
+
+public class EntitiesTenantIdAsyncLoader {
+
+    public static ListenableFuture<TenantId> findEntityIdAsync(TbContext ctx, EntityId original) {
+
+        switch (original.getEntityType()) {
+            case TENANT:
+                return Futures.immediateFuture((TenantId) original);
+            case CUSTOMER:
+                return getTenantAsync(ctx.getCustomerService().findCustomerByIdAsync((CustomerId) original));
+            case USER:
+                return getTenantAsync(ctx.getUserService().findUserByIdAsync((UserId) original));
+            case PLUGIN:
+                return getTenantAsync(ctx.getPluginService().findPluginByIdAsync((PluginId) original));
+            case ASSET:
+                return getTenantAsync(ctx.getAssetService().findAssetByIdAsync((AssetId) original));
+            case DEVICE:
+                return getTenantAsync(ctx.getDeviceService().findDeviceByIdAsync((DeviceId) original));
+            case ALARM:
+                return getTenantAsync(ctx.getAlarmService().findAlarmByIdAsync((AlarmId) original));
+            case RULE_CHAIN:
+                return getTenantAsync(ctx.getRuleChainService().findRuleChainByIdAsync((RuleChainId) original));
+            default:
+                return Futures.immediateFailedFuture(new TbNodeException("Unexpected original EntityType " + original));
+        }
+    }
+
+    private static <T extends HasTenantId> ListenableFuture<TenantId> getTenantAsync(ListenableFuture<T> future) {
+        return Futures.transform(future, (AsyncFunction<HasTenantId, TenantId>) in -> {
+            return in != null ? Futures.immediateFuture(in.getTenantId())
+                    : Futures.immediateFailedFuture(new IllegalStateException("Tenant not found"));});
+    }
+}
diff --git a/rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.css b/rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.css
new file mode 100644
index 0000000..b1728b1
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.css
@@ -0,0 +1,2 @@
+.tb-message-type-autocomplete .tb-not-found{display:block;line-height:1.5;height:48px}.tb-message-type-autocomplete .tb-not-found .tb-no-entries{line-height:48px}.tb-message-type-autocomplete li{height:auto!important;white-space:normal!important}.tb-generator-config tb-json-content.tb-message-body,.tb-generator-config tb-json-object-edit.tb-metadata-json{height:200px;display:block}.tb-kv-map-config .header{padding-left:5px;padding-right:5px;padding-bottom:5px}.tb-kv-map-config .header .cell{padding-left:5px;padding-right:5px;color:rgba(0,0,0,.54);font-size:12px;font-weight:700;white-space:nowrap}.tb-kv-map-config .body{padding-left:5px;padding-right:5px;padding-bottom:20px;max-height:300px;overflow:auto}.tb-kv-map-config .body .row{padding-top:5px;max-height:40px}.tb-kv-map-config .body .cell{padding-left:5px;padding-right:5px}.tb-kv-map-config .body md-input-container.cell{margin:0;max-height:40px}.tb-kv-map-config .body .md-button{margin:0}
+/*# sourceMappingURL=rulenode-core-config.css.map*/
\ No newline at end of file
diff --git a/rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js b/rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js
new file mode 100644
index 0000000..edf7ddc
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js
@@ -0,0 +1,3 @@
+!function(e){function t(r){if(n[r])return n[r].exports;var a=n[r]={exports:{},id:r,loaded:!1};return e[r].call(a.exports,a,a.exports,t),a.loaded=!0,a.exports}var n={};return t.m=e,t.c=n,t.p="/static/",t(0)}(function(e){for(var t in e)if(Object.prototype.hasOwnProperty.call(e,t))switch(typeof e[t]){case"function":break;case"object":e[t]=function(t){var n=t.slice(1),r=e[t[0]];return function(e,t,a){r.apply(this,[e,t,a].concat(n))}}(e[t]);break;default:e[t]=e[e[t]]}return e}([function(e,t,n){e.exports=n(40)},function(e,t){},1,1,function(e,t){e.exports=" <section class=tb-alarm-config ng-form name=alarmConfigForm layout=column> <label translate class=\"tb-title no-padding\">tb.rulenode.alarm-create-condition</label> <tb-js-func ng-model=configuration.createConditionJs function-name=isAlarm function-args=\"{{ ['msg', 'metadata', 'msgType'] }}\" no-validate=true> </tb-js-func> <div layout=row style=padding-bottom:15px> <md-button ng-click=\"testConditionJs($event, true)\" class=\"md-primary md-raised\"> {{ 'tb.rulenode.test-condition-function' | translate }} </md-button> </div> <label translate class=\"tb-title no-padding\">tb.rulenode.alarm-clear-condition</label> <tb-js-func ng-model=configuration.clearConditionJs function-name=isCleared function-args=\"{{ ['msg', 'metadata', 'msgType'] }}\" no-validate=true> </tb-js-func> <div layout=row style=padding-bottom:15px> <md-button ng-click=\"testConditionJs($event, false)\" class=\"md-primary md-raised\"> {{ 'tb.rulenode.test-condition-function' | translate }} </md-button> </div> <label translate class=\"tb-title no-padding\">tb.rulenode.alarm-details-builder</label> <tb-js-func ng-model=configuration.alarmDetailsBuildJs function-name=Details function-args=\"{{ ['msg', 'metadata', 'msgType'] }}\" no-validate=true> </tb-js-func> <div layout=row style=padding-bottom:15px> <md-button ng-click=testDetailsBuildJs($event) class=\"md-primary md-raised\"> {{ 'tb.rulenode.test-details-function' | translate }} </md-button> </div> <section layout=column layout-gt-sm=row> <md-input-container flex class=md-block> <label translate>tb.rulenode.alarm-type</label> <input ng-required=true name=alarmType ng-model=configuration.alarmType> <div ng-messages=alarmConfigForm.alarmType.$error> <div ng-message=required translate>tb.rulenode.alarm-type-required</div> </div> </md-input-container> <md-input-container flex class=md-block> <label translate>tb.rulenode.alarm-severity</label> <md-select required name=severity ng-model=configuration.severity> <md-option ng-repeat=\"(severityKey, severity) in types.alarmSeverity\" ng-value=severityKey> {{ severity.name | translate}} </md-option> </md-select> <div ng-messages=alarmConfigForm.severity.$error> <div ng-message=required translate>tb.rulenode.alarm-severity-required</div> </div> </md-input-container> </section> <md-checkbox aria-label=\"{{ 'tb.rulenode.propagate' | translate }}\" ng-model=configuration.propagate>{{ 'tb.rulenode.propagate' | translate }} </md-checkbox> </section> "},function(e,t){e.exports=" <section class=tb-generator-config ng-form name=generatorConfigForm layout=column> <md-input-container class=md-block> <label translate>tb.rulenode.message-count</label> <input ng-required=true type=number step=1 name=messageCount ng-model=configuration.msgCount min=0> <div ng-messages=generatorConfigForm.messageCount.$error multiple=multiple md-auto-hide=false> <div ng-message=required translate>tb.rulenode.message-count-required</div> <div ng-message=min translate>tb.rulenode.min-message-count-message</div> </div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.period-seconds</label> <input ng-required=true type=number step=1 name=periodInSeconds ng-model=configuration.periodInSeconds min=1> <div ng-messages=generatorConfigForm.periodInSeconds.$error multiple=multiple md-auto-hide=false> <div ng-message=required translate>tb.rulenode.period-seconds-required</div> <div ng-message=min translate>tb.rulenode.min-period-seconds-message</div> </div> </md-input-container> <div layout=column> <label class=tb-small>{{ 'tb.rulenode.originator' | translate }}</label> <tb-entity-select the-form=generatorConfigForm tb-required=false ng-model=originator> </tb-entity-select> </div> <label translate class=\"tb-title no-padding\">tb.rulenode.generate</label> <tb-js-func ng-model=configuration.jsScript function-name=Generate function-args=\"{{ ['prevMsg', 'prevMetadata', 'prevMsgType'] }}\" no-validate=true> </tb-js-func> <div layout=row> <md-button ng-click=testScript($event) class=\"md-primary md-raised\"> {{ 'tb.rulenode.test-generator-function' | translate }} </md-button> </div> </section> "},function(e,t){e.exports=" <section layout=column> <label translate class=\"tb-title no-padding\">tb.rulenode.to-string</label> <tb-js-func ng-model=configuration.jsScript function-name=ToString function-args=\"{{ ['msg', 'metadata', 'msgType'] }}\" no-validate=true> </tb-js-func> <div layout=row> <md-button ng-click=testScript($event) class=\"md-primary md-raised\"> {{ 'tb.rulenode.test-to-string-function' | translate }} </md-button> </div> </section> "},function(e,t){e.exports=" <section ng-form name=telemetryConfigForm layout=column> <md-input-container class=md-block> <label translate>tb.rulenode.default-ttl</label> <input ng-required=true type=number step=1 name=defaultTTL ng-model=configuration.defaultTTL min=0> <div ng-messages=telemetryConfigForm.defaultTTL.$error multiple=multiple md-auto-hide=false> <div ng-message=required translate>tb.rulenode.default-ttl-required</div> <div ng-message=min translate>tb.rulenode.min-default-ttl-message</div> </div> </md-input-container> </section> "},function(e,t){e.exports=" <section layout=column> <label translate class=\"tb-title tb-required\">tb.rulenode.attr-mapping</label> <md-checkbox aria-label=\"{{ 'tb.rulenode.latest-telemetry' | translate }}\" ng-model=configuration.telemetry>{{ 'tb.rulenode.latest-telemetry' | translate }} </md-checkbox> <tb-kv-map-config ng-model=configuration.attrMapping ng-required=true required-text=\"'tb.rulenode.attr-mapping-required'\" key-text=\"configuration.telemetry ? 'tb.rulenode.source-telemetry' : 'tb.rulenode.source-attribute'\" key-required-text=\"configuration.telemetry ? 'tb.rulenode.source-telemetry-required' : 'tb.rulenode.source-attribute-required'\" val-text=\"'tb.rulenode.target-attribute'\" val-required-text=\"'tb.rulenode.target-attribute-required'\"> </tb-kv-map-config> </section> "},function(e,t){e.exports=' <section layout=column> <label translate class="tb-title no-padding">tb.rulenode.client-attributes</label> <md-chips style=padding-bottom:15px ng-required=false readonly=readonly ng-model=configuration.clientAttributeNames placeholder="{{\'tb.rulenode.client-attributes\' | translate}}" md-separator-keys=separatorKeys> </md-chips> <label translate class="tb-title no-padding">tb.rulenode.shared-attributes</label> <md-chips style=padding-bottom:15px ng-required=false readonly=readonly ng-model=configuration.sharedAttributeNames placeholder="{{\'tb.rulenode.shared-attributes\' | translate}}" md-separator-keys=separatorKeys> </md-chips> <label translate class="tb-title no-padding">tb.rulenode.server-attributes</label> <md-chips style=padding-bottom:15px ng-required=false readonly=readonly ng-model=configuration.serverAttributeNames placeholder="{{\'tb.rulenode.server-attributes\' | translate}}" md-separator-keys=separatorKeys> </md-chips> <label translate class="tb-title no-padding">tb.rulenode.latest-timeseries</label> <md-chips ng-required=false readonly=readonly ng-model=configuration.latestTsKeyNames placeholder="{{\'tb.rulenode.latest-timeseries\' | translate}}" md-separator-keys=separatorKeys> </md-chips> </section> '},function(e,t){e.exports=" <section layout=column> <label translate class=\"tb-title tb-required\">tb.rulenode.relations-query</label> <tb-relations-query-config style=padding-bottom:15px ng-model=configuration.relationsQuery> </tb-relations-query-config> <label translate class=\"tb-title tb-required\">tb.rulenode.attr-mapping</label> <md-checkbox aria-label=\"{{ 'tb.rulenode.latest-telemetry' | translate }}\" ng-model=configuration.telemetry>{{ 'tb.rulenode.latest-telemetry' | translate }} </md-checkbox> <tb-kv-map-config ng-model=configuration.attrMapping ng-required=true required-text=\"'tb.rulenode.attr-mapping-required'\" key-text=\"configuration.telemetry ? 'tb.rulenode.source-telemetry' : 'tb.rulenode.source-attribute'\" key-required-text=\"configuration.telemetry ? 'tb.rulenode.source-telemetry-required' : 'tb.rulenode.source-attribute-required'\" val-text=\"'tb.rulenode.target-attribute'\" val-required-text=\"'tb.rulenode.target-attribute-required'\"> </tb-kv-map-config> </section> "},8,function(e,t){e.exports=' <section layout=column> <label translate class="tb-title no-padding" ng-class="{\'tb-required\': required}">tb.rulenode.message-types-filter</label> <md-chips id=message_type_chips ng-required=required readonly=readonly ng-model=messageTypes md-autocomplete-snap md-transform-chip=transformMessageTypeChip($chip) md-require-match=false> <md-autocomplete id=message_type md-no-cache=true md-selected-item=selectedMessageType md-search-text=messageTypeSearchText md-items="item in messageTypesSearch(messageTypeSearchText)" md-item-text=item.name md-min-length=0 placeholder="{{\'tb.rulenode.message-type\' | translate }}" md-menu-class=tb-message-type-autocomplete> <span md-highlight-text=messageTypeSearchText md-highlight-flags=^i>{{item}}</span> <md-not-found> <div class=tb-not-found> <div class=tb-no-entries ng-if="!messageTypeSearchText || !messageTypeSearchText.length"> <span translate>tb.rulenode.no-message-types-found</span> </div> <div ng-if="messageTypeSearchText && messageTypeSearchText.length"> <span translate translate-values=\'{ messageType: "{{messageTypeSearchText | truncate:true:6:&apos;...&apos;}}" }\'>tb.rulenode.no-message-type-matching</span> <span> <a translate ng-click="createMessageType($event, \'#message_type_chips\')">tb.rulenode.create-new-message-type</a> </span> </div> </div> </md-not-found> </md-autocomplete> <md-chip-template> <span>{{$chip.name}}</span> </md-chip-template> </md-chips> <div class=tb-error-messages ng-messages=ngModelCtrl.$error role=alert> <div translate ng-message=messageTypes class=tb-error-message>tb.rulenode.message-types-required</div> </div> </section>'},function(e,t){e.exports=" <section layout=column> <label translate class=\"tb-title no-padding\">tb.rulenode.filter</label> <tb-js-func ng-model=configuration.jsScript function-name=Filter function-args=\"{{ ['msg', 'metadata', 'msgType'] }}\" no-validate=true> </tb-js-func> <div layout=row> <md-button ng-click=testScript($event) class=\"md-primary md-raised\"> {{ 'tb.rulenode.test-filter-function' | translate }} </md-button> </div> </section> "},function(e,t){e.exports=" <section layout=column> <label translate class=\"tb-title no-padding\">tb.rulenode.switch</label> <tb-js-func ng-model=configuration.jsScript function-name=Switch function-args=\"{{ ['msg', 'metadata', 'msgType'] }}\" no-validate=true> </tb-js-func> <div layout=row> <md-button ng-click=testScript($event) class=\"md-primary md-raised\"> {{ 'tb.rulenode.test-switch-function' | translate }} </md-button> </div> </section> "},function(e,t){e.exports=' <section class=tb-kv-map-config layout=column> <div class=header flex layout=row> <span class=cell flex translate>{{ keyText }}</span> <span class=cell flex translate>{{ valText }}</span> <span ng-show=!disabled style=width:52px>&nbsp</span> </div> <div class=body> <div class=row ng-form name=kvForm flex layout=row layout-align="start center" ng-repeat="keyVal in kvList track by $index"> <md-input-container class="cell md-block" flex md-no-float> <input placeholder="{{ keyText | translate }}" ng-required=true name=key ng-model=keyVal.key> <div ng-messages=kvForm.key.$error> <div translate ng-message=required>{{keyRequiredText}}</div> </div> </md-input-container> <md-input-container class="cell md-block" flex md-no-float> <input placeholder="{{ valText | translate }}" ng-required=true name=value ng-model=keyVal.value> <div ng-messages=kvForm.value.$error> <div translate ng-message=required>{{valRequiredText}}</div> </div> </md-input-container> <md-button ng-show=!disabled ng-disabled=loading class="md-icon-button md-primary" ng-click=removeKeyVal($index) aria-label="{{ \'action.remove\' | translate }}"> <md-tooltip md-direction=top> {{ \'tb.key-val.remove-entry\' | translate }} </md-tooltip> <md-icon aria-label="{{ \'action.delete\' | translate }}" class=material-icons> close </md-icon> </md-button> </div> </div> <div class=tb-error-messages ng-messages=ngModelCtrl.$error role=alert> <div translate ng-message=kvMap class=tb-error-message>{{requiredText}}</div> </div> <div> <md-button ng-show=!disabled ng-disabled=loading class="md-primary md-raised" ng-click=addKeyVal() aria-label="{{ \'action.add\' | translate }}"> <md-tooltip md-direction=top> {{ \'tb.key-val.add-entry\' | translate }} </md-tooltip> <md-icon aria-label="{{ \'action.add\' | translate }}" class=material-icons> add </md-icon> {{ \'action.add\' | translate }} </md-button> </div> </section> '},function(e,t){e.exports=" <section layout=column> <div layout=row> <md-input-container class=md-block style=min-width:100px> <label translate>relation.direction</label> <md-select required ng-model=query.direction> <md-option ng-repeat=\"direction in types.entitySearchDirection\" ng-value=direction> {{ ('relation.search-direction.' + direction) | translate}} </md-option> </md-select> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.max-relation-level</label> <input name=maxRelationLevel type=number min=1 step=1 placeholder=\"{{ 'tb.rulenode.unlimited-level' | translate }}\" ng-model=query.maxLevel aria-label=\"{{ 'tb.rulenode.max-relation-level' | translate }}\"> </md-input-container> </div> <div class=md-caption style=padding-bottom:10px;color:rgba(0,0,0,.57) translate>relation.relation-filters</div> <tb-relation-filters ng-model=query.filters> </tb-relation-filters> </section> "},function(e,t){e.exports=' <section layout=column> <md-input-container class=md-block> <label translate>tb.rulenode.originator-source</label> <md-select required ng-model=configuration.originatorSource> <md-option ng-repeat="source in ruleNodeTypes.originatorSource" ng-value=source.value> {{ source.name | translate}} </md-option> </md-select> </md-input-container> <section layout=column ng-if="configuration.originatorSource == ruleNodeTypes.originatorSource.RELATED.value"> <label translate class="tb-title tb-required">tb.rulenode.relations-query</label> <tb-relations-query-config style=padding-bottom:15px ng-model=configuration.relationsQuery> </tb-relations-query-config> </section> <md-checkbox aria-label="{{ \'tb.rulenode.clone-message\' | translate }}" ng-model=configuration.startNewChain>{{ \'tb.rulenode.clone-message\' | translate }} </md-checkbox> </section> '},function(e,t){e.exports=" <section layout=column> <label translate class=\"tb-title no-padding\">tb.rulenode.transform</label> <tb-js-func ng-model=configuration.jsScript function-name=Transform function-args=\"{{ ['msg', 'metadata', 'msgType'] }}\" no-validate=true> </tb-js-func> <div layout=row style=padding-bottom:15px> <md-button ng-click=testScript($event) class=\"md-primary md-raised\"> {{ 'tb.rulenode.test-transformer-function' | translate }} </md-button> </div> <md-checkbox aria-label=\"{{ 'tb.rulenode.clone-message' | translate }}\" ng-model=configuration.startNewChain>{{ 'tb.rulenode.clone-message' | translate }} </md-checkbox> </section> "},function(e,t){e.exports=" <section ng-form name=toEmailConfigForm layout=column> <md-input-container class=md-block> <label translate>tb.rulenode.from-template</label> <textarea ng-required=true name=fromTemplate ng-model=configuration.fromTemplate rows=2></textarea> <div ng-messages=toEmailConfigForm.fromTemplate.$error> <div ng-message=required translate>tb.rulenode.from-template-required</div> </div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.to-template</label> <textarea ng-required=true name=toTemplate ng-model=configuration.toTemplate rows=2></textarea> <div ng-messages=toEmailConfigForm.toTemplate.$error> <div ng-message=required translate>tb.rulenode.to-template-required</div> </div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.cc-template</label> <textarea name=ccTemplate ng-model=configuration.ccTemplate rows=2></textarea> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.bcc-template</label> <textarea name=ccTemplate ng-model=configuration.bccTemplate rows=2></textarea> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.subject-template</label> <textarea ng-required=true name=subjectTemplate ng-model=configuration.subjectTemplate rows=2></textarea> <div ng-messages=toEmailConfigForm.subjectTemplate.$error> <div ng-message=required translate>tb.rulenode.subject-template-required</div> </div> </md-input-container> <md-input-container class=md-block> <label translate>tb.rulenode.body-template</label> <textarea ng-required=true name=bodyTemplate ng-model=configuration.bodyTemplate rows=6></textarea> <div ng-messages=toEmailConfigForm.bodyTemplate.$error> <div ng-message=required translate>tb.rulenode.body-template-required</div> </div> </md-input-container> </section> "},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e,t,n,r){var a=function(a,i,l,s){var u=o.default;i.html(u),a.types=n,a.$watch("configuration",function(e,t){angular.equals(e,t)||s.$setViewValue(a.configuration)}),s.$render=function(){a.configuration=s.$viewValue},a.testConditionJs=function(e,n){var i=angular.copy(n?a.configuration.createConditionJs:a.configuration.clearConditionJs),o={temperature:22.4,humidity:78},l={sensorType:"temperature"};r.testNodeScript(e,i,"filter",t.instant("tb.rulenode.condition")+"",n?"isAlarm":"isCleared",["msg","metadata","msgType"],o,l,"POST_TELEMETRY").then(function(e){n?a.configuration.createConditionJs=e:a.configuration.clearConditionJs=e,s.$setDirty()})},a.testDetailsBuildJs=function(e){var n=angular.copy(a.configuration.alarmDetailsBuildJs),i={temperature:22.4,humidity:78},o={sensorType:"temperature"};r.testNodeScript(e,n,"json",t.instant("tb.rulenode.details")+"","Details",["msg","metadata","msgType"],i,o,"POST_TELEMETRY").then(function(e){a.configuration.alarmDetailsBuildJs=e,s.$setDirty()})},e(i.contents())(a)};return{restrict:"E",require:"^ngModel",scope:{},link:a}}a.$inject=["$compile","$translate","types","ruleNodeScriptTest"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var i=n(4),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e,t,n,r){var a=function(a,i,l,s){var u=o.default;i.html(u),a.types=n,a.originator=null,a.$watch("configuration",function(e,t){angular.equals(e,t)||s.$setViewValue(a.configuration)}),s.$render=function(){a.configuration=s.$viewValue,a.configuration.originatorId&&a.configuration.originatorType?a.originator={id:a.configuration.originatorId,entityType:a.configuration.originatorType}:a.originator=null,a.$watch("originator",function(e,t){angular.equals(e,t)||(a.originator?(s.$viewValue.originatorId=a.originator.id,s.$viewValue.originatorType=a.originator.entityType):(s.$viewValue.originatorId=null,s.$viewValue.originatorType=null))},!0)},a.testScript=function(e){var n=angular.copy(a.configuration.jsScript),i={temperature:22.4,humidity:78},o={sensorType:"temperature"};r.testNodeScript(e,n,"generate",t.instant("tb.rulenode.generator")+"","Generate",["prevMsg","prevMetadata","prevMsgType"],i,o,"DebugMsg").then(function(e){a.configuration.jsScript=e,s.$setDirty()})},e(i.contents())(a)};return{restrict:"E",require:"^ngModel",scope:{},link:a}}a.$inject=["$compile","$translate","types","ruleNodeScriptTest"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a,n(1);var i=n(5),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var a=n(24),i=r(a),o=n(21),l=r(o),s=n(20),u=r(s),d=n(23),c=r(d);t.default=angular.module("thingsboard.ruleChain.config.action",[]).directive("tbActionNodeTelemetryConfig",i.default).directive("tbActionNodeGeneratorConfig",l.default).directive("tbActionNodeAlarmConfig",u.default).directive("tbActionNodeLogConfig",c.default).name},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e,t,n){var r=function(r,a,i,l){var s=o.default;a.html(s),r.$watch("configuration",function(e,t){angular.equals(e,t)||l.$setViewValue(r.configuration)}),l.$render=function(){r.configuration=l.$viewValue},r.testScript=function(e){var a=angular.copy(r.configuration.jsScript),i={temperature:22.4,humidity:78},o={sensorType:"temperature"};n.testNodeScript(e,a,"string",t.instant("tb.rulenode.to-string")+"","ToString",["msg","metadata","msgType"],i,o,"POST_TELEMETRY").then(function(e){r.configuration.jsScript=e,l.$setDirty()})},e(a.contents())(r)};return{restrict:"E",require:"^ngModel",scope:{},link:r}}a.$inject=["$compile","$translate","ruleNodeScriptTest"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var i=n(6),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,r,a){var i=o.default;n.html(i),t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var i=n(7),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,r,a){var i=o.default;n.html(i),t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var i=n(8),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var a=n(27),i=r(a),o=n(28),l=r(o),s=n(25),u=r(s),d=n(29),c=r(d);t.default=angular.module("thingsboard.ruleChain.config.enrichment",[]).directive("tbEnrichmentNodeOriginatorAttributesConfig",i.default).directive("tbEnrichmentNodeRelatedAttributesConfig",l.default).directive("tbEnrichmentNodeCustomerAttributesConfig",u.default).directive("tbEnrichmentNodeTenantAttributesConfig",c.default).name},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,r,a,i){var l=o.default;r.html(l);var s=186;n.separatorKeys=[t.KEY_CODE.ENTER,t.KEY_CODE.COMMA,s],n.$watch("configuration",function(e,t){angular.equals(e,t)||i.$setViewValue(n.configuration)}),i.$render=function(){n.configuration=i.$viewValue},e(r.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}a.$inject=["$compile","$mdConstant"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var i=n(9),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,r,a){var i=o.default;n.html(i),t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var i=n(10),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,r,a){var i=o.default;n.html(i),t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var i=n(11),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var a=n(32),i=r(a),o=n(31),l=r(o),s=n(33),u=r(s);t.default=angular.module("thingsboard.ruleChain.config.filter",[]).directive("tbFilterNodeScriptConfig",i.default).directive("tbFilterNodeMessageTypeConfig",l.default).directive("tbFilterNodeSwitchConfig",u.default).name},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e,t,n){var r=function(r,a,i,l){function s(){if(l.$viewValue){for(var e=[],t=0;t<r.messageTypes.length;t++)e.push(r.messageTypes[t].value);l.$viewValue.messageTypes=e,u()}}function u(){if(r.required){var e=!(!l.$viewValue.messageTypes||!l.$viewValue.messageTypes.length);l.$setValidity("messageTypes",e)}else l.$setValidity("messageTypes",!0)}var d=o.default;a.html(d),r.selectedMessageType=null,r.messageTypeSearchText=null,r.ngModelCtrl=l;var c=[];for(var m in n.messageType){var g={name:n.messageType[m].name,value:n.messageType[m].value};c.push(g)}r.transformMessageTypeChip=function(e){var n,r=t("filter")(c,{name:e},!0);return n=r&&r.length?angular.copy(r[0]):{name:e,value:e}},r.messageTypesSearch=function(e){var n=e?t("filter")(c,{name:e}):c;return n.map(function(e){return e.name})},r.createMessageType=function(e,t){var n=angular.element(t,a)[0].firstElementChild,r=angular.element(n),i=r.scope().$mdChipsCtrl.getChipBuffer();e.preventDefault(),e.stopPropagation(),r.scope().$mdChipsCtrl.appendChip(i.trim()),r.scope().$mdChipsCtrl.resetChipBuffer()},l.$render=function(){var e=l.$viewValue,t=[];if(e&&e.messageTypes)for(var a=0;a<e.messageTypes.length;a++){var i=e.messageTypes[a];n.messageType[i]?t.push(angular.copy(n.messageType[i])):t.push({name:i,value:i})}r.messageTypes=t,r.$watch("messageTypes",function(e,t){angular.equals(e,t)||s()},!0)},e(a.contents())(r)};return{restrict:"E",require:"^ngModel",scope:{required:"=ngRequired",readonly:"=ngReadonly"},link:r}}a.$inject=["$compile","$filter","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a,n(2);var i=n(12),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e,t,n){var r=function(r,a,i,l){var s=o.default;a.html(s),r.$watch("configuration",function(e,t){angular.equals(e,t)||l.$setViewValue(r.configuration)}),l.$render=function(){r.configuration=l.$viewValue},r.testScript=function(e){var a=angular.copy(r.configuration.jsScript),i={passed:12,name:"Vit",bigObj:{prop:42}},o={temp:10};n.testNodeScript(e,a,"filter",t.instant("tb.rulenode.filter")+"","Filter",["msg","metadata","msgType"],i,o,"POST_TELEMETRY").then(function(e){r.configuration.jsScript=e,l.$setDirty()})},e(a.contents())(r)};return{restrict:"E",require:"^ngModel",scope:{},link:r}}a.$inject=["$compile","$translate","ruleNodeScriptTest"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var i=n(13),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e,t,n){var r=function(r,a,i,l){var s=o.default;a.html(s),r.$watch("configuration",function(e,t){angular.equals(e,t)||l.$setViewValue(r.configuration)}),l.$render=function(){r.configuration=l.$viewValue},r.testScript=function(e){var a=angular.copy(r.configuration.jsScript),i={temperature:22.4,humidity:78},o={sensorType:"temperature"};n.testNodeScript(e,a,"switch",t.instant("tb.rulenode.switch")+"","Switch",["msg","metadata","msgType"],i,o,"POST_TELEMETRY").then(function(e){r.configuration.jsScript=e,l.$setDirty()})},e(a.contents())(r)};return{restrict:"E",require:"^ngModel",scope:{},link:r}}a.$inject=["$compile","$translate","ruleNodeScriptTest"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var i=n(14),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,r,a){function i(e){e>-1&&t.kvList.splice(e,1)}function l(){t.kvList||(t.kvList=[]),t.kvList.push({key:"",value:""})}function s(){var e={};t.kvList.forEach(function(t){t.key&&(e[t.key]=t.value)}),a.$setViewValue(e),u()}function u(){var e=!0;t.required&&!t.kvList.length&&(e=!1),a.$setValidity("kvMap",e)}var d=o.default;n.html(d),t.ngModelCtrl=a,t.removeKeyVal=i,t.addKeyVal=l,t.kvList=[],t.$watch("query",function(e,n){angular.equals(e,n)||a.$setViewValue(t.query)}),a.$render=function(){if(a.$viewValue){var e=a.$viewValue;t.kvList.length=0;for(var n in e)t.kvList.push({key:n,value:e[n]})}t.$watch("kvList",function(e,t){angular.equals(e,t)||s()},!0),u()},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{required:"=ngRequired",disabled:"=ngDisabled",requiredText:"=",keyText:"=",keyRequiredText:"=",valText:"=",valRequiredText:"="},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var i=n(15),o=r(i);n(3)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,r,a,i){var l=o.default;r.html(l),n.types=t,n.$watch("query",function(e,t){angular.equals(e,t)||i.$setViewValue(n.query)}),i.$render=function(){n.query=i.$viewValue},e(r.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}a.$inject=["$compile","types"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var i=n(16),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e,t){var n=function(n,r,a,i){var l=o.default;r.html(l),n.ruleNodeTypes=t,n.$watch("configuration",function(e,t){angular.equals(e,t)||i.$setViewValue(n.configuration)}),i.$render=function(){n.configuration=i.$viewValue},e(r.contents())(n)};return{restrict:"E",require:"^ngModel",scope:{},link:n}}a.$inject=["$compile","ruleNodeTypes"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var i=n(17),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var a=n(36),i=r(a),o=n(38),l=r(o),s=n(39),u=r(s);t.default=angular.module("thingsboard.ruleChain.config.transform",[]).directive("tbTransformationNodeChangeOriginatorConfig",i.default).directive("tbTransformationNodeScriptConfig",l.default).directive("tbTransformationNodeToEmailConfig",u.default).name},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e,t,n){var r=function(r,a,i,l){var s=o.default;a.html(s),r.$watch("configuration",function(e,t){angular.equals(e,t)||l.$setViewValue(r.configuration)}),l.$render=function(){r.configuration=l.$viewValue},r.testScript=function(e){var a=angular.copy(r.configuration.jsScript),i={temperature:22.4,humidity:78},o={sensorType:"temperature"};n.testNodeScript(e,a,"update",t.instant("tb.rulenode.transformer")+"","Transform",["msg","metadata","msgType"],i,o,"POST_TELEMETRY").then(function(e){r.configuration.jsScript=e,l.$setDirty()})},e(a.contents())(r)};return{restrict:"E",require:"^ngModel",scope:{},link:r}}a.$inject=["$compile","$translate","ruleNodeScriptTest"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var i=n(18),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e){var t=function(t,n,r,a){var i=o.default;n.html(i),t.$watch("configuration",function(e,n){angular.equals(e,n)||a.$setViewValue(t.configuration)}),a.$render=function(){t.configuration=a.$viewValue},e(n.contents())(t)};return{restrict:"E",require:"^ngModel",scope:{},link:t}}a.$inject=["$compile"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var i=n(19),o=r(i)},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{
+default:e}}Object.defineProperty(t,"__esModule",{value:!0});var a=n(43),i=r(a),o=n(30),l=r(o),s=n(26),u=r(s),d=n(37),c=r(d),m=n(22),g=r(m),f=n(35),p=r(f),b=n(34),v=r(b),y=n(42),T=r(y);t.default=angular.module("thingsboard.ruleChain.config",[i.default,l.default,u.default,c.default,g.default]).directive("tbRelationsQueryConfig",p.default).directive("tbKvMapConfig",v.default).config(T.default).name},function(e,t){"use strict";function n(e){var t={tb:{rulenode:{filter:"Filter",switch:"Switch","message-type":"Message type","message-type-required":"Message type is required.","message-types-filter":"Message types filter","no-message-types-found":"No message types found","no-message-type-matching":"'{{messageType}}' not found.","create-new-message-type":"Create a new one!","message-types-required":"Message types are required.","client-attributes":"Client attributes","shared-attributes":"Shared attributes","server-attributes":"Server attributes","latest-timeseries":"Latest timeseries","relations-query":"Relations query","max-relation-level":"Max relation level","unlimited-level":"Unlimited level","latest-telemetry":"Latest telemetry","attr-mapping":"Attributes mapping","source-attribute":"Source attribute","source-attribute-required":"Source attribute is required.","source-telemetry":"Source telemetry","source-telemetry-required":"Source telemetry is required.","target-attribute":"Target attribute","target-attribute-required":"Target attribute is required.","attr-mapping-required":"At least one attribute mapping should be specified.","originator-source":"Originator source","originator-customer":"Customer","originator-tenant":"Tenant","originator-related":"Related","clone-message":"Clone message",transform:"Transform","default-ttl":"Default TTL in seconds","default-ttl-required":"Default TTL is required.","min-default-ttl-message":"Only 0 minimum TTL is allowed.","message-count":"Message count (0 - unlimited)","message-count-required":"Message count is required.","min-message-count-message":"Only 0 minimum message count is allowed.","period-seconds":"Period in seconds","period-seconds-required":"Period is required.","min-period-seconds-message":"Only 1 second minimum period is allowed.",originator:"Originator","message-body":"Message body","message-metadata":"Message metadata",generate:"Generate","test-generator-function":"Test generator function",generator:"Generator","test-filter-function":"Test filter function","test-switch-function":"Test switch function","test-transformer-function":"Test transformer function",transformer:"Transformer","alarm-create-condition":"Alarm create condition","test-condition-function":"Test condition function","alarm-clear-condition":"Alarm clear condition","alarm-details-builder":"Alarm details builder","test-details-function":"Test details function","alarm-type":"Alarm type","alarm-type-required":"Alarm type is required.","alarm-severity":"Alarm severity","alarm-severity-required":"Alarm severity is required",propagate:"Propagate",condition:"Condition",details:"Details","to-string":"To string","test-to-string-function":"Test to string function","from-template":"From Template","from-template-required":"From Template is required","to-template":"To Template","to-template-required":"To Template is required","cc-template":"Cc Template","bcc-template":"Bcc Template","subject-template":"Subject Template","subject-template-required":"Subject Template is required","body-template":"Body Template","body-template-required":"Body Template is required"},"key-val":{key:"Key",value:"Value","remove-entry":"Remove entry","add-entry":"Add entry"}}};angular.merge(e.en_US,t)}Object.defineProperty(t,"__esModule",{value:!0}),t.default=n},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function a(e,t){(0,o.default)(t);for(var n in t){var r=t[n];e.translations(n,r)}}a.$inject=["$translateProvider","locales"],Object.defineProperty(t,"__esModule",{value:!0}),t.default=a;var i=n(41),o=r(i)},function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=angular.module("thingsboard.ruleChain.config.types",[]).constant("ruleNodeTypes",{messageType:{POST_ATTRIBUTES:{name:"Post attributes",value:"POST_ATTRIBUTES"},POST_TELEMETRY:{name:"Post telemetry",value:"POST_TELEMETRY"},RPC_REQUEST:{name:"RPC Request",value:"RPC_REQUEST"}},originatorSource:{CUSTOMER:{name:"tb.rulenode.originator-customer",value:"CUSTOMER"},TENANT:{name:"tb.rulenode.originator-tenant",value:"TENANT"},RELATED:{name:"tb.rulenode.originator-related",value:"RELATED"}}}).name}]));
+//# sourceMappingURL=rulenode-core-config.js.map
\ No newline at end of file
diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbAlarmNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbAlarmNodeTest.java
new file mode 100644
index 0000000..69c3aca
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbAlarmNodeTest.java
@@ -0,0 +1,341 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.rule.engine.action;
+
+import com.datastax.driver.core.utils.UUIDs;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import org.apache.commons.lang3.NotImplementedException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+import org.mockito.stubbing.Answer;
+import org.thingsboard.rule.engine.api.*;
+import org.thingsboard.server.common.data.alarm.Alarm;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.TbMsgMetaData;
+import org.thingsboard.server.dao.alarm.AlarmService;
+
+import javax.script.ScriptException;
+import java.io.IOException;
+import java.util.concurrent.Callable;
+
+import static org.junit.Assert.*;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.*;
+import static org.thingsboard.rule.engine.action.TbAlarmNode.*;
+import static org.thingsboard.server.common.data.alarm.AlarmSeverity.CRITICAL;
+import static org.thingsboard.server.common.data.alarm.AlarmSeverity.WARNING;
+import static org.thingsboard.server.common.data.alarm.AlarmStatus.*;
+
+@RunWith(MockitoJUnitRunner.class)
+public class TbAlarmNodeTest {
+
+    private TbAlarmNode node;
+
+    @Mock
+    private TbContext ctx;
+    @Mock
+    private ListeningExecutor executor;
+    @Mock
+    private AlarmService alarmService;
+
+    @Mock
+    private ScriptEngine createJs;
+    @Mock
+    private ScriptEngine clearJs;
+    @Mock
+    private ScriptEngine detailsJs;
+
+    private EntityId originator = new DeviceId(UUIDs.timeBased());
+    private TenantId tenantId = new TenantId(UUIDs.timeBased());
+    private TbMsgMetaData metaData = new TbMsgMetaData();
+    private String rawJson = "{\"name\": \"Vit\", \"passed\": 5}";
+
+    @Test
+    public void newAlarmCanBeCreated() throws ScriptException, IOException {
+        initWithScript();
+        metaData.putValue("key", "value");
+        TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", originator, metaData, rawJson);
+
+        when(createJs.executeFilter(msg)).thenReturn(true);
+        when(detailsJs.executeJson(msg)).thenReturn(null);
+        when(alarmService.findLatestByOriginatorAndType(tenantId, originator, "SomeType")).thenReturn(Futures.immediateFuture(null));
+
+        doAnswer((Answer<Alarm>) invocationOnMock -> (Alarm) (invocationOnMock.getArguments())[0]).when(alarmService).createOrUpdateAlarm(any(Alarm.class));
+
+        node.onMsg(ctx, msg);
+
+        ArgumentCaptor<TbMsg> captor = ArgumentCaptor.forClass(TbMsg.class);
+        verify(ctx).tellNext(captor.capture(), eq("Created"));
+        TbMsg actualMsg = captor.getValue();
+
+        assertEquals("ALARM", actualMsg.getType());
+        assertEquals(originator, actualMsg.getOriginator());
+        assertEquals("value", actualMsg.getMetaData().getValue("key"));
+        assertEquals(Boolean.TRUE.toString(), actualMsg.getMetaData().getValue(IS_NEW_ALARM));
+        assertNotSame(metaData, actualMsg.getMetaData());
+
+        Alarm actualAlarm = new ObjectMapper().readValue(actualMsg.getData().getBytes(), Alarm.class);
+        Alarm expectedAlarm = Alarm.builder()
+                .tenantId(tenantId)
+                .originator(originator)
+                .status(ACTIVE_UNACK)
+                .severity(CRITICAL)
+                .propagate(true)
+                .type("SomeType")
+                .details(null)
+                .build();
+
+        assertEquals(expectedAlarm, actualAlarm);
+
+        verify(executor, times(2)).executeAsync(any(Callable.class));
+    }
+
+    @Test
+    public void shouldCreateScriptThrowsException() throws ScriptException {
+        initWithScript();
+        metaData.putValue("key", "value");
+        TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", originator, metaData, rawJson);
+
+        when(createJs.executeFilter(msg)).thenThrow(new NotImplementedException("message"));
+
+        node.onMsg(ctx, msg);
+
+        verifyError(msg, "message", NotImplementedException.class);
+
+
+        verify(ctx).createJsScriptEngine("CREATE", "isAlarm");
+        verify(ctx).createJsScriptEngine("CLEAR", "isCleared");
+        verify(ctx).createJsScriptEngine("DETAILS", "Details");
+        verify(ctx).getJsExecutor();
+
+        verifyNoMoreInteractions(ctx, alarmService, clearJs, detailsJs);
+    }
+
+    @Test
+    public void buildDetailsThrowsException() throws ScriptException, IOException {
+        initWithScript();
+        metaData.putValue("key", "value");
+        TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", originator, metaData, rawJson);
+
+        when(createJs.executeFilter(msg)).thenReturn(true);
+        when(detailsJs.executeJson(msg)).thenThrow(new NotImplementedException("message"));
+        when(alarmService.findLatestByOriginatorAndType(tenantId, originator, "SomeType")).thenReturn(Futures.immediateFuture(null));
+
+        node.onMsg(ctx, msg);
+
+        verifyError(msg, "message", NotImplementedException.class);
+
+        verify(ctx).createJsScriptEngine("CREATE", "isAlarm");
+        verify(ctx).createJsScriptEngine("CLEAR", "isCleared");
+        verify(ctx).createJsScriptEngine("DETAILS", "Details");
+        verify(ctx, times(2)).getJsExecutor();
+        verify(ctx).getAlarmService();
+        verify(ctx).getTenantId();
+        verify(alarmService).findLatestByOriginatorAndType(tenantId, originator, "SomeType");
+
+        verifyNoMoreInteractions(ctx, alarmService, clearJs);
+    }
+
+    @Test
+    public void ifAlarmClearedCreateNew() throws ScriptException, IOException {
+        initWithScript();
+        metaData.putValue("key", "value");
+        TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", originator, metaData, rawJson);
+
+        Alarm clearedAlarm = Alarm.builder().status(CLEARED_ACK).build();
+
+        when(createJs.executeFilter(msg)).thenReturn(true);
+        when(detailsJs.executeJson(msg)).thenReturn(null);
+        when(alarmService.findLatestByOriginatorAndType(tenantId, originator, "SomeType")).thenReturn(Futures.immediateFuture(clearedAlarm));
+
+        doAnswer((Answer<Alarm>) invocationOnMock -> (Alarm) (invocationOnMock.getArguments())[0]).when(alarmService).createOrUpdateAlarm(any(Alarm.class));
+
+        node.onMsg(ctx, msg);
+
+        ArgumentCaptor<TbMsg> captor = ArgumentCaptor.forClass(TbMsg.class);
+        verify(ctx).tellNext(captor.capture(), eq("Created"));
+        TbMsg actualMsg = captor.getValue();
+
+        assertEquals("ALARM", actualMsg.getType());
+        assertEquals(originator, actualMsg.getOriginator());
+        assertEquals("value", actualMsg.getMetaData().getValue("key"));
+        assertEquals(Boolean.TRUE.toString(), actualMsg.getMetaData().getValue(IS_NEW_ALARM));
+        assertNotSame(metaData, actualMsg.getMetaData());
+
+        Alarm actualAlarm = new ObjectMapper().readValue(actualMsg.getData().getBytes(), Alarm.class);
+        Alarm expectedAlarm = Alarm.builder()
+                .tenantId(tenantId)
+                .originator(originator)
+                .status(ACTIVE_UNACK)
+                .severity(CRITICAL)
+                .propagate(true)
+                .type("SomeType")
+                .details(null)
+                .build();
+
+        assertEquals(expectedAlarm, actualAlarm);
+
+        verify(executor, times(2)).executeAsync(any(Callable.class));
+    }
+
+    @Test
+    public void alarmCanBeUpdated() throws ScriptException, IOException {
+        initWithScript();
+        metaData.putValue("key", "value");
+        TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", originator, metaData, rawJson);
+
+        long oldEndDate = System.currentTimeMillis();
+        Alarm activeAlarm = Alarm.builder().type("SomeType").tenantId(tenantId).originator(originator).status(ACTIVE_UNACK).severity(WARNING).endTs(oldEndDate).build();
+
+        when(createJs.executeFilter(msg)).thenReturn(true);
+        when(clearJs.executeFilter(msg)).thenReturn(false);
+        when(detailsJs.executeJson(msg)).thenReturn(null);
+        when(alarmService.findLatestByOriginatorAndType(tenantId, originator, "SomeType")).thenReturn(Futures.immediateFuture(activeAlarm));
+
+        doAnswer((Answer<Alarm>) invocationOnMock -> (Alarm) (invocationOnMock.getArguments())[0]).when(alarmService).createOrUpdateAlarm(activeAlarm);
+
+        node.onMsg(ctx, msg);
+
+        ArgumentCaptor<TbMsg> captor = ArgumentCaptor.forClass(TbMsg.class);
+        verify(ctx).tellNext(captor.capture(), eq("Updated"));
+        TbMsg actualMsg = captor.getValue();
+
+        assertEquals("ALARM", actualMsg.getType());
+        assertEquals(originator, actualMsg.getOriginator());
+        assertEquals("value", actualMsg.getMetaData().getValue("key"));
+        assertEquals(Boolean.TRUE.toString(), actualMsg.getMetaData().getValue(IS_EXISTING_ALARM));
+        assertNotSame(metaData, actualMsg.getMetaData());
+
+        Alarm actualAlarm = new ObjectMapper().readValue(actualMsg.getData().getBytes(), Alarm.class);
+        assertTrue(activeAlarm.getEndTs() > oldEndDate);
+        Alarm expectedAlarm = Alarm.builder()
+                .tenantId(tenantId)
+                .originator(originator)
+                .status(ACTIVE_UNACK)
+                .severity(CRITICAL)
+                .propagate(true)
+                .type("SomeType")
+                .details(null)
+                .endTs(activeAlarm.getEndTs())
+                .build();
+
+        assertEquals(expectedAlarm, actualAlarm);
+
+        verify(executor, times(2)).executeAsync(any(Callable.class));
+    }
+
+    @Test
+    public void alarmCanBeCleared() throws ScriptException, IOException {
+        initWithScript();
+        metaData.putValue("key", "value");
+        TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", originator, metaData, rawJson);
+
+        long oldEndDate = System.currentTimeMillis();
+        Alarm activeAlarm = Alarm.builder().type("SomeType").tenantId(tenantId).originator(originator).status(ACTIVE_UNACK).severity(WARNING).endTs(oldEndDate).build();
+
+        when(createJs.executeFilter(msg)).thenReturn(false);
+        when(clearJs.executeFilter(msg)).thenReturn(true);
+//        when(detailsJs.executeJson(msg)).thenReturn(null);
+        when(alarmService.findLatestByOriginatorAndType(tenantId, originator, "SomeType")).thenReturn(Futures.immediateFuture(activeAlarm));
+        when(alarmService.clearAlarm(eq(activeAlarm.getId()), anyLong())).thenReturn(Futures.immediateFuture(true));
+//        doAnswer((Answer<Alarm>) invocationOnMock -> (Alarm) (invocationOnMock.getArguments())[0]).when(alarmService).createOrUpdateAlarm(activeAlarm);
+
+        node.onMsg(ctx, msg);
+
+        ArgumentCaptor<TbMsg> captor = ArgumentCaptor.forClass(TbMsg.class);
+        verify(ctx).tellNext(captor.capture(), eq("Cleared"));
+        TbMsg actualMsg = captor.getValue();
+
+        assertEquals("ALARM", actualMsg.getType());
+        assertEquals(originator, actualMsg.getOriginator());
+        assertEquals("value", actualMsg.getMetaData().getValue("key"));
+        assertEquals(Boolean.TRUE.toString(), actualMsg.getMetaData().getValue(IS_CLEARED_ALARM));
+        assertNotSame(metaData, actualMsg.getMetaData());
+
+        Alarm actualAlarm = new ObjectMapper().readValue(actualMsg.getData().getBytes(), Alarm.class);
+        Alarm expectedAlarm = Alarm.builder()
+                .tenantId(tenantId)
+                .originator(originator)
+                .status(CLEARED_UNACK)
+                .severity(WARNING)
+                .propagate(false)
+                .type("SomeType")
+                .details(null)
+                .endTs(oldEndDate)
+                .build();
+
+        assertEquals(expectedAlarm, actualAlarm);
+    }
+
+    private void initWithScript() {
+        try {
+            TbAlarmNodeConfiguration config = new TbAlarmNodeConfiguration();
+            config.setPropagate(true);
+            config.setSeverity(CRITICAL);
+            config.setAlarmType("SomeType");
+            config.setCreateConditionJs("CREATE");
+            config.setClearConditionJs("CLEAR");
+            config.setAlarmDetailsBuildJs("DETAILS");
+            ObjectMapper mapper = new ObjectMapper();
+            TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config));
+
+            when(ctx.createJsScriptEngine("CREATE", "isAlarm")).thenReturn(createJs);
+            when(ctx.createJsScriptEngine("CLEAR", "isCleared")).thenReturn(clearJs);
+            when(ctx.createJsScriptEngine("DETAILS", "Details")).thenReturn(detailsJs);
+
+            when(ctx.getTenantId()).thenReturn(tenantId);
+            when(ctx.getJsExecutor()).thenReturn(executor);
+            when(ctx.getAlarmService()).thenReturn(alarmService);
+
+            mockJsExecutor();
+
+            node = new TbAlarmNode();
+            node.init(ctx, nodeConfiguration);
+        } catch (TbNodeException ex) {
+            throw new IllegalStateException(ex);
+        }
+    }
+
+    private void mockJsExecutor() {
+        when(ctx.getJsExecutor()).thenReturn(executor);
+        doAnswer((Answer<ListenableFuture<Boolean>>) invocationOnMock -> {
+            try {
+                Callable task = (Callable) (invocationOnMock.getArguments())[0];
+                return Futures.immediateFuture((Boolean) task.call());
+            } catch (Throwable th) {
+                return Futures.immediateFailedFuture(th);
+            }
+        }).when(executor).executeAsync(any(Callable.class));
+    }
+
+    private void verifyError(TbMsg msg, String message, Class expectedClass) {
+        ArgumentCaptor<Throwable> captor = ArgumentCaptor.forClass(Throwable.class);
+        verify(ctx).tellError(same(msg), captor.capture());
+
+        Throwable value = captor.getValue();
+        assertEquals(expectedClass, value.getClass());
+        assertEquals(message, value.getMessage());
+    }
+
+}
\ No newline at end of file
diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbJsFilterNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbJsFilterNodeTest.java
new file mode 100644
index 0000000..08a22f0
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbJsFilterNodeTest.java
@@ -0,0 +1,121 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.rule.engine.filter;
+
+import com.datastax.driver.core.utils.UUIDs;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Matchers;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+import org.mockito.stubbing.Answer;
+import org.thingsboard.rule.engine.api.*;
+import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.TbMsgMetaData;
+
+import javax.script.ScriptException;
+import java.util.concurrent.Callable;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.*;
+
+@RunWith(MockitoJUnitRunner.class)
+public class TbJsFilterNodeTest {
+
+    private TbJsFilterNode node;
+
+    @Mock
+    private TbContext ctx;
+    @Mock
+    private ListeningExecutor executor;
+    @Mock
+    private ScriptEngine scriptEngine;
+
+    @Test
+    public void falseEvaluationDoNotSendMsg() throws TbNodeException, ScriptException {
+        initWithScript();
+        TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, new TbMsgMetaData(), "{}");
+        mockJsExecutor();
+        when(scriptEngine.executeFilter(msg)).thenReturn(false);
+
+        node.onMsg(ctx, msg);
+        verify(ctx).getJsExecutor();
+        verify(ctx).tellNext(msg, "false");
+    }
+
+    @Test
+    public void exceptionInJsThrowsException() throws TbNodeException, ScriptException {
+        initWithScript();
+        TbMsgMetaData metaData = new TbMsgMetaData();
+        TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, metaData, "{}");
+        mockJsExecutor();
+        when(scriptEngine.executeFilter(msg)).thenThrow(new ScriptException("error"));
+
+
+        node.onMsg(ctx, msg);
+        verifyError(msg, "error", ScriptException.class);
+    }
+
+    @Test
+    public void metadataConditionCanBeTrue() throws TbNodeException, ScriptException {
+        initWithScript();
+        TbMsgMetaData metaData = new TbMsgMetaData();
+        TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, metaData, "{}");
+        mockJsExecutor();
+        when(scriptEngine.executeFilter(msg)).thenReturn(true);
+
+        node.onMsg(ctx, msg);
+        verify(ctx).getJsExecutor();
+        verify(ctx).tellNext(msg, "true");
+    }
+
+    private void initWithScript() throws TbNodeException {
+        TbJsFilterNodeConfiguration config = new TbJsFilterNodeConfiguration();
+        config.setJsScript("scr");
+        ObjectMapper mapper = new ObjectMapper();
+        TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config));
+
+        when(ctx.createJsScriptEngine("scr", "Filter")).thenReturn(scriptEngine);
+
+        node = new TbJsFilterNode();
+        node.init(ctx, nodeConfiguration);
+    }
+
+    private void mockJsExecutor() {
+        when(ctx.getJsExecutor()).thenReturn(executor);
+        doAnswer((Answer<ListenableFuture<Boolean>>) invocationOnMock -> {
+            try {
+                Callable task = (Callable) (invocationOnMock.getArguments())[0];
+                return Futures.immediateFuture((Boolean) task.call());
+            } catch (Throwable th) {
+                return Futures.immediateFailedFuture(th);
+            }
+        }).when(executor).executeAsync(Matchers.any(Callable.class));
+    }
+
+    private void verifyError(TbMsg msg, String message, Class expectedClass) {
+        ArgumentCaptor<Throwable> captor = ArgumentCaptor.forClass(Throwable.class);
+        verify(ctx).tellError(same(msg), captor.capture());
+
+        Throwable value = captor.getValue();
+        assertEquals(expectedClass, value.getClass());
+        assertEquals(message, value.getMessage());
+    }
+}
\ No newline at end of file
diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbJsSwitchNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbJsSwitchNodeTest.java
new file mode 100644
index 0000000..a495124
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/filter/TbJsSwitchNodeTest.java
@@ -0,0 +1,103 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.rule.engine.filter;
+
+import com.datastax.driver.core.utils.UUIDs;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.collect.Sets;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Matchers;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+import org.mockito.stubbing.Answer;
+import org.thingsboard.rule.engine.api.*;
+import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.TbMsgMetaData;
+
+import javax.script.ScriptException;
+import java.util.Set;
+import java.util.concurrent.Callable;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Matchers.same;
+import static org.mockito.Mockito.*;
+
+@RunWith(MockitoJUnitRunner.class)
+public class TbJsSwitchNodeTest {
+
+    private TbJsSwitchNode node;
+
+    @Mock
+    private TbContext ctx;
+    @Mock
+    private ListeningExecutor executor;
+    @Mock
+    private ScriptEngine scriptEngine;
+
+    @Test
+    public void multipleRoutesAreAllowed() throws TbNodeException, ScriptException {
+        initWithScript();
+        TbMsgMetaData metaData = new TbMsgMetaData();
+        metaData.putValue("temp", "10");
+        metaData.putValue("humidity", "99");
+        String rawJson = "{\"name\": \"Vit\", \"passed\": 5}";
+
+        TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, metaData, rawJson);
+        mockJsExecutor();
+        when(scriptEngine.executeSwitch(msg)).thenReturn(Sets.newHashSet("one", "three"));
+
+        node.onMsg(ctx, msg);
+        verify(ctx).getJsExecutor();
+        verify(ctx).tellNext(msg, Sets.newHashSet("one", "three"));
+    }
+
+    private void initWithScript() throws TbNodeException {
+        TbJsSwitchNodeConfiguration config = new TbJsSwitchNodeConfiguration();
+        config.setJsScript("scr");
+        ObjectMapper mapper = new ObjectMapper();
+        TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config));
+
+        when(ctx.createJsScriptEngine("scr", "Switch")).thenReturn(scriptEngine);
+
+        node = new TbJsSwitchNode();
+        node.init(ctx, nodeConfiguration);
+    }
+
+    private void mockJsExecutor() {
+        when(ctx.getJsExecutor()).thenReturn(executor);
+        doAnswer((Answer<ListenableFuture<Set<String>>>) invocationOnMock -> {
+            try {
+                Callable task = (Callable) (invocationOnMock.getArguments())[0];
+                return Futures.immediateFuture((Set<String>) task.call());
+            } catch (Throwable th) {
+                return Futures.immediateFailedFuture(th);
+            }
+        }).when(executor).executeAsync(Matchers.any(Callable.class));
+    }
+
+    private void verifyError(TbMsg msg, String message, Class expectedClass) {
+        ArgumentCaptor<Throwable> captor = ArgumentCaptor.forClass(Throwable.class);
+        verify(ctx).tellError(same(msg), captor.capture());
+
+        Throwable value = captor.getValue();
+        assertEquals(expectedClass, value.getClass());
+        assertEquals(message, value.getMessage());
+    }
+}
\ No newline at end of file
diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNodeTest.java
new file mode 100644
index 0000000..877047c
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNodeTest.java
@@ -0,0 +1,98 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.rule.engine.mail;
+
+import com.datastax.driver.core.utils.UUIDs;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+import org.thingsboard.rule.engine.api.TbContext;
+import org.thingsboard.rule.engine.api.TbNodeConfiguration;
+import org.thingsboard.rule.engine.api.TbNodeException;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.TbMsgMetaData;
+
+import java.io.IOException;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotSame;
+import static org.mockito.Mockito.verify;
+
+@RunWith(MockitoJUnitRunner.class)
+public class TbMsgToEmailNodeTest {
+
+    private TbMsgToEmailNode emailNode;
+
+    @Mock
+    private TbContext ctx;
+
+    private EntityId originator = new DeviceId(UUIDs.timeBased());
+    private TbMsgMetaData metaData = new TbMsgMetaData();
+    private String rawJson = "{\"name\": \"temp\", \"passed\": 5 , \"complex\": {\"val\":12, \"count\":100}}";
+
+    @Test
+    public void msgCanBeConverted() throws IOException {
+        initWithScript();
+        metaData.putValue("username", "oreo");
+        metaData.putValue("userEmail", "user@email.io");
+        TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", originator, metaData, rawJson);
+
+        emailNode.onMsg(ctx, msg);
+
+        ArgumentCaptor<TbMsg> captor = ArgumentCaptor.forClass(TbMsg.class);
+        verify(ctx).tellNext(captor.capture());
+        TbMsg actualMsg = captor.getValue();
+
+        assertEquals("SEND_EMAIL", actualMsg.getType());
+        assertEquals(originator, actualMsg.getOriginator());
+        assertEquals("oreo", actualMsg.getMetaData().getValue("username"));
+        assertNotSame(metaData, actualMsg.getMetaData());
+
+
+        EmailPojo actual = new ObjectMapper().readValue(actualMsg.getData().getBytes(), EmailPojo.class);
+
+        EmailPojo expected = new EmailPojo.EmailPojoBuilder()
+                .from("test@mail.org")
+                .to("user@email.io")
+                .subject("Hi oreo there")
+                .body("temp is to high. Current 5 and 100")
+                .build();
+        assertEquals(expected, actual);
+    }
+
+    private void initWithScript() {
+        try {
+            TbMsgToEmailNodeConfiguration config = new TbMsgToEmailNodeConfiguration();
+            config.setFromTemplate("test@mail.org");
+            config.setToTemplate("$metadata.userEmail");
+            config.setSubjectTemplate("Hi $metadata.username there");
+            config.setBodyTemplate("$msg.name is to high. Current $msg.passed and $msg.complex.count");
+            ObjectMapper mapper = new ObjectMapper();
+            TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config));
+
+            emailNode = new TbMsgToEmailNode();
+            emailNode.init(ctx, nodeConfiguration);
+        } catch (TbNodeException ex) {
+            throw new IllegalStateException(ex);
+        }
+    }
+
+}
\ No newline at end of file
diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNodeTest.java
new file mode 100644
index 0000000..e26312b
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNodeTest.java
@@ -0,0 +1,264 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.rule.engine.metadata;
+
+import com.datastax.driver.core.utils.UUIDs;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.collect.Lists;
+import com.google.common.util.concurrent.Futures;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+import org.thingsboard.rule.engine.api.TbContext;
+import org.thingsboard.rule.engine.api.TbNodeConfiguration;
+import org.thingsboard.rule.engine.api.TbNodeException;
+import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.User;
+import org.thingsboard.server.common.data.asset.Asset;
+import org.thingsboard.server.common.data.id.AssetId;
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.UserId;
+import org.thingsboard.server.common.data.kv.*;
+import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.TbMsgMetaData;
+import org.thingsboard.server.dao.asset.AssetService;
+import org.thingsboard.server.dao.attributes.AttributesService;
+import org.thingsboard.server.dao.device.DeviceService;
+import org.thingsboard.server.dao.timeseries.TimeseriesService;
+import org.thingsboard.server.dao.user.UserService;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Matchers.same;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.thingsboard.server.common.data.DataConstants.SERVER_SCOPE;
+
+@RunWith(MockitoJUnitRunner.class)
+public class TbGetCustomerAttributeNodeTest {
+
+    private TbGetCustomerAttributeNode node;
+
+    @Mock
+    private TbContext ctx;
+
+    @Mock
+    private AttributesService attributesService;
+    @Mock
+    private TimeseriesService timeseriesService;
+    @Mock
+    private UserService userService;
+    @Mock
+    private AssetService assetService;
+    @Mock
+    private DeviceService deviceService;
+
+    private TbMsg msg;
+
+    @Before
+    public void init() throws TbNodeException {
+        TbGetEntityAttrNodeConfiguration config = new TbGetEntityAttrNodeConfiguration();
+        Map<String, String> attrMapping = new HashMap<>();
+        attrMapping.putIfAbsent("temperature", "tempo");
+        config.setAttrMapping(attrMapping);
+        config.setTelemetry(false);
+        ObjectMapper mapper = new ObjectMapper();
+        TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config));
+
+        node = new TbGetCustomerAttributeNode();
+        node.init(null, nodeConfiguration);
+    }
+
+    @Test
+    public void errorThrownIfCannotLoadAttributes() {
+        UserId userId = new UserId(UUIDs.timeBased());
+        CustomerId customerId = new CustomerId(UUIDs.timeBased());
+        User user = new User();
+        user.setCustomerId(customerId);
+
+        msg = new TbMsg(UUIDs.timeBased(), "USER", userId, new TbMsgMetaData(), "{}");
+
+        when(ctx.getUserService()).thenReturn(userService);
+        when(userService.findUserByIdAsync(userId)).thenReturn(Futures.immediateFuture(user));
+
+        when(ctx.getAttributesService()).thenReturn(attributesService);
+        when(attributesService.find(customerId, SERVER_SCOPE, Collections.singleton("temperature")))
+                .thenThrow(new IllegalStateException("something wrong"));
+
+        node.onMsg(ctx, msg);
+        final ArgumentCaptor<Throwable> captor = ArgumentCaptor.forClass(Throwable.class);
+        verify(ctx).tellError(same(msg), captor.capture());
+
+        Throwable value = captor.getValue();
+        assertEquals("something wrong", value.getMessage());
+        assertTrue(msg.getMetaData().getData().isEmpty());
+    }
+
+    @Test
+    public void errorThrownIfCannotLoadAttributesAsync() {
+        UserId userId = new UserId(UUIDs.timeBased());
+        CustomerId customerId = new CustomerId(UUIDs.timeBased());
+        User user = new User();
+        user.setCustomerId(customerId);
+
+        msg = new TbMsg(UUIDs.timeBased(), "USER", userId, new TbMsgMetaData(), "{}");
+
+        when(ctx.getUserService()).thenReturn(userService);
+        when(userService.findUserByIdAsync(userId)).thenReturn(Futures.immediateFuture(user));
+
+        when(ctx.getAttributesService()).thenReturn(attributesService);
+        when(attributesService.find(customerId, SERVER_SCOPE, Collections.singleton("temperature")))
+                .thenReturn(Futures.immediateFailedFuture(new IllegalStateException("something wrong")));
+
+        node.onMsg(ctx, msg);
+        final ArgumentCaptor<Throwable> captor = ArgumentCaptor.forClass(Throwable.class);
+        verify(ctx).tellError(same(msg), captor.capture());
+
+        Throwable value = captor.getValue();
+        assertEquals("something wrong", value.getMessage());
+        assertTrue(msg.getMetaData().getData().isEmpty());
+    }
+
+    @Test
+    public void errorThrownIfCustomerCannotBeFound() {
+        UserId userId = new UserId(UUIDs.timeBased());
+        CustomerId customerId = new CustomerId(UUIDs.timeBased());
+        User user = new User();
+        user.setCustomerId(customerId);
+
+        msg = new TbMsg(UUIDs.timeBased(), "USER", userId, new TbMsgMetaData(), "{}");
+
+        when(ctx.getUserService()).thenReturn(userService);
+        when(userService.findUserByIdAsync(userId)).thenReturn(Futures.immediateFuture(null));
+
+        node.onMsg(ctx, msg);
+        final ArgumentCaptor<Throwable> captor = ArgumentCaptor.forClass(Throwable.class);
+        verify(ctx).tellError(same(msg), captor.capture());
+
+        Throwable value = captor.getValue();
+        assertEquals(IllegalStateException.class, value.getClass());
+        assertEquals("Customer not found", value.getMessage());
+        assertTrue(msg.getMetaData().getData().isEmpty());
+    }
+
+    @Test
+    public void customerAttributeAddedInMetadata() {
+        CustomerId customerId = new CustomerId(UUIDs.timeBased());
+        msg = new TbMsg(UUIDs.timeBased(), "CUSTOMER", customerId, new TbMsgMetaData(), "{}");
+        entityAttributeFetched(customerId);
+    }
+
+    @Test
+    public void usersCustomerAttributesFetched() {
+        UserId userId = new UserId(UUIDs.timeBased());
+        CustomerId customerId = new CustomerId(UUIDs.timeBased());
+        User user = new User();
+        user.setCustomerId(customerId);
+
+        msg = new TbMsg(UUIDs.timeBased(), "USER", userId, new TbMsgMetaData(), "{}");
+
+        when(ctx.getUserService()).thenReturn(userService);
+        when(userService.findUserByIdAsync(userId)).thenReturn(Futures.immediateFuture(user));
+
+        entityAttributeFetched(customerId);
+    }
+
+    @Test
+    public void assetsCustomerAttributesFetched() {
+        AssetId assetId = new AssetId(UUIDs.timeBased());
+        CustomerId customerId = new CustomerId(UUIDs.timeBased());
+        Asset asset = new Asset();
+        asset.setCustomerId(customerId);
+
+        msg = new TbMsg(UUIDs.timeBased(), "USER", assetId, new TbMsgMetaData(), "{}");
+
+        when(ctx.getAssetService()).thenReturn(assetService);
+        when(assetService.findAssetByIdAsync(assetId)).thenReturn(Futures.immediateFuture(asset));
+
+        entityAttributeFetched(customerId);
+    }
+
+    @Test
+    public void deviceCustomerAttributesFetched() {
+        DeviceId deviceId = new DeviceId(UUIDs.timeBased());
+        CustomerId customerId = new CustomerId(UUIDs.timeBased());
+        Device device = new Device();
+        device.setCustomerId(customerId);
+
+        msg = new TbMsg(UUIDs.timeBased(), "USER", deviceId, new TbMsgMetaData(), "{}");
+
+        when(ctx.getDeviceService()).thenReturn(deviceService);
+        when(deviceService.findDeviceByIdAsync(deviceId)).thenReturn(Futures.immediateFuture(device));
+
+        entityAttributeFetched(customerId);
+    }
+
+    @Test
+    public void deviceCustomerTelemetryFetched() throws TbNodeException {
+        TbGetEntityAttrNodeConfiguration config = new TbGetEntityAttrNodeConfiguration();
+        Map<String, String> attrMapping = new HashMap<>();
+        attrMapping.putIfAbsent("temperature", "tempo");
+        config.setAttrMapping(attrMapping);
+        config.setTelemetry(true);
+        ObjectMapper mapper = new ObjectMapper();
+        TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config));
+
+        node = new TbGetCustomerAttributeNode();
+        node.init(null, nodeConfiguration);
+
+
+        DeviceId deviceId = new DeviceId(UUIDs.timeBased());
+        CustomerId customerId = new CustomerId(UUIDs.timeBased());
+        Device device = new Device();
+        device.setCustomerId(customerId);
+
+        msg = new TbMsg(UUIDs.timeBased(), "USER", deviceId, new TbMsgMetaData(), "{}");
+
+        when(ctx.getDeviceService()).thenReturn(deviceService);
+        when(deviceService.findDeviceByIdAsync(deviceId)).thenReturn(Futures.immediateFuture(device));
+
+        List<TsKvEntry> timeseries = Lists.newArrayList(new BasicTsKvEntry(1L, new StringDataEntry("temperature", "highest")));
+
+        when(ctx.getTimeseriesService()).thenReturn(timeseriesService);
+        when(timeseriesService.findLatest(customerId, Collections.singleton("temperature")))
+                .thenReturn(Futures.immediateFuture(timeseries));
+
+        node.onMsg(ctx, msg);
+        verify(ctx).tellNext(msg);
+        assertEquals(msg.getMetaData().getValue("tempo"), "highest");
+    }
+
+    private void entityAttributeFetched(CustomerId customerId) {
+        List<AttributeKvEntry> attributes = Lists.newArrayList(new BaseAttributeKvEntry(new StringDataEntry("temperature", "high"), 1L));
+
+        when(ctx.getAttributesService()).thenReturn(attributesService);
+        when(attributesService.find(customerId, SERVER_SCOPE, Collections.singleton("temperature")))
+                .thenReturn(Futures.immediateFuture(attributes));
+
+        node.onMsg(ctx, msg);
+        verify(ctx).tellNext(msg);
+        assertEquals(msg.getMetaData().getValue("tempo"), "high");
+    }
+}
\ No newline at end of file
diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNodeTest.java
new file mode 100644
index 0000000..1b66433
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNodeTest.java
@@ -0,0 +1,124 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.rule.engine.transform;
+
+import com.datastax.driver.core.utils.UUIDs;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.util.concurrent.Futures;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+import org.thingsboard.rule.engine.api.TbContext;
+import org.thingsboard.rule.engine.api.TbNodeConfiguration;
+import org.thingsboard.rule.engine.api.TbNodeException;
+import org.thingsboard.server.common.data.asset.Asset;
+import org.thingsboard.server.common.data.id.AssetId;
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.TbMsgMetaData;
+import org.thingsboard.server.dao.asset.AssetService;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Matchers.same;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@RunWith(MockitoJUnitRunner.class)
+public class TbChangeOriginatorNodeTest {
+
+    private TbChangeOriginatorNode node;
+
+    @Mock
+    private TbContext ctx;
+    @Mock
+    private AssetService assetService;
+
+
+    @Test
+    public void originatorCanBeChangedToCustomerId() throws TbNodeException {
+        init(false);
+        AssetId assetId = new AssetId(UUIDs.timeBased());
+        CustomerId customerId = new CustomerId(UUIDs.timeBased());
+        Asset asset = new Asset();
+        asset.setCustomerId(customerId);
+
+        TbMsg msg = new TbMsg(UUIDs.timeBased(), "ASSET", assetId, new TbMsgMetaData(), "{}");
+
+        when(ctx.getAssetService()).thenReturn(assetService);
+        when(assetService.findAssetByIdAsync(assetId)).thenReturn(Futures.immediateFuture(asset));
+
+        node.onMsg(ctx, msg);
+        ArgumentCaptor<TbMsg> captor = ArgumentCaptor.forClass(TbMsg.class);
+        verify(ctx).tellNext(captor.capture());
+        TbMsg actualMsg = captor.getValue();
+        assertEquals(customerId, actualMsg.getOriginator());
+        assertEquals(msg.getId(), actualMsg.getId());
+    }
+
+    @Test
+    public void newChainCanBeStarted() throws TbNodeException {
+        init(true);
+        AssetId assetId = new AssetId(UUIDs.timeBased());
+        CustomerId customerId = new CustomerId(UUIDs.timeBased());
+        Asset asset = new Asset();
+        asset.setCustomerId(customerId);
+
+        TbMsg msg = new TbMsg(UUIDs.timeBased(), "ASSET", assetId, new TbMsgMetaData(), "{}");
+
+        when(ctx.getAssetService()).thenReturn(assetService);
+        when(assetService.findAssetByIdAsync(assetId)).thenReturn(Futures.immediateFuture(asset));
+
+        node.onMsg(ctx, msg);
+        ArgumentCaptor<TbMsg> captor = ArgumentCaptor.forClass(TbMsg.class);
+        verify(ctx).spawn(captor.capture());
+        TbMsg actualMsg = captor.getValue();
+        assertEquals(customerId, actualMsg.getOriginator());
+        assertEquals(msg.getId(), actualMsg.getId());
+    }
+
+    @Test
+    public void exceptionThrownIfCannotFindNewOriginator() throws TbNodeException {
+        init(true);
+        AssetId assetId = new AssetId(UUIDs.timeBased());
+        CustomerId customerId = new CustomerId(UUIDs.timeBased());
+        Asset asset = new Asset();
+        asset.setCustomerId(customerId);
+
+        TbMsg msg = new TbMsg(UUIDs.timeBased(), "ASSET", assetId, new TbMsgMetaData(), "{}");
+
+        when(ctx.getAssetService()).thenReturn(assetService);
+        when(assetService.findAssetByIdAsync(assetId)).thenReturn(Futures.immediateFailedFuture(new IllegalStateException("wrong")));
+
+        node.onMsg(ctx, msg);
+        ArgumentCaptor<Throwable> captor = ArgumentCaptor.forClass(Throwable.class);
+        verify(ctx).tellError(same(msg), captor.capture());
+        Throwable value = captor.getValue();
+        assertEquals("wrong", value.getMessage());
+    }
+
+    public void init(boolean startNewChain) throws TbNodeException {
+        TbChangeOriginatorNodeConfiguration config = new TbChangeOriginatorNodeConfiguration();
+        config.setOriginatorSource(TbChangeOriginatorNode.CUSTOMER_SOURCE);
+        config.setStartNewChain(startNewChain);
+        ObjectMapper mapper = new ObjectMapper();
+        TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config));
+
+        node = new TbChangeOriginatorNode();
+        node.init(null, nodeConfiguration);
+    }
+}
\ No newline at end of file
diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbTransformMsgNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbTransformMsgNodeTest.java
new file mode 100644
index 0000000..b904d7e
--- /dev/null
+++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbTransformMsgNodeTest.java
@@ -0,0 +1,141 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.thingsboard.rule.engine.transform;
+
+import com.datastax.driver.core.utils.UUIDs;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Matchers;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+import org.mockito.stubbing.Answer;
+import org.thingsboard.rule.engine.api.*;
+import org.thingsboard.server.common.msg.TbMsg;
+import org.thingsboard.server.common.msg.TbMsgMetaData;
+
+import javax.script.ScriptException;
+import java.util.concurrent.Callable;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Matchers.same;
+import static org.mockito.Mockito.*;
+
+@RunWith(MockitoJUnitRunner.class)
+public class TbTransformMsgNodeTest {
+
+    private TbTransformMsgNode node;
+
+    @Mock
+    private TbContext ctx;
+    @Mock
+    private ListeningExecutor executor;
+    @Mock
+    private ScriptEngine scriptEngine;
+
+    @Test
+    public void metadataCanBeUpdated() throws TbNodeException, ScriptException {
+        initWithScript(false);
+        TbMsgMetaData metaData = new TbMsgMetaData();
+        metaData.putValue("temp", "7");
+        String rawJson = "{\"passed\": 5}";
+
+        TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, metaData, rawJson);
+        TbMsg transformedMsg = new TbMsg(UUIDs.timeBased(), "USER", null, metaData, "{new}");
+        mockJsExecutor();
+        when(scriptEngine.executeUpdate(msg)).thenReturn(transformedMsg);
+
+        node.onMsg(ctx, msg);
+        verify(ctx).getJsExecutor();
+        ArgumentCaptor<TbMsg> captor = ArgumentCaptor.forClass(TbMsg.class);
+        verify(ctx).tellNext(captor.capture());
+        TbMsg actualMsg = captor.getValue();
+        assertEquals(transformedMsg, actualMsg);
+    }
+
+
+    @Test
+    public void newChainCanBeStarted() throws TbNodeException, ScriptException {
+        initWithScript(true);
+        TbMsgMetaData metaData = new TbMsgMetaData();
+        metaData.putValue("temp", "7");
+        String rawJson = "{\"passed\": 5";
+
+        TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, metaData, rawJson);
+        TbMsg transformedMsg = new TbMsg(UUIDs.timeBased(), "USER", null, metaData, "{new}");
+        mockJsExecutor();
+        when(scriptEngine.executeUpdate(msg)).thenReturn(transformedMsg);
+
+        node.onMsg(ctx, msg);
+        verify(ctx).getJsExecutor();
+        ArgumentCaptor<TbMsg> captor = ArgumentCaptor.forClass(TbMsg.class);
+        verify(ctx).spawn(captor.capture());
+        TbMsg actualMsg = captor.getValue();
+        assertEquals(transformedMsg, actualMsg);
+    }
+
+    @Test
+    public void exceptionHandledCorrectly() throws TbNodeException, ScriptException {
+        initWithScript(false);
+        TbMsgMetaData metaData = new TbMsgMetaData();
+        metaData.putValue("temp", "7");
+        String rawJson = "{\"passed\": 5";
+
+        TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, metaData, rawJson);
+        mockJsExecutor();
+        when(scriptEngine.executeUpdate(msg)).thenThrow(new IllegalStateException("error"));
+
+        node.onMsg(ctx, msg);
+        verifyError(msg, "error", IllegalStateException.class);
+    }
+
+    private void initWithScript(boolean startChain) throws TbNodeException {
+        TbTransformMsgNodeConfiguration config = new TbTransformMsgNodeConfiguration();
+        config.setJsScript("scr");
+        config.setStartNewChain(startChain);
+        ObjectMapper mapper = new ObjectMapper();
+        TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config));
+
+        when(ctx.createJsScriptEngine("scr", "Transform")).thenReturn(scriptEngine);
+
+        node = new TbTransformMsgNode();
+        node.init(ctx, nodeConfiguration);
+    }
+
+    private void mockJsExecutor() {
+        when(ctx.getJsExecutor()).thenReturn(executor);
+        doAnswer((Answer<ListenableFuture<TbMsg>>) invocationOnMock -> {
+            try {
+                Callable task = (Callable) (invocationOnMock.getArguments())[0];
+                return Futures.immediateFuture((TbMsg) task.call());
+            } catch (Throwable th) {
+                return Futures.immediateFailedFuture(th);
+            }
+        }).when(executor).executeAsync(Matchers.any(Callable.class));
+    }
+
+    private void verifyError(TbMsg msg, String message, Class expectedClass) {
+        ArgumentCaptor<Throwable> captor = ArgumentCaptor.forClass(Throwable.class);
+        verify(ctx).tellError(same(msg), captor.capture());
+
+        Throwable value = captor.getValue();
+        assertEquals(expectedClass, value.getClass());
+        assertEquals(message, value.getMessage());
+    }
+}
\ No newline at end of file
diff --git a/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java b/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java
index 7ba5e36..af07737 100644
--- a/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java
+++ b/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java
@@ -35,6 +35,7 @@ import org.thingsboard.server.common.transport.SessionMsgProcessor;
 import org.thingsboard.server.common.transport.adaptor.AdaptorException;
 import org.thingsboard.server.common.transport.auth.DeviceAuthService;
 import org.thingsboard.server.common.transport.quota.QuotaService;
+import org.thingsboard.server.dao.device.DeviceOfflineService;
 import org.thingsboard.server.transport.coap.adaptors.CoapTransportAdaptor;
 import org.thingsboard.server.transport.coap.session.CoapExchangeObserverProxy;
 import org.thingsboard.server.transport.coap.session.CoapSessionCtx;
@@ -53,15 +54,17 @@ public class CoapTransportResource extends CoapResource {
     private final SessionMsgProcessor processor;
     private final DeviceAuthService authService;
     private final QuotaService quotaService;
+    private final DeviceOfflineService offlineService;
     private final Field observerField;
     private final long timeout;
 
     public CoapTransportResource(SessionMsgProcessor processor, DeviceAuthService authService, CoapTransportAdaptor adaptor, String name,
-                                 long timeout, QuotaService quotaService) {
+                                 long timeout, QuotaService quotaService, DeviceOfflineService offlineService) {
         super(name);
         this.processor = processor;
         this.authService = authService;
         this.quotaService = quotaService;
+        this.offlineService = offlineService;
         this.adaptor = adaptor;
         this.timeout = timeout;
         // This is important to turn off existing observable logic in
@@ -168,6 +171,7 @@ public class CoapTransportResource extends CoapResource {
                 case TO_SERVER_RPC_REQUEST:
                     ctx.setSessionType(SessionType.SYNC);
                     msg = adaptor.convertToActorMsg(ctx, type, request);
+                    offlineService.online(ctx.getDevice(), true);
                     break;
                 case SUBSCRIBE_ATTRIBUTES_REQUEST:
                 case SUBSCRIBE_RPC_COMMANDS_REQUEST:
@@ -175,11 +179,13 @@ public class CoapTransportResource extends CoapResource {
                     advanced.setObserver(new CoapExchangeObserverProxy(systemObserver, ctx));
                     ctx.setSessionType(SessionType.ASYNC);
                     msg = adaptor.convertToActorMsg(ctx, type, request);
+                    offlineService.online(ctx.getDevice(), false);
                     break;
                 case UNSUBSCRIBE_ATTRIBUTES_REQUEST:
                 case UNSUBSCRIBE_RPC_COMMANDS_REQUEST:
                     ctx.setSessionType(SessionType.ASYNC);
                     msg = adaptor.convertToActorMsg(ctx, type, request);
+                    offlineService.online(ctx.getDevice(), false);
                     break;
                 default:
                     log.trace("[{}] Unsupported msg type: {}", ctx.getSessionId(), type);
diff --git a/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportService.java b/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportService.java
index 15706d4..4037ee7 100644
--- a/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportService.java
+++ b/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportService.java
@@ -27,6 +27,7 @@ import org.springframework.stereotype.Service;
 import org.thingsboard.server.common.transport.SessionMsgProcessor;
 import org.thingsboard.server.common.transport.auth.DeviceAuthService;
 import org.thingsboard.server.common.transport.quota.QuotaService;
+import org.thingsboard.server.dao.device.DeviceOfflineService;
 import org.thingsboard.server.transport.coap.adaptors.CoapTransportAdaptor;
 
 import javax.annotation.PostConstruct;
@@ -57,6 +58,9 @@ public class CoapTransportService {
     @Autowired(required = false)
     private QuotaService quotaService;
 
+    @Autowired(required = false)
+    private DeviceOfflineService offlineService;
+
 
     @Value("${coap.bind_address}")
     private String host;
@@ -86,7 +90,7 @@ public class CoapTransportService {
 
     private void createResources() {
         CoapResource api = new CoapResource(API);
-        api.add(new CoapTransportResource(processor, authService, adaptor, V1, timeout, quotaService));
+        api.add(new CoapTransportResource(processor, authService, adaptor, V1, timeout, quotaService, offlineService));
         server.add(api);
     }
 
diff --git a/transport/coap/src/test/java/org/thingsboard/server/transport/coap/CoapServerTest.java b/transport/coap/src/test/java/org/thingsboard/server/transport/coap/CoapServerTest.java
index 072c735..fd0346a 100644
--- a/transport/coap/src/test/java/org/thingsboard/server/transport/coap/CoapServerTest.java
+++ b/transport/coap/src/test/java/org/thingsboard/server/transport/coap/CoapServerTest.java
@@ -15,6 +15,7 @@
  */
 package org.thingsboard.server.transport.coap;
 
+import com.google.common.util.concurrent.ListenableFuture;
 import lombok.extern.slf4j.Slf4j;
 import org.eclipse.californium.core.CoapClient;
 import org.eclipse.californium.core.CoapResponse;
@@ -31,6 +32,7 @@ import org.springframework.test.annotation.DirtiesContext;
 import org.springframework.test.annotation.DirtiesContext.ClassMode;
 import org.springframework.test.context.junit4.SpringRunner;
 import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.device.DeviceStatusQuery;
 import org.thingsboard.server.common.data.id.CustomerId;
 import org.thingsboard.server.common.data.id.DeviceId;
 import org.thingsboard.server.common.data.id.TenantId;
@@ -51,6 +53,7 @@ import org.thingsboard.server.common.transport.SessionMsgProcessor;
 import org.thingsboard.server.common.transport.auth.DeviceAuthResult;
 import org.thingsboard.server.common.transport.auth.DeviceAuthService;
 import org.thingsboard.server.common.transport.quota.QuotaService;
+import org.thingsboard.server.dao.device.DeviceOfflineService;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -137,6 +140,31 @@ public class CoapServerTest {
         public static QuotaService quotaService() {
             return key -> false;
         }
+
+        @Bean
+        public static DeviceOfflineService offlineService() {
+            return new DeviceOfflineService() {
+                @Override
+                public void online(Device device, boolean isUpdate) {
+
+                }
+
+                @Override
+                public void offline(Device device) {
+
+                }
+
+                @Override
+                public ListenableFuture<List<Device>> findOfflineDevices(UUID tenantId, DeviceStatusQuery.ContactType contactType, long offlineThreshold) {
+                    return null;
+                }
+
+                @Override
+                public ListenableFuture<List<Device>> findOnlineDevices(UUID tenantId, DeviceStatusQuery.ContactType contactType, long offlineThreshold) {
+                    return null;
+                }
+            };
+        }
     }
 
     @Autowired
diff --git a/transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java b/transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java
index 4d90b5f..03a4201 100644
--- a/transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java
+++ b/transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java
@@ -26,6 +26,7 @@ import org.springframework.http.ResponseEntity;
 import org.springframework.util.StringUtils;
 import org.springframework.web.bind.annotation.*;
 import org.springframework.web.context.request.async.DeferredResult;
+import org.thingsboard.server.common.data.Device;
 import org.thingsboard.server.common.data.security.DeviceTokenCredentials;
 import org.thingsboard.server.common.msg.core.*;
 import org.thingsboard.server.common.msg.session.AdaptorToSessionActorMsg;
@@ -36,6 +37,7 @@ import org.thingsboard.server.common.transport.SessionMsgProcessor;
 import org.thingsboard.server.common.transport.adaptor.JsonConverter;
 import org.thingsboard.server.common.transport.auth.DeviceAuthService;
 import org.thingsboard.server.common.transport.quota.QuotaService;
+import org.thingsboard.server.dao.device.DeviceOfflineService;
 import org.thingsboard.server.transport.http.session.HttpSessionCtx;
 
 import javax.servlet.http.HttpServletRequest;
@@ -63,6 +65,9 @@ public class DeviceApiController {
     @Autowired(required = false)
     private QuotaService quotaService;
 
+    @Autowired(required = false)
+    private DeviceOfflineService offlineService;
+
     @RequestMapping(value = "/{deviceToken}/attributes", method = RequestMethod.GET, produces = "application/json")
     public DeferredResult<ResponseEntity> getDeviceAttributes(@PathVariable("deviceToken") String deviceToken,
                                                               @RequestParam(value = "clientKeys", required = false, defaultValue = "") String clientKeys,
@@ -82,7 +87,7 @@ public class DeviceApiController {
                 Set<String> sharedKeySet = !StringUtils.isEmpty(sharedKeys) ? new HashSet<>(Arrays.asList(sharedKeys.split(","))) : null;
                 request = new BasicGetAttributesRequest(0, clientKeySet, sharedKeySet);
             }
-            process(ctx, request);
+            process(ctx, request, false);
         } else {
             responseWriter.setResult(new ResponseEntity<>(HttpStatus.UNAUTHORIZED));
         }
@@ -100,7 +105,7 @@ public class DeviceApiController {
         HttpSessionCtx ctx = getHttpSessionCtx(responseWriter);
         if (ctx.login(new DeviceTokenCredentials(deviceToken))) {
             try {
-                process(ctx, JsonConverter.convertToAttributes(new JsonParser().parse(json)));
+                process(ctx, JsonConverter.convertToAttributes(new JsonParser().parse(json)), true);
             } catch (IllegalStateException | JsonSyntaxException ex) {
                 responseWriter.setResult(new ResponseEntity<>(HttpStatus.BAD_REQUEST));
             }
@@ -120,7 +125,7 @@ public class DeviceApiController {
         HttpSessionCtx ctx = getHttpSessionCtx(responseWriter);
         if (ctx.login(new DeviceTokenCredentials(deviceToken))) {
             try {
-                process(ctx, JsonConverter.convertToTelemetry(new JsonParser().parse(json)));
+                process(ctx, JsonConverter.convertToTelemetry(new JsonParser().parse(json)), true);
             } catch (IllegalStateException | JsonSyntaxException ex) {
                 responseWriter.setResult(new ResponseEntity<>(HttpStatus.BAD_REQUEST));
             }
@@ -150,7 +155,7 @@ public class DeviceApiController {
         if (ctx.login(new DeviceTokenCredentials(deviceToken))) {
             try {
                 JsonObject response = new JsonParser().parse(json).getAsJsonObject();
-                process(ctx, new ToDeviceRpcResponseMsg(requestId, response.toString()));
+                process(ctx, new ToDeviceRpcResponseMsg(requestId, response.toString()), true);
             } catch (IllegalStateException | JsonSyntaxException ex) {
                 responseWriter.setResult(new ResponseEntity<>(HttpStatus.BAD_REQUEST));
             }
@@ -173,7 +178,7 @@ public class DeviceApiController {
                 JsonObject request = new JsonParser().parse(json).getAsJsonObject();
                 process(ctx, new ToServerRpcRequestMsg(0,
                         request.get("method").getAsString(),
-                        request.get("params").toString()));
+                        request.get("params").toString()), true);
             } catch (IllegalStateException | JsonSyntaxException ex) {
                 responseWriter.setResult(new ResponseEntity<>(HttpStatus.BAD_REQUEST));
             }
@@ -199,7 +204,7 @@ public class DeviceApiController {
         HttpSessionCtx ctx = getHttpSessionCtx(responseWriter, timeout);
         if (ctx.login(new DeviceTokenCredentials(deviceToken))) {
             try {
-                process(ctx, msg);
+                process(ctx, msg, false);
             } catch (IllegalStateException | JsonSyntaxException ex) {
                 responseWriter.setResult(new ResponseEntity<>(HttpStatus.BAD_REQUEST));
             }
@@ -217,9 +222,10 @@ public class DeviceApiController {
         return new HttpSessionCtx(processor, authService, responseWriter, timeout != 0 ? timeout : defaultTimeout);
     }
 
-    private void process(HttpSessionCtx ctx, FromDeviceMsg request) {
+    private void process(HttpSessionCtx ctx, FromDeviceMsg request, boolean isUpdate) {
         AdaptorToSessionActorMsg msg = new BasicAdaptorToSessionActorMsg(ctx, request);
         processor.process(new BasicToDeviceActorSessionMsg(ctx.getDevice(), msg));
+        offlineService.online(ctx.getDevice(), isUpdate);
     }
 
     private boolean quotaExceeded(HttpServletRequest request, DeferredResult<ResponseEntity> responseWriter) {
diff --git a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java
index 8766599..8d475a4 100644
--- a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java
+++ b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java
@@ -37,6 +37,7 @@ import org.thingsboard.server.common.transport.adaptor.AdaptorException;
 import org.thingsboard.server.common.transport.auth.DeviceAuthService;
 import org.thingsboard.server.common.transport.quota.QuotaService;
 import org.thingsboard.server.dao.EncryptionUtil;
+import org.thingsboard.server.dao.device.DeviceOfflineService;
 import org.thingsboard.server.dao.device.DeviceService;
 import org.thingsboard.server.dao.relation.RelationService;
 import org.thingsboard.server.transport.mqtt.adaptors.MqttTransportAdaptor;
@@ -72,13 +73,14 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
     private final DeviceAuthService authService;
     private final RelationService relationService;
     private final QuotaService quotaService;
+    private final DeviceOfflineService offlineService;
     private final SslHandler sslHandler;
     private volatile boolean connected;
     private volatile InetSocketAddress address;
     private volatile GatewaySessionCtx gatewaySessionCtx;
 
     public MqttTransportHandler(SessionMsgProcessor processor, DeviceService deviceService, DeviceAuthService authService, RelationService relationService,
-                                MqttTransportAdaptor adaptor, SslHandler sslHandler, QuotaService quotaService) {
+                                MqttTransportAdaptor adaptor, SslHandler sslHandler, QuotaService quotaService, DeviceOfflineService offlineService) {
         this.processor = processor;
         this.deviceService = deviceService;
         this.relationService = relationService;
@@ -88,6 +90,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
         this.sessionId = deviceSessionCtx.getSessionId().toUidStr();
         this.sslHandler = sslHandler;
         this.quotaService = quotaService;
+        this.offlineService = offlineService;
     }
 
     @Override
@@ -129,11 +132,13 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
             case PINGREQ:
                 if (checkConnected(ctx)) {
                     ctx.writeAndFlush(new MqttMessage(new MqttFixedHeader(PINGRESP, false, AT_MOST_ONCE, false, 0)));
+                    offlineService.online(deviceSessionCtx.getDevice(), false);
                 }
                 break;
             case DISCONNECT:
                 if (checkConnected(ctx)) {
                     processDisconnect(ctx);
+                    offlineService.offline(deviceSessionCtx.getDevice());
                 }
                 break;
             default:
@@ -185,23 +190,28 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
         try {
             if (topicName.equals(DEVICE_TELEMETRY_TOPIC)) {
                 msg = adaptor.convertToActorMsg(deviceSessionCtx, POST_TELEMETRY_REQUEST, mqttMsg);
+                offlineService.online(deviceSessionCtx.getDevice(), true);
             } else if (topicName.equals(DEVICE_ATTRIBUTES_TOPIC)) {
                 msg = adaptor.convertToActorMsg(deviceSessionCtx, POST_ATTRIBUTES_REQUEST, mqttMsg);
+                offlineService.online(deviceSessionCtx.getDevice(), true);
             } else if (topicName.startsWith(DEVICE_ATTRIBUTES_REQUEST_TOPIC_PREFIX)) {
                 msg = adaptor.convertToActorMsg(deviceSessionCtx, GET_ATTRIBUTES_REQUEST, mqttMsg);
                 if (msgId >= 0) {
                     ctx.writeAndFlush(createMqttPubAckMsg(msgId));
                 }
+                offlineService.online(deviceSessionCtx.getDevice(), false);
             } else if (topicName.startsWith(DEVICE_RPC_RESPONSE_TOPIC)) {
                 msg = adaptor.convertToActorMsg(deviceSessionCtx, TO_DEVICE_RPC_RESPONSE, mqttMsg);
                 if (msgId >= 0) {
                     ctx.writeAndFlush(createMqttPubAckMsg(msgId));
                 }
+                offlineService.online(deviceSessionCtx.getDevice(), true);
             } else if (topicName.startsWith(DEVICE_RPC_REQUESTS_TOPIC)) {
                 msg = adaptor.convertToActorMsg(deviceSessionCtx, TO_SERVER_RPC_REQUEST, mqttMsg);
                 if (msgId >= 0) {
                     ctx.writeAndFlush(createMqttPubAckMsg(msgId));
                 }
+                offlineService.online(deviceSessionCtx.getDevice(), true);
             }
         } catch (AdaptorException e) {
             log.warn("[{}] Failed to process publish msg [{}][{}]", sessionId, topicName, msgId, e);
@@ -250,6 +260,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
             }
         }
         ctx.writeAndFlush(createSubAckMessage(mqttMsg.variableHeader().messageId(), grantedQoSList));
+        offlineService.online(deviceSessionCtx.getDevice(), false);
     }
 
     private void processUnsubscribe(ChannelHandlerContext ctx, MqttUnsubscribeMessage mqttMsg) {
@@ -273,6 +284,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
             }
         }
         ctx.writeAndFlush(createUnSubAckMessage(mqttMsg.variableHeader().messageId()));
+        offlineService.online(deviceSessionCtx.getDevice(), false);
     }
 
     private MqttMessage createUnSubAckMessage(int msgId) {
@@ -304,6 +316,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
             ctx.writeAndFlush(createMqttConnAckMsg(CONNECTION_ACCEPTED));
             connected = true;
             checkGatewaySession();
+            offlineService.online(deviceSessionCtx.getDevice(), false);
         }
     }
 
@@ -315,6 +328,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
                 ctx.writeAndFlush(createMqttConnAckMsg(CONNECTION_ACCEPTED));
                 connected = true;
                 checkGatewaySession();
+                offlineService.online(deviceSessionCtx.getDevice(), false);
             } else {
                 ctx.writeAndFlush(createMqttConnAckMsg(CONNECTION_REFUSED_NOT_AUTHORIZED));
                 ctx.close();
@@ -365,6 +379,9 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
     public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
         log.error("[{}] Unexpected Exception", sessionId, cause);
         ctx.close();
+        if(deviceSessionCtx.getDevice() != null) {
+            offlineService.offline(deviceSessionCtx.getDevice());
+        }
     }
 
     private static MqttSubAckMessage createSubAckMessage(Integer msgId, List<Integer> grantedQoSList) {
@@ -403,7 +420,8 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
         if (infoNode != null) {
             JsonNode gatewayNode = infoNode.get("gateway");
             if (gatewayNode != null && gatewayNode.asBoolean()) {
-                gatewaySessionCtx = new GatewaySessionCtx(processor, deviceService, authService, relationService, deviceSessionCtx);
+                gatewaySessionCtx = new GatewaySessionCtx(processor, deviceService, authService,
+                        relationService, deviceSessionCtx, offlineService);
             }
         }
     }
@@ -411,5 +429,8 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
     @Override
     public void operationComplete(Future<? super Void> future) throws Exception {
         processor.process(SessionCloseMsg.onError(deviceSessionCtx.getSessionId()));
+        if(deviceSessionCtx.getDevice() != null) {
+            offlineService.offline(deviceSessionCtx.getDevice());
+        }
     }
 }
diff --git a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportServerInitializer.java b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportServerInitializer.java
index 976d8ba..94cf940 100644
--- a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportServerInitializer.java
+++ b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportServerInitializer.java
@@ -24,6 +24,7 @@ import io.netty.handler.ssl.SslHandler;
 import org.thingsboard.server.common.transport.SessionMsgProcessor;
 import org.thingsboard.server.common.transport.auth.DeviceAuthService;
 import org.thingsboard.server.common.transport.quota.QuotaService;
+import org.thingsboard.server.dao.device.DeviceOfflineService;
 import org.thingsboard.server.dao.device.DeviceService;
 import org.thingsboard.server.dao.relation.RelationService;
 import org.thingsboard.server.transport.mqtt.adaptors.MqttTransportAdaptor;
@@ -42,10 +43,11 @@ public class MqttTransportServerInitializer extends ChannelInitializer<SocketCha
     private final MqttTransportAdaptor adaptor;
     private final MqttSslHandlerProvider sslHandlerProvider;
     private final QuotaService quotaService;
+    private final DeviceOfflineService offlineService;
 
     public MqttTransportServerInitializer(SessionMsgProcessor processor, DeviceService deviceService, DeviceAuthService authService, RelationService relationService,
                                           MqttTransportAdaptor adaptor, MqttSslHandlerProvider sslHandlerProvider,
-                                          QuotaService quotaService) {
+                                          QuotaService quotaService, DeviceOfflineService offlineService) {
         this.processor = processor;
         this.deviceService = deviceService;
         this.authService = authService;
@@ -53,6 +55,7 @@ public class MqttTransportServerInitializer extends ChannelInitializer<SocketCha
         this.adaptor = adaptor;
         this.sslHandlerProvider = sslHandlerProvider;
         this.quotaService = quotaService;
+        this.offlineService = offlineService;
     }
 
     @Override
@@ -67,7 +70,7 @@ public class MqttTransportServerInitializer extends ChannelInitializer<SocketCha
         pipeline.addLast("encoder", MqttEncoder.INSTANCE);
 
         MqttTransportHandler handler = new MqttTransportHandler(processor, deviceService, authService, relationService,
-                adaptor, sslHandler, quotaService);
+                adaptor, sslHandler, quotaService, offlineService);
 
         pipeline.addLast(handler);
         ch.closeFuture().addListener(handler);
diff --git a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportService.java b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportService.java
index 1ae7d38..90b4591 100644
--- a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportService.java
+++ b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportService.java
@@ -30,6 +30,7 @@ import org.springframework.stereotype.Service;
 import org.thingsboard.server.common.transport.SessionMsgProcessor;
 import org.thingsboard.server.common.transport.auth.DeviceAuthService;
 import org.thingsboard.server.common.transport.quota.QuotaService;
+import org.thingsboard.server.dao.device.DeviceOfflineService;
 import org.thingsboard.server.dao.device.DeviceService;
 import org.thingsboard.server.dao.relation.RelationService;
 import org.thingsboard.server.transport.mqtt.adaptors.MqttTransportAdaptor;
@@ -69,6 +70,9 @@ public class MqttTransportService {
     @Autowired(required = false)
     private QuotaService quotaService;
 
+    @Autowired(required = false)
+    private DeviceOfflineService offlineService;
+
     @Value("${mqtt.bind_address}")
     private String host;
     @Value("${mqtt.bind_port}")
@@ -106,7 +110,7 @@ public class MqttTransportService {
         b.group(bossGroup, workerGroup)
                 .channel(NioServerSocketChannel.class)
                 .childHandler(new MqttTransportServerInitializer(processor, deviceService, authService, relationService,
-                        adaptor, sslHandlerProvider, quotaService));
+                        adaptor, sslHandlerProvider, quotaService, offlineService));
 
         serverChannel = b.bind(host, port).sync().channel();
         log.info("Mqtt transport started!");
diff --git a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionCtx.java b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionCtx.java
index 7eda5bd..b4dd8db 100644
--- a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionCtx.java
+++ b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionCtx.java
@@ -36,6 +36,7 @@ import org.thingsboard.server.common.transport.SessionMsgProcessor;
 import org.thingsboard.server.common.transport.adaptor.AdaptorException;
 import org.thingsboard.server.common.transport.adaptor.JsonConverter;
 import org.thingsboard.server.common.transport.auth.DeviceAuthService;
+import org.thingsboard.server.dao.device.DeviceOfflineService;
 import org.thingsboard.server.dao.device.DeviceService;
 import org.thingsboard.server.dao.relation.RelationService;
 import org.thingsboard.server.transport.mqtt.MqttTransportHandler;
@@ -61,14 +62,17 @@ public class GatewaySessionCtx {
     private final DeviceService deviceService;
     private final DeviceAuthService authService;
     private final RelationService relationService;
+    private final DeviceOfflineService offlineService;
     private final Map<String, GatewayDeviceSessionCtx> devices;
     private ChannelHandlerContext channel;
 
-    public GatewaySessionCtx(SessionMsgProcessor processor, DeviceService deviceService, DeviceAuthService authService, RelationService relationService, DeviceSessionCtx gatewaySessionCtx) {
+    public GatewaySessionCtx(SessionMsgProcessor processor, DeviceService deviceService, DeviceAuthService authService,
+                             RelationService relationService, DeviceSessionCtx gatewaySessionCtx, DeviceOfflineService offlineService) {
         this.processor = processor;
         this.deviceService = deviceService;
         this.authService = authService;
         this.relationService = relationService;
+        this.offlineService = offlineService;
         this.gateway = gatewaySessionCtx.getDevice();
         this.gatewaySessionId = gatewaySessionCtx.getSessionId();
         this.devices = new HashMap<>();
@@ -98,6 +102,7 @@ public class GatewaySessionCtx {
             log.debug("[{}] Added device [{}] to the gateway session", gatewaySessionId, deviceName);
             processor.process(new BasicToDeviceActorSessionMsg(device, new BasicAdaptorToSessionActorMsg(ctx, new AttributesSubscribeMsg())));
             processor.process(new BasicToDeviceActorSessionMsg(device, new BasicAdaptorToSessionActorMsg(ctx, new RpcSubscribeMsg())));
+            offlineService.online(device, false);
         }
     }
 
@@ -107,6 +112,7 @@ public class GatewaySessionCtx {
         if (deviceSessionCtx != null) {
             processor.process(SessionCloseMsg.onDisconnect(deviceSessionCtx.getSessionId()));
             deviceSessionCtx.setClosed(true);
+            offlineService.offline(deviceSessionCtx.getDevice());
             log.debug("[{}] Removed device [{}] from the gateway session", gatewaySessionId, deviceName);
         } else {
             log.debug("[{}] Device [{}] was already removed from the gateway session", gatewaySessionId, deviceName);
@@ -117,6 +123,7 @@ public class GatewaySessionCtx {
     public void onGatewayDisconnect() {
         devices.forEach((k, v) -> {
             processor.process(SessionCloseMsg.onDisconnect(v.getSessionId()));
+            offlineService.offline(v.getDevice());
         });
     }
 
@@ -138,6 +145,7 @@ public class GatewaySessionCtx {
                 GatewayDeviceSessionCtx deviceSessionCtx = devices.get(deviceName);
                 processor.process(new BasicToDeviceActorSessionMsg(deviceSessionCtx.getDevice(),
                         new BasicAdaptorToSessionActorMsg(deviceSessionCtx, request)));
+                offlineService.online(deviceSessionCtx.getDevice(), true);
             }
         } else {
             throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json);
@@ -154,6 +162,7 @@ public class GatewaySessionCtx {
             GatewayDeviceSessionCtx deviceSessionCtx = devices.get(deviceName);
             processor.process(new BasicToDeviceActorSessionMsg(deviceSessionCtx.getDevice(),
                     new BasicAdaptorToSessionActorMsg(deviceSessionCtx, new ToDeviceRpcResponseMsg(requestId, data))));
+            offlineService.online(deviceSessionCtx.getDevice(), true);
         } else {
             throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json);
         }
@@ -176,6 +185,7 @@ public class GatewaySessionCtx {
                 GatewayDeviceSessionCtx deviceSessionCtx = devices.get(deviceName);
                 processor.process(new BasicToDeviceActorSessionMsg(deviceSessionCtx.getDevice(),
                         new BasicAdaptorToSessionActorMsg(deviceSessionCtx, request)));
+                offlineService.online(deviceSessionCtx.getDevice(), true);
             }
         } else {
             throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json);
@@ -210,6 +220,7 @@ public class GatewaySessionCtx {
             processor.process(new BasicToDeviceActorSessionMsg(deviceSessionCtx.getDevice(),
                     new BasicAdaptorToSessionActorMsg(deviceSessionCtx, request)));
             ack(msg);
+            offlineService.online(deviceSessionCtx.getDevice(), false);
         } else {
             throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json);
         }

ui/package.json 3(+2 -1)

diff --git a/ui/package.json b/ui/package.json
index ad95ef4..ec3f880 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -15,7 +15,7 @@
   },
   "dependencies": {
     "@flowjs/ng-flow": "^2.7.1",
-    "ace-builds": "^1.2.5",
+    "ace-builds": "1.3.1",
     "angular": "1.5.8",
     "angular-animate": "1.5.8",
     "angular-aria": "1.5.8",
@@ -69,6 +69,7 @@
     "moment": "^2.15.0",
     "ngclipboard": "^1.1.1",
     "ngreact": "^0.3.0",
+    "ngFlowchart": "git://github.com/thingsboard/ngFlowchart.git#master",
     "objectpath": "^1.2.1",
     "oclazyload": "^1.0.9",
     "raphael": "^2.2.7",

ui/server.js 20(+20 -0)

diff --git a/ui/server.js b/ui/server.js
index fae132f..65a2bc7 100644
--- a/ui/server.js
+++ b/ui/server.js
@@ -30,6 +30,9 @@ const httpProxy = require('http-proxy');
 const forwardHost = 'localhost';
 const forwardPort = 8080;
 
+const ruleNodeUiforwardHost = 'localhost';
+const ruleNodeUiforwardPort = 8080;
+
 const app = express();
 const server = http.createServer(app);
 
@@ -52,17 +55,34 @@ const apiProxy = httpProxy.createProxyServer({
     }
 });
 
+const ruleNodeUiApiProxy = httpProxy.createProxyServer({
+    target: {
+        host: ruleNodeUiforwardHost,
+        port: ruleNodeUiforwardPort
+    }
+});
+
 apiProxy.on('error', function (err, req, res) {
     console.warn('API proxy error: ' + err);
     res.end('Error.');
 });
 
+ruleNodeUiApiProxy.on('error', function (err, req, res) {
+    console.warn('RuleNode UI API proxy error: ' + err);
+    res.end('Error.');
+});
+
 console.info(`Forwarding API requests to http://${forwardHost}:${forwardPort}`);
+console.info(`Forwarding Rule Node UI requests to http://${ruleNodeUiforwardHost}:${ruleNodeUiforwardPort}`);
 
 app.all('/api/*', (req, res) => {
     apiProxy.web(req, res);
 });
 
+app.all('/static/rulenode/*', (req, res) => {
+    ruleNodeUiApiProxy.web(req, res);
+});
+
 app.get('*', function(req, res) {
     res.sendFile(path.join(__dirname, 'src/index.html'));
 });
diff --git a/ui/src/app/api/component-descriptor.service.js b/ui/src/app/api/component-descriptor.service.js
index 4478d71..cc3f710 100644
--- a/ui/src/app/api/component-descriptor.service.js
+++ b/ui/src/app/api/component-descriptor.service.js
@@ -26,7 +26,8 @@ function ComponentDescriptorService($http, $q) {
     var service = {
         getComponentDescriptorsByType: getComponentDescriptorsByType,
         getComponentDescriptorByClazz: getComponentDescriptorByClazz,
-        getPluginActionsByPluginClazz: getPluginActionsByPluginClazz
+        getPluginActionsByPluginClazz: getPluginActionsByPluginClazz,
+        getComponentDescriptorsByTypes: getComponentDescriptorsByTypes
     }
 
     return service;
@@ -52,6 +53,41 @@ function ComponentDescriptorService($http, $q) {
         return deferred.promise;
     }
 
+    function getComponentDescriptorsByTypes(componentTypes) {
+        var deferred = $q.defer();
+        var result = [];
+        for (var i=componentTypes.length-1;i>=0;i--) {
+            var componentType = componentTypes[i];
+            if (componentsByType[componentType]) {
+                result = result.concat(componentsByType[componentType]);
+                componentTypes.splice(i, 1);
+            }
+        }
+        if (!componentTypes.length) {
+            deferred.resolve(result);
+        } else {
+            var url = '/api/components?componentTypes=' + componentTypes.join(',');
+            $http.get(url, null).then(function success(response) {
+                var components = response.data;
+                for (var i = 0; i < components.length; i++) {
+                    var component = components[i];
+                    var componentsList = componentsByType[component.type];
+                    if (!componentsList) {
+                        componentsList = [];
+                        componentsByType[component.type] = componentsList;
+                    }
+                    componentsList.push(component);
+                    componentsByClazz[component.clazz] = component;
+                }
+                result = result.concat(components);
+                deferred.resolve(components);
+            }, function fail() {
+                deferred.reject();
+            });
+        }
+        return deferred.promise;
+    }
+
     function getComponentDescriptorByClazz(componentDescriptorClazz) {
         var deferred = $q.defer();
         if (componentsByClazz[componentDescriptorClazz]) {
diff --git a/ui/src/app/api/entity.service.js b/ui/src/app/api/entity.service.js
index e4c51a2..ba1265f 100644
--- a/ui/src/app/api/entity.service.js
+++ b/ui/src/app/api/entity.service.js
@@ -22,7 +22,7 @@ export default angular.module('thingsboard.api.entity', [thingsboardTypes])
 /*@ngInject*/
 function EntityService($http, $q, $filter, $translate, $log, userService, deviceService,
                        assetService, tenantService, customerService,
-                       ruleService, pluginService, dashboardService, entityRelationService, attributeService, types, utils) {
+                       ruleService, pluginService, ruleChainService, dashboardService, entityRelationService, attributeService, types, utils) {
     var service = {
         getEntity: getEntity,
         getEntities: getEntities,
@@ -73,6 +73,9 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device
             case types.entityType.user:
                 promise = userService.getUser(entityId, true, config);
                 break;
+            case types.entityType.rulechain:
+                promise = ruleChainService.getRuleChain(entityId, config);
+                break;
             case types.entityType.alarm:
                 $log.error('Get Alarm Entity is not implemented!');
                 break;
@@ -271,6 +274,9 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device
             case types.entityType.plugin:
                 promise = pluginService.getAllPlugins(pageLink, config);
                 break;
+            case types.entityType.rulechain:
+                promise = ruleChainService.getRuleChains(pageLink, config);
+                break;
             case types.entityType.dashboard:
                 if (user.authority === 'CUSTOMER_USER') {
                     promise = dashboardService.getCustomerDashboards(customerId, pageLink, config);
diff --git a/ui/src/app/api/rule-chain.service.js b/ui/src/app/api/rule-chain.service.js
new file mode 100644
index 0000000..03b3a82
--- /dev/null
+++ b/ui/src/app/api/rule-chain.service.js
@@ -0,0 +1,307 @@
+/*
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.
+ */
+export default angular.module('thingsboard.api.ruleChain', [])
+    .factory('ruleChainService', RuleChainService).name;
+
+/*@ngInject*/
+function RuleChainService($http, $q, $filter, $ocLazyLoad, $translate, types, componentDescriptorService) {
+
+    var ruleNodeComponents = null;
+
+    var service = {
+        getSystemRuleChains: getSystemRuleChains,
+        getTenantRuleChains: getTenantRuleChains,
+        getRuleChains: getRuleChains,
+        getRuleChain: getRuleChain,
+        saveRuleChain: saveRuleChain,
+        deleteRuleChain: deleteRuleChain,
+        getRuleChainMetaData: getRuleChainMetaData,
+        saveRuleChainMetaData: saveRuleChainMetaData,
+        getRuleNodeComponents: getRuleNodeComponents,
+        getRuleNodeComponentByClazz: getRuleNodeComponentByClazz,
+        getRuleNodeSupportedLinks: getRuleNodeSupportedLinks,
+        resolveTargetRuleChains: resolveTargetRuleChains,
+        testScript: testScript
+    };
+
+    return service;
+
+    function getSystemRuleChains (pageLink, config) {
+        var deferred = $q.defer();
+        var url = '/api/system/ruleChains?limit=' + pageLink.limit;
+        if (angular.isDefined(pageLink.textSearch)) {
+            url += '&textSearch=' + pageLink.textSearch;
+        }
+        if (angular.isDefined(pageLink.idOffset)) {
+            url += '&idOffset=' + pageLink.idOffset;
+        }
+        if (angular.isDefined(pageLink.textOffset)) {
+            url += '&textOffset=' + pageLink.textOffset;
+        }
+        $http.get(url, config).then(function success(response) {
+            deferred.resolve(response.data);
+        }, function fail() {
+            deferred.reject();
+        });
+        return deferred.promise;
+    }
+
+    function getTenantRuleChains (pageLink, config) {
+        var deferred = $q.defer();
+        var url = '/api/tenant/ruleChains?limit=' + pageLink.limit;
+        if (angular.isDefined(pageLink.textSearch)) {
+            url += '&textSearch=' + pageLink.textSearch;
+        }
+        if (angular.isDefined(pageLink.idOffset)) {
+            url += '&idOffset=' + pageLink.idOffset;
+        }
+        if (angular.isDefined(pageLink.textOffset)) {
+            url += '&textOffset=' + pageLink.textOffset;
+        }
+        $http.get(url, config).then(function success(response) {
+            deferred.resolve(response.data);
+        }, function fail() {
+            deferred.reject();
+        });
+        return deferred.promise;
+    }
+
+    function getRuleChains (pageLink, config) {
+        var deferred = $q.defer();
+        var url = '/api/ruleChains?limit=' + pageLink.limit;
+        if (angular.isDefined(pageLink.textSearch)) {
+            url += '&textSearch=' + pageLink.textSearch;
+        }
+        if (angular.isDefined(pageLink.idOffset)) {
+            url += '&idOffset=' + pageLink.idOffset;
+        }
+        if (angular.isDefined(pageLink.textOffset)) {
+            url += '&textOffset=' + pageLink.textOffset;
+        }
+        $http.get(url, config).then(function success(response) {
+            deferred.resolve(response.data);
+        }, function fail() {
+            deferred.reject();
+        });
+        return deferred.promise;
+    }
+
+    function getRuleChain(ruleChainId, config) {
+        var deferred = $q.defer();
+        var url = '/api/ruleChain/' + ruleChainId;
+        $http.get(url, config).then(function success(response) {
+            deferred.resolve(response.data);
+        }, function fail() {
+            deferred.reject();
+        });
+        return deferred.promise;
+    }
+
+    function saveRuleChain(ruleChain) {
+        var deferred = $q.defer();
+        var url = '/api/ruleChain';
+        $http.post(url, ruleChain).then(function success(response) {
+            deferred.resolve(response.data);
+        }, function fail() {
+            deferred.reject();
+        });
+        return deferred.promise;
+    }
+
+    function deleteRuleChain(ruleChainId) {
+        var deferred = $q.defer();
+        var url = '/api/ruleChain/' + ruleChainId;
+        $http.delete(url).then(function success() {
+            deferred.resolve();
+        }, function fail() {
+            deferred.reject();
+        });
+        return deferred.promise;
+    }
+
+    function getRuleChainMetaData(ruleChainId, config) {
+        var deferred = $q.defer();
+        var url = '/api/ruleChain/' + ruleChainId + '/metadata';
+        $http.get(url, config).then(function success(response) {
+            deferred.resolve(response.data);
+        }, function fail() {
+            deferred.reject();
+        });
+        return deferred.promise;
+    }
+
+    function saveRuleChainMetaData(ruleChainMetaData) {
+        var deferred = $q.defer();
+        var url = '/api/ruleChain/metadata';
+        $http.post(url, ruleChainMetaData).then(function success(response) {
+            deferred.resolve(response.data);
+        }, function fail() {
+            deferred.reject();
+        });
+        return deferred.promise;
+    }
+
+    function getRuleNodeSupportedLinks(component) {
+        var relationTypes = component.configurationDescriptor.nodeDefinition.relationTypes;
+        var customRelations = component.configurationDescriptor.nodeDefinition.customRelations;
+        var linkLabels = [];
+        for (var i=0;i<relationTypes.length;i++) {
+            linkLabels.push({
+                name: relationTypes[i], custom: false
+            });
+        }
+        if (customRelations) {
+            linkLabels.push(
+                { name: 'Custom', custom: true }
+            );
+        }
+        return linkLabels;
+    }
+
+    function getRuleNodeComponents() {
+        var deferred = $q.defer();
+        if (ruleNodeComponents) {
+            deferred.resolve(ruleNodeComponents);
+        } else {
+            loadRuleNodeComponents().then(
+                (components) => {
+                    resolveRuleNodeComponentsUiResources(components).then(
+                        (components) => {
+                            ruleNodeComponents = components;
+                            ruleNodeComponents.push(
+                                types.ruleChainNodeComponent
+                            );
+                            deferred.resolve(ruleNodeComponents);
+                        },
+                        () => {
+                            deferred.reject();
+                        }
+                    );
+                },
+                () => {
+                    deferred.reject();
+                }
+            );
+        }
+        return deferred.promise;
+    }
+
+    function resolveRuleNodeComponentsUiResources(components) {
+        var deferred = $q.defer();
+        var tasks = [];
+        for (var i=0;i<components.length;i++) {
+            var component = components[i];
+            tasks.push(resolveRuleNodeComponentUiResources(component));
+        }
+        $q.all(tasks).then(
+            (components) => {
+                deferred.resolve(components);
+            },
+            () => {
+                deferred.resolve(components);
+            }
+        );
+        return deferred.promise;
+    }
+
+    function resolveRuleNodeComponentUiResources(component) {
+        var deferred = $q.defer();
+        var uiResources = component.configurationDescriptor.nodeDefinition.uiResources;
+        if (uiResources && uiResources.length) {
+            var tasks = [];
+            for (var i=0;i<uiResources.length;i++) {
+                var uiResource = uiResources[i];
+                tasks.push($ocLazyLoad.load(uiResource));
+            }
+            $q.all(tasks).then(
+                () => {
+                    deferred.resolve(component);
+                },
+                () => {
+                    component.configurationDescriptor.nodeDefinition.uiResourceLoadError = $translate.instant('rulenode.ui-resources-load-error');
+                    deferred.resolve(component);
+                }
+            )
+        } else {
+            deferred.resolve(component);
+        }
+        return deferred.promise;
+    }
+
+    function getRuleNodeComponentByClazz(clazz) {
+        var res = $filter('filter')(ruleNodeComponents, {clazz: clazz}, true);
+        if (res && res.length) {
+            return res[0];
+        }
+        return null;
+    }
+
+    function resolveTargetRuleChains(ruleChainConnections) {
+        var deferred = $q.defer();
+        if (ruleChainConnections && ruleChainConnections.length) {
+            var tasks = [];
+            for (var i = 0; i < ruleChainConnections.length; i++) {
+                tasks.push(resolveRuleChain(ruleChainConnections[i].targetRuleChainId.id));
+            }
+            $q.all(tasks).then(
+                (ruleChains) => {
+                    var ruleChainsMap = {};
+                    for (var i = 0; i < ruleChains.length; i++) {
+                        ruleChainsMap[ruleChains[i].id.id] = ruleChains[i];
+                    }
+                    deferred.resolve(ruleChainsMap);
+                },
+                () => {
+                    deferred.reject();
+                }
+            );
+        } else {
+            deferred.resolve({});
+        }
+        return deferred.promise;
+    }
+
+    function resolveRuleChain(ruleChainId) {
+        var deferred = $q.defer();
+        getRuleChain(ruleChainId, {ignoreErrors: true}).then(
+            (ruleChain) => {
+                deferred.resolve(ruleChain);
+            },
+            () => {
+                deferred.resolve({
+                    id: {id: ruleChainId, entityType: types.entityType.rulechain}
+                });
+            }
+        );
+        return deferred.promise;
+    }
+
+    function loadRuleNodeComponents() {
+        return componentDescriptorService.getComponentDescriptorsByTypes(types.ruleNodeTypeComponentTypes);
+    }
+
+    function testScript(inputParams) {
+        var deferred = $q.defer();
+        var url = '/api/ruleChain/testScript';
+        $http.post(url, inputParams).then(function success(response) {
+            deferred.resolve(response.data);
+        }, function fail() {
+            deferred.reject();
+        });
+        return deferred.promise;
+    }
+
+}
diff --git a/ui/src/app/app.js b/ui/src/app/app.js
index 0b6a141..3131013 100644
--- a/ui/src/app/app.js
+++ b/ui/src/app/app.js
@@ -49,6 +49,7 @@ import 'material-ui';
 import 'react-schema-form';
 import react from 'ngreact';
 import '@flowjs/ng-flow/dist/ng-flow-standalone.min';
+import 'ngFlowchart/dist/ngFlowchart';
 
 import thingsboardLocales from './locale/locale.constant';
 import thingsboardLogin from './login';
@@ -73,6 +74,7 @@ import thingsboardApiAttribute from './api/attribute.service';
 import thingsboardApiEntity from './api/entity.service';
 import thingsboardApiAlarm from './api/alarm.service';
 import thingsboardApiAuditLog from './api/audit-log.service';
+import thingsboardApiRuleChain from './api/rule-chain.service';
 
 import 'typeface-roboto';
 import 'font-awesome/css/font-awesome.min.css';
@@ -85,6 +87,7 @@ import 'mdPickers/dist/mdPickers.min.css';
 import 'angular-hotkeys/build/hotkeys.min.css';
 import 'angular-carousel/dist/angular-carousel.min.css';
 import 'angular-material-expansion-panel/dist/md-expansion-panel.min.css';
+import 'ngFlowchart/dist/flowchart.css';
 import '../scss/main.scss';
 
 import AppConfig from './app.config';
@@ -112,6 +115,7 @@ angular.module('thingsboard', [
     'ngclipboard',
     react.name,
     'flow',
+    'flowchart',
     thingsboardLocales,
     thingsboardLogin,
     thingsboardDialogs,
@@ -135,6 +139,7 @@ angular.module('thingsboard', [
     thingsboardApiEntity,
     thingsboardApiAlarm,
     thingsboardApiAuditLog,
+    thingsboardApiRuleChain,
     uiRouter])
     .config(AppConfig)
     .factory('globalInterceptor', GlobalInterceptor)
diff --git a/ui/src/app/common/types.constant.js b/ui/src/app/common/types.constant.js
index ef6ffde..2186508 100644
--- a/ui/src/app/common/types.constant.js
+++ b/ui/src/app/common/types.constant.js
@@ -279,6 +279,23 @@ export default angular.module('thingsboard.types', [])
                 function: "function",
                 alarm: "alarm"
             },
+            contentType: {
+                "JSON": {
+                    value: "JSON",
+                    name: "content-type.json",
+                    code: "json"
+                },
+                "TEXT": {
+                    value: "TEXT",
+                    name: "content-type.text",
+                    code: "text"
+                },
+                "BINARY": {
+                    value: "BINARY",
+                    name: "content-type.binary",
+                    code: "text"
+                }
+            },
             componentType: {
                 filter: "FILTER",
                 processor: "PROCESSOR",
@@ -294,7 +311,9 @@ export default angular.module('thingsboard.types', [])
                 customer: "CUSTOMER",
                 user: "USER",
                 dashboard: "DASHBOARD",
-                alarm: "ALARM"
+                alarm: "ALARM",
+                rulechain: "RULE_CHAIN",
+                rulenode: "RULE_NODE"
             },
             aliasEntityType: {
                 current_customer: "CURRENT_CUSTOMER"
@@ -354,6 +373,12 @@ export default angular.module('thingsboard.types', [])
                     list: 'entity.list-of-alarms',
                     nameStartsWith: 'entity.alarm-name-starts-with'
                 },
+                "RULE_CHAIN": {
+                    type: 'entity.type-rulechain',
+                    typePlural: 'entity.type-rulechains',
+                    list: 'entity.list-of-rulechains',
+                    nameStartsWith: 'entity.rulechain-name-starts-with'
+                },
                 "CURRENT_CUSTOMER": {
                     type: 'entity.type-current-customer',
                     list: 'entity.type-current-customer'
@@ -381,6 +406,16 @@ export default angular.module('thingsboard.types', [])
                     name: "event.type-stats"
                 }
             },
+            debugEventType: {
+                debugRuleNode: {
+                    value: "DEBUG_RULE_NODE",
+                    name: "event.type-debug-rule-node"
+                },
+                debugRuleChain: {
+                    value: "DEBUG_RULE_CHAIN",
+                    name: "event.type-debug-rule-chain"
+                }
+            },
             extensionType: {
                 http: "HTTP",
                 mqtt: "MQTT",
@@ -450,6 +485,71 @@ export default angular.module('thingsboard.types', [])
                     clientSide: false
                 }
             },
+            ruleNodeTypeComponentTypes: ["FILTER", "ENRICHMENT", "TRANSFORMATION", "ACTION"],
+            ruleChainNodeComponent: {
+                type: 'RULE_CHAIN',
+                name: 'rule chain',
+                clazz: 'tb.internal.RuleChain',
+                configurationDescriptor: {
+                    nodeDefinition: {
+                        description: "",
+                        details: "Forwards incoming messages to specified Rule Chain",
+                        inEnabled: true,
+                        outEnabled: false,
+                        relationTypes: [],
+                        customRelations: false,
+                        defaultConfiguration: {}
+                    }
+                }
+            },
+            inputNodeComponent: {
+                type: 'INPUT',
+                name: 'Input',
+                clazz: 'tb.internal.Input'
+            },
+            ruleNodeType: {
+                FILTER: {
+                    value: "FILTER",
+                    name: "rulenode.type-filter",
+                    details: "rulenode.type-filter-details",
+                    nodeClass: "tb-filter-type",
+                    icon: "filter_list"
+                },
+                ENRICHMENT: {
+                    value: "ENRICHMENT",
+                    name: "rulenode.type-enrichment",
+                    details: "rulenode.type-enrichment-details",
+                    nodeClass: "tb-enrichment-type",
+                    icon: "playlist_add"
+                },
+                TRANSFORMATION: {
+                    value: "TRANSFORMATION",
+                    name: "rulenode.type-transformation",
+                    details: "rulenode.type-transformation-details",
+                    nodeClass: "tb-transformation-type",
+                    icon: "transform"
+                },
+                ACTION: {
+                    value: "ACTION",
+                    name: "rulenode.type-action",
+                    details: "rulenode.type-action-details",
+                    nodeClass: "tb-action-type",
+                    icon: "flash_on"
+                },
+                RULE_CHAIN: {
+                    value: "RULE_CHAIN",
+                    name: "rulenode.type-rule-chain",
+                    details: "rulenode.type-rule-chain-details",
+                    nodeClass: "tb-rule-chain-type",
+                    icon: "settings_ethernet"
+                },
+                INPUT: {
+                    value: "INPUT",
+                    nodeClass: "tb-input-type",
+                    icon: "input",
+                    special: true
+                }
+            },
             valueType: {
                 string: {
                     value: "string",
diff --git a/ui/src/app/components/ace-editor-fix.js b/ui/src/app/components/ace-editor-fix.js
new file mode 100644
index 0000000..f68767e
--- /dev/null
+++ b/ui/src/app/components/ace-editor-fix.js
@@ -0,0 +1,45 @@
+/*
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.
+ */
+
+export default function fixAceEditor(aceEditor) {
+    aceEditor.$blockScrolling = Infinity;
+    aceEditor.on("showGutterTooltip", function (tooltip) {
+        if (!tooltip.isAttachedToBody) {
+            document.body.appendChild(tooltip.$element); //eslint-disable-line
+            tooltip.isAttachedToBody = true;
+            onElementRemoved(tooltip.$parentNode, () => {
+                if (tooltip.$element.parentNode != null) {
+                    tooltip.$element.parentNode.removeChild(tooltip.$element);
+                }
+            });
+        }
+    });
+}
+
+function onElementRemoved(element, callback) {
+    if (!document.body.contains(element)) { //eslint-disable-line
+        callback();
+    } else {
+        var observer;
+        observer = new MutationObserver(function(mutations) { //eslint-disable-line
+            if (!document.body.contains(element)) { //eslint-disable-line
+                callback();
+                observer.disconnect();
+            }
+        });
+        observer.observe(document.body, {childList: true}); //eslint-disable-line
+    }
+}
diff --git a/ui/src/app/components/confirm-on-exit.directive.js b/ui/src/app/components/confirm-on-exit.directive.js
index fe9a9bd..e04e110 100644
--- a/ui/src/app/components/confirm-on-exit.directive.js
+++ b/ui/src/app/components/confirm-on-exit.directive.js
@@ -18,17 +18,17 @@ export default angular.module('thingsboard.directives.confirmOnExit', [])
     .name;
 
 /*@ngInject*/
-function ConfirmOnExit($state, $mdDialog, $window, $filter, userService) {
+function ConfirmOnExit($state, $mdDialog, $window, $filter, $parse, userService) {
     return {
-        link: function ($scope) {
-
+        link: function ($scope, $element, $attributes) {
+            $scope.confirmForm = $scope.$eval($attributes.confirmForm);
             $window.onbeforeunload = function () {
-                if (userService.isAuthenticated() && (($scope.confirmForm && $scope.confirmForm.$dirty) || $scope.isDirty)) {
+                if (userService.isAuthenticated() && (($scope.confirmForm && $scope.confirmForm.$dirty) || $scope.$eval($attributes.isDirty))) {
                     return $filter('translate')('confirm-on-exit.message');
                 }
             }
             $scope.$on('$stateChangeStart', function (event, next, current, params) {
-                if (userService.isAuthenticated() && (($scope.confirmForm && $scope.confirmForm.$dirty) || $scope.isDirty)) {
+                if (userService.isAuthenticated() && (($scope.confirmForm && $scope.confirmForm.$dirty) || $scope.$eval($attributes.isDirty))) {
                     event.preventDefault();
                     var confirm = $mdDialog.confirm()
                         .title($filter('translate')('confirm-on-exit.title'))
@@ -40,7 +40,9 @@ function ConfirmOnExit($state, $mdDialog, $window, $filter, userService) {
                         if ($scope.confirmForm) {
                             $scope.confirmForm.$setPristine();
                         } else {
-                            $scope.isDirty = false;
+                            var remoteSetter = $parse($attributes.isDirty).assign;
+                            remoteSetter($scope, false);
+                            //$scope.isDirty = false;
                         }
                         $state.go(next.name, params);
                     }, function () {
@@ -48,9 +50,6 @@ function ConfirmOnExit($state, $mdDialog, $window, $filter, userService) {
                 }
             });
         },
-        scope: {
-            confirmForm: '=',
-            isDirty: '='
-        }
+        scope: false
     };
 }
\ No newline at end of file
diff --git a/ui/src/app/components/dashboard.tpl.html b/ui/src/app/components/dashboard.tpl.html
index 376f2d6..54a1a09 100644
--- a/ui/src/app/components/dashboard.tpl.html
+++ b/ui/src/app/components/dashboard.tpl.html
@@ -122,9 +122,9 @@
 							<md-menu-content id="menu" width="4" ng-mouseleave="$mdCloseMousepointMenu()">
 								<md-menu-item ng-repeat ="item in vm.widgetContextMenuItems">
 									<md-button ng-disabled="!item.enabled" ng-click="item.action(vm.widgetContextMenuEvent, widget)">
+										<span ng-if="item.shortcut" class="tb-alt-text"> {{ item.shortcut | keyboardShortcut }}</span>
 										<md-icon ng-if="item.icon" md-menu-align-target aria-label="{{ item.value | translate }}" class="material-icons">{{item.icon}}</md-icon>
 										<span translate>{{item.value}}</span>
-										<span ng-if="item.shortcut" class="tb-alt-text"> {{ item.shortcut | keyboardShortcut }}</span>
 									</md-button>
 								</md-menu-item>
 							</md-menu-content>
@@ -137,9 +137,9 @@
 	<md-menu-content id="menu" width="4" ng-mouseleave="$mdCloseMousepointMenu()">
 		<md-menu-item ng-repeat ="item in vm.contextMenuItems">
 			<md-button ng-disabled="!item.enabled" ng-click="item.action(vm.contextMenuEvent)">
+				<span ng-if="item.shortcut" class="tb-alt-text"> {{ item.shortcut | keyboardShortcut }}</span>
 				<md-icon ng-if="item.icon" md-menu-align-target aria-label="{{ item.value | translate }}" class="material-icons">{{item.icon}}</md-icon>
 				<span translate>{{item.value}}</span>
-				<span ng-if="item.shortcut" class="tb-alt-text"> {{ item.shortcut | keyboardShortcut }}</span>
 			</md-button>
 		</md-menu-item>
 	</md-menu-content>
diff --git a/ui/src/app/components/details-sidenav.directive.js b/ui/src/app/components/details-sidenav.directive.js
index e455a80..2516134 100644
--- a/ui/src/app/components/details-sidenav.directive.js
+++ b/ui/src/app/components/details-sidenav.directive.js
@@ -26,7 +26,7 @@ export default angular.module('thingsboard.directives.detailsSidenav', [])
     .name;
 
 /*@ngInject*/
-function DetailsSidenav($timeout) {
+function DetailsSidenav($timeout, $mdUtil, $q, $animate) {
 
     var linker = function (scope, element, attrs) {
 
@@ -42,6 +42,63 @@ function DetailsSidenav($timeout) {
             scope.isEdit = true;
         }
 
+        var backdrop;
+        var previousContainerStyles;
+
+        if (attrs.hasOwnProperty('tbEnableBackdrop')) {
+            backdrop = $mdUtil.createBackdrop(scope, "md-sidenav-backdrop md-opaque ng-enter");
+            element.on('$destroy', function() {
+                backdrop && backdrop.remove();
+            });
+            scope.$on('$destroy', function(){
+                backdrop && backdrop.remove();
+            });
+            scope.$watch('isOpen', updateIsOpen);
+        }
+
+        function updateIsOpen(isOpen) {
+            backdrop[isOpen ? 'on' : 'off']('click', (ev)=>{
+                ev.preventDefault();
+                scope.isOpen = false;
+                scope.$apply();
+            });
+            var parent = element.parent();
+            var restorePositioning = updateContainerPositions(parent, isOpen);
+
+            return $q.all([
+                isOpen && backdrop ? $animate.enter(backdrop, parent) : backdrop ?
+                    $animate.leave(backdrop) : $q.when(true)
+            ]).then(function() {
+                restorePositioning && restorePositioning();
+            });
+        }
+
+        function updateContainerPositions(parent, willOpen) {
+            var drawerEl = element[0];
+            var scrollTop = parent[0].scrollTop;
+            if (willOpen && scrollTop) {
+                previousContainerStyles = {
+                    top: drawerEl.style.top,
+                    bottom: drawerEl.style.bottom,
+                    height: drawerEl.style.height
+                };
+                var positionStyle = {
+                    top: scrollTop + 'px',
+                    bottom: 'auto',
+                    height: parent[0].clientHeight + 'px'
+                };
+                backdrop.css(positionStyle);
+            }
+            if (!willOpen && previousContainerStyles) {
+                return function() {
+                    backdrop[0].style.top = null;
+                    backdrop[0].style.bottom = null;
+                    backdrop[0].style.height = null;
+                    previousContainerStyles = null;
+                };
+            }
+        }
+
         scope.toggleDetailsEditMode = function () {
             if (!scope.isAlwaysEdit) {
                 if (!scope.isEdit) {
diff --git a/ui/src/app/components/details-sidenav.scss b/ui/src/app/components/details-sidenav.scss
index c7e9919..360b133 100644
--- a/ui/src/app/components/details-sidenav.scss
+++ b/ui/src/app/components/details-sidenav.scss
@@ -59,4 +59,14 @@ md-sidenav.tb-sidenav-details {
       background-color: $primary-hue-3;
     }
   }
+
+  md-tab-content.md-active > div {
+    height: 100%;
+    & > *:first-child {
+      height: 100%;
+    }
+    md-content {
+      height: 100%;
+    }
+  }
 }
diff --git a/ui/src/app/components/details-sidenav.tpl.html b/ui/src/app/components/details-sidenav.tpl.html
index c504a24..763bc22 100644
--- a/ui/src/app/components/details-sidenav.tpl.html
+++ b/ui/src/app/components/details-sidenav.tpl.html
@@ -16,7 +16,7 @@
 
 -->
 <md-sidenav class="md-sidenav-right md-whiteframe-4dp tb-sidenav-details"
-      md-disable-backdrop="true"
+      md-disable-backdrop
       md-is-open="isOpen"
       md-component-id="right"
       layout="column">
diff --git a/ui/src/app/components/js-func.directive.js b/ui/src/app/components/js-func.directive.js
index f95d003..ef77df7 100644
--- a/ui/src/app/components/js-func.directive.js
+++ b/ui/src/app/components/js-func.directive.js
@@ -22,6 +22,8 @@ import thingsboardToast from '../services/toast';
 import thingsboardUtils from '../common/utils.service';
 import thingsboardExpandFullscreen from './expand-fullscreen.directive';
 
+import fixAceEditor from './ace-editor-fix';
+
 /* eslint-disable import/no-unresolved, import/default */
 
 import jsFuncTemplate from './js-func.tpl.html';
@@ -41,6 +43,7 @@ function JsFunc($compile, $templateCache, toast, utils, $translate) {
         var template = $templateCache.get(jsFuncTemplate);
         element.html(template);
 
+        scope.functionName = attrs.functionName;
         scope.functionArgs = scope.$eval(attrs.functionArgs);
         scope.validationArgs = scope.$eval(attrs.validationArgs);
         scope.resultType = attrs.resultType;
@@ -48,6 +51,8 @@ function JsFunc($compile, $templateCache, toast, utils, $translate) {
             scope.resultType = "nocheck";
         }
 
+        scope.validationTriggerArg = attrs.validationTriggerArg;
+
         scope.functionValid = true;
 
         var Range = ace.acequire("ace/range").Range;
@@ -56,7 +61,7 @@ function JsFunc($compile, $templateCache, toast, utils, $translate) {
 
 
         scope.functionArgsString = '';
-        for (var i in scope.functionArgs) {
+        for (var i = 0; i < scope.functionArgs.length; i++) {
             if (scope.functionArgsString.length > 0) {
                 scope.functionArgsString += ', ';
             }
@@ -64,11 +69,15 @@ function JsFunc($compile, $templateCache, toast, utils, $translate) {
         }
 
         scope.onFullscreenChanged = function () {
+            updateEditorSize();
+        };
+
+        function updateEditorSize() {
             if (scope.js_editor) {
                 scope.js_editor.resize();
                 scope.js_editor.renderer.updateFull();
             }
-        };
+        }
 
         scope.jsEditorOptions = {
             useWrapMode: true,
@@ -83,6 +92,7 @@ function JsFunc($compile, $templateCache, toast, utils, $translate) {
                 scope.js_editor.session.on("change", function () {
                     scope.cleanupJsErrors();
                 });
+                fixAceEditor(_ace);
             }
         };
 
@@ -128,6 +138,9 @@ function JsFunc($compile, $templateCache, toast, utils, $translate) {
         scope.validate = function () {
             try {
                 var toValidate = new Function(scope.functionArgsString, scope.functionBody);
+                if (scope.noValidate) {
+                    return true;
+                }
                 var res;
                 var validationError;
                 for (var i=0;i<scope.validationArgs.length;i++) {
@@ -197,9 +210,19 @@ function JsFunc($compile, $templateCache, toast, utils, $translate) {
             }
         };
 
-        scope.$on('form-submit', function () {
-            scope.functionValid = scope.validate();
-            scope.updateValidity();
+        scope.$on('form-submit', function (event, args) {
+            if (!args || scope.validationTriggerArg && scope.validationTriggerArg == args) {
+                scope.validationArgs = scope.$eval(attrs.validationArgs);
+                scope.cleanupJsErrors();
+                scope.functionValid = true;
+                scope.updateValidity();
+                scope.functionValid = scope.validate();
+                scope.updateValidity();
+            }
+        });
+
+        scope.$on('update-ace-editor-size', function () {
+            updateEditorSize();
         });
 
         $compile(element.contents())(scope);
@@ -208,7 +231,11 @@ function JsFunc($compile, $templateCache, toast, utils, $translate) {
     return {
         restrict: "E",
         require: "^ngModel",
-        scope: {},
+        scope: {
+            disabled:'=ngDisabled',
+            noValidate: '=?',
+            fillHeight:'=?'
+        },
         link: linker
     };
 }
diff --git a/ui/src/app/components/js-func.scss b/ui/src/app/components/js-func.scss
index 2bd5df1..d800d5f 100644
--- a/ui/src/app/components/js-func.scss
+++ b/ui/src/app/components/js-func.scss
@@ -15,16 +15,24 @@
  */
 tb-js-func {
   position: relative;
+  .tb-disabled {
+    color: rgba(0,0,0,0.38);
+  }
+  .fill-height {
+    height: 100%;
+  }
 }
 
 .tb-js-func-panel {
   margin-left: 15px;
   border: 1px solid #C0C0C0;
-  height: 100%;
+  height: calc(100% - 80px);
   #tb-javascript-input {
     min-width: 200px;
-    min-height: 200px;
     width: 100%;
     height: 100%;
+    &:not(.fill-height) {
+      min-height: 200px;
+    }
   }
 }
diff --git a/ui/src/app/components/js-func.tpl.html b/ui/src/app/components/js-func.tpl.html
index 806de4a..d048598 100644
--- a/ui/src/app/components/js-func.tpl.html
+++ b/ui/src/app/components/js-func.tpl.html
@@ -15,19 +15,20 @@
     limitations under the License.
 
 -->
-<div style="background: #fff;" tb-expand-fullscreen fullscreen-zindex="100" expand-button-id="expand-button" on-fullscreen-changed="onFullscreenChanged()" layout="column">
+<div style="background: #fff;" ng-class="{'tb-disabled': disabled, 'fill-height': fillHeight}" tb-expand-fullscreen fullscreen-zindex="100" expand-button-id="expand-button" on-fullscreen-changed="onFullscreenChanged()">
 	<div layout="row" layout-align="start center" style="height: 40px;">
-		<span style="font-style: italic;">function({{ functionArgsString }}) {</span>
+		<label class="tb-title no-padding">function {{ functionName }}({{ functionArgsString }}) {</label>
 		<span flex></span>
 		<div id="expand-button" layout="column" aria-label="Fullscreen" class="md-button md-icon-button tb-md-32 tb-fullscreen-button-style"></div>
 	</div>
-	<div flex id="tb-javascript-panel" class="tb-js-func-panel" layout="column">
-		<div flex id="tb-javascript-input"
-			 ui-ace="jsEditorOptions" 
+	<div id="tb-javascript-panel" class="tb-js-func-panel">
+		<div id="tb-javascript-input" ng-class="{'fill-height': fillHeight}"
+			 ui-ace="jsEditorOptions"
+			 ng-readonly="disabled"
 			 ng-model="functionBody">
 		</div>
 	</div>
 	<div layout="row" layout-align="start center"  style="height: 40px;">
-		<span style="font-style: italic;">}</span>
-	</div>	   
-</div>
\ No newline at end of file
+		<label class="tb-title no-padding">}</label>
+	</div>
+</div>
diff --git a/ui/src/app/components/json-content.directive.js b/ui/src/app/components/json-content.directive.js
new file mode 100644
index 0000000..84f8417
--- /dev/null
+++ b/ui/src/app/components/json-content.directive.js
@@ -0,0 +1,175 @@
+/*
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.
+ */
+import './json-content.scss';
+
+import 'brace/ext/language_tools';
+import 'brace/mode/json';
+import 'brace/mode/text';
+import 'ace-builds/src-min-noconflict/snippets/json';
+import 'ace-builds/src-min-noconflict/snippets/text';
+
+import fixAceEditor from './ace-editor-fix';
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import jsonContentTemplate from './json-content.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+export default angular.module('thingsboard.directives.jsonContent', [])
+    .directive('tbJsonContent', JsonContent)
+    .name;
+
+/*@ngInject*/
+function JsonContent($compile, $templateCache, toast, types, utils) {
+
+    var linker = function (scope, element, attrs, ngModelCtrl) {
+        var template = $templateCache.get(jsonContentTemplate);
+        element.html(template);
+
+        scope.label = attrs.label;
+
+        scope.validationTriggerArg = attrs.validationTriggerArg;
+
+        scope.contentValid = true;
+
+        scope.json_editor;
+
+        scope.onFullscreenChanged = function () {
+            updateEditorSize();
+        };
+
+        function updateEditorSize() {
+            if (scope.json_editor) {
+                scope.json_editor.resize();
+                scope.json_editor.renderer.updateFull();
+            }
+        }
+
+        var mode;
+        if (scope.contentType) {
+            mode = types.contentType[scope.contentType].code;
+        } else {
+            mode = 'text';
+        }
+
+        scope.jsonEditorOptions = {
+            useWrapMode: true,
+            mode: mode,
+            advanced: {
+                enableSnippets: true,
+                enableBasicAutocompletion: true,
+                enableLiveAutocompletion: true
+            },
+            onLoad: function (_ace) {
+                scope.json_editor = _ace;
+                scope.json_editor.session.on("change", function () {
+                    scope.cleanupJsonErrors();
+                });
+                fixAceEditor(_ace);
+            }
+        };
+
+        scope.$watch('contentType', () => {
+            var mode;
+            if (scope.contentType) {
+                mode = types.contentType[scope.contentType].code;
+            } else {
+                mode = 'text';
+            }
+            if (scope.json_editor) {
+                scope.json_editor.session.setMode('ace/mode/' + mode);
+            }
+        });
+
+        scope.cleanupJsonErrors = function () {
+            toast.hide();
+        };
+
+        scope.updateValidity = function () {
+            ngModelCtrl.$setValidity('contentBody', scope.contentValid);
+        };
+
+        scope.$watch('contentBody', function (newContent, oldContent) {
+            ngModelCtrl.$setViewValue(scope.contentBody);
+            if (!angular.equals(newContent, oldContent)) {
+                scope.contentValid = true;
+            }
+            scope.updateValidity();
+        });
+
+        ngModelCtrl.$render = function () {
+            scope.contentBody = ngModelCtrl.$viewValue;
+        };
+
+        scope.showError = function (error) {
+            var toastParent = angular.element('#tb-json-panel', element);
+            toast.showError(error, toastParent, 'bottom left');
+        };
+
+        scope.validate = function () {
+            try {
+                if (scope.validateContent) {
+                    if (scope.contentType == types.contentType.JSON.value) {
+                        angular.fromJson(scope.contentBody);
+                    }
+                }
+                return true;
+            } catch (e) {
+                var details = utils.parseException(e);
+                var errorInfo = 'Error:';
+                if (details.name) {
+                    errorInfo += ' ' + details.name + ':';
+                }
+                if (details.message) {
+                    errorInfo += ' ' + details.message;
+                }
+                scope.showError(errorInfo);
+                return false;
+            }
+        };
+
+        scope.$on('form-submit', function (event, args) {
+            if (!scope.readonly) {
+                if (!args || scope.validationTriggerArg && scope.validationTriggerArg == args) {
+                    scope.cleanupJsonErrors();
+                    scope.contentValid = true;
+                    scope.updateValidity();
+                    scope.contentValid = scope.validate();
+                    scope.updateValidity();
+                }
+            }
+        });
+
+        scope.$on('update-ace-editor-size', function () {
+            updateEditorSize();
+        });
+
+        $compile(element.contents())(scope);
+    }
+
+    return {
+        restrict: "E",
+        require: "^ngModel",
+        scope: {
+            contentType: '=',
+            validateContent: '=?',
+            readonly:'=ngReadonly',
+            fillHeight:'=?'
+        },
+        link: linker
+    };
+}
diff --git a/ui/src/app/components/json-content.scss b/ui/src/app/components/json-content.scss
new file mode 100644
index 0000000..db57451
--- /dev/null
+++ b/ui/src/app/components/json-content.scss
@@ -0,0 +1,35 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.
+ */
+tb-json-content {
+  position: relative;
+  .fill-height {
+    height: 100%;
+  }
+}
+
+.tb-json-content-panel {
+  margin-left: 15px;
+  border: 1px solid #C0C0C0;
+  height: 100%;
+  #tb-json-input {
+    min-width: 200px;
+    width: 100%;
+    height: 100%;
+    &:not(.fill-height) {
+      min-height: 200px;
+    }
+  }
+}
diff --git a/ui/src/app/components/json-content.tpl.html b/ui/src/app/components/json-content.tpl.html
new file mode 100644
index 0000000..4fad30e
--- /dev/null
+++ b/ui/src/app/components/json-content.tpl.html
@@ -0,0 +1,31 @@
+<!--
+
+    Copyright © 2016-2018 The Thingsboard Authors
+
+    Licensed 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.
+
+-->
+<div style="background: #fff;" ng-class="{'fill-height': fillHeight}" tb-expand-fullscreen fullscreen-zindex="100" expand-button-id="expand-button" on-fullscreen-changed="onFullscreenChanged()" layout="column">
+    <div layout="row" layout-align="start center">
+        <label class="tb-title no-padding">{{ label }}</label>
+        <span flex></span>
+        <md-button id="expand-button" aria-label="Fullscreen" class="md-icon-button tb-md-32 tb-fullscreen-button-style"></md-button>
+    </div>
+    <div flex id="tb-json-panel" class="tb-json-content-panel" layout="column">
+        <div flex id="tb-json-input" ng-class="{'fill-height': fillHeight}"
+             ng-readonly="readonly"
+             ui-ace="jsonEditorOptions"
+             ng-model="contentBody">
+        </div>
+    </div>
+</div>
diff --git a/ui/src/app/components/json-object-edit.directive.js b/ui/src/app/components/json-object-edit.directive.js
new file mode 100644
index 0000000..215b7b9
--- /dev/null
+++ b/ui/src/app/components/json-object-edit.directive.js
@@ -0,0 +1,183 @@
+/*
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.
+ */
+import './json-object-edit.scss';
+
+import 'brace/ext/language_tools';
+import 'brace/mode/json';
+import 'ace-builds/src-min-noconflict/snippets/json';
+
+import fixAceEditor from './ace-editor-fix';
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import jsonObjectEditTemplate from './json-object-edit.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+export default angular.module('thingsboard.directives.jsonObjectEdit', [])
+    .directive('tbJsonObjectEdit', JsonObjectEdit)
+    .name;
+
+/*@ngInject*/
+function JsonObjectEdit($compile, $templateCache, $document, toast, utils) {
+
+    var linker = function (scope, element, attrs, ngModelCtrl) {
+        var template = $templateCache.get(jsonObjectEditTemplate);
+        element.html(template);
+
+        scope.label = attrs.label;
+
+        scope.objectValid = true;
+        scope.validationError = '';
+
+        scope.json_editor;
+
+        scope.onFullscreenChanged = function () {
+            updateEditorSize();
+        };
+
+        function updateEditorSize() {
+            if (scope.json_editor) {
+                scope.json_editor.resize();
+                scope.json_editor.renderer.updateFull();
+            }
+        }
+
+        scope.jsonEditorOptions = {
+            useWrapMode: true,
+            mode: 'json',
+            advanced: {
+                enableSnippets: true,
+                enableBasicAutocompletion: true,
+                enableLiveAutocompletion: true
+            },
+            onLoad: function (_ace) {
+                scope.json_editor = _ace;
+                scope.json_editor.session.on("change", function () {
+                    scope.cleanupJsonErrors();
+                });
+                fixAceEditor(_ace);
+            }
+        };
+
+        scope.cleanupJsonErrors = function () {
+            toast.hide();
+        };
+
+        scope.updateValidity = function () {
+            ngModelCtrl.$setValidity('objectValid', scope.objectValid);
+        };
+
+        scope.$watch('contentBody', function (newVal, prevVal) {
+            if (!angular.equals(newVal, prevVal)) {
+                var object = scope.validate();
+                if (scope.objectValid) {
+                    if (object == null) {
+                        scope.object = null;
+                    } else {
+                        if (scope.object == null) {
+                            scope.object = {};
+                        }
+                        Object.keys(scope.object).forEach(function (key) {
+                            delete scope.object[key];
+                        });
+                        Object.keys(object).forEach(function (key) {
+                            scope.object[key] = object[key];
+                        });
+                    }
+                    ngModelCtrl.$setViewValue(scope.object);
+                }
+                scope.updateValidity();
+            }
+        });
+
+        ngModelCtrl.$render = function () {
+            scope.object = ngModelCtrl.$viewValue;
+            var content = '';
+            try {
+                if (scope.object) {
+                    content = angular.toJson(scope.object, true);
+                }
+            } catch (e) {
+                //
+            }
+            scope.contentBody = content;
+        };
+
+        scope.showError = function (error) {
+            var toastParent = angular.element('#tb-json-panel', element);
+            toast.showError(error, toastParent, 'bottom left');
+        };
+
+        scope.validate = function () {
+            if (!scope.contentBody || !scope.contentBody.length) {
+                if (scope.required) {
+                    scope.validationError = 'Json object is required.';
+                    scope.objectValid = false;
+                } else {
+                    scope.validationError = '';
+                    scope.objectValid = true;
+                }
+                return null;
+            } else {
+                try {
+                    var object = angular.fromJson(scope.contentBody);
+                    scope.validationError = '';
+                    scope.objectValid = true;
+                    return object;
+                } catch (e) {
+                    var details = utils.parseException(e);
+                    var errorInfo = 'Error:';
+                    if (details.name) {
+                        errorInfo += ' ' + details.name + ':';
+                    }
+                    if (details.message) {
+                        errorInfo += ' ' + details.message;
+                    }
+                    scope.validationError = errorInfo;
+                    scope.objectValid = false;
+                    return null;
+                }
+            }
+        };
+
+        scope.$on('form-submit', function () {
+            if (!scope.readonly) {
+                scope.cleanupJsonErrors();
+                if (!scope.objectValid) {
+                    scope.showError(scope.validationError);
+                }
+            }
+        });
+
+        scope.$on('update-ace-editor-size', function () {
+            updateEditorSize();
+        });
+
+        $compile(element.contents())(scope);
+    }
+
+    return {
+        restrict: "E",
+        require: "^ngModel",
+        scope: {
+            required:'=ngRequired',
+            readonly:'=ngReadonly',
+            fillHeight:'=?'
+        },
+        link: linker
+    };
+}
diff --git a/ui/src/app/components/json-object-edit.scss b/ui/src/app/components/json-object-edit.scss
new file mode 100644
index 0000000..232d69a
--- /dev/null
+++ b/ui/src/app/components/json-object-edit.scss
@@ -0,0 +1,35 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.
+ */
+tb-json-object-edit {
+  position: relative;
+  .fill-height {
+    height: 100%;
+  }
+}
+
+.tb-json-object-panel {
+  margin-left: 15px;
+  border: 1px solid #C0C0C0;
+  height: 100%;
+  #tb-json-input {
+    min-width: 200px;
+    width: 100%;
+    height: 100%;
+    &:not(.fill-height) {
+      min-height: 200px;
+    }
+  }
+}
diff --git a/ui/src/app/components/json-object-edit.tpl.html b/ui/src/app/components/json-object-edit.tpl.html
new file mode 100644
index 0000000..ebab3c7
--- /dev/null
+++ b/ui/src/app/components/json-object-edit.tpl.html
@@ -0,0 +1,34 @@
+<!--
+
+    Copyright © 2016-2018 The Thingsboard Authors
+
+    Licensed 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.
+
+-->
+<div style="background: #fff;" ng-class="{'fill-height': fillHeight}" tb-expand-fullscreen fullscreen-zindex="100" expand-button-id="expand-button" on-fullscreen-changed="onFullscreenChanged()" layout="column">
+    <div layout="row" layout-align="start center">
+        <label class="tb-title no-padding"
+               ng-class="{'tb-required': required,
+                          'tb-readonly': readonly,
+                          'tb-error': !objectValid}">{{ label }}</label>
+        <span flex></span>
+        <md-button id="expand-button" aria-label="Fullscreen" class="md-icon-button tb-md-32 tb-fullscreen-button-style"></md-button>
+    </div>
+    <div flex id="tb-json-panel" class="tb-json-object-panel" layout="column">
+        <div flex id="tb-json-input" ng-class="{'fill-height': fillHeight}"
+             ng-readonly="readonly"
+             ui-ace="jsonEditorOptions"
+             ng-model="contentBody">
+        </div>
+    </div>
+</div>
diff --git a/ui/src/app/components/kv-map.directive.js b/ui/src/app/components/kv-map.directive.js
new file mode 100644
index 0000000..bc8865f
--- /dev/null
+++ b/ui/src/app/components/kv-map.directive.js
@@ -0,0 +1,119 @@
+/*
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.
+ */
+import './kv-map.scss';
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import kvMapTemplate from './kv-map.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+export default angular.module('thingsboard.directives.keyValMap', [])
+    .directive('tbKeyValMap', KeyValMap)
+    .name;
+
+/*@ngInject*/
+function KeyValMap() {
+    return {
+        restrict: "E",
+        scope: true,
+        bindToController: {
+            disabled:'=ngDisabled',
+            titleText: '@?',
+            keyPlaceholderText: '@?',
+            valuePlaceholderText: '@?',
+            noDataText: '@?',
+            keyValMap: '='
+        },
+        controller: KeyValMapController,
+        controllerAs: 'vm',
+        templateUrl: kvMapTemplate
+    };
+}
+
+/*@ngInject*/
+function KeyValMapController($scope, $mdUtil) {
+
+    let vm = this;
+
+    vm.kvList = [];
+
+    vm.removeKeyVal = removeKeyVal;
+    vm.addKeyVal = addKeyVal;
+
+    $scope.$watch('vm.keyValMap', () => {
+        stopWatchKvList();
+        vm.kvList.length = 0;
+        if (vm.keyValMap) {
+            for (var property in vm.keyValMap) {
+                if (vm.keyValMap.hasOwnProperty(property)) {
+                    vm.kvList.push(
+                        {
+                            key: property + '',
+                            value: vm.keyValMap[property]
+                        }
+                    );
+                }
+            }
+        }
+        $mdUtil.nextTick(() => {
+            watchKvList();
+        });
+    });
+
+    function watchKvList() {
+        $scope.kvListWatcher = $scope.$watch('vm.kvList', () => {
+            if (!vm.keyValMap) {
+                return;
+            }
+            for (var property in vm.keyValMap) {
+                if (vm.keyValMap.hasOwnProperty(property)) {
+                    delete vm.keyValMap[property];
+                }
+            }
+            for (var i=0;i<vm.kvList.length;i++) {
+                var entry = vm.kvList[i];
+                vm.keyValMap[entry.key] = entry.value;
+            }
+        }, true);
+    }
+
+    function stopWatchKvList() {
+        if ($scope.kvListWatcher) {
+            $scope.kvListWatcher();
+            $scope.kvListWatcher = null;
+        }
+    }
+
+
+    function removeKeyVal(index) {
+        if (index > -1) {
+            vm.kvList.splice(index, 1);
+        }
+    }
+
+    function addKeyVal() {
+        if (!vm.kvList) {
+            vm.kvList = [];
+        }
+        vm.kvList.push(
+            {
+                key: '',
+                value: ''
+            }
+        );
+    }
+}
diff --git a/ui/src/app/components/kv-map.tpl.html b/ui/src/app/components/kv-map.tpl.html
new file mode 100644
index 0000000..3e550f3
--- /dev/null
+++ b/ui/src/app/components/kv-map.tpl.html
@@ -0,0 +1,58 @@
+<!--
+
+    Copyright © 2016-2018 The Thingsboard Authors
+
+    Licensed 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.
+
+-->
+<section layout="column" class="tb-kv-map">
+    <label translate class="tb-title no-padding">{{ vm.titleText }}</label>
+    <div layout="row"
+         ng-repeat="keyVal in vm.kvList track by $index"
+         style="max-height: 40px;" layout-align="start center">
+        <md-input-container flex md-no-float class="md-block"
+                            style="margin: 10px 0px 0px 0px; max-height: 40px;">
+            <input placeholder="{{ (vm.keyPlaceholderText ? vm.keyPlaceholderText : 'key-val.key') | translate }}"
+                   ng-disabled="vm.disabled" ng-required="true" name="key" ng-model="keyVal.key">
+        </md-input-container>
+        <md-input-container flex md-no-float class="md-block"
+                            style="margin: 10px 0px 0px 0px; max-height: 40px;">
+            <input placeholder="{{ (vm.valuePlaceholderText ? vm.valuePlaceholderText : 'key-val.value') | translate }}"
+                   ng-disabled="vm.disabled" ng-required="true" name="value" ng-model="keyVal.value">
+        </md-input-container>
+        <md-button ng-show="!vm.disabled" ng-disabled="$root.loading" class="md-icon-button md-primary"
+                   ng-click="vm.removeKeyVal($index)"
+                   aria-label="{{ 'action.remove' | translate }}">
+            <md-tooltip md-direction="top">
+                {{ 'key-val.remove-entry' | translate }}
+            </md-tooltip>
+            <md-icon aria-label="{{ 'action.delete' | translate }}"
+                     class="material-icons">
+                close
+            </md-icon>
+        </md-button>
+    </div>
+    <span ng-show="!vm.kvList.length"
+          layout-align="center center" ng-class="{'disabled': vm.disabled}"
+          class="no-data-found" translate>{{vm.noDataText ? vm.noDataText : 'key-val.no-data'}}</span>
+    <div>
+        <md-button ng-show="!vm.disabled" ng-disabled="$root.loading" class="md-primary md-raised"
+                   ng-click="vm.addKeyVal()"
+                   aria-label="{{ 'action.add' | translate }}">
+            <md-tooltip md-direction="top">
+                {{ 'key-val.add-entry' | translate }}
+            </md-tooltip>
+            <span translate>action.add</span>
+        </md-button>
+    </div>
+</section>
diff --git a/ui/src/app/components/mousepoint-menu.directive.js b/ui/src/app/components/mousepoint-menu.directive.js
index 2a0ab52..9015268 100644
--- a/ui/src/app/components/mousepoint-menu.directive.js
+++ b/ui/src/app/components/mousepoint-menu.directive.js
@@ -27,7 +27,12 @@ function MousepointMenu() {
                 var offset = $element.offset();
                 var x = $event.pageX - offset.left;
                 var y = $event.pageY - offset.top;
-
+                if ($attrs.tbOffsetX) {
+                    x += Number($attrs.tbOffsetX);
+                }
+                if ($attrs.tbOffsetY) {
+                    y += Number($attrs.tbOffsetY);
+                }
                 var offsets = {
                     left: x,
                     top: y
diff --git a/ui/src/app/components/react/json-form-ace-editor.jsx b/ui/src/app/components/react/json-form-ace-editor.jsx
index 1c4c02e..5afd3d1 100644
--- a/ui/src/app/components/react/json-form-ace-editor.jsx
+++ b/ui/src/app/components/react/json-form-ace-editor.jsx
@@ -23,6 +23,8 @@ import FlatButton from 'material-ui/FlatButton';
 import 'brace/ext/language_tools';
 import 'brace/theme/github';
 
+import fixAceEditor from './../ace-editor-fix';
+
 class ThingsboardAceEditor extends React.Component {
 
     constructor(props) {
@@ -31,6 +33,7 @@ class ThingsboardAceEditor extends React.Component {
         this.onBlur = this.onBlur.bind(this);
         this.onFocus = this.onFocus.bind(this);
         this.onTidy = this.onTidy.bind(this);
+        this.onLoad = this.onLoad.bind(this);
         var value = props.value ? props.value + '' : '';
         this.state = {
             value: value,
@@ -72,6 +75,10 @@ class ThingsboardAceEditor extends React.Component {
         }
     }
 
+    onLoad(editor) {
+        fixAceEditor(editor);
+    }
+
     render() {
 
         const styles = reactCSS({
@@ -117,6 +124,7 @@ class ThingsboardAceEditor extends React.Component {
                                onChange={this.onValueChanged}
                                onFocus={this.onFocus}
                                onBlur={this.onBlur}
+                               onLoad={this.onLoad}
                                name={this.props.form.title}
                                value={this.state.value}
                                readOnly={this.props.form.readonly}
diff --git a/ui/src/app/components/widget/widget-config.directive.js b/ui/src/app/components/widget/widget-config.directive.js
index d0ee6a3..4d4d958 100644
--- a/ui/src/app/components/widget/widget-config.directive.js
+++ b/ui/src/app/components/widget/widget-config.directive.js
@@ -23,6 +23,8 @@ import thingsboardJsonForm from '../json-form.directive';
 import thingsboardManageWidgetActions from './action/manage-widget-actions.directive';
 import 'angular-ui-ace';
 
+import fixAceEditor from './../ace-editor-fix';
+
 import './widget-config.scss';
 
 /* eslint-disable import/no-unresolved, import/default */
@@ -72,6 +74,9 @@ function WidgetConfig($compile, $templateCache, $rootScope, $translate, $timeout
                 enableSnippets: true,
                 enableBasicAutocompletion: true,
                 enableLiveAutocompletion: true
+            },
+            onLoad: function (_ace) {
+                fixAceEditor(_ace);
             }
         };
 
diff --git a/ui/src/app/entity/entity-autocomplete.directive.js b/ui/src/app/entity/entity-autocomplete.directive.js
index c2053c0..2dfc3be 100644
--- a/ui/src/app/entity/entity-autocomplete.directive.js
+++ b/ui/src/app/entity/entity-autocomplete.directive.js
@@ -143,6 +143,12 @@ export default function EntityAutocomplete($compile, $templateCache, $q, $filter
                     scope.noEntitiesMatchingText = 'plugin.no-plugins-matching';
                     scope.entityRequiredText = 'plugin.plugin-required';
                     break;
+                case types.entityType.rulechain:
+                    scope.selectEntityText = 'rulechain.select-rulechain';
+                    scope.entityText = 'rulechain.rulechain';
+                    scope.noEntitiesMatchingText = 'rulechain.no-rulechains-matching';
+                    scope.entityRequiredText = 'rulechain.rulechain-required';
+                    break;
                 case types.entityType.tenant:
                     scope.selectEntityText = 'tenant.select-tenant';
                     scope.entityText = 'tenant.tenant';
diff --git a/ui/src/app/entity/entity-type-list.tpl.html b/ui/src/app/entity/entity-type-list.tpl.html
index eb32124..d7d3918 100644
--- a/ui/src/app/entity/entity-type-list.tpl.html
+++ b/ui/src/app/entity/entity-type-list.tpl.html
@@ -16,8 +16,7 @@
 
 -->
 <section flex layout='column' class="tb-entity-type-list">
-    <md-chips flex
-              readonly="disabled"
+    <md-chips readonly="disabled"
               id="entity_type_list_chips"
               ng-required="tbRequired"
               ng-model="entityTypeList"
diff --git a/ui/src/app/entity/relation/relation-filters.directive.js b/ui/src/app/entity/relation/relation-filters.directive.js
index 00d3b26..9ab66ca 100644
--- a/ui/src/app/entity/relation/relation-filters.directive.js
+++ b/ui/src/app/entity/relation/relation-filters.directive.js
@@ -46,6 +46,7 @@ export default function RelationFilters($compile, $templateCache) {
         ngModelCtrl.$render = function () {
             if (ngModelCtrl.$viewValue) {
                 var value = ngModelCtrl.$viewValue;
+                scope.relationFilters.length = 0;
                 value.forEach(function (filter) {
                     scope.relationFilters.push(filter);
                 });
diff --git a/ui/src/app/entity/relation/relation-filters.scss b/ui/src/app/entity/relation/relation-filters.scss
index 649879d..34254d0 100644
--- a/ui/src/app/entity/relation/relation-filters.scss
+++ b/ui/src/app/entity/relation/relation-filters.scss
@@ -51,9 +51,6 @@
       md-chips-wrap {
         padding: 0px;
         margin: 0 0 24px;
-        .md-chip-input-container {
-          margin: 0;
-        }
         md-autocomplete {
           height: 30px;
           md-autocomplete-wrap {
diff --git a/ui/src/app/event/event.scss b/ui/src/app/event/event.scss
index b3be35c..b0fc46f 100644
--- a/ui/src/app/event/event.scss
+++ b/ui/src/app/event/event.scss
@@ -24,6 +24,17 @@ md-list.tb-event-table {
     height: 48px;
     padding: 0px;
     overflow: hidden;
+    .tb-cell {
+      text-overflow: ellipsis;
+      &.tb-scroll {
+        white-space: nowrap;
+        overflow-y: hidden;
+        overflow-x: auto;
+      }
+      &.tb-nowrap {
+        white-space: nowrap;
+      }
+    }
   }
 
   .tb-row:hover {
@@ -39,13 +50,19 @@ md-list.tb-event-table {
         color: rgba(0,0,0,.54);
         font-size: 12px;
         font-weight: 700;
-        white-space: nowrap;
         background: none;
+        white-space: nowrap;
       }
   }
 
   .tb-cell {
-      padding: 0 24px;
+      &:first-child {
+        padding-left: 14px;
+      }
+      &:last-child {
+        padding-right: 14px;
+      }
+      padding: 0 6px;
       margin: auto 0;
       color: rgba(0,0,0,.87);
       font-size: 13px;
@@ -53,8 +70,8 @@ md-list.tb-event-table {
       text-align: left;
       overflow: hidden;
       .md-button {
-        padding: 0;
-        margin: 0;
+          padding: 0;
+          margin: 0;
       }
   }
 
diff --git a/ui/src/app/event/event-content-dialog.controller.js b/ui/src/app/event/event-content-dialog.controller.js
index 108f95e..b780b78 100644
--- a/ui/src/app/event/event-content-dialog.controller.js
+++ b/ui/src/app/event/event-content-dialog.controller.js
@@ -17,11 +17,14 @@ import $ from 'jquery';
 import 'brace/ext/language_tools';
 import 'brace/mode/java';
 import 'brace/theme/github';
+import beautify from 'js-beautify';
 
 /* eslint-disable angular/angularelement */
 
+const js_beautify = beautify.js;
+
 /*@ngInject*/
-export default function EventContentDialogController($mdDialog, content, title, showingCallback) {
+export default function EventContentDialogController($mdDialog, types, content, contentType, title, showingCallback) {
 
     var vm = this;
 
@@ -32,9 +35,19 @@ export default function EventContentDialogController($mdDialog, content, title, 
     vm.content = content;
     vm.title = title;
 
+    var mode;
+    if (contentType) {
+        mode = types.contentType[contentType].code;
+        if (contentType == types.contentType.JSON.value && vm.content) {
+            vm.content = js_beautify(vm.content, {indent_size: 4});
+        }
+    } else {
+        mode = 'java';
+    }
+
     vm.contentOptions = {
         useWrapMode: false,
-        mode: 'java',
+        mode: mode,
         showGutter: false,
         showPrintMargin: false,
         theme: 'github',
@@ -55,7 +68,7 @@ export default function EventContentDialogController($mdDialog, content, title, 
             var lines = vm.content.split('\n');
             newHeight = 16 * lines.length + 16;
             var maxLineLength = 0;
-            for (var i in lines) {
+            for (var i = 0; i < lines.length; i++) {
                 var line = lines[i].replace(/\t/g, '    ').replace(/\n/g, '');
                 var lineLength = line.length;
                 maxLineLength = Math.max(maxLineLength, lineLength);
diff --git a/ui/src/app/event/event-header.directive.js b/ui/src/app/event/event-header.directive.js
index afac804..bc4cdbe 100644
--- a/ui/src/app/event/event-header.directive.js
+++ b/ui/src/app/event/event-header.directive.js
@@ -18,6 +18,7 @@
 import eventHeaderLcEventTemplate from './event-header-lc-event.tpl.html';
 import eventHeaderStatsTemplate from './event-header-stats.tpl.html';
 import eventHeaderErrorTemplate from './event-header-error.tpl.html';
+import eventHeaderDebugRuleNodeTemplate from './event-header-debug-rulenode.tpl.html';
 
 /* eslint-enable import/no-unresolved, import/default */
 
@@ -38,6 +39,12 @@ export default function EventHeaderDirective($compile, $templateCache, types) {
                 case types.eventType.error.value:
                     template = eventHeaderErrorTemplate;
                     break;
+                case types.debugEventType.debugRuleNode.value:
+                    template = eventHeaderDebugRuleNodeTemplate;
+                    break;
+                case types.debugEventType.debugRuleChain.value:
+                    template = eventHeaderDebugRuleNodeTemplate;
+                    break;
             }
             return $templateCache.get(template);
         }
diff --git a/ui/src/app/event/event-header-debug-rulenode.tpl.html b/ui/src/app/event/event-header-debug-rulenode.tpl.html
new file mode 100644
index 0000000..34f4513
--- /dev/null
+++ b/ui/src/app/event/event-header-debug-rulenode.tpl.html
@@ -0,0 +1,27 @@
+<!--
+
+    Copyright © 2016-2018 The Thingsboard Authors
+
+    Licensed 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.
+
+-->
+<div hide-xs hide-sm translate class="tb-cell" flex="25">event.event-time</div>
+<div translate class="tb-cell" flex="20">event.server</div>
+<div translate class="tb-cell" flex="10">event.type</div>
+<div translate class="tb-cell" flex="15">event.entity</div>
+<div translate class="tb-cell" flex="20">event.message-id</div>
+<div translate class="tb-cell" flex="20">event.message-type</div>
+<div translate class="tb-cell" flex="15">event.data-type</div>
+<div translate class="tb-cell" flex="10">event.data</div>
+<div translate class="tb-cell" flex="10">event.metadata</div>
+<div translate class="tb-cell" flex="10">event.error</div>
diff --git a/ui/src/app/event/event-row.directive.js b/ui/src/app/event/event-row.directive.js
index f005542..b808fb8 100644
--- a/ui/src/app/event/event-row.directive.js
+++ b/ui/src/app/event/event-row.directive.js
@@ -20,6 +20,7 @@ import eventErrorDialogTemplate from './event-content-dialog.tpl.html';
 import eventRowLcEventTemplate from './event-row-lc-event.tpl.html';
 import eventRowStatsTemplate from './event-row-stats.tpl.html';
 import eventRowErrorTemplate from './event-row-error.tpl.html';
+import eventRowDebugRuleNodeTemplate from './event-row-debug-rulenode.tpl.html';
 
 /* eslint-enable import/no-unresolved, import/default */
 
@@ -40,6 +41,12 @@ export default function EventRowDirective($compile, $templateCache, $mdDialog, $
                 case types.eventType.error.value:
                     template = eventRowErrorTemplate;
                     break;
+                case types.debugEventType.debugRuleNode.value:
+                    template = eventRowDebugRuleNodeTemplate;
+                    break;
+                case types.debugEventType.debugRuleChain.value:
+                    template = eventRowDebugRuleNodeTemplate;
+                    break;
             }
             return $templateCache.get(template);
         }
@@ -53,17 +60,22 @@ export default function EventRowDirective($compile, $templateCache, $mdDialog, $
             scope.loadTemplate();
         });
 
+        scope.types = types;
+
         scope.event = attrs.event;
 
-        scope.showContent = function($event, content, title) {
+        scope.showContent = function($event, content, title, contentType) {
             var onShowingCallback = {
                 onShowing: function(){}
             }
+            if (!contentType) {
+                contentType = null;
+            }
             $mdDialog.show({
                 controller: 'EventContentDialogController',
                 controllerAs: 'vm',
                 templateUrl: eventErrorDialogTemplate,
-                locals: {content: content, title: title, showingCallback: onShowingCallback},
+                locals: {content: content, title: title, contentType: contentType, showingCallback: onShowingCallback},
                 parent: angular.element($document[0].body),
                 fullscreen: true,
                 targetEvent: $event,
@@ -74,6 +86,14 @@ export default function EventRowDirective($compile, $templateCache, $mdDialog, $
             });
         }
 
+        scope.checkTooltip = function($event) {
+            var el = $event.target;
+            var $el = angular.element(el);
+            if(el.offsetWidth < el.scrollWidth && !$el.attr('title')){
+                $el.attr('title', $el.text());
+            }
+        }
+
         $compile(element.contents())(scope);
     }
 
diff --git a/ui/src/app/event/event-row-debug-rulenode.tpl.html b/ui/src/app/event/event-row-debug-rulenode.tpl.html
new file mode 100644
index 0000000..bb832b1
--- /dev/null
+++ b/ui/src/app/event/event-row-debug-rulenode.tpl.html
@@ -0,0 +1,63 @@
+<!--
+
+    Copyright © 2016-2018 The Thingsboard Authors
+
+    Licensed 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.
+
+-->
+<div hide-xs hide-sm class="tb-cell" flex="25">{{event.createdTime | date :  'yyyy-MM-dd HH:mm:ss'}}</div>
+<div class="tb-cell" flex="20">{{event.body.server}}</div>
+<div class="tb-cell" flex="10">{{event.body.type}}</div>
+<div class="tb-cell" flex="15">{{event.body.entityName}}</div>
+<div class="tb-cell tb-nowrap" flex="20" ng-mouseenter="checkTooltip($event)">{{event.body.msgId}}</div>
+<div class="tb-cell" flex="20" ng-mouseenter="checkTooltip($event)">{{event.body.msgType}}</div>
+<div class="tb-cell" flex="15">{{event.body.dataType}}</div>
+<div class="tb-cell" flex="10">
+    <md-button ng-if="event.body.data" class="md-icon-button md-primary"
+               ng-click="showContent($event, event.body.data, 'event.data', event.body.dataType)"
+               aria-label="{{ 'action.view' | translate }}">
+        <md-tooltip md-direction="top">
+            {{ 'action.view' | translate }}
+        </md-tooltip>
+        <md-icon aria-label="{{ 'action.view' | translate }}"
+                 class="material-icons">
+            more_horiz
+        </md-icon>
+    </md-button>
+</div>
+<div class="tb-cell" flex="10">
+    <md-button ng-if="event.body.metadata" class="md-icon-button md-primary"
+               ng-click="showContent($event, event.body.metadata, 'event.metadata', 'JSON')"
+               aria-label="{{ 'action.view' | translate }}">
+        <md-tooltip md-direction="top">
+            {{ 'action.view' | translate }}
+        </md-tooltip>
+        <md-icon aria-label="{{ 'action.view' | translate }}"
+                 class="material-icons">
+            more_horiz
+        </md-icon>
+    </md-button>
+</div>
+<div class="tb-cell" flex="10">
+    <md-button ng-if="event.body.error" class="md-icon-button md-primary"
+               ng-click="showContent($event, event.body.error, 'event.error')"
+               aria-label="{{ 'action.view' | translate }}">
+        <md-tooltip md-direction="top">
+            {{ 'action.view' | translate }}
+        </md-tooltip>
+        <md-icon aria-label="{{ 'action.view' | translate }}"
+                 class="material-icons">
+            more_horiz
+        </md-icon>
+    </md-button>
+</div>
diff --git a/ui/src/app/event/event-table.directive.js b/ui/src/app/event/event-table.directive.js
index 4291014..c61078d 100644
--- a/ui/src/app/event/event-table.directive.js
+++ b/ui/src/app/event/event-table.directive.js
@@ -36,8 +36,8 @@ export default function EventTableDirective($compile, $templateCache, $rootScope
             for (var type in types.eventType) {
                 var eventType = types.eventType[type];
                 var enabled = true;
-                for (var disabledType in disabledEventTypes) {
-                    if (eventType.value === disabledEventTypes[disabledType]) {
+                for (var i=0;i<disabledEventTypes.length;i++) {
+                    if (eventType.value === disabledEventTypes[i]) {
                         enabled = false;
                         break;
                     }
@@ -47,7 +47,19 @@ export default function EventTableDirective($compile, $templateCache, $rootScope
                 }
             }
         } else {
-            scope.eventTypes = types.eventType;
+            scope.eventTypes = angular.copy(types.eventType);
+        }
+
+        if (attrs.debugEventTypes) {
+            var debugEventTypes = attrs.debugEventTypes.split(',');
+            for (i=0;i<debugEventTypes.length;i++) {
+                for (type in types.debugEventType) {
+                    eventType = types.debugEventType[type];
+                    if (eventType.value === debugEventTypes[i]) {
+                        scope.eventTypes[type] = eventType;
+                    }
+                }
+            }
         }
 
         scope.eventType = attrs.defaultEventType;
diff --git a/ui/src/app/extension/extensions-forms/extension-form-opc.directive.js b/ui/src/app/extension/extensions-forms/extension-form-opc.directive.js
index bf4886c..e1d2519 100644
--- a/ui/src/app/extension/extensions-forms/extension-form-opc.directive.js
+++ b/ui/src/app/extension/extensions-forms/extension-form-opc.directive.js
@@ -128,8 +128,8 @@ export default function ExtensionFormOpcDirective($compile, $templateCache, $tra
                         let addedFile = event.target.result;
 
                         if (addedFile && addedFile.length > 0) {
-                            model[options.fileName] = $file.name;
-                            model[options.file] = addedFile.replace(/^data.*base64,/, "");
+                            model[options.location] = $file.name;
+                            model[options.fileContent] = addedFile.replace(/^data.*base64,/, "");
 
                         }
                     }
@@ -142,8 +142,8 @@ export default function ExtensionFormOpcDirective($compile, $templateCache, $tra
         scope.clearFile = function(model, options) {
             scope.theForm.$setDirty();
 
-            model[options.fileName] = null;
-            model[options.file] = null;
+            model[options.location] = null;
+            model[options.fileContent] = null;
 
         };
 
diff --git a/ui/src/app/extension/extensions-forms/extension-form-opc.tpl.html b/ui/src/app/extension/extensions-forms/extension-form-opc.tpl.html
index 501eeeb..5a7c00b 100644
--- a/ui/src/app/extension/extensions-forms/extension-form-opc.tpl.html
+++ b/ui/src/app/extension/extensions-forms/extension-form-opc.tpl.html
@@ -212,8 +212,8 @@
                                                     </md-input-container>
 
                                                     <section class="dropdown-section">
-                                                        <div class="tb-container" ng-class="{'ng-invalid':!server.keystore.file}">
-                                                            <span ng-init='fieldsToFill = {"fileName":"fileName", "file":"file"}'></span>
+                                                        <div class="tb-container" ng-class="{'ng-invalid':!server.keystore.fileContent}">
+                                                            <span ng-init='fieldsToFill = {"location":"location", "fileContent":"fileContent"}'></span>
                                                             <label class="tb-label" translate>extension.opc-keystore-location</label>
                                                             <div flow-init="{singleFile:true}" flow-file-added='fileAdded($file, server.keystore, fieldsToFill)' class="tb-file-select-container">
                                                                 <div class="tb-file-clear-container">
@@ -231,14 +231,14 @@
                                                                            class="file-input"
                                                                            flow-btn id="dropFileKeystore_{{serverIndex}}"
                                                                            name="keystoreFile"
-                                                                           ng-model="server.keystore.file"
+                                                                           ng-model="server.keystore.fileContent"
                                                                     >
                                                                 </div>
                                                             </div>
                                                         </div>
                                                         <div class="dropdown-messages">
-                                                            <div ng-if="!server.keystore[fieldsToFill.fileName]" class="tb-error-message" translate>extension.no-file</div>
-                                                            <div ng-if="server.keystore[fieldsToFill.fileName]">{{server.keystore[fieldsToFill.fileName]}}</div>
+                                                            <div ng-if="!server.keystore[fieldsToFill.location]" class="tb-error-message" translate>extension.no-file</div>
+                                                            <div ng-if="server.keystore[fieldsToFill.location]">{{server.keystore[fieldsToFill.location]}}</div>
                                                         </div>
                                                     </section>
 
diff --git a/ui/src/app/import-export/import-export.service.js b/ui/src/app/import-export/import-export.service.js
index 6071fd2..88c6353 100644
--- a/ui/src/app/import-export/import-export.service.js
+++ b/ui/src/app/import-export/import-export.service.js
@@ -26,7 +26,7 @@ import entityAliasesTemplate from '../entity/alias/entity-aliases.tpl.html';
 /*@ngInject*/
 export default function ImportExport($log, $translate, $q, $mdDialog, $document, $http, itembuffer, utils, types,
                                      dashboardUtils, entityService, dashboardService, pluginService, ruleService,
-                                     widgetService, toast, attributeService) {
+                                     ruleChainService, widgetService, toast, attributeService) {
 
 
     var service = {
@@ -38,6 +38,8 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
         importPlugin: importPlugin,
         exportRule: exportRule,
         importRule: importRule,
+        exportRuleChain: exportRuleChain,
+        importRuleChain: importRuleChain,
         exportWidgetType: exportWidgetType,
         importWidgetType: importWidgetType,
         exportWidgetsBundle: exportWidgetsBundle,
@@ -275,6 +277,89 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
         return true;
     }
 
+    // Rule chain functions
+
+    function exportRuleChain(ruleChainId) {
+        ruleChainService.getRuleChain(ruleChainId).then(
+            (ruleChain) => {
+                ruleChainService.getRuleChainMetaData(ruleChainId).then(
+                    (ruleChainMetaData) => {
+                        var ruleChainExport = {
+                            ruleChain: prepareRuleChain(ruleChain),
+                            metadata: prepareRuleChainMetaData(ruleChainMetaData)
+                        };
+                        var name = ruleChain.name;
+                        name = name.toLowerCase().replace(/\W/g,"_");
+                        exportToPc(ruleChainExport, name + '.json');
+                    },
+                    (rejection) => {
+                        processExportRuleChainRejection(rejection);
+                    }
+                );
+            },
+            (rejection) => {
+                processExportRuleChainRejection(rejection);
+            }
+        );
+    }
+
+    function prepareRuleChain(ruleChain) {
+        ruleChain = prepareExport(ruleChain);
+        if (ruleChain.firstRuleNodeId) {
+            ruleChain.firstRuleNodeId = null;
+        }
+        ruleChain.root = false;
+        return ruleChain;
+    }
+
+    function prepareRuleChainMetaData(ruleChainMetaData) {
+        delete ruleChainMetaData.ruleChainId;
+        for (var i=0;i<ruleChainMetaData.nodes.length;i++) {
+            var node = ruleChainMetaData.nodes[i];
+            ruleChainMetaData.nodes[i] = prepareExport(node);
+        }
+        return ruleChainMetaData;
+    }
+
+    function processExportRuleChainRejection(rejection) {
+        var message = rejection;
+        if (!message) {
+            message = $translate.instant('error.unknown-error');
+        }
+        toast.showError($translate.instant('rulechain.export-failed-error', {error: message}));
+    }
+
+    function importRuleChain($event) {
+        var deferred = $q.defer();
+        openImportDialog($event, 'rulechain.import', 'rulechain.rulechain-file').then(
+            function success(ruleChainImport) {
+                if (!validateImportedRuleChain(ruleChainImport)) {
+                    toast.showError($translate.instant('rulechain.invalid-rulechain-file-error'));
+                    deferred.reject();
+                } else {
+                    deferred.resolve(ruleChainImport);
+                }
+            },
+            function fail() {
+                deferred.reject();
+            }
+        );
+        return deferred.promise;
+    }
+
+    function validateImportedRuleChain(ruleChainImport) {
+        if (angular.isUndefined(ruleChainImport.ruleChain)) {
+            return false;
+        }
+        if (angular.isUndefined(ruleChainImport.metadata)) {
+            return false;
+        }
+        if (angular.isUndefined(ruleChainImport.ruleChain.name)) {
+            return false;
+        }
+        return true;
+    }
+
     // Plugin functions
 
     function exportPlugin(pluginId) {
diff --git a/ui/src/app/layout/index.js b/ui/src/app/layout/index.js
index e5ca958..6ec7ef7 100644
--- a/ui/src/app/layout/index.js
+++ b/ui/src/app/layout/index.js
@@ -29,6 +29,9 @@ import thingsboardNoAnimate from '../components/no-animate.directive';
 import thingsboardOnFinishRender from '../components/finish-render.directive';
 import thingsboardSideMenu from '../components/side-menu.directive';
 import thingsboardDashboardAutocomplete from '../components/dashboard-autocomplete.directive';
+import thingsboardKvMap from '../components/kv-map.directive';
+import thingsboardJsonObjectEdit from '../components/json-object-edit.directive';
+import thingsboardJsonContent from '../components/json-content.directive';
 
 import thingsboardUserMenu from './user-menu.directive';
 
@@ -49,6 +52,7 @@ import thingsboardWidgetLibrary from '../widget';
 import thingsboardDashboard from '../dashboard';
 import thingsboardPlugin from '../plugin';
 import thingsboardRule from '../rule';
+import thingsboardRuleChain from '../rulechain';
 
 import thingsboardJsonForm from '../jsonform';
 
@@ -81,6 +85,7 @@ export default angular.module('thingsboard.home', [
     thingsboardDashboard,
     thingsboardPlugin,
     thingsboardRule,
+    thingsboardRuleChain,
     thingsboardJsonForm,
     thingsboardApiDevice,
     thingsboardApiLogin,
@@ -88,7 +93,10 @@ export default angular.module('thingsboard.home', [
     thingsboardNoAnimate,
     thingsboardOnFinishRender,
     thingsboardSideMenu,
-    thingsboardDashboardAutocomplete
+    thingsboardDashboardAutocomplete,
+    thingsboardKvMap,
+    thingsboardJsonObjectEdit,
+    thingsboardJsonContent
 ])
     .config(HomeRoutes)
     .controller('HomeController', HomeController)
diff --git a/ui/src/app/locale/locale.constant.js b/ui/src/app/locale/locale.constant.js
index 252884d..c515e3e 100644
--- a/ui/src/app/locale/locale.constant.js
+++ b/ui/src/app/locale/locale.constant.js
@@ -43,6 +43,7 @@ export default angular.module('thingsboard.locale', [])
                     "update": "Update",
                     "remove": "Remove",
                     "search": "Search",
+                    "clear-search": "Clear search",
                     "assign": "Assign",
                     "unassign": "Unassign",
                     "share": "Share",
@@ -341,6 +342,11 @@ export default angular.module('thingsboard.locale', [])
                     "enter-password": "Enter password",
                     "enter-search": "Enter search"
                 },
+                "content-type": {
+                    "json": "Json",
+                    "text": "Text",
+                    "binary": "Binary (Base64)"
+                },
                 "customer": {
                     "customer": "Customer",
                     "customers": "Customers",
@@ -745,6 +751,10 @@ export default angular.module('thingsboard.locale', [])
                     "type-alarms": "Alarms",
                     "list-of-alarms": "{ count, select, 1 {One alarms} other {List of # alarms} }",
                     "alarm-name-starts-with": "Alarms whose names start with '{{prefix}}'",
+                    "type-rulechain": "Rule chain",
+                    "type-rulechains": "Rule chains",
+                    "list-of-rulechains": "{ count, select, 1 {One rule chain} other {List of # rule chains} }",
+                    "rulechain-name-starts-with": "Rule chains whose names start with '{{prefix}}'",
                     "type-current-customer": "Current Customer",
                     "search": "Search entities",
                     "selected-entities": "{ count, select, 1 {1 entity} other {# entities} } selected",
@@ -758,6 +768,8 @@ export default angular.module('thingsboard.locale', [])
                     "type-error": "Error",
                     "type-lc-event": "Lifecycle event",
                     "type-stats": "Statistics",
+                    "type-debug-rule-node": "Debug",
+                    "type-debug-rule-chain": "Debug",
                     "no-events-prompt": "No events found",
                     "error": "Error",
                     "alarm": "Alarm",
@@ -765,6 +777,13 @@ export default angular.module('thingsboard.locale', [])
                     "server": "Server",
                     "body": "Body",
                     "method": "Method",
+                    "type": "Type",
+                    "entity": "Entity",
+                    "message-id": "Message Id",
+                    "message-type": "Message Type",
+                    "data-type": "Data Type",
+                    "metadata": "Metadata",
+                    "data": "Data",
                     "event": "Event",
                     "status": "Status",
                     "success": "Success",
@@ -943,6 +962,13 @@ export default angular.module('thingsboard.locale', [])
                     "no-return-error": "Function must return value!",
                     "return-type-mismatch": "Function must return value of '{{type}}' type!"
                 },
+                "key-val": {
+                    "key": "Key",
+                    "value": "Value",
+                    "remove-entry": "Remove entry",
+                    "add-entry": "Add entry",
+                    "no-data": "No entries"
+                },
                 "layout": {
                     "layout": "Layout",
                     "manage": "Manage layouts",
@@ -1133,6 +1159,89 @@ export default angular.module('thingsboard.locale', [])
                     "no-rules-matching": "No rules matching '{{entity}}' were found.",
                     "rule-required": "Rule is required"
                 },
+                "rulechain": {
+                    "rulechain": "Rule chain",
+                    "rulechains": "Rule chains",
+                    "delete": "Delete rule chain",
+                    "name": "Name",
+                    "name-required": "Name is required.",
+                    "description": "Description",
+                    "add": "Add Rule Chain",
+                    "delete-rulechain-title": "Are you sure you want to delete the rule chain '{{ruleChainName}}'?",
+                    "delete-rulechain-text": "Be careful, after the confirmation the rule chain and all related data will become unrecoverable.",
+                    "delete-rulechains-title": "Are you sure you want to delete { count, select, 1 {1 rule chain} other {# rule chains} }?",
+                    "delete-rulechains-action-title": "Delete { count, select, 1 {1 rule chain} other {# rule chains} }",
+                    "delete-rulechains-text": "Be careful, after the confirmation all selected rule chains will be removed and all related data will become unrecoverable.",
+                    "add-rulechain-text": "Add new rule chain",
+                    "no-rulechains-text": "No rule chains found",
+                    "rulechain-details": "Rule chain details",
+                    "details": "Details",
+                    "events": "Events",
+                    "system": "System",
+                    "import": "Import rule chain",
+                    "export": "Export rule chain",
+                    "export-failed-error": "Unable to export rule chain: {{error}}",
+                    "create-new-rulechain": "Create new rule chain",
+                    "rulechain-file": "Rule chain file",
+                    "invalid-rulechain-file-error": "Unable to import rule chain: Invalid rule chain data structure.",
+                    "copyId": "Copy rule chain Id",
+                    "idCopiedMessage": "Rule chain Id has been copied to clipboard",
+                    "select-rulechain": "Select rule chain",
+                    "no-rulechains-matching": "No rule chains matching '{{entity}}' were found.",
+                    "rulechain-required": "Rule chain is required",
+                    "management": "Rules management",
+                    "debug-mode": "Debug mode"
+                },
+                "rulenode": {
+                    "details": "Details",
+                    "events": "Events",
+                    "search": "Search nodes",
+                    "open-node-library": "Open node library",
+                    "add": "Add rule node",
+                    "name": "Name",
+                    "name-required": "Name is required.",
+                    "type": "Type",
+                    "description": "Description",
+                    "delete": "Delete rule node",
+                    "select-all-objects": "Select all nodes and connections",
+                    "deselect-all-objects": "Deselect all nodes and connections",
+                    "delete-selected-objects": "Delete selected nodes and connections",
+                    "delete-selected": "Delete selected",
+                    "select-all": "Select all",
+                    "copy-selected": "Copy selected",
+                    "deselect-all": "Deselect all",
+                    "rulenode-details": "Rule node details",
+                    "debug-mode": "Debug mode",
+                    "configuration": "Configuration",
+                    "link": "Link",
+                    "link-details": "Rule node link details",
+                    "add-link": "Add link",
+                    "link-label": "Link label",
+                    "link-label-required": "Link label is required.",
+                    "custom-link-label": "Custom link label",
+                    "custom-link-label-required": "Custom link label is required.",
+                    "type-filter": "Filter",
+                    "type-filter-details": "Filter incoming messages with configured conditions",
+                    "type-enrichment": "Enrichment",
+                    "type-enrichment-details": "Add additional information into Message Metadata",
+                    "type-transformation": "Transformation",
+                    "type-transformation-details": "Change Message payload and Metadata",
+                    "type-action": "Action",
+                    "type-action-details": "Perform special action",
+                    "type-rule-chain": "Rule Chain",
+                    "type-rule-chain-details": "Forwards incoming messages to specified Rule Chain",
+                    "directive-is-not-loaded": "Defined configuration directive '{{directiveName}}' is not available.",
+                    "ui-resources-load-error": "Failed to load configuration ui resources.",
+                    "invalid-target-rulechain": "Unable to resolve target rule chain!",
+                    "test-script-function": "Test script function",
+                    "message": "Message",
+                    "message-type": "Message type",
+                    "message-type-required": "Message type is required",
+                    "metadata": "Metadata",
+                    "metadata-required": "Metadata entries can't be empty.",
+                    "output": "Output",
+                    "test": "Test"
+                },
                 "rule-plugin": {
                     "management": "Rules and plugins management"
                 },
diff --git a/ui/src/app/rulechain/add-link.tpl.html b/ui/src/app/rulechain/add-link.tpl.html
new file mode 100644
index 0000000..42c0777
--- /dev/null
+++ b/ui/src/app/rulechain/add-link.tpl.html
@@ -0,0 +1,48 @@
+<!--
+
+    Copyright © 2016-2018 The Thingsboard Authors
+
+    Licensed 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.
+
+-->
+<md-dialog aria-label="{{ 'rulenode.add-link' | translate }}" tb-help="'rulechains'" help-container-id="help-container">
+    <form name="theForm" ng-submit="vm.add()">
+        <md-toolbar>
+            <div class="md-toolbar-tools">
+                <h2 translate>rulenode.add-link</h2>
+                <span flex></span>
+                <div id="help-container"></div>
+                <md-button class="md-icon-button" ng-click="vm.cancel()">
+                    <ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
+                </md-button>
+            </div>
+        </md-toolbar>
+        <md-progress-linear class="md-warn" md-mode="indeterminate" ng-disabled="!$root.loading" ng-show="$root.loading"></md-progress-linear>
+        <span style="min-height: 5px;" flex="" ng-show="!$root.loading"></span>
+        <md-dialog-content>
+            <div class="md-dialog-content">
+                <tb-rule-node-link link="vm.link" labels="vm.labels" is-edit="true" the-form="theForm"></tb-rule-node-link>
+            </div>
+        </md-dialog-content>
+        <md-dialog-actions layout="row">
+            <span flex></span>
+            <md-button ng-disabled="$root.loading || theForm.$invalid || !theForm.$dirty" type="submit"
+                       class="md-raised md-primary">
+                {{ 'action.add' | translate }}
+            </md-button>
+            <md-button ng-disabled="$root.loading" ng-click="vm.cancel()" style="margin-right:20px;">{{ 'action.cancel' |
+                translate }}
+            </md-button>
+        </md-dialog-actions>
+    </form>
+</md-dialog>
diff --git a/ui/src/app/rulechain/add-rulechain.tpl.html b/ui/src/app/rulechain/add-rulechain.tpl.html
new file mode 100644
index 0000000..44d0ec3
--- /dev/null
+++ b/ui/src/app/rulechain/add-rulechain.tpl.html
@@ -0,0 +1,48 @@
+<!--
+
+    Copyright © 2016-2018 The Thingsboard Authors
+
+    Licensed 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.
+
+-->
+<md-dialog aria-label="{{ 'rulechain.add' | translate }}" tb-help="'rulechains'" help-container-id="help-container">
+    <form name="theForm" ng-submit="vm.add()">
+        <md-toolbar>
+            <div class="md-toolbar-tools">
+                <h2 translate>rulechain.add</h2>
+                <span flex></span>
+                <div id="help-container"></div>
+                <md-button class="md-icon-button" ng-click="vm.cancel()">
+                    <ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
+                </md-button>
+            </div>
+        </md-toolbar>
+        <md-progress-linear class="md-warn" md-mode="indeterminate" ng-disabled="!$root.loading" ng-show="$root.loading"></md-progress-linear>
+        <span style="min-height: 5px;" flex="" ng-show="!$root.loading"></span>
+        <md-dialog-content>
+            <div class="md-dialog-content">
+                <tb-rule-chain rule-chain="vm.item" is-edit="true" the-form="theForm"></tb-rule-chain>
+            </div>
+        </md-dialog-content>
+        <md-dialog-actions layout="row">
+            <span flex></span>
+            <md-button ng-disabled="$root.loading || theForm.$invalid || !theForm.$dirty" type="submit"
+                       class="md-raised md-primary">
+                {{ 'action.add' | translate }}
+            </md-button>
+            <md-button ng-disabled="$root.loading" ng-click="vm.cancel()" style="margin-right:20px;">{{ 'action.cancel' |
+                translate }}
+            </md-button>
+        </md-dialog-actions>
+    </form>
+</md-dialog>
diff --git a/ui/src/app/rulechain/add-rulenode.tpl.html b/ui/src/app/rulechain/add-rulenode.tpl.html
new file mode 100644
index 0000000..c36b43b
--- /dev/null
+++ b/ui/src/app/rulechain/add-rulenode.tpl.html
@@ -0,0 +1,48 @@
+<!--
+
+    Copyright © 2016-2018 The Thingsboard Authors
+
+    Licensed 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.
+
+-->
+<md-dialog aria-label="{{ 'rulenode.add' | translate }}" tb-help="'rulechains'" help-container-id="help-container" style="min-width: 650px;">
+    <form name="theForm" ng-submit="vm.add()">
+        <md-toolbar>
+            <div class="md-toolbar-tools">
+                <h2 translate>rulenode.add</h2>
+                <span flex></span>
+                <div id="help-container"></div>
+                <md-button class="md-icon-button" ng-click="vm.cancel()">
+                    <ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
+                </md-button>
+            </div>
+        </md-toolbar>
+        <md-progress-linear class="md-warn" md-mode="indeterminate" ng-disabled="!$root.loading" ng-show="$root.loading"></md-progress-linear>
+        <span style="min-height: 5px;" flex="" ng-show="!$root.loading"></span>
+        <md-dialog-content>
+            <div class="md-dialog-content">
+                <tb-rule-node rule-node="vm.ruleNode" rule-chain-id="vm.ruleChainId" is-edit="true" the-form="theForm"></tb-rule-node>
+            </div>
+        </md-dialog-content>
+        <md-dialog-actions layout="row">
+            <span flex></span>
+            <md-button ng-disabled="$root.loading || theForm.$invalid || !theForm.$dirty" type="submit"
+                       class="md-raised md-primary">
+                {{ 'action.add' | translate }}
+            </md-button>
+            <md-button ng-disabled="$root.loading" ng-click="vm.cancel()" style="margin-right:20px;">{{ 'action.cancel' |
+                translate }}
+            </md-button>
+        </md-dialog-actions>
+    </form>
+</md-dialog>
diff --git a/ui/src/app/rulechain/index.js b/ui/src/app/rulechain/index.js
new file mode 100644
index 0000000..7740dd0
--- /dev/null
+++ b/ui/src/app/rulechain/index.js
@@ -0,0 +1,41 @@
+/*
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.
+ */
+
+import RuleChainRoutes from './rulechain.routes';
+import RuleChainsController from './rulechains.controller';
+import {RuleChainController, AddRuleNodeController, AddRuleNodeLinkController} from './rulechain.controller';
+import NodeScriptTestController from './script/node-script-test.controller';
+import RuleChainDirective from './rulechain.directive';
+import RuleNodeDefinedConfigDirective from './rulenode-defined-config.directive';
+import RuleNodeConfigDirective from './rulenode-config.directive';
+import RuleNodeDirective from './rulenode.directive';
+import LinkDirective from './link.directive';
+import NodeScriptTest from './script/node-script-test.service';
+
+export default angular.module('thingsboard.ruleChain', [])
+    .config(RuleChainRoutes)
+    .controller('RuleChainsController', RuleChainsController)
+    .controller('RuleChainController', RuleChainController)
+    .controller('AddRuleNodeController', AddRuleNodeController)
+    .controller('AddRuleNodeLinkController', AddRuleNodeLinkController)
+    .controller('NodeScriptTestController', NodeScriptTestController)
+    .directive('tbRuleChain', RuleChainDirective)
+    .directive('tbRuleNodeDefinedConfig', RuleNodeDefinedConfigDirective)
+    .directive('tbRuleNodeConfig', RuleNodeConfigDirective)
+    .directive('tbRuleNode', RuleNodeDirective)
+    .directive('tbRuleNodeLink', LinkDirective)
+    .factory('ruleNodeScriptTest', NodeScriptTest)
+    .name;
diff --git a/ui/src/app/rulechain/link.directive.js b/ui/src/app/rulechain/link.directive.js
new file mode 100644
index 0000000..b3565a3
--- /dev/null
+++ b/ui/src/app/rulechain/link.directive.js
@@ -0,0 +1,71 @@
+/*
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.
+ */
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import linkFieldsetTemplate from './link-fieldset.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function LinkDirective($compile, $templateCache, $filter) {
+    var linker = function (scope, element) {
+        var template = $templateCache.get(linkFieldsetTemplate);
+        element.html(template);
+
+        scope.selectedLabel = null;
+
+        scope.$watch('link', function() {
+            scope.selectedLabel = null;
+             if (scope.link && scope.labels) {
+                 if (scope.link.label) {
+                     var result = $filter('filter')(scope.labels, {name: scope.link.label});
+                     if (result && result.length) {
+                         scope.selectedLabel = result[0];
+                     } else {
+                         result = $filter('filter')(scope.labels, {custom: true});
+                         if (result && result.length && result[0].custom) {
+                             scope.selectedLabel = result[0];
+                         }
+                     }
+                 }
+             }
+        });
+
+        scope.selectedLabelChanged = function() {
+            if (scope.link && scope.selectedLabel) {
+                if (!scope.selectedLabel.custom) {
+                    scope.link.label = scope.selectedLabel.name;
+                } else {
+                    scope.link.label = "";
+                }
+            }
+        };
+
+        $compile(element.contents())(scope);
+    }
+    return {
+        restrict: "E",
+        link: linker,
+        scope: {
+            link: '=',
+            labels: '=',
+            isEdit: '=',
+            isReadOnly: '=',
+            theForm: '='
+        }
+    };
+}
diff --git a/ui/src/app/rulechain/link-fieldset.tpl.html b/ui/src/app/rulechain/link-fieldset.tpl.html
new file mode 100644
index 0000000..13ec6c3
--- /dev/null
+++ b/ui/src/app/rulechain/link-fieldset.tpl.html
@@ -0,0 +1,39 @@
+<!--
+
+    Copyright © 2016-2018 The Thingsboard Authors
+
+    Licensed 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.
+
+-->
+<md-content class="md-padding tb-link" layout="column">
+    <fieldset ng-disabled="$root.loading || !isEdit || isReadOnly">
+        <md-input-container class="md-block">
+            <label translate>rulenode.link-label</label>
+            <md-select ng-model="selectedLabel" ng-change="selectedLabelChanged()">
+                <md-option ng-repeat="label in labels" ng-value="label">
+                    {{label.name}}
+                </md-option>
+            </md-select>
+            <div ng-messages="theForm.linkLabel.$error">
+                <div translate ng-message="required">rulenode.link-label-required</div>
+            </div>
+        </md-input-container>
+        <md-input-container ng-if="selectedLabel.custom" class="md-block">
+            <label translate>rulenode.link-label</label>
+            <input required name="customLinkLabel" ng-model="link.label">
+            <div ng-messages="theForm.customLinkLabel.$error">
+                <div translate ng-message="required">rulenode.custom-link-label-required</div>
+            </div>
+        </md-input-container>
+    </fieldset>
+</md-content>
diff --git a/ui/src/app/rulechain/rulechain.controller.js b/ui/src/app/rulechain/rulechain.controller.js
new file mode 100644
index 0000000..7358b44
--- /dev/null
+++ b/ui/src/app/rulechain/rulechain.controller.js
@@ -0,0 +1,1287 @@
+/*
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.
+ */
+
+import './rulechain.scss';
+
+import 'tooltipster/dist/css/tooltipster.bundle.min.css';
+import 'tooltipster/dist/js/tooltipster.bundle.min.js';
+import 'tooltipster/dist/css/plugins/tooltipster/sideTip/themes/tooltipster-sideTip-shadow.min.css';
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import addRuleNodeTemplate from './add-rulenode.tpl.html';
+import addRuleNodeLinkTemplate from './add-link.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export function RuleChainController($state, $scope, $compile, $q, $mdUtil, $timeout, $mdExpansionPanel, $window, $document, $mdDialog,
+                                    $filter, $translate, hotkeys, types, ruleChainService, itembuffer, Modelfactory, flowchartConstants,
+                                    ruleChain, ruleChainMetaData, ruleNodeComponents) {
+
+    var vm = this;
+
+    vm.$mdExpansionPanel = $mdExpansionPanel;
+    vm.types = types;
+
+    if ($state.current.data.import && !ruleChain) {
+        $state.go('home.ruleChains');
+        return;
+    }
+
+    vm.isImport = $state.current.data.import;
+    vm.isConfirmOnExit = false;
+
+    $scope.$watch(function() {
+        return vm.isDirty || vm.isImport;
+    }, (val) => {
+        vm.isConfirmOnExit = val;
+    });
+
+    vm.errorTooltips = {};
+
+    vm.isFullscreen = false;
+
+    vm.editingRuleNode = null;
+    vm.isEditingRuleNode = false;
+
+    vm.editingRuleNodeLink = null;
+    vm.isEditingRuleNodeLink = false;
+
+    vm.isLibraryOpen = true;
+    vm.enableHotKeys = true;
+
+    Object.defineProperty(vm, 'isLibraryOpenReadonly', {
+        get: function() { return vm.isLibraryOpen },
+        set: function() {}
+    });
+
+    vm.ruleNodeSearch = '';
+
+    vm.ruleChain = ruleChain;
+    vm.ruleChainMetaData = ruleChainMetaData;
+
+    vm.canvasControl = {};
+
+    vm.ruleChainModel = {
+        nodes: [],
+        edges: []
+    };
+
+    vm.ruleNodeTypesModel = {};
+    vm.ruleNodeTypesCanvasControl = {};
+    vm.ruleChainLibraryLoaded = false;
+    for (var type in types.ruleNodeType) {
+        if (!types.ruleNodeType[type].special) {
+            vm.ruleNodeTypesModel[type] = {
+                model: {
+                    nodes: [],
+                    edges: []
+                },
+                selectedObjects: []
+            };
+            vm.ruleNodeTypesCanvasControl[type] = {};
+        }
+    }
+
+
+
+    vm.selectedObjects = [];
+
+    vm.modelservice = Modelfactory(vm.ruleChainModel, vm.selectedObjects);
+
+    vm.saveRuleChain = saveRuleChain;
+    vm.revertRuleChain = revertRuleChain;
+
+    vm.objectsSelected = objectsSelected;
+    vm.deleteSelected = deleteSelected;
+
+    vm.triggerResize = triggerResize;
+
+    vm.openRuleChainContextMenu = openRuleChainContextMenu;
+
+    initHotKeys();
+
+    function openRuleChainContextMenu($event, $mdOpenMousepointMenu) {
+        if (vm.canvasControl.modelservice && !$event.ctrlKey && !$event.metaKey) {
+            var x = $event.clientX;
+            var y = $event.clientY;
+            var item = vm.canvasControl.modelservice.getItemInfoAtPoint(x, y);
+            vm.contextInfo = prepareContextMenu(item);
+            if (vm.contextInfo.items && vm.contextInfo.items.length > 0) {
+                vm.contextMenuEvent = $event;
+                $mdOpenMousepointMenu($event);
+                return false;
+            }
+        }
+    }
+
+    function prepareContextMenu(item) {
+        if (objectsSelected() || (!item.node && !item.edge)) {
+            return prepareRuleChainContextMenu();
+        } else if (item.node) {
+            return prepareRuleNodeContextMenu(item.node);
+        } else if (item.edge) {
+            return prepareEdgeContextMenu(item.edge);
+        }
+    }
+
+    function prepareRuleChainContextMenu() {
+        var contextInfo = {
+            headerClass: 'tb-rulechain',
+            icon: 'settings_ethernet',
+            title: vm.ruleChain.name,
+            subtitle: $translate.instant('rulechain.rulechain')
+        };
+        contextInfo.items = [];
+        if (vm.modelservice.nodes.getSelectedNodes().length) {
+            contextInfo.items.push(
+                {
+                    action: function () {
+                        copyRuleNodes();
+                    },
+                    enabled: true,
+                    value: "rulenode.copy-selected",
+                    icon: "content_copy",
+                    shortcut: "M-C"
+                }
+            );
+        }
+        contextInfo.items.push(
+            {
+                action: function ($event) {
+                    pasteRuleNodes($event);
+                },
+                enabled: itembuffer.hasRuleNodes(),
+                value: "action.paste",
+                icon: "content_paste",
+                shortcut: "M-V"
+            }
+        );
+        contextInfo.items.push(
+            {
+                divider: true
+            }
+        );
+        if (objectsSelected()) {
+            contextInfo.items.push(
+                {
+                    action: function () {
+                        vm.modelservice.deselectAll();
+                    },
+                    enabled: true,
+                    value: "rulenode.deselect-all",
+                    icon: "tab_unselected",
+                    shortcut: "Esc"
+                }
+            );
+            contextInfo.items.push(
+                {
+                    action: function () {
+                        vm.modelservice.deleteSelected();
+                    },
+                    enabled: true,
+                    value: "rulenode.delete-selected",
+                    icon: "clear",
+                    shortcut: "Del"
+                }
+            );
+        } else {
+            contextInfo.items.push(
+                {
+                    action: function () {
+                        vm.modelservice.selectAll();
+                    },
+                    enabled: true,
+                    value: "rulenode.select-all",
+                    icon: "select_all",
+                    shortcut: "M-A"
+                }
+            );
+        }
+        contextInfo.items.push(
+            {
+                divider: true
+            }
+        );
+        contextInfo.items.push(
+            {
+                action: function () {
+                    vm.saveRuleChain();
+                },
+                enabled: !(vm.isInvalid || (!vm.isDirty && !vm.isImport)),
+                value: "action.apply-changes",
+                icon: "done",
+                shortcut: "M-S"
+            }
+        );
+        contextInfo.items.push(
+            {
+                action: function () {
+                    vm.revertRuleChain();
+                },
+                enabled: vm.isDirty,
+                value: "action.decline-changes",
+                icon: "close",
+                shortcut: "M-Z"
+            }
+        );
+        return contextInfo;
+    }
+
+    function prepareRuleNodeContextMenu(node) {
+        var contextInfo = {
+            headerClass: node.nodeClass,
+            icon: node.icon,
+            title: node.name,
+            subtitle: node.component.name
+        };
+        contextInfo.items = [];
+        if (!node.readonly) {
+            contextInfo.items.push(
+                {
+                    action: function () {
+                        openNodeDetails(node);
+                    },
+                    enabled: true,
+                    value: "rulenode.details",
+                    icon: "menu"
+                }
+            );
+            contextInfo.items.push(
+                {
+                    action: function () {
+                        copyNode(node);
+                    },
+                    enabled: true,
+                    value: "action.copy",
+                    icon: "content_copy"
+                }
+            );
+            contextInfo.items.push(
+                {
+                    action: function () {
+                        vm.canvasControl.modelservice.nodes.delete(node);
+                    },
+                    enabled: true,
+                    value: "action.delete",
+                    icon: "clear",
+                    shortcut: "M-X"
+                }
+            );
+        }
+        return contextInfo;
+    }
+
+    function prepareEdgeContextMenu(edge) {
+        var contextInfo = {
+            headerClass: 'tb-link',
+            icon: 'trending_flat',
+            title: edge.label,
+            subtitle: $translate.instant('rulenode.link')
+        };
+        contextInfo.items = [];
+        var sourceNode = vm.modelservice.nodes.getNodeByConnectorId(edge.source);
+        if (sourceNode.component.type != types.ruleNodeType.INPUT.value) {
+            contextInfo.items.push(
+                {
+                    action: function () {
+                        openLinkDetails(edge);
+                    },
+                    enabled: true,
+                    value: "rulenode.details",
+                    icon: "menu"
+                }
+            );
+        }
+        contextInfo.items.push(
+            {
+                action: function () {
+                    vm.canvasControl.modelservice.edges.delete(edge);
+                },
+                enabled: true,
+                value: "action.delete",
+                icon: "clear",
+                shortcut: "M-X"
+            }
+        );
+        return contextInfo;
+    }
+
+    function initHotKeys() {
+        hotkeys.bindTo($scope)
+            .add({
+                combo: 'ctrl+a',
+                description: $translate.instant('rulenode.select-all-objects'),
+                allowIn: ['INPUT', 'SELECT', 'TEXTAREA'],
+                callback: function (event) {
+                    if (vm.enableHotKeys) {
+                        event.preventDefault();
+                        vm.modelservice.selectAll();
+                    }
+                }
+            })
+            .add({
+                combo: 'ctrl+c',
+                description: $translate.instant('rulenode.copy-selected'),
+                allowIn: ['INPUT', 'SELECT', 'TEXTAREA'],
+                callback: function (event) {
+                    if (vm.enableHotKeys) {
+                        event.preventDefault();
+                        copyRuleNodes();
+                    }
+                }
+            })
+            .add({
+                combo: 'ctrl+v',
+                description: $translate.instant('action.paste'),
+                allowIn: ['INPUT', 'SELECT', 'TEXTAREA'],
+                callback: function (event) {
+                    if (vm.enableHotKeys) {
+                        event.preventDefault();
+                        if (itembuffer.hasRuleNodes()) {
+                            pasteRuleNodes();
+                        }
+                    }
+                }
+            })
+            .add({
+                combo: 'esc',
+                description: $translate.instant('rulenode.deselect-all-objects'),
+                allowIn: ['INPUT', 'SELECT', 'TEXTAREA'],
+                callback: function (event) {
+                    if (vm.enableHotKeys) {
+                        event.preventDefault();
+                        event.stopPropagation();
+                        vm.modelservice.deselectAll();
+                    }
+                }
+            })
+            .add({
+                combo: 'ctrl+s',
+                description: $translate.instant('action.apply'),
+                allowIn: ['INPUT', 'SELECT', 'TEXTAREA'],
+                callback: function (event) {
+                    if (vm.enableHotKeys) {
+                        event.preventDefault();
+                        vm.saveRuleChain();
+                    }
+                }
+            })
+            .add({
+                combo: 'ctrl+z',
+                description: $translate.instant('action.decline-changes'),
+                allowIn: ['INPUT', 'SELECT', 'TEXTAREA'],
+                callback: function (event) {
+                    if (vm.enableHotKeys) {
+                        event.preventDefault();
+                        vm.revertRuleChain();
+                    }
+                }
+            })
+            .add({
+                combo: 'del',
+                description: $translate.instant('rulenode.delete-selected-objects'),
+                allowIn: ['INPUT', 'SELECT', 'TEXTAREA'],
+                callback: function (event) {
+                    if (vm.enableHotKeys) {
+                        event.preventDefault();
+                        vm.modelservice.deleteSelected();
+                    }
+                }
+            })
+    }
+
+    vm.onEditRuleNodeClosed = function() {
+        vm.editingRuleNode = null;
+    };
+
+    vm.onEditRuleNodeLinkClosed = function() {
+        vm.editingRuleNodeLink = null;
+    };
+
+    vm.saveRuleNode = function(theForm) {
+        $scope.$broadcast('form-submit');
+        if (theForm.$valid) {
+            theForm.$setPristine();
+            if (vm.editingRuleNode.error) {
+                delete vm.editingRuleNode.error;
+            }
+            vm.ruleChainModel.nodes[vm.editingRuleNodeIndex] = vm.editingRuleNode;
+            vm.editingRuleNode = angular.copy(vm.editingRuleNode);
+            updateRuleNodesHighlight();
+        }
+    };
+
+    vm.saveRuleNodeLink = function(theForm) {
+        theForm.$setPristine();
+        vm.ruleChainModel.edges[vm.editingRuleNodeLinkIndex] = vm.editingRuleNodeLink;
+        vm.editingRuleNodeLink = angular.copy(vm.editingRuleNodeLink);
+    };
+
+    vm.onRevertRuleNodeEdit = function(theForm) {
+        theForm.$setPristine();
+        var node = vm.ruleChainModel.nodes[vm.editingRuleNodeIndex];
+        vm.editingRuleNode = angular.copy(node);
+    };
+
+    vm.onRevertRuleNodeLinkEdit = function(theForm) {
+        theForm.$setPristine();
+        var edge = vm.ruleChainModel.edges[vm.editingRuleNodeLinkIndex];
+        vm.editingRuleNodeLink = angular.copy(edge);
+    };
+
+    vm.nodeLibCallbacks = {
+        nodeCallbacks: {
+            'mouseEnter': function (event, node) {
+                displayNodeDescriptionTooltip(event, node);
+            },
+            'mouseLeave': function () {
+                destroyTooltips();
+            },
+            'mouseDown': function () {
+                destroyTooltips();
+            }
+        }
+    };
+
+    vm.typeHeaderMouseEnter = function(event, typeId) {
+        var ruleNodeType = types.ruleNodeType[typeId];
+        displayTooltip(event,
+            '<div class="tb-rule-node-tooltip">' +
+            '<div id="tooltip-content" layout="column">' +
+            '<div class="tb-node-title">' + $translate.instant(ruleNodeType.name) + '</div>' +
+            '<div class="tb-node-details">' + $translate.instant(ruleNodeType.details) + '</div>' +
+            '</div>' +
+            '</div>'
+        );
+    };
+
+    vm.destroyTooltips = destroyTooltips;
+
+    function destroyTooltips() {
+        if (vm.tooltipTimeout) {
+            $timeout.cancel(vm.tooltipTimeout);
+            vm.tooltipTimeout = null;
+        }
+        var instances = angular.element.tooltipster.instances();
+        instances.forEach((instance) => {
+            if (!instance.isErrorTooltip) {
+                instance.destroy();
+            }
+        });
+    }
+
+    function displayNodeDescriptionTooltip(event, node) {
+        displayTooltip(event,
+            '<div class="tb-rule-node-tooltip">' +
+            '<div id="tooltip-content" layout="column">' +
+            '<div class="tb-node-title">' + node.component.name + '</div>' +
+            '<div class="tb-node-description">' + node.component.configurationDescriptor.nodeDefinition.description + '</div>' +
+            '<div class="tb-node-details">' + node.component.configurationDescriptor.nodeDefinition.details + '</div>' +
+            '</div>' +
+            '</div>'
+        );
+    }
+
+    function displayTooltip(event, content) {
+        destroyTooltips();
+        vm.tooltipTimeout = $timeout(() => {
+            var element = angular.element(event.target);
+            element.tooltipster(
+                {
+                    theme: 'tooltipster-shadow',
+                    delay: 100,
+                    trigger: 'custom',
+                    triggerOpen: {
+                        click: false,
+                        tap: false
+                    },
+                    triggerClose: {
+                        click: true,
+                        tap: true,
+                        scroll: true
+                    },
+                    side: 'right',
+                    trackOrigin: true
+                }
+            );
+            var contentElement = angular.element(content);
+            $compile(contentElement)($scope);
+            var tooltip = element.tooltipster('instance');
+            tooltip.content(contentElement);
+            tooltip.open();
+        }, 500);
+    }
+
+    function updateNodeErrorTooltip(node) {
+        if (node.error) {
+            var element = angular.element('#' + node.id);
+            var tooltip = vm.errorTooltips[node.id];
+            if (!tooltip || !element.hasClass("tooltipstered")) {
+                element.tooltipster(
+                    {
+                        theme: 'tooltipster-shadow',
+                        delay: 0,
+                        animationDuration: 0,
+                        trigger: 'custom',
+                        triggerOpen: {
+                            click: false,
+                            tap: false
+                        },
+                        triggerClose: {
+                            click: false,
+                            tap: false,
+                            scroll: false
+                        },
+                        side: 'top',
+                        trackOrigin: true
+                    }
+                );
+                var content = '<div class="tb-rule-node-error-tooltip">' +
+                    '<div id="tooltip-content" layout="column">' +
+                    '<div class="tb-node-details">' + node.error + '</div>' +
+                    '</div>' +
+                    '</div>';
+                var contentElement = angular.element(content);
+                $compile(contentElement)($scope);
+                tooltip = element.tooltipster('instance');
+                tooltip.isErrorTooltip = true;
+                tooltip.content(contentElement);
+                vm.errorTooltips[node.id] = tooltip;
+            }
+            $mdUtil.nextTick(() => {
+                tooltip.open();
+            });
+        } else {
+            if (vm.errorTooltips[node.id]) {
+                tooltip = vm.errorTooltips[node.id];
+                tooltip.destroy();
+                delete vm.errorTooltips[node.id];
+            }
+        }
+    }
+
+    function updateErrorTooltips(hide) {
+        for (var nodeId in vm.errorTooltips) {
+            var tooltip = vm.errorTooltips[nodeId];
+            if (hide) {
+                tooltip.close();
+            } else {
+                tooltip.open();
+            }
+        }
+    }
+
+    $scope.$watch(function() {
+        return vm.isEditingRuleNode || vm.isEditingRuleNodeLink;
+    }, (val) => {
+        vm.enableHotKeys = !val;
+        updateErrorTooltips(val);
+    });
+
+    vm.editCallbacks = {
+        edgeDoubleClick: function (event, edge) {
+            openLinkDetails(edge);
+        },
+        nodeCallbacks: {
+            'doubleClick': function (event, node) {
+                openNodeDetails(node);
+            }
+        },
+        isValidEdge: function (source, destination) {
+            return source.type === flowchartConstants.rightConnectorType && destination.type === flowchartConstants.leftConnectorType;
+        },
+        createEdge: function (event, edge) {
+            var deferred = $q.defer();
+            var sourceNode = vm.modelservice.nodes.getNodeByConnectorId(edge.source);
+            if (sourceNode.component.type == types.ruleNodeType.INPUT.value) {
+                var destNode = vm.modelservice.nodes.getNodeByConnectorId(edge.destination);
+                if (destNode.component.type == types.ruleNodeType.RULE_CHAIN.value) {
+                    deferred.reject();
+                } else {
+                    var res = $filter('filter')(vm.ruleChainModel.edges, {source: vm.inputConnectorId}, true);
+                    if (res && res.length) {
+                        vm.modelservice.edges.delete(res[0]);
+                    }
+                    deferred.resolve(edge);
+                }
+            } else {
+                var labels = ruleChainService.getRuleNodeSupportedLinks(sourceNode.component);
+                vm.enableHotKeys = false;
+                addRuleNodeLink(event, edge, labels).then(
+                    (link) => {
+                        deferred.resolve(link);
+                        vm.enableHotKeys = true;
+                    },
+                    () => {
+                        deferred.reject();
+                        vm.enableHotKeys = true;
+                    }
+                );
+            }
+            return deferred.promise;
+        },
+        dropNode: function (event, node) {
+            addRuleNode(event, node);
+        }
+    };
+
+    function openNodeDetails(node) {
+        if (node.component.type != types.ruleNodeType.INPUT.value) {
+            vm.isEditingRuleNodeLink = false;
+            vm.editingRuleNodeLink = null;
+            vm.isEditingRuleNode = true;
+            vm.editingRuleNodeIndex = vm.ruleChainModel.nodes.indexOf(node);
+            vm.editingRuleNode = angular.copy(node);
+            $mdUtil.nextTick(() => {
+                if (vm.ruleNodeForm) {
+                    vm.ruleNodeForm.$setPristine();
+                }
+            });
+        }
+    }
+
+    function openLinkDetails(edge) {
+        var sourceNode = vm.modelservice.nodes.getNodeByConnectorId(edge.source);
+        if (sourceNode.component.type != types.ruleNodeType.INPUT.value) {
+            vm.isEditingRuleNode = false;
+            vm.editingRuleNode = null;
+            vm.editingRuleNodeLinkLabels = ruleChainService.getRuleNodeSupportedLinks(sourceNode.component);
+            vm.isEditingRuleNodeLink = true;
+            vm.editingRuleNodeLinkIndex = vm.ruleChainModel.edges.indexOf(edge);
+            vm.editingRuleNodeLink = angular.copy(edge);
+            $mdUtil.nextTick(() => {
+                if (vm.ruleNodeLinkForm) {
+                    vm.ruleNodeLinkForm.$setPristine();
+                }
+            });
+        }
+    }
+
+    function copyNode(node) {
+        itembuffer.copyRuleNodes([node], []);
+    }
+
+    function copyRuleNodes() {
+        var nodes = vm.modelservice.nodes.getSelectedNodes();
+        var edges = vm.modelservice.edges.getSelectedEdges();
+        var connections = [];
+        for (var i=0;i<edges.length;i++) {
+            var edge = edges[i];
+            var sourceNode = vm.modelservice.nodes.getNodeByConnectorId(edge.source);
+            var destNode = vm.modelservice.nodes.getNodeByConnectorId(edge.destination);
+            var isInputSource = sourceNode.component.type == types.ruleNodeType.INPUT.value;
+            var fromIndex = nodes.indexOf(sourceNode);
+            var toIndex = nodes.indexOf(destNode);
+            if ( (isInputSource || fromIndex > -1) && toIndex > -1 ) {
+                var connection = {
+                    isInputSource: isInputSource,
+                    fromIndex: fromIndex,
+                    toIndex: toIndex,
+                    label: edge.label
+                };
+                connections.push(connection);
+            }
+        }
+        itembuffer.copyRuleNodes(nodes, connections);
+    }
+
+    function pasteRuleNodes(event) {
+        var canvas = angular.element(vm.canvasControl.modelservice.getCanvasHtmlElement());
+        var x,y;
+        if (event) {
+            var offset = canvas.offset();
+            x = Math.round(event.clientX - offset.left);
+            y = Math.round(event.clientY - offset.top);
+        } else {
+            var scrollParent = canvas.parent();
+            var scrollTop = scrollParent.scrollTop();
+            var scrollLeft = scrollParent.scrollLeft();
+            x = scrollLeft + scrollParent.width()/2;
+            y = scrollTop + scrollParent.height()/2;
+        }
+        var ruleNodes = itembuffer.pasteRuleNodes(x, y, event);
+        if (ruleNodes) {
+            vm.modelservice.deselectAll();
+            var nodes = [];
+            for (var i=0;i<ruleNodes.nodes.length;i++) {
+                var node = ruleNodes.nodes[i];
+                node.id = 'rule-chain-node-' + vm.nextNodeID++;
+                var component = node.component;
+                if (component.configurationDescriptor.nodeDefinition.inEnabled) {
+                    node.connectors.push(
+                        {
+                            type: flowchartConstants.leftConnectorType,
+                            id: vm.nextConnectorID++
+                        }
+                    );
+                }
+                if (component.configurationDescriptor.nodeDefinition.outEnabled) {
+                    node.connectors.push(
+                        {
+                            type: flowchartConstants.rightConnectorType,
+                            id: vm.nextConnectorID++
+                        }
+                    );
+                }
+                nodes.push(node);
+                vm.ruleChainModel.nodes.push(node);
+                vm.modelservice.nodes.select(node);
+            }
+            for (i=0;i<ruleNodes.connections.length;i++) {
+                var connection = ruleNodes.connections[i];
+                var sourceNode = nodes[connection.fromIndex];
+                var destNode = nodes[connection.toIndex];
+                if ( (connection.isInputSource || sourceNode) &&  destNode ) {
+                    var source, destination;
+                    if (connection.isInputSource) {
+                        source = vm.inputConnectorId;
+                    } else {
+                        var sourceConnectors = vm.modelservice.nodes.getConnectorsByType(sourceNode, flowchartConstants.rightConnectorType);
+                        if (sourceConnectors && sourceConnectors.length) {
+                            source = sourceConnectors[0].id;
+                        }
+                    }
+                    var destConnectors = vm.modelservice.nodes.getConnectorsByType(destNode, flowchartConstants.leftConnectorType);
+                    if (destConnectors && destConnectors.length) {
+                        destination = destConnectors[0].id;
+                    }
+                    if (source && destination) {
+                        var edge = {
+                            source: source,
+                            destination: destination,
+                            label: connection.label
+                        };
+                        vm.ruleChainModel.edges.push(edge);
+                        vm.modelservice.edges.select(edge);
+                    }
+                }
+            }
+
+            if (vm.canvasControl.adjustCanvasSize) {
+                vm.canvasControl.adjustCanvasSize();
+            }
+
+            updateRuleNodesHighlight();
+
+            validate();
+        }
+    }
+
+    loadRuleChainLibrary(ruleNodeComponents, true);
+
+    $scope.$watch('vm.ruleNodeSearch',
+        function (newVal, oldVal) {
+            if (!angular.equals(newVal, oldVal)) {
+                var res = $filter('filter')(ruleNodeComponents, {name: vm.ruleNodeSearch});
+                loadRuleChainLibrary(res);
+            }
+        }
+    );
+
+    $scope.$on('searchTextUpdated', function () {
+        updateRuleNodesHighlight();
+    });
+
+    function loadRuleChainLibrary(ruleNodeComponents, loadRuleChain) {
+        for (var componentType in vm.ruleNodeTypesModel) {
+            vm.ruleNodeTypesModel[componentType].model.nodes.length = 0;
+        }
+        for (var i=0;i<ruleNodeComponents.length;i++) {
+            var ruleNodeComponent = ruleNodeComponents[i];
+            componentType = ruleNodeComponent.type;
+            var model = vm.ruleNodeTypesModel[componentType].model;
+            var node = {
+                id: 'node-lib-' + componentType + '-' + model.nodes.length,
+                component: ruleNodeComponent,
+                name: '',
+                nodeClass: vm.types.ruleNodeType[componentType].nodeClass,
+                icon: vm.types.ruleNodeType[componentType].icon,
+                x: 30,
+                y: 10+50*model.nodes.length,
+                connectors: []
+            };
+            if (ruleNodeComponent.configurationDescriptor.nodeDefinition.inEnabled) {
+                node.connectors.push(
+                    {
+                        type: flowchartConstants.leftConnectorType,
+                        id: model.nodes.length * 2
+                    }
+                );
+            }
+            if (ruleNodeComponent.configurationDescriptor.nodeDefinition.outEnabled) {
+                node.connectors.push(
+                    {
+                        type: flowchartConstants.rightConnectorType,
+                        id: model.nodes.length * 2 + 1
+                    }
+                );
+            }
+            model.nodes.push(node);
+        }
+        vm.ruleChainLibraryLoaded = true;
+        if (loadRuleChain) {
+            prepareRuleChain();
+        }
+        $mdUtil.nextTick(() => {
+            for (componentType in vm.ruleNodeTypesCanvasControl) {
+                if (vm.ruleNodeTypesCanvasControl[componentType].adjustCanvasSize) {
+                    vm.ruleNodeTypesCanvasControl[componentType].adjustCanvasSize(true);
+                }
+            }
+            for (componentType in vm.ruleNodeTypesModel) {
+                var panel = vm.$mdExpansionPanel(componentType);
+                if (panel) {
+                    if (!vm.ruleNodeTypesModel[componentType].model.nodes.length) {
+                        panel.collapse();
+                    } else {
+                        panel.expand();
+                    }
+                }
+            }
+        });
+    }
+
+    function prepareRuleChain() {
+
+        if (vm.ruleChainWatch) {
+            vm.ruleChainWatch();
+            vm.ruleChainWatch = null;
+        }
+
+        vm.nextNodeID = 1;
+        vm.nextConnectorID = 1;
+
+        vm.selectedObjects.length = 0;
+        vm.ruleChainModel.nodes.length = 0;
+        vm.ruleChainModel.edges.length = 0;
+
+        vm.inputConnectorId = vm.nextConnectorID++;
+
+        vm.ruleChainModel.nodes.push(
+            {
+                id: 'rule-chain-node-' + vm.nextNodeID++,
+                component: types.inputNodeComponent,
+                name: "",
+                nodeClass: types.ruleNodeType.INPUT.nodeClass,
+                icon: types.ruleNodeType.INPUT.icon,
+                readonly: true,
+                x: 50,
+                y: 150,
+                connectors: [
+                    {
+                        type: flowchartConstants.rightConnectorType,
+                        id: vm.inputConnectorId
+                    },
+                ]
+
+            }
+        );
+        ruleChainService.resolveTargetRuleChains(vm.ruleChainMetaData.ruleChainConnections)
+            .then((ruleChainsMap) => {
+                createRuleChainModel(ruleChainsMap);
+            }
+        );
+    }
+
+    function createRuleChainModel(ruleChainsMap) {
+        var nodes = [];
+        for (var i=0;i<vm.ruleChainMetaData.nodes.length;i++) {
+            var ruleNode = vm.ruleChainMetaData.nodes[i];
+            var component = ruleChainService.getRuleNodeComponentByClazz(ruleNode.type);
+            if (component) {
+                var node = {
+                    id: 'rule-chain-node-' + vm.nextNodeID++,
+                    ruleNodeId: ruleNode.id,
+                    additionalInfo: ruleNode.additionalInfo,
+                    configuration: ruleNode.configuration,
+                    debugMode: ruleNode.debugMode,
+                    x: ruleNode.additionalInfo.layoutX,
+                    y: ruleNode.additionalInfo.layoutY,
+                    component: component,
+                    name: ruleNode.name,
+                    nodeClass: vm.types.ruleNodeType[component.type].nodeClass,
+                    icon: vm.types.ruleNodeType[component.type].icon,
+                    connectors: []
+                };
+                if (component.configurationDescriptor.nodeDefinition.inEnabled) {
+                    node.connectors.push(
+                        {
+                            type: flowchartConstants.leftConnectorType,
+                            id: vm.nextConnectorID++
+                        }
+                    );
+                }
+                if (component.configurationDescriptor.nodeDefinition.outEnabled) {
+                    node.connectors.push(
+                        {
+                            type: flowchartConstants.rightConnectorType,
+                            id: vm.nextConnectorID++
+                        }
+                    );
+                }
+                nodes.push(node);
+                vm.ruleChainModel.nodes.push(node);
+            }
+        }
+
+        if (vm.ruleChainMetaData.firstNodeIndex > -1) {
+            var destNode = nodes[vm.ruleChainMetaData.firstNodeIndex];
+            if (destNode) {
+                var connectors = vm.modelservice.nodes.getConnectorsByType(destNode, flowchartConstants.leftConnectorType);
+                if (connectors && connectors.length) {
+                    var edge = {
+                        source: vm.inputConnectorId,
+                        destination: connectors[0].id
+                    };
+                    vm.ruleChainModel.edges.push(edge);
+                }
+            }
+        }
+
+        if (vm.ruleChainMetaData.connections) {
+            for (i = 0; i < vm.ruleChainMetaData.connections.length; i++) {
+                var connection = vm.ruleChainMetaData.connections[i];
+                var sourceNode = nodes[connection.fromIndex];
+                destNode = nodes[connection.toIndex];
+                if (sourceNode && destNode) {
+                    var sourceConnectors = vm.modelservice.nodes.getConnectorsByType(sourceNode, flowchartConstants.rightConnectorType);
+                    var destConnectors = vm.modelservice.nodes.getConnectorsByType(destNode, flowchartConstants.leftConnectorType);
+                    if (sourceConnectors && sourceConnectors.length && destConnectors && destConnectors.length) {
+                        edge = {
+                            source: sourceConnectors[0].id,
+                            destination: destConnectors[0].id,
+                            label: connection.type
+                        };
+                        vm.ruleChainModel.edges.push(edge);
+                    }
+                }
+            }
+        }
+
+        if (vm.ruleChainMetaData.ruleChainConnections) {
+            var ruleChainNodesMap = {};
+            for (i = 0; i < vm.ruleChainMetaData.ruleChainConnections.length; i++) {
+                var ruleChainConnection = vm.ruleChainMetaData.ruleChainConnections[i];
+                var ruleChain = ruleChainsMap[ruleChainConnection.targetRuleChainId.id];
+                if (ruleChainConnection.additionalInfo && ruleChainConnection.additionalInfo.ruleChainNodeId) {
+                    var ruleChainNode = ruleChainNodesMap[ruleChainConnection.additionalInfo.ruleChainNodeId];
+                    if (!ruleChainNode) {
+                        ruleChainNode = {
+                            id: 'rule-chain-node-' + vm.nextNodeID++,
+                            additionalInfo: ruleChainConnection.additionalInfo,
+                            x: ruleChainConnection.additionalInfo.layoutX,
+                            y: ruleChainConnection.additionalInfo.layoutY,
+                            component: types.ruleChainNodeComponent,
+                            nodeClass: vm.types.ruleNodeType.RULE_CHAIN.nodeClass,
+                            icon: vm.types.ruleNodeType.RULE_CHAIN.icon,
+                            connectors: [
+                                {
+                                    type: flowchartConstants.leftConnectorType,
+                                    id: vm.nextConnectorID++
+                                }
+                            ]
+                        };
+                        if (ruleChain.name) {
+                            ruleChainNode.name = ruleChain.name;
+                            ruleChainNode.targetRuleChainId = ruleChainConnection.targetRuleChainId.id;
+                        } else {
+                            ruleChainNode.name = "Unresolved";
+                            ruleChainNode.targetRuleChainId = null;
+                            ruleChainNode.error = $translate.instant('rulenode.invalid-target-rulechain');
+                        }
+                        ruleChainNodesMap[ruleChainConnection.additionalInfo.ruleChainNodeId] = ruleChainNode;
+                        vm.ruleChainModel.nodes.push(ruleChainNode);
+                    }
+                    sourceNode = nodes[ruleChainConnection.fromIndex];
+                    if (sourceNode) {
+                        connectors = vm.modelservice.nodes.getConnectorsByType(sourceNode, flowchartConstants.rightConnectorType);
+                        if (connectors && connectors.length) {
+                            var ruleChainEdge = {
+                                source: connectors[0].id,
+                                destination: ruleChainNode.connectors[0].id,
+                                label: ruleChainConnection.type
+                            };
+                            vm.ruleChainModel.edges.push(ruleChainEdge);
+                        }
+                    }
+                }
+            }
+        }
+
+        if (vm.canvasControl.adjustCanvasSize) {
+            vm.canvasControl.adjustCanvasSize(true);
+        }
+
+        vm.isDirty = false;
+
+        updateRuleNodesHighlight();
+
+        validate();
+
+        $mdUtil.nextTick(() => {
+            vm.ruleChainWatch = $scope.$watch('vm.ruleChainModel',
+                function (newVal, oldVal) {
+                    if (!angular.equals(newVal, oldVal)) {
+                        validate();
+                        if (!vm.isDirty) {
+                            vm.isDirty = true;
+                        }
+                    }
+                }, true
+            );
+        });
+    }
+
+    function updateRuleNodesHighlight() {
+        for (var i = 0; i < vm.ruleChainModel.nodes.length; i++) {
+            vm.ruleChainModel.nodes[i].highlighted = false;
+        }
+        if ($scope.searchConfig.searchText) {
+            var res = $filter('filter')(vm.ruleChainModel.nodes, {name: $scope.searchConfig.searchText});
+            if (res) {
+                for (i = 0; i < res.length; i++) {
+                    res[i].highlighted = true;
+                }
+            }
+        }
+    }
+
+    function validate() {
+        $mdUtil.nextTick(() => {
+            vm.isInvalid = false;
+            for (var i = 0; i < vm.ruleChainModel.nodes.length; i++) {
+                if (vm.ruleChainModel.nodes[i].error) {
+                    vm.isInvalid = true;
+                }
+                updateNodeErrorTooltip(vm.ruleChainModel.nodes[i]);
+            }
+        });
+    }
+
+    function saveRuleChain() {
+        var saveRuleChainPromise;
+        if (vm.isImport) {
+            saveRuleChainPromise = ruleChainService.saveRuleChain(vm.ruleChain);
+        } else {
+            saveRuleChainPromise = $q.when(vm.ruleChain);
+        }
+        saveRuleChainPromise.then(
+            (ruleChain) => {
+                vm.ruleChain = ruleChain;
+                var ruleChainMetaData = {
+                    ruleChainId: vm.ruleChain.id,
+                    nodes: [],
+                    connections: [],
+                    ruleChainConnections: []
+                };
+
+                var nodes = [];
+
+                for (var i=0;i<vm.ruleChainModel.nodes.length;i++) {
+                    var node = vm.ruleChainModel.nodes[i];
+                    if (node.component.type != types.ruleNodeType.INPUT.value && node.component.type != types.ruleNodeType.RULE_CHAIN.value) {
+                        var ruleNode = {};
+                        if (node.ruleNodeId) {
+                            ruleNode.id = node.ruleNodeId;
+                        }
+                        ruleNode.type = node.component.clazz;
+                        ruleNode.name = node.name;
+                        ruleNode.configuration = node.configuration;
+                        ruleNode.additionalInfo = node.additionalInfo;
+                        ruleNode.debugMode = node.debugMode;
+                        if (!ruleNode.additionalInfo) {
+                            ruleNode.additionalInfo = {};
+                        }
+                        ruleNode.additionalInfo.layoutX = node.x;
+                        ruleNode.additionalInfo.layoutY = node.y;
+                        ruleChainMetaData.nodes.push(ruleNode);
+                        nodes.push(node);
+                    }
+                }
+                var res = $filter('filter')(vm.ruleChainModel.edges, {source: vm.inputConnectorId}, true);
+                if (res && res.length) {
+                    var firstNodeEdge = res[0];
+                    var firstNode = vm.modelservice.nodes.getNodeByConnectorId(firstNodeEdge.destination);
+                    ruleChainMetaData.firstNodeIndex = nodes.indexOf(firstNode);
+                }
+                for (i=0;i<vm.ruleChainModel.edges.length;i++) {
+                    var edge = vm.ruleChainModel.edges[i];
+                    var sourceNode = vm.modelservice.nodes.getNodeByConnectorId(edge.source);
+                    var destNode = vm.modelservice.nodes.getNodeByConnectorId(edge.destination);
+                    if (sourceNode.component.type != types.ruleNodeType.INPUT.value) {
+                        var fromIndex = nodes.indexOf(sourceNode);
+                        if (destNode.component.type == types.ruleNodeType.RULE_CHAIN.value) {
+                            var ruleChainConnection = {
+                                fromIndex: fromIndex,
+                                targetRuleChainId: {entityType: vm.types.entityType.rulechain, id: destNode.targetRuleChainId},
+                                additionalInfo: destNode.additionalInfo,
+                                type: edge.label
+                            };
+                            if (!ruleChainConnection.additionalInfo) {
+                                ruleChainConnection.additionalInfo = {};
+                            }
+                            ruleChainConnection.additionalInfo.layoutX = destNode.x;
+                            ruleChainConnection.additionalInfo.layoutY = destNode.y;
+                            ruleChainConnection.additionalInfo.ruleChainNodeId = destNode.id;
+                            ruleChainMetaData.ruleChainConnections.push(ruleChainConnection);
+                        } else {
+                            var toIndex = nodes.indexOf(destNode);
+                            var nodeConnection = {
+                                fromIndex: fromIndex,
+                                toIndex: toIndex,
+                                type: edge.label
+                            };
+                            ruleChainMetaData.connections.push(nodeConnection);
+                        }
+                    }
+                }
+                ruleChainService.saveRuleChainMetaData(ruleChainMetaData).then(
+                    (ruleChainMetaData) => {
+                        vm.ruleChainMetaData = ruleChainMetaData;
+                        if (vm.isImport) {
+                            vm.isDirty = false;
+                            vm.isImport = false;
+                            $mdUtil.nextTick(() => {
+                                $state.go('home.ruleChains.ruleChain', {ruleChainId: vm.ruleChain.id.id});
+                            });
+                        } else {
+                            prepareRuleChain();
+                        }
+                    }
+                );
+            }
+        );
+    }
+
+    function revertRuleChain() {
+        prepareRuleChain();
+    }
+
+    function addRuleNode($event, ruleNode) {
+
+        ruleNode.configuration = angular.copy(ruleNode.component.configurationDescriptor.nodeDefinition.defaultConfiguration);
+
+        var ruleChainId = vm.ruleChain.id ? vm.ruleChain.id.id : null;
+
+        vm.enableHotKeys = false;
+
+        $mdDialog.show({
+            controller: 'AddRuleNodeController',
+            controllerAs: 'vm',
+            templateUrl: addRuleNodeTemplate,
+            parent: angular.element($document[0].body),
+            locals: {ruleNode: ruleNode, ruleChainId: ruleChainId},
+            fullscreen: true,
+            targetEvent: $event
+        }).then(function (ruleNode) {
+            ruleNode.id = 'rule-chain-node-' + vm.nextNodeID++;
+            ruleNode.connectors = [];
+            if (ruleNode.component.configurationDescriptor.nodeDefinition.inEnabled) {
+                ruleNode.connectors.push(
+                    {
+                        id: vm.nextConnectorID++,
+                        type: flowchartConstants.leftConnectorType
+                    }
+                );
+            }
+            if (ruleNode.component.configurationDescriptor.nodeDefinition.outEnabled) {
+                ruleNode.connectors.push(
+                    {
+                        id: vm.nextConnectorID++,
+                        type: flowchartConstants.rightConnectorType
+                    }
+                );
+            }
+            vm.ruleChainModel.nodes.push(ruleNode);
+            updateRuleNodesHighlight();
+            vm.enableHotKeys = true;
+        }, function () {
+            vm.enableHotKeys = true;
+        });
+    }
+
+    function addRuleNodeLink($event, link, labels) {
+        return $mdDialog.show({
+            controller: 'AddRuleNodeLinkController',
+            controllerAs: 'vm',
+            templateUrl: addRuleNodeLinkTemplate,
+            parent: angular.element($document[0].body),
+            locals: {link: link, labels: labels},
+            fullscreen: true,
+            targetEvent: $event
+        });
+    }
+
+    function objectsSelected() {
+        return vm.modelservice.nodes.getSelectedNodes().length > 0 ||
+            vm.modelservice.edges.getSelectedEdges().length > 0
+    }
+
+    function deleteSelected() {
+        vm.modelservice.deleteSelected();
+    }
+
+    function triggerResize() {
+        var w = angular.element($window);
+        w.triggerHandler('resize');
+    }
+}
+
+/*@ngInject*/
+export function AddRuleNodeController($scope, $mdDialog, ruleNode, ruleChainId, helpLinks) {
+
+    var vm = this;
+
+    vm.helpLinks = helpLinks;
+    vm.ruleNode = ruleNode;
+    vm.ruleChainId = ruleChainId;
+
+    vm.add = add;
+    vm.cancel = cancel;
+
+    function cancel() {
+        $mdDialog.cancel();
+    }
+
+    function add() {
+        $scope.theForm.$setPristine();
+        $mdDialog.hide(vm.ruleNode);
+    }
+}
+
+/*@ngInject*/
+export function AddRuleNodeLinkController($scope, $mdDialog, link, labels, helpLinks) {
+
+    var vm = this;
+
+    vm.helpLinks = helpLinks;
+    vm.link = link;
+    vm.labels = labels;
+
+    vm.add = add;
+    vm.cancel = cancel;
+
+    function cancel() {
+        $mdDialog.cancel();
+    }
+
+    function add() {
+        $scope.theForm.$setPristine();
+        $mdDialog.hide(vm.link);
+    }
+}
diff --git a/ui/src/app/rulechain/rulechain.directive.js b/ui/src/app/rulechain/rulechain.directive.js
new file mode 100644
index 0000000..b23cd98
--- /dev/null
+++ b/ui/src/app/rulechain/rulechain.directive.js
@@ -0,0 +1,47 @@
+/*
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.
+ */
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import ruleChainFieldsetTemplate from './rulechain-fieldset.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function RuleChainDirective($compile, $templateCache, $mdDialog, $document, $q, $translate, types, toast) {
+    var linker = function (scope, element) {
+        var template = $templateCache.get(ruleChainFieldsetTemplate);
+        element.html(template);
+
+        scope.onRuleChainIdCopied = function() {
+            toast.showSuccess($translate.instant('rulechain.idCopiedMessage'), 750, angular.element(element).parent().parent(), 'bottom left');
+        };
+
+        $compile(element.contents())(scope);
+    }
+    return {
+        restrict: "E",
+        link: linker,
+        scope: {
+            ruleChain: '=',
+            isEdit: '=',
+            isReadOnly: '=',
+            theForm: '=',
+            onExportRuleChain: '&',
+            onDeleteRuleChain: '&'
+        }
+    };
+}
diff --git a/ui/src/app/rulechain/rulechain.routes.js b/ui/src/app/rulechain/rulechain.routes.js
new file mode 100644
index 0000000..2aefd82
--- /dev/null
+++ b/ui/src/app/rulechain/rulechain.routes.js
@@ -0,0 +1,127 @@
+/*
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.
+ */
+/* eslint-disable import/no-unresolved, import/default */
+
+import ruleNodeTemplate from './rulenode.tpl.html';
+import ruleChainsTemplate from './rulechains.tpl.html';
+import ruleChainTemplate from './rulechain.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function RuleChainRoutes($stateProvider, NodeTemplatePathProvider) {
+
+    NodeTemplatePathProvider.setTemplatePath(ruleNodeTemplate);
+
+    $stateProvider
+        .state('home.ruleChains', {
+            url: '/ruleChains',
+            params: {'topIndex': 0},
+            module: 'private',
+            auth: ['SYS_ADMIN', 'TENANT_ADMIN'],
+            views: {
+                "content@home": {
+                    templateUrl: ruleChainsTemplate,
+                    controllerAs: 'vm',
+                    controller: 'RuleChainsController'
+                }
+            },
+            data: {
+                searchEnabled: true,
+                pageTitle: 'rulechain.rulechains'
+            },
+            ncyBreadcrumb: {
+                label: '{"icon": "settings_ethernet", "label": "rulechain.rulechains"}'
+            }
+        }).state('home.ruleChains.ruleChain', {
+            url: '/:ruleChainId',
+            reloadOnSearch: false,
+            module: 'private',
+            auth: ['SYS_ADMIN', 'TENANT_ADMIN'],
+            views: {
+                "content@home": {
+                    templateUrl: ruleChainTemplate,
+                    controller: 'RuleChainController',
+                    controllerAs: 'vm'
+                }
+            },
+            resolve: {
+                ruleChain:
+                    /*@ngInject*/
+                    function($stateParams, ruleChainService) {
+                        return ruleChainService.getRuleChain($stateParams.ruleChainId);
+                    },
+                ruleChainMetaData:
+                /*@ngInject*/
+                    function($stateParams, ruleChainService) {
+                        return ruleChainService.getRuleChainMetaData($stateParams.ruleChainId);
+                    },
+                ruleNodeComponents:
+                /*@ngInject*/
+                    function($stateParams, ruleChainService) {
+                        return ruleChainService.getRuleNodeComponents();
+                    }
+            },
+            data: {
+                import: false,
+                searchEnabled: true,
+                pageTitle: 'rulechain.rulechain'
+            },
+            ncyBreadcrumb: {
+                label: '{"icon": "settings_ethernet", "label": "{{ vm.ruleChain.name }}", "translate": "false"}'
+            }
+    }).state('home.ruleChains.importRuleChain', {
+        url: '/ruleChain/import',
+        reloadOnSearch: false,
+        module: 'private',
+        auth: ['SYS_ADMIN', 'TENANT_ADMIN'],
+        views: {
+            "content@home": {
+                templateUrl: ruleChainTemplate,
+                controller: 'RuleChainController',
+                controllerAs: 'vm'
+            }
+        },
+        params: {
+            ruleChainImport: {}
+        },
+        resolve: {
+            ruleChain:
+            /*@ngInject*/
+                function($stateParams) {
+                    return $stateParams.ruleChainImport.ruleChain;
+                },
+            ruleChainMetaData:
+            /*@ngInject*/
+                function($stateParams) {
+                    return $stateParams.ruleChainImport.metadata;
+                },
+            ruleNodeComponents:
+            /*@ngInject*/
+                function($stateParams, ruleChainService) {
+                    return ruleChainService.getRuleNodeComponents();
+                }
+        },
+        data: {
+            import: true,
+            searchEnabled: true,
+            pageTitle: 'rulechain.rulechain'
+        },
+        ncyBreadcrumb: {
+            label: '{"icon": "settings_ethernet", "label": "{{ (\'rulechain.import\' | translate) + \': \'+ vm.ruleChain.name }}", "translate": "false"}'
+        }
+    });
+}
diff --git a/ui/src/app/rulechain/rulechain.scss b/ui/src/app/rulechain/rulechain.scss
new file mode 100644
index 0000000..c9006db
--- /dev/null
+++ b/ui/src/app/rulechain/rulechain.scss
@@ -0,0 +1,466 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.
+ */
+
+.tb-rulechain {
+  .tb-fullscreen-button-style {
+    z-index: 1;
+  }
+  section.tb-header-buttons.tb-library-open {
+    pointer-events: none;
+    position: absolute;
+    left: 0px;
+    top: 0px;
+    z-index: 1;
+    .md-button.tb-btn-open-library {
+      left: 0px;
+      top: 0px;
+      line-height: 36px;
+      width: 36px;
+      height: 36px;
+      margin: 4px 0 0 4px;
+      opacity: 0.5;
+    }
+  }
+  .tb-rulechain-library {
+    width: 250px;
+    min-width: 250px;
+    z-index: 1;
+    md-toolbar {
+      min-height: 48px;
+      height: 48px;
+      .md-toolbar-tools>.md-button:last-child {
+        margin-right: 0px;
+      }
+      .md-toolbar-tools {
+        font-size: 14px;
+        padding: 0px 6px;
+        height: 48px;
+        .md-button.md-icon-button {
+          margin: 0px;
+          &.tb-small {
+            height: 32px;
+            min-height: 32px;
+            line-height: 20px;
+            padding: 6px;
+            width: 32px;
+            md-icon {
+              line-height: 20px;
+              font-size: 20px;
+              height: 20px;
+              width: 20px;
+              min-height: 20px;
+              min-width: 20px;
+            }
+          }
+        }
+      }
+    }
+    .tb-rulechain-library-panel-group {
+      overflow-y: auto;
+      overflow-x: hidden;
+      .tb-panel-title {
+        -webkit-user-select: none;
+        -moz-user-select: none;
+        -ms-user-select: none;
+        user-select: none;
+        min-width: 180px;
+      }
+      .fc-canvas {
+        background: #f9f9f9;
+      }
+      md-icon.md-expansion-panel-icon {
+        margin-right: 0px;
+      }
+      md-expansion-panel-collapsed, .md-expansion-panel-header-container {
+        background: #e6e6e6;
+        border-color: #909090;
+        position: static;
+      }
+      md-expansion-panel {
+        &.md-open {
+          margin-top: 0;
+          margin-bottom: 0;
+        }
+      }
+      md-expansion-panel-content {
+        padding: 0px;
+      }
+    }
+  }
+  .tb-rulechain-graph {
+    z-index: 0;
+    overflow: auto;
+  }
+}
+
+#tb-rule-chain-context-menu {
+  padding-top: 0px;
+  border-radius: 8px;
+  max-height: 404px;
+  .tb-context-menu-header {
+    padding: 8px 5px 5px;
+    font-size: 14px;
+    display: flex;
+    flex-direction: row;
+    height: 36px;
+    min-height: 36px;
+    &.tb-rulechain {
+      background-color: #aac7e4;
+    }
+    &.tb-link {
+      background-color: #aac7e4;
+    }
+    md-icon {
+      padding-left: 2px;
+      padding-right: 10px;
+    }
+    .tb-context-menu-title {
+      font-weight: 500;
+    }
+    .tb-context-menu-subtitle {
+      font-size: 12px;
+    }
+  }
+}
+
+.fc-canvas {
+  min-width: 100%;
+  min-height: 100%;
+  outline: none;
+  -webkit-touch-callout: none;
+  -webkit-user-select: none;
+  -khtml-user-select: none;
+  -moz-user-select: none;
+  -ms-user-select: none;
+  user-select: none;
+}
+
+.tb-rule-node, #tb-rule-chain-context-menu .tb-context-menu-header {
+  &.tb-filter-type {
+    background-color: #f1e861;
+  }
+  &.tb-enrichment-type {
+    background-color: #cdf14e;
+  }
+  &.tb-transformation-type {
+    background-color: #79cef1;
+  }
+  &.tb-action-type {
+    background-color: #f1928f;
+  }
+  &.tb-rule-chain-type {
+    background-color: #d6c4f1;
+  }
+}
+
+.tb-rule-node {
+  display: flex;
+  flex-direction: row;
+  min-width: 150px;
+  max-width: 150px;
+  min-height: 32px;
+  max-height: 32px;
+  height: 32px;
+  padding: 5px 10px;
+  border-radius: 5px;
+  background-color: #F15B26;
+  pointer-events: none;
+  color: #333;
+  border: solid 1px #777;
+  font-size: 12px;
+  line-height: 16px;
+  &.tb-rule-node-highlighted:not(.tb-rule-node-invalid) {
+    box-shadow: 0 0 10px 6px #51cbee;
+    .tb-node-title {
+      text-decoration: underline;
+      font-weight: bold;
+    }
+  }
+  &.tb-rule-node-invalid {
+    box-shadow: 0 0 10px 6px #ff5c50;
+  }
+  &.tb-input-type {
+    background-color: #a3eaa9;
+    user-select: none;
+  }
+  md-icon {
+    font-size: 20px;
+    width: 20px;
+    height: 20px;
+    min-height: 20px;
+    min-width: 20px;
+    padding-right: 4px;
+  }
+  .tb-node-type {
+
+  }
+  .tb-node-title {
+    font-weight: 500;
+  }
+  .tb-node-type, .tb-node-title {
+    overflow: hidden;
+    white-space: nowrap;
+    text-overflow: ellipsis;
+  }
+}
+
+.fc-node {
+  z-index: 1;
+  outline: none;
+  &.fc-dragging {
+    z-index: 10;
+  }
+  p {
+    padding: 0 15px;
+    text-align: center;
+  }
+  .fc-node-overlay {
+    position: absolute;
+    pointer-events: none;
+    left: 0;
+    top: 0;
+    right: 0;
+    bottom: 0;
+    background-color: #000;
+    opacity: 0;
+  }
+  &.fc-hover {
+    .fc-node-overlay {
+      opacity: 0.25;
+    }
+  }
+  &.fc-selected {
+    .fc-node-overlay {
+      opacity: 0.25;
+    }
+  }
+}
+
+.fc-leftConnectors, .fc-rightConnectors {
+  position: absolute;
+  top: 0;
+  height: 100%;
+
+  display: flex;
+  flex-direction: column;
+
+  z-index: 0;
+  .fc-magnet {
+    align-items: center;
+  }
+}
+
+.fc-leftConnectors {
+  left: -20px;
+}
+
+.fc-rightConnectors {
+  right: -20px;
+}
+
+.fc-magnet {
+  display: flex;
+  flex-grow: 1;
+  height: 60px;
+  justify-content: center;
+}
+
+.fc-connector {
+  width: 14px;
+  height: 14px;
+  border: 1px solid #333;
+  margin: 10px;
+  border-radius: 5px;
+  background-color: #ccc;
+  pointer-events: all;
+}
+
+.fc-connector.fc-hover {
+  background-color: #000;
+}
+
+.fc-arrow-marker {
+  polygon {
+    stroke: gray;
+    fill: gray;
+  }
+}
+
+.fc-arrow-marker-selected {
+  polygon {
+    stroke: red;
+    fill: red;
+  }
+}
+
+.fc-edge {
+  outline: none;
+  stroke: gray;
+  stroke-width: 4;
+  fill: transparent;
+  transition: stroke-width .2s;
+  &.fc-selected {
+    stroke: red;
+    stroke-width: 4;
+    fill: transparent;
+  }
+  &.fc-active {
+    animation: dash 3s linear infinite;
+    stroke-dasharray: 20;
+  }
+  &.fc-hover {
+    stroke: gray;
+    stroke-width: 6;
+    fill: transparent;
+  }
+  &.fc-dragging {
+    pointer-events: none;
+  }
+}
+
+.edge-endpoint {
+  fill: gray;
+}
+
+.fc-nodedelete {
+  display: none;
+}
+
+.fc-selected .fc-nodedelete {
+  outline: none;
+  display: block;
+  position: absolute;
+  right: -13px;
+  top: -16px;
+  border: solid 2px white;
+  border-radius: 50%;
+  font-weight: 600;
+  font-size: 18px;
+  line-height: 18px;
+  height: 20px;
+  padding-top: 2px;
+  width: 22px;
+  background: #494949;
+  color: #fff;
+  text-align: center;
+  vertical-align: bottom;
+  cursor: pointer;
+}
+
+.fc-noselect {
+  -webkit-touch-callout: none; /* iOS Safari */
+  -webkit-user-select: none; /* Safari */
+  -khtml-user-select: none; /* Konqueror HTML */
+  -moz-user-select: none; /* Firefox */
+  -ms-user-select: none; /* Internet Explorer/Edge */
+  user-select: none; /* Non-prefixed version, currently
+                                  supported by Chrome and Opera */
+}
+
+.fc-edge-label {
+  position: absolute;
+  transition: transform .2s;
+  opacity: 0.8;
+  &.ng-leave {
+    transition: 0s none;
+  }
+  &.fc-hover {
+    transform: scale(1.25);
+  }
+  &.fc-selected {
+    .fc-edge-label-text {
+      span {
+        border: solid red;
+        color: red;
+      }
+    }
+  }
+  .fc-nodedelete {
+    right: -13px;
+    top: -30px;
+  }
+  &:focus {
+    outline: 0;
+  }
+}
+
+.fc-edge-label-text {
+  position: absolute;
+  -webkit-transform: translate(-50%, -50%);
+  transform: translate(-50%, -50%);
+  white-space: nowrap;
+  text-align: center;
+  font-size: 14px;
+  font-weight: 600;
+  span {
+    cursor: default;
+    border: solid 2px #003a79;
+    border-radius: 10px;
+    color: #003a79;
+    background-color: #fff;
+    padding: 3px 5px;
+  }
+}
+
+.fc-select-rectangle {
+  border: 2px dashed #5262ff;
+  position: absolute;
+  background: rgba(20,125,255,0.1);
+  z-index: 2;
+}
+
+@keyframes dash {
+  from {
+    stroke-dashoffset: 500;
+  }
+}
+
+.tb-rule-node-tooltip {
+  font-size: 14px;
+  width: 300px;
+  color: #333;
+}
+
+.tb-rule-node-error-tooltip {
+  font-size: 16px;
+  color: #ea0d0d;
+}
+
+.tb-rule-node-tooltip, .tb-rule-node-error-tooltip {
+  #tooltip-content {
+    .tb-node-title {
+      font-weight: 600;
+    }
+    .tb-node-description {
+      font-style: italic;
+      color: #555;
+    }
+    .tb-node-details {
+      padding-top: 10px;
+      padding-bottom: 10px;
+    }
+    code {
+      padding: 0px 3px 2px 3px;
+      margin: 1px;
+      color: #AD1625;
+      white-space: nowrap;
+      background-color: #f7f7f9;
+      border: 1px solid #e1e1e8;
+      border-radius: 2px;
+      font-size: 12px;
+    }
+  }
+}
\ No newline at end of file
diff --git a/ui/src/app/rulechain/rulechain.tpl.html b/ui/src/app/rulechain/rulechain.tpl.html
new file mode 100644
index 0000000..59f246a
--- /dev/null
+++ b/ui/src/app/rulechain/rulechain.tpl.html
@@ -0,0 +1,229 @@
+<!--
+
+    Copyright © 2016-2018 The Thingsboard Authors
+
+    Licensed 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.
+
+-->
+
+<md-content flex tb-expand-fullscreen tb-confirm-on-exit is-dirty="vm.isConfirmOnExit"
+            expand-tooltip-direction="bottom" layout="column" class="tb-rulechain"
+            ng-keydown="vm.keyDown($event)"
+            ng-keyup="vm.keyUp($event)" on-fullscreen-changed="vm.isFullscreen = expanded">
+    <section class="tb-rulechain-container" flex layout="column">
+        <div class="tb-rulechain-layout" flex layout="row">
+            <section layout="row" layout-wrap
+                     class="tb-header-buttons md-fab tb-library-open">
+                <md-button ng-show="!vm.isLibraryOpen"
+                           class="tb-btn-header tb-btn-open-library md-primary md-fab md-fab-top-left"
+                           aria-label="{{ 'rulenode.open-node-library' | translate }}"
+                           ng-click="vm.isLibraryOpen = true">
+                    <md-tooltip md-direction="{{vm.isFullscreen ? 'bottom' : 'top'}}">
+                        {{ 'rulenode.open-node-library' | translate }}
+                    </md-tooltip>
+                    <ng-md-icon icon="menu"></ng-md-icon>
+                </md-button>
+            </section>
+            <md-sidenav class="tb-rulechain-library md-sidenav-left md-whiteframe-4dp"
+                        md-disable-backdrop
+                        md-is-locked-open="vm.isLibraryOpenReadonly"
+                        md-is-open="vm.isLibraryOpenReadonly"
+                        md-component-id="rulechain-library-sidenav" layout="column">
+                <md-toolbar>
+                    <div class="md-toolbar-tools">
+                        <md-button class="md-icon-button tb-small" aria-label="{{ 'action.search' | translate }}">
+                            <md-icon aria-label="{{ 'action.search' | translate }}" class="material-icons">search</md-icon>
+                            <md-tooltip md-direction="{{vm.isFullscreen ? 'bottom' : 'top'}}">
+                                {{'rulenode.search' | translate}}
+                            </md-tooltip>
+                        </md-button>
+                        <div layout="row" md-theme="tb-dark" flex>
+                            <md-input-container flex>
+                                <label>&nbsp;</label>
+                                <input ng-model="vm.ruleNodeSearch" placeholder="{{'rulenode.search' | translate}}"/>
+                            </md-input-container>
+                        </div>
+                        <md-button class="md-icon-button tb-small" aria-label="Close"
+                                   ng-show="vm.ruleNodeSearch"
+                                   ng-click="vm.ruleNodeSearch = ''">
+                            <md-icon aria-label="Close" class="material-icons">close</md-icon>
+                            <md-tooltip md-direction="{{vm.isFullscreen ? 'bottom' : 'top'}}">
+                                {{ 'action.clear-search' | translate }}
+                            </md-tooltip>
+                        </md-button>
+                        <md-button class="md-icon-button tb-small" aria-label="Close" ng-click="vm.isLibraryOpen = false">
+                            <md-icon aria-label="Close" class="material-icons">chevron_left</md-icon>
+                            <md-tooltip md-direction="{{vm.isFullscreen ? 'bottom' : 'top'}}">
+                                {{ 'action.close' | translate }}
+                            </md-tooltip>
+                        </md-button>
+                    </div>
+                </md-toolbar>
+                <md-expansion-panel-group flex
+                                          ng-if="vm.ruleChainLibraryLoaded" class="tb-rulechain-library-panel-group"
+                                          md-component-id="libraryPanelGroup" auto-expand="true" multiple>
+                    <md-expansion-panel md-component-id="{{typeId}}" id="{{typeId}}" ng-repeat="(typeId, typeModel) in vm.ruleNodeTypesModel">
+                        <md-expansion-panel-collapsed ng-mouseenter="vm.typeHeaderMouseEnter($event, typeId)"
+                                                      ng-mouseleave="vm.destroyTooltips()">
+                            <div class="tb-panel-title" translate>{{vm.types.ruleNodeType[typeId].name}}</div>
+                            <md-expansion-panel-icon></md-expansion-panel-icon>
+                        </md-expansion-panel-collapsed>
+                        <md-expansion-panel-expanded>
+                            <md-expansion-panel-header ng-mouseenter="vm.typeHeaderMouseEnter($event, typeId)"
+                                                       ng-mouseleave="vm.destroyTooltips()"
+                                                       ng-click="vm.$mdExpansionPanel(typeId).collapse()">
+                                <div class="tb-panel-title" translate>{{vm.types.ruleNodeType[typeId].name}}</div>
+                                <md-expansion-panel-icon></md-expansion-panel-icon>
+                            </md-expansion-panel-header>
+                            <md-expansion-panel-content>
+                                <fc-canvas id="tb-rulechain-{{typeId}}"
+                                           model="vm.ruleNodeTypesModel[typeId].model" selected-objects="vm.ruleNodeTypesModel[typeId].selectedObjects"
+                                           automatic-resize="false"
+                                           callbacks="vm.nodeLibCallbacks"
+                                           node-width="170"
+                                           node-height="50"
+                                           control="vm.ruleNodeTypesCanvasControl[typeId]"
+                                           drop-target-id="'tb-rulchain-canvas'"></fc-canvas>
+                            </md-expansion-panel-content>
+                        </md-expansion-panel-expanded>
+                    </md-expansion-panel>
+                </md-expansion-panel-group>
+            </md-sidenav>
+            <md-menu flex style="position: relative;" md-position-mode="target target" tb-offset-x="-20" tb-offset-y="-45" tb-mousepoint-menu>
+                <div class="tb-absolute-fill tb-rulechain-graph" ng-click="" tb-contextmenu="vm.openRuleChainContextMenu($event, $mdOpenMousepointMenu)">
+                    <fc-canvas id="tb-rulchain-canvas"
+                               model="vm.ruleChainModel"
+                               selected-objects="vm.selectedObjects"
+                               edge-style="curved"
+                               node-width="170"
+                               node-height="50"
+                               automatic-resize="true"
+                               control="vm.canvasControl"
+                               callbacks="vm.editCallbacks">
+                    </fc-canvas>
+                </div>
+                <md-menu-content id="tb-rule-chain-context-menu" width="4" ng-mouseleave="$mdCloseMousepointMenu()">
+                    <div class="tb-context-menu-header {{vm.contextInfo.headerClass}}">
+                        <md-icon aria-label="node-type-icon"
+                                 class="material-icons">{{vm.contextInfo.icon}}</md-icon>
+                        <div flex>
+                            <div class="tb-context-menu-title">{{vm.contextInfo.title}}</div>
+                            <div class="tb-context-menu-subtitle">{{vm.contextInfo.subtitle}}</div>
+                        </div>
+                    </div>
+                    <div ng-repeat="item in vm.contextInfo.items">
+                        <md-divider ng-if="item.divider"></md-divider>
+                        <md-menu-item ng-if="!item.divider">
+                            <md-button ng-disabled="!item.enabled" ng-click="item.action(vm.contextMenuEvent)">
+                                <span ng-if="item.shortcut" class="tb-alt-text"> {{ item.shortcut | keyboardShortcut }}</span>
+                                <md-icon ng-if="item.icon" md-menu-align-target aria-label="{{ item.value | translate }}" class="material-icons">{{item.icon}}</md-icon>
+                                <span translate>{{item.value}}</span>
+                            </md-button>
+                        </md-menu-item>
+                    </div>
+                </md-menu-content>
+            </md-menu>
+        </div>
+        <tb-details-sidenav class="tb-rulenode-details-sidenav"
+                            header-title="{{vm.editingRuleNode.name}}"
+                            header-subtitle="{{(vm.types.ruleNodeType[vm.editingRuleNode.component.type].name | translate)
+                            + ' - ' + vm.editingRuleNode.component.name}}"
+                            is-read-only="vm.selectedRuleNodeTabIndex > 0"
+                            is-open="vm.isEditingRuleNode"
+                            tb-enable-backdrop
+                            is-always-edit="true"
+                            on-close-details="vm.onEditRuleNodeClosed()"
+                            on-toggle-details-edit-mode="vm.onRevertRuleNodeEdit(vm.ruleNodeForm)"
+                            on-apply-details="vm.saveRuleNode(vm.ruleNodeForm)"
+                            the-form="vm.ruleNodeForm">
+            <details-buttons tb-help="vm.helpLinkIdForRuleNodeType()" help-container-id="help-container">
+                <div id="help-container"></div>
+            </details-buttons>
+            <md-tabs md-selected="vm.selectedRuleNodeTabIndex"
+                     id="ruleNodeTabs" md-border-bottom flex class="tb-absolute-fill" ng-if="vm.isEditingRuleNode">
+                <md-tab label="{{ 'rulenode.details' | translate }}">
+                    <form name="vm.ruleNodeForm">
+                        <tb-rule-node
+                                rule-node="vm.editingRuleNode"
+                                rule-chain-id="vm.ruleChain.id.id"
+                                is-edit="true"
+                                is-read-only="false"
+                                on-delete-rule-node="vm.deleteRuleNode(event, vm.editingRuleNode)"
+                                the-form="vm.ruleNodeForm">
+                        </tb-rule-node>
+                    </form>
+                </md-tab>
+                <md-tab ng-if="vm.isEditingRuleNode && vm.editingRuleNode.ruleNodeId"
+                        md-on-select="vm.triggerResize()" label="{{ 'rulenode.events' | translate }}">
+                    <tb-event-table flex entity-type="vm.types.entityType.rulenode"
+                                    entity-id="vm.editingRuleNode.ruleNodeId.id"
+                                    tenant-id="vm.ruleChain.tenantId.id"
+                                    debug-event-types="{{vm.types.debugEventType.debugRuleNode.value}}"
+                                    default-event-type="{{vm.types.debugEventType.debugRuleNode.value}}">
+                    </tb-event-table>
+                </md-tab>
+            </md-tabs>
+        </tb-details-sidenav>
+        <tb-details-sidenav class="tb-rulenode-link-details-sidenav"
+                            header-title="{{vm.editingRuleNodeLink.label}}"
+                            header-subtitle="{{'rulenode.link-details' | translate}}"
+                            is-read-only="false"
+                            is-open="vm.isEditingRuleNodeLink"
+                            tb-enable-backdrop
+                            is-always-edit="true"
+                            on-close-details="vm.onEditRuleNodeLinkClosed()"
+                            on-toggle-details-edit-mode="vm.onRevertRuleNodeLinkEdit(vm.ruleNodeLinkForm)"
+                            on-apply-details="vm.saveRuleNodeLink(vm.ruleNodeLinkForm)"
+                            the-form="vm.ruleNodeLinkForm">
+            <details-buttons tb-help="vm.helpLinkIdForRuleNodeLink()" help-container-id="link-help-container">
+                <div id="link-help-container"></div>
+            </details-buttons>
+            <form name="vm.ruleNodeLinkForm" ng-if="vm.isEditingRuleNodeLink">
+                <tb-rule-node-link
+                        link="vm.editingRuleNodeLink"
+                        labels="vm.editingRuleNodeLinkLabels"
+                        is-edit="true"
+                        is-read-only="false"
+                        the-form="vm.ruleNodeLinkForm">
+                </tb-rule-node-link>
+            </form>
+        </tb-details-sidenav>
+    </section>
+    <section layout="row" layout-wrap class="tb-footer-buttons md-fab" layout-align="start end">
+        <md-button ng-disabled="$root.loading" ng-show="vm.objectsSelected()" class="tb-btn-footer md-accent md-hue-2 md-fab"
+                   ng-click="vm.deleteSelected()" aria-label="{{ 'action.delete' | translate }}">
+            <md-tooltip md-direction="top">
+                {{ 'rulenode.delete-selected-objects' | translate }}
+            </md-tooltip>
+            <ng-md-icon icon="delete"></ng-md-icon>
+        </md-button>
+        <md-button ng-disabled="$root.loading  || vm.isInvalid || (!vm.isDirty && !vm.isImport)"
+                   class="tb-btn-footer md-accent md-hue-2 md-fab"
+                   aria-label="{{ 'action.apply' | translate }}"
+                   ng-click="vm.saveRuleChain()">
+            <md-tooltip md-direction="top">
+                {{ 'action.apply-changes' | translate }}
+            </md-tooltip>
+            <ng-md-icon icon="done"></ng-md-icon>
+        </md-button>
+        <md-button ng-disabled="$root.loading || !vm.isDirty"
+                   class="tb-btn-footer md-accent md-hue-2 md-fab"
+                   aria-label="{{ 'action.decline-changes' | translate }}"
+                   ng-click="vm.revertRuleChain()">
+            <md-tooltip md-direction="top">
+                {{ 'action.decline-changes' | translate }}
+            </md-tooltip>
+            <ng-md-icon icon="close"></ng-md-icon>
+        </md-button>
+    </section>
+</md-content>
diff --git a/ui/src/app/rulechain/rulechain-card.tpl.html b/ui/src/app/rulechain/rulechain-card.tpl.html
new file mode 100644
index 0000000..48a572c
--- /dev/null
+++ b/ui/src/app/rulechain/rulechain-card.tpl.html
@@ -0,0 +1,18 @@
+<!--
+
+    Copyright © 2016-2018 The Thingsboard Authors
+
+    Licensed 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.
+
+-->
+<div class="tb-uppercase" ng-if="item && parentCtl.types.id.nullUid === item.tenantId.id" translate>rulechain.system</div>
diff --git a/ui/src/app/rulechain/rulechain-fieldset.tpl.html b/ui/src/app/rulechain/rulechain-fieldset.tpl.html
new file mode 100644
index 0000000..2189daa
--- /dev/null
+++ b/ui/src/app/rulechain/rulechain-fieldset.tpl.html
@@ -0,0 +1,54 @@
+<!--
+
+    Copyright © 2016-2018 The Thingsboard Authors
+
+    Licensed 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.
+
+-->
+<md-button ng-click="onExportRuleChain({event: $event})"
+           ng-show="!isEdit"
+           class="md-raised md-primary">{{ 'rulechain.export' | translate }}</md-button>
+<md-button ng-click="onDeleteRuleChain({event: $event})"
+           ng-show="!isEdit && !isReadOnly"
+           class="md-raised md-primary">{{ 'rulechain.delete' | translate }}</md-button>
+
+<div layout="row">
+    <md-button ngclipboard data-clipboard-action="copy"
+               ngclipboard-success="onRuleChainIdCopied(e)"
+               data-clipboard-text="{{ruleChain.id.id}}" ng-show="!isEdit"
+               class="md-raised">
+        <md-icon md-svg-icon="mdi:clipboard-arrow-left"></md-icon>
+        <span translate>rulechain.copyId</span>
+    </md-button>
+</div>
+
+<md-content class="md-padding tb-rulechain-fieldset" layout="column">
+    <fieldset ng-disabled="$root.loading || !isEdit || isReadOnly">
+        <md-input-container class="md-block">
+            <label translate>rulechain.name</label>
+            <input required name="name" ng-model="ruleChain.name">
+            <div ng-messages="theForm.name.$error">
+                <div translate ng-message="required">rulechain.name-required</div>
+            </div>
+        </md-input-container>
+        <md-input-container class="md-block">
+            <md-checkbox ng-disabled="$root.loading || !isEdit" aria-label="{{ 'rulechain.debug-mode' | translate }}"
+                         ng-model="ruleChain.debugMode">{{ 'rulechain.debug-mode' | translate }}
+            </md-checkbox>
+        </md-input-container>
+        <md-input-container class="md-block">
+            <label translate>rulechain.description</label>
+            <textarea ng-model="ruleChain.additionalInfo.description" rows="2"></textarea>
+        </md-input-container>
+    </fieldset>
+</md-content>
diff --git a/ui/src/app/rulechain/rulechains.controller.js b/ui/src/app/rulechain/rulechains.controller.js
new file mode 100644
index 0000000..7c857f2
--- /dev/null
+++ b/ui/src/app/rulechain/rulechains.controller.js
@@ -0,0 +1,188 @@
+/*
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.
+ */
+/* eslint-disable import/no-unresolved, import/default */
+
+import addRuleChainTemplate from './add-rulechain.tpl.html';
+import ruleChainCard from './rulechain-card.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function RuleChainsController(ruleChainService, userService, importExport, $state, $stateParams, $filter, $translate, types) {
+
+    var ruleChainActionsList = [
+        {
+            onAction: function ($event, item) {
+                vm.grid.openItem($event, item);
+            },
+            name: function() { return $translate.instant('rulechain.details') },
+            details: function() { return $translate.instant('rulechain.rulechain-details') },
+            icon: "edit"
+        },
+        {
+            onAction: function ($event, item) {
+                exportRuleChain($event, item);
+            },
+            name: function() { $translate.instant('action.export') },
+            details: function() { return $translate.instant('rulechain.export') },
+            icon: "file_download"
+        },
+        {
+            onAction: function ($event, item) {
+                vm.grid.deleteItem($event, item);
+            },
+            name: function() { return $translate.instant('action.delete') },
+            details: function() { return $translate.instant('rulechain.delete') },
+            icon: "delete",
+            isEnabled: isRuleChainEditable
+        }
+    ];
+
+    var ruleChainAddItemActionsList = [
+        {
+            onAction: function ($event) {
+                vm.grid.addItem($event);
+            },
+            name: function() { return $translate.instant('action.create') },
+            details: function() { return $translate.instant('rulechain.create-new-rulechain') },
+            icon: "insert_drive_file"
+        },
+        {
+            onAction: function ($event) {
+                importExport.importRuleChain($event).then(
+                    function(ruleChainImport) {
+                        $state.go('home.ruleChains.importRuleChain', {ruleChainImport:ruleChainImport});
+                    }
+                );
+            },
+            name: function() { return $translate.instant('action.import') },
+            details: function() { return $translate.instant('rulechain.import') },
+            icon: "file_upload"
+        }
+    ];
+
+    var vm = this;
+
+    vm.types = types;
+
+    vm.ruleChainGridConfig = {
+
+        refreshParamsFunc: null,
+
+        deleteItemTitleFunc: deleteRuleChainTitle,
+        deleteItemContentFunc: deleteRuleChainText,
+        deleteItemsTitleFunc: deleteRuleChainsTitle,
+        deleteItemsActionTitleFunc: deleteRuleChainsActionTitle,
+        deleteItemsContentFunc: deleteRuleChainsText,
+
+        fetchItemsFunc: fetchRuleChains,
+        saveItemFunc: saveRuleChain,
+        clickItemFunc: openRuleChain,
+        deleteItemFunc: deleteRuleChain,
+
+        getItemTitleFunc: getRuleChainTitle,
+        itemCardTemplateUrl: ruleChainCard,
+        parentCtl: vm,
+
+        actionsList: ruleChainActionsList,
+        addItemActions: ruleChainAddItemActionsList,
+
+        onGridInited: gridInited,
+
+        addItemTemplateUrl: addRuleChainTemplate,
+
+        addItemText: function() { return $translate.instant('rulechain.add-rulechain-text') },
+        noItemsText: function() { return $translate.instant('rulechain.no-rulechains-text') },
+        itemDetailsText: function() { return $translate.instant('rulechain.rulechain-details') },
+        isSelectionEnabled: isRuleChainEditable,
+        isDetailsReadOnly: function(ruleChain) {
+            return !isRuleChainEditable(ruleChain);
+        }
+    };
+
+    if (angular.isDefined($stateParams.items) && $stateParams.items !== null) {
+        vm.ruleChainGridConfig.items = $stateParams.items;
+    }
+
+    if (angular.isDefined($stateParams.topIndex) && $stateParams.topIndex > 0) {
+        vm.ruleChainGridConfig.topIndex = $stateParams.topIndex;
+    }
+
+    vm.isRuleChainEditable = isRuleChainEditable;
+
+    vm.exportRuleChain = exportRuleChain;
+
+    function deleteRuleChainTitle(ruleChain) {
+        return $translate.instant('rulechain.delete-rulechain-title', {ruleChainName: ruleChain.name});
+    }
+
+    function deleteRuleChainText() {
+        return $translate.instant('rulechain.delete-rulechain-text');
+    }
+
+    function deleteRuleChainsTitle(selectedCount) {
+        return $translate.instant('rulechain.delete-rulechains-title', {count: selectedCount}, 'messageformat');
+    }
+
+    function deleteRuleChainsActionTitle(selectedCount) {
+        return $translate.instant('rulechain.delete-rulechains-action-title', {count: selectedCount}, 'messageformat');
+    }
+
+    function deleteRuleChainsText() {
+        return $translate.instant('rulechain.delete-rulechains-text');
+    }
+
+    function gridInited(grid) {
+        vm.grid = grid;
+    }
+
+    function fetchRuleChains(pageLink) {
+        return ruleChainService.getRuleChains(pageLink);
+    }
+
+    function saveRuleChain(ruleChain) {
+        return ruleChainService.saveRuleChain(ruleChain);
+    }
+
+    function openRuleChain($event, ruleChain) {
+        if ($event) {
+            $event.stopPropagation();
+        }
+        $state.go('home.ruleChains.ruleChain', {ruleChainId: ruleChain.id.id});
+    }
+
+    function deleteRuleChain(ruleChainId) {
+        return ruleChainService.deleteRuleChain(ruleChainId);
+    }
+
+    function getRuleChainTitle(ruleChain) {
+        return ruleChain ? ruleChain.name : '';
+    }
+
+    function isRuleChainEditable(ruleChain) {
+        if (userService.getAuthority() === 'TENANT_ADMIN') {
+            return ruleChain && ruleChain.tenantId.id != types.id.nullUid;
+        } else {
+            return userService.getAuthority() === 'SYS_ADMIN';
+        }
+    }
+
+    function exportRuleChain($event, ruleChain) {
+        $event.stopPropagation();
+        importExport.exportRuleChain(ruleChain.id.id);
+    }
+
+}
diff --git a/ui/src/app/rulechain/rulechains.tpl.html b/ui/src/app/rulechain/rulechains.tpl.html
new file mode 100644
index 0000000..cf9d256
--- /dev/null
+++ b/ui/src/app/rulechain/rulechains.tpl.html
@@ -0,0 +1,76 @@
+<!--
+
+    Copyright © 2016-2018 The Thingsboard Authors
+
+    Licensed 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.
+
+-->
+<tb-grid grid-configuration="vm.ruleChainGridConfig">
+    <details-buttons tb-help="'rulechains'" help-container-id="help-container">
+        <div id="help-container"></div>
+    </details-buttons>
+    <md-tabs ng-class="{'tb-headless': (vm.grid.detailsConfig.isDetailsEditMode || !vm.isRuleChainEditable(vm.grid.operatingItem()))}"
+             id="tabs" md-border-bottom flex class="tb-absolute-fill">
+        <md-tab label="{{ 'rulechain.details' | translate }}">
+            <tb-rule-chain rule-chain="vm.grid.operatingItem()"
+                     is-edit="vm.grid.detailsConfig.isDetailsEditMode"
+                     is-read-only="vm.grid.isDetailsReadOnly(vm.grid.operatingItem())"
+                     the-form="vm.grid.detailsForm"
+                     on-export-rule-chain="vm.exportRuleChain(event, vm.grid.detailsConfig.currentItem)"
+                     on-delete-rule-chain="vm.grid.deleteItem(event, vm.grid.detailsConfig.currentItem)"></tb-rule-chain>
+        </md-tab>
+        <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isRuleChainEditable(vm.grid.operatingItem())" md-on-select="vm.grid.triggerResize()" label="{{ 'attribute.attributes' | translate }}">
+            <tb-attribute-table flex
+                                entity-id="vm.grid.operatingItem().id.id"
+                                entity-type="{{vm.types.entityType.rulechain}}"
+                                entity-name="vm.grid.operatingItem().name"
+                                default-attribute-scope="{{vm.types.attributesScope.server.value}}">
+            </tb-attribute-table>
+        </md-tab>
+        <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isRuleChainEditable(vm.grid.operatingItem())" md-on-select="vm.grid.triggerResize()" label="{{ 'attribute.latest-telemetry' | translate }}">
+            <tb-attribute-table flex
+                                entity-id="vm.grid.operatingItem().id.id"
+                                entity-type="{{vm.types.entityType.rulechain}}"
+                                entity-name="vm.grid.operatingItem().name"
+                                default-attribute-scope="{{vm.types.latestTelemetry.value}}"
+                                disable-attribute-scope-selection="true">
+            </tb-attribute-table>
+        </md-tab>
+        <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isRuleChainEditable(vm.grid.operatingItem())" md-on-select="vm.grid.triggerResize()" label="{{ 'alarm.alarms' | translate }}">
+            <tb-alarm-table flex entity-type="vm.types.entityType.rulechain"
+                            entity-id="vm.grid.operatingItem().id.id">
+            </tb-alarm-table>
+        </md-tab>
+        <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isRuleChainEditable(vm.grid.operatingItem())" md-on-select="vm.grid.triggerResize()" label="{{ 'rulechain.events' | translate }}">
+            <tb-event-table flex entity-type="vm.types.entityType.rulechain"
+                            entity-id="vm.grid.operatingItem().id.id"
+                            tenant-id="vm.grid.operatingItem().tenantId.id"
+                            debug-event-types="{{vm.types.debugEventType.debugRuleChain.value}}"
+                            default-event-type="{{vm.types.debugEventType.debugRuleChain.value}}">
+            </tb-event-table>
+        </md-tab>
+        <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isRuleChainEditable(vm.grid.operatingItem())" md-on-select="vm.grid.triggerResize()" label="{{ 'relation.relations' | translate }}">
+            <tb-relation-table flex
+                               entity-id="vm.grid.operatingItem().id.id"
+                               entity-type="{{vm.types.entityType.rulechain}}">
+            </tb-relation-table>
+        </md-tab>
+        <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.isRuleChainEditable(vm.grid.operatingItem()) && vm.grid.isTenantAdmin()"
+                md-on-select="vm.grid.triggerResize()" label="{{ 'audit-log.audit-logs' | translate }}">
+            <tb-audit-log-table flex entity-type="vm.types.entityType.rulechain"
+                                entity-id="vm.grid.operatingItem().id.id"
+                                audit-log-mode="{{vm.types.auditLogMode.entity}}">
+            </tb-audit-log-table>
+        </md-tab>
+    </md-tabs>
+</tb-grid>
diff --git a/ui/src/app/rulechain/rulenode.directive.js b/ui/src/app/rulechain/rulenode.directive.js
new file mode 100644
index 0000000..be3e9c3
--- /dev/null
+++ b/ui/src/app/rulechain/rulenode.directive.js
@@ -0,0 +1,81 @@
+/*
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.
+ */
+
+import './rulenode.scss';
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import ruleNodeFieldsetTemplate from './rulenode-fieldset.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function RuleNodeDirective($compile, $templateCache, ruleChainService, types) {
+    var linker = function (scope, element) {
+        var template = $templateCache.get(ruleNodeFieldsetTemplate);
+        element.html(template);
+
+        scope.types = types;
+
+        scope.params = {
+            targetRuleChainId: null
+        };
+
+        scope.$watch('ruleNode', function() {
+            if (scope.ruleNode && scope.ruleNode.component.type == types.ruleNodeType.RULE_CHAIN.value) {
+                scope.params.targetRuleChainId = scope.ruleNode.targetRuleChainId;
+                watchTargetRuleChain();
+            } else {
+                if (scope.targetRuleChainWatch) {
+                    scope.targetRuleChainWatch();
+                    scope.targetRuleChainWatch = null;
+                }
+            }
+        });
+
+        function watchTargetRuleChain() {
+            scope.targetRuleChainWatch = scope.$watch('params.targetRuleChainId',
+                function(targetRuleChainId) {
+                    if (scope.ruleNode.targetRuleChainId != targetRuleChainId) {
+                        scope.ruleNode.targetRuleChainId = targetRuleChainId;
+                        if (targetRuleChainId) {
+                            ruleChainService.getRuleChain(targetRuleChainId).then(
+                                (ruleChain) => {
+                                    scope.ruleNode.name = ruleChain.name;
+                                }
+                            );
+                        } else {
+                            scope.ruleNode.name = "";
+                        }
+                    }
+                }
+            );
+        }
+        $compile(element.contents())(scope);
+    }
+    return {
+        restrict: "E",
+        link: linker,
+        scope: {
+            ruleChainId: '=',
+            ruleNode: '=',
+            isEdit: '=',
+            isReadOnly: '=',
+            theForm: '=',
+            onDeleteRuleNode: '&'
+        }
+    };
+}
diff --git a/ui/src/app/rulechain/rulenode.tpl.html b/ui/src/app/rulechain/rulenode.tpl.html
new file mode 100644
index 0000000..973ea1f
--- /dev/null
+++ b/ui/src/app/rulechain/rulenode.tpl.html
@@ -0,0 +1,49 @@
+<!--
+
+    Copyright © 2016-2018 The Thingsboard Authors
+
+    Licensed 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.
+
+-->
+<div
+        id="{{node.id}}"
+        ng-attr-style="position: absolute; top: {{ node.y }}px; left: {{ node.x }}px;"
+        ng-dblclick="callbacks.doubleClick($event, node)"
+        ng-mousedown="callbacks.mouseDown($event, node)"
+        ng-mouseenter="callbacks.mouseEnter($event, node)"
+        ng-mouseleave="callbacks.mouseLeave($event, node)">
+    <div class="{{flowchartConstants.nodeOverlayClass}}"></div>
+    <div class="tb-rule-node {{node.nodeClass}}" ng-class="{'tb-rule-node-highlighted' : node.highlighted, 'tb-rule-node-invalid': node.error }">
+        <md-icon aria-label="node-type-icon" flex="15"
+                 class="material-icons">{{node.icon}}</md-icon>
+        <div layout="column" flex="85" layout-align="center">
+            <span class="tb-node-type">{{ node.component.name }}</span>
+            <span class="tb-node-title" ng-if="node.name">{{ node.name }}</span>
+        </div>
+        <div class="{{flowchartConstants.leftConnectorClass}}">
+            <div fc-magnet
+                 ng-repeat="connector in modelservice.nodes.getConnectorsByType(node, flowchartConstants.leftConnectorType)">
+                <div fc-connector></div>
+            </div>
+        </div>
+        <div class="{{flowchartConstants.rightConnectorClass}}">
+            <div fc-magnet
+                 ng-repeat="connector in modelservice.nodes.getConnectorsByType(node, flowchartConstants.rightConnectorType)">
+                <div fc-connector></div>
+            </div>
+        </div>
+    </div>
+    <div ng-if="modelservice.isEditable() && !node.readonly" class="fc-nodedelete" ng-click="modelservice.nodes.delete(node)">
+        &times;
+    </div>
+</div>
diff --git a/ui/src/app/rulechain/rulenode-config.directive.js b/ui/src/app/rulechain/rulenode-config.directive.js
new file mode 100644
index 0000000..9bb8c48
--- /dev/null
+++ b/ui/src/app/rulechain/rulenode-config.directive.js
@@ -0,0 +1,78 @@
+/*
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.
+ */
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import ruleNodeConfigTemplate from './rulenode-config.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function RuleNodeConfigDirective($compile, $templateCache, $injector, $translate) {
+
+    var linker = function (scope, element, attrs, ngModelCtrl) {
+        var template = $templateCache.get(ruleNodeConfigTemplate);
+        element.html(template);
+
+        scope.$watch('configuration', function (newVal, prevVal) {
+            if (!angular.equals(newVal, prevVal)) {
+                ngModelCtrl.$setViewValue(scope.configuration);
+            }
+        });
+
+        ngModelCtrl.$render = function () {
+            scope.configuration = ngModelCtrl.$viewValue;
+        };
+
+        scope.useDefinedDirective = function() {
+            return scope.nodeDefinition &&
+                scope.nodeDefinition.configDirective && !scope.definedDirectiveError;
+        };
+
+        scope.$watch('nodeDefinition', () => {
+            if (scope.nodeDefinition) {
+                validateDefinedDirective();
+            }
+        });
+
+        function validateDefinedDirective() {
+            if (scope.nodeDefinition.uiResourceLoadError && scope.nodeDefinition.uiResourceLoadError.length) {
+                scope.definedDirectiveError = scope.nodeDefinition.uiResourceLoadError;
+            } else {
+                var definedDirective = scope.nodeDefinition.configDirective;
+                if (definedDirective && definedDirective.length) {
+                    if (!$injector.has(definedDirective + 'Directive')) {
+                        scope.definedDirectiveError = $translate.instant('rulenode.directive-is-not-loaded', {directiveName: definedDirective});
+                    }
+                }
+            }
+        }
+
+        $compile(element.contents())(scope);
+    };
+
+    return {
+        restrict: "E",
+        require: "^ngModel",
+        scope: {
+            nodeDefinition:'=',
+            required:'=ngRequired',
+            readonly:'=ngReadonly'
+        },
+        link: linker
+    };
+
+}
diff --git a/ui/src/app/rulechain/rulenode-config.tpl.html b/ui/src/app/rulechain/rulenode-config.tpl.html
new file mode 100644
index 0000000..32d5347
--- /dev/null
+++ b/ui/src/app/rulechain/rulenode-config.tpl.html
@@ -0,0 +1,32 @@
+<!--
+
+    Copyright © 2016-2018 The Thingsboard Authors
+
+    Licensed 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.
+
+-->
+
+<tb-rule-node-defined-config ng-if="useDefinedDirective()"
+                             ng-model="configuration"
+                             rule-node-directive="{{nodeDefinition.configDirective}}"
+                             ng-required="required"
+                             ng-readonly="readonly">
+</tb-rule-node-defined-config>
+<div class="tb-rulenode-directive-error" ng-if="definedDirectiveError">{{definedDirectiveError}}</div>
+<tb-json-object-edit ng-if="!useDefinedDirective()"
+                     class="tb-rule-node-configuration-json"
+                     ng-model="configuration"
+                     label="{{ 'rulenode.configuration' | translate }}"
+                     ng-required="required"
+                     fill-height="true">
+</tb-json-object-edit>
diff --git a/ui/src/app/rulechain/rulenode-defined-config.directive.js b/ui/src/app/rulechain/rulenode-defined-config.directive.js
new file mode 100644
index 0000000..5100fbb
--- /dev/null
+++ b/ui/src/app/rulechain/rulenode-defined-config.directive.js
@@ -0,0 +1,67 @@
+/*
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.
+ */
+
+const SNAKE_CASE_REGEXP = /[A-Z]/g;
+
+/*@ngInject*/
+export default function RuleNodeDefinedConfigDirective($compile) {
+
+    var linker = function (scope, element, attrs, ngModelCtrl) {
+
+        attrs.$observe('ruleNodeDirective', function() {
+            loadTemplate();
+        });
+
+        scope.$watch('configuration', function (newVal, prevVal) {
+            if (!angular.equals(newVal, prevVal)) {
+                ngModelCtrl.$setViewValue(scope.configuration);
+            }
+        });
+
+        ngModelCtrl.$render = function () {
+            scope.configuration = ngModelCtrl.$viewValue;
+        };
+
+        function loadTemplate() {
+            if (scope.ruleNodeConfigScope) {
+                scope.ruleNodeConfigScope.$destroy();
+            }
+            var directive = snake_case(attrs.ruleNodeDirective, '-');
+            var template = `<${directive} ng-model="configuration" ng-required="required" ng-readonly="readonly"></${directive}>`;
+            element.html(template);
+            scope.ruleNodeConfigScope = scope.$new();
+            $compile(element.contents())(scope.ruleNodeConfigScope);
+        }
+
+        function snake_case(name, separator) {
+            separator = separator || '_';
+            return name.replace(SNAKE_CASE_REGEXP, function(letter, pos) {
+                return (pos ? separator : '') + letter.toLowerCase();
+            });
+        }
+    };
+
+    return {
+        restrict: "E",
+        require: "^ngModel",
+        scope: {
+            required:'=ngRequired',
+            readonly:'=ngReadonly'
+        },
+        link: linker
+    };
+
+}
diff --git a/ui/src/app/rulechain/rulenode-fieldset.tpl.html b/ui/src/app/rulechain/rulenode-fieldset.tpl.html
new file mode 100644
index 0000000..7b0fae5
--- /dev/null
+++ b/ui/src/app/rulechain/rulenode-fieldset.tpl.html
@@ -0,0 +1,63 @@
+<!--
+
+    Copyright © 2016-2018 The Thingsboard Authors
+
+    Licensed 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.
+
+-->
+<md-button ng-click="onDeleteRuleNode({event: $event})"
+           ng-show="!isEdit && !isReadOnly"
+           class="md-raised md-primary">{{ 'rulenode.delete' | translate }}</md-button>
+
+<md-content class="md-padding tb-rulenode" layout="column">
+    <fieldset ng-disabled="$root.loading || !isEdit || isReadOnly">
+        <section ng-if="ruleNode.component.type != types.ruleNodeType.RULE_CHAIN.value">
+            <section layout="column" layout-gt-sm="row">
+                <md-input-container flex class="md-block">
+                    <label translate>rulenode.name</label>
+                    <input required name="name" ng-model="ruleNode.name">
+                    <div ng-messages="theForm.name.$error">
+                        <div translate ng-message="required">rulenode.name-required</div>
+                    </div>
+                </md-input-container>
+                <md-input-container class="md-block">
+                    <md-checkbox ng-disabled="$root.loading || !isEdit" aria-label="{{ 'rulenode.debug-mode' | translate }}"
+                                 ng-model="ruleNode.debugMode">{{ 'rulenode.debug-mode' | translate }}
+                    </md-checkbox>
+                </md-input-container>
+            </section>
+            <tb-rule-node-config ng-model="ruleNode.configuration"
+                                 ng-required="true"
+                                 node-definition="ruleNode.component.configurationDescriptor.nodeDefinition"
+                                 ng-readonly="$root.loading || !isEdit || isReadOnly">
+            </tb-rule-node-config>
+            <md-input-container class="md-block">
+                <label translate>rulenode.description</label>
+                <textarea ng-model="ruleNode.additionalInfo.description" rows="2"></textarea>
+            </md-input-container>
+        </section>
+        <section ng-if="ruleNode.component.type == types.ruleNodeType.RULE_CHAIN.value">
+            <tb-entity-autocomplete the-form="theForm"
+                                    ng-disabled="$root.loading || !isEdit || isReadOnly"
+                                    tb-required="true"
+                                    exclude-entity-ids="[ruleChainId]"
+                                    entity-type="types.entityType.rulechain"
+                                    ng-model="params.targetRuleChainId">
+            </tb-entity-autocomplete>
+            <md-input-container class="md-block">
+                <label translate>rulenode.description</label>
+                <textarea ng-model="ruleNode.additionalInfo.description" rows="2"></textarea>
+            </md-input-container>
+        </section>
+    </fieldset>
+</md-content>
diff --git a/ui/src/app/rulechain/script/node-script-test.controller.js b/ui/src/app/rulechain/script/node-script-test.controller.js
new file mode 100644
index 0000000..487d11f
--- /dev/null
+++ b/ui/src/app/rulechain/script/node-script-test.controller.js
@@ -0,0 +1,177 @@
+/*
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.
+ */
+
+import './node-script-test.scss';
+
+import Split from 'split.js';
+
+import beautify from 'js-beautify';
+
+const js_beautify = beautify.js;
+
+/*@ngInject*/
+export default function NodeScriptTestController($scope, $mdDialog, $window, $document, $timeout,
+                                                $q, $mdUtil, $translate, toast, types, utils,
+                                                 ruleChainService, onShowingCallback, msg, msgType, metadata,
+                                                 functionTitle, inputParams) {
+
+    var vm = this;
+
+    vm.types = types;
+    vm.functionTitle = functionTitle;
+    vm.inputParams = inputParams;
+    vm.inputParams.msg = js_beautify(angular.toJson(msg), {indent_size: 4});
+    vm.inputParams.metadata = metadata;
+    vm.inputParams.msgType = msgType;
+
+    vm.output = '';
+
+    vm.test = test;
+    vm.save = save;
+    vm.cancel = cancel;
+
+    $scope.$watch('theForm.metadataForm.$dirty', (newVal) => {
+        if (newVal) {
+            toast.hide();
+        }
+    });
+
+    onShowingCallback.onShowed = () => {
+        vm.nodeScriptTestDialogElement = angular.element('.tb-node-script-test-dialog');
+        var w = vm.nodeScriptTestDialogElement.width();
+        if (w > 0) {
+            initSplitLayout();
+        } else {
+            $scope.$watch(
+                function () {
+                    return vm.nodeScriptTestDialogElement[0].offsetWidth || parseInt(vm.nodeScriptTestDialogElement.css('width'), 10);
+                },
+                function (newSize) {
+                    if (newSize > 0) {
+                        initSplitLayout();
+                    }
+                }
+            );
+        }
+    };
+
+    function onDividerDrag() {
+        $scope.$broadcast('update-ace-editor-size');
+    }
+
+    function initSplitLayout() {
+        if (!vm.layoutInited) {
+            Split([angular.element('#top_panel', vm.nodeScriptTestDialogElement)[0], angular.element('#bottom_panel', vm.nodeScriptTestDialogElement)[0]], {
+                sizes: [35, 65],
+                gutterSize: 8,
+                cursor: 'row-resize',
+                direction: 'vertical',
+                onDrag: function () {
+                    onDividerDrag()
+                }
+            });
+
+            Split([angular.element('#top_left_panel', vm.nodeScriptTestDialogElement)[0], angular.element('#top_right_panel', vm.nodeScriptTestDialogElement)[0]], {
+                sizes: [50, 50],
+                gutterSize: 8,
+                cursor: 'col-resize',
+                onDrag: function () {
+                    onDividerDrag()
+                }
+            });
+
+            Split([angular.element('#bottom_left_panel', vm.nodeScriptTestDialogElement)[0], angular.element('#bottom_right_panel', vm.nodeScriptTestDialogElement)[0]], {
+                sizes: [50, 50],
+                gutterSize: 8,
+                cursor: 'col-resize',
+                onDrag: function () {
+                    onDividerDrag()
+                }
+            });
+
+            onDividerDrag();
+
+            $scope.$applyAsync(function () {
+                vm.layoutInited = true;
+                var w = angular.element($window);
+                $timeout(function () {
+                    w.triggerHandler('resize')
+                });
+            });
+
+        }
+    }
+
+    function test() {
+        testNodeScript().then(
+            (output) => {
+                vm.output = js_beautify(output, {indent_size: 4});
+            }
+        );
+    }
+
+    function checkInputParamErrors() {
+        $scope.theForm.metadataForm.$setPristine();
+        $scope.$broadcast('form-submit', 'validatePayload');
+        if (!$scope.theForm.payloadForm.$valid) {
+            return false;
+        } else if (!$scope.theForm.metadataForm.$valid) {
+            showMetadataError($translate.instant('rulenode.metadata-required'));
+            return false;
+        }
+        return true;
+    }
+
+    function showMetadataError(error) {
+        var toastParent = angular.element('#metadata-panel', vm.nodeScriptTestDialogElement);
+        toast.showError(error, toastParent, 'bottom left');
+    }
+
+    function testNodeScript() {
+        var deferred = $q.defer();
+        if (checkInputParamErrors()) {
+            $mdUtil.nextTick(() => {
+                ruleChainService.testScript(vm.inputParams).then(
+                    (result) => {
+                        if (result.error) {
+                            toast.showError(result.error);
+                            deferred.reject();
+                        } else {
+                            deferred.resolve(result.output);
+                        }
+                    },
+                    () => {
+                        deferred.reject();
+                    }
+                );
+            });
+        } else {
+            deferred.reject();
+        }
+        return deferred.promise;
+    }
+
+    function cancel() {
+        $mdDialog.cancel();
+    }
+
+    function save() {
+        testNodeScript().then(() => {
+            $scope.theForm.funcBodyForm.$setPristine();
+            $mdDialog.hide(vm.inputParams.script);
+        });
+    }
+}
diff --git a/ui/src/app/rulechain/script/node-script-test.scss b/ui/src/app/rulechain/script/node-script-test.scss
new file mode 100644
index 0000000..42124fb
--- /dev/null
+++ b/ui/src/app/rulechain/script/node-script-test.scss
@@ -0,0 +1,112 @@
+/**
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.
+ */
+@import '../../../scss/constants';
+@import "~compass-sass-mixins/lib/compass";
+
+md-dialog.tb-node-script-test-dialog {
+  &.md-dialog-fullscreen {
+    min-height: 100%;
+    min-width: 100%;
+    max-height: 100%;
+    max-width: 100%;
+    width: 100%;
+    height: 100%;
+    border-radius: 0;
+  }
+
+  .tb-split {
+    @include box-sizing(border-box);
+    overflow-y: auto;
+    overflow-x: hidden;
+  }
+
+  .ace_editor {
+    font-size: 14px !important;
+  }
+
+  .tb-content {
+    border: 1px solid #C0C0C0;
+    padding-top: 5px;
+    padding-left: 5px;
+  }
+
+  .gutter {
+    background-color: #eeeeee;
+
+    background-repeat: no-repeat;
+    background-position: 50%;
+  }
+
+  .gutter.gutter-horizontal {
+    cursor: col-resize;
+    background-image: url('../../../../node_modules/split.js/grips/vertical.png');
+  }
+
+  .gutter.gutter-vertical {
+    cursor: row-resize;
+    background-image: url('../../../../node_modules/split.js/grips/horizontal.png');
+  }
+
+  .tb-split.tb-split-horizontal, .gutter.gutter-horizontal {
+    height: 100%;
+    float: left;
+  }
+
+  .tb-split.tb-split-vertical {
+    display: flex;
+    .tb-split.tb-content {
+      height: 100%;
+    }
+  }
+
+  div.tb-editor-area-title-panel {
+    position: absolute;
+    font-size: 0.800rem;
+    font-weight: 500;
+    top: 10px;
+    right: 40px;
+    z-index: 5;
+    label {
+      color: #00acc1;
+      background: rgba(220, 220, 220, 0.35);
+      border-radius: 5px;
+      padding: 4px;
+      text-transform: uppercase;
+    }
+    .md-button {
+      color: #7B7B7B;
+      min-width: 32px;
+      min-height: 15px;
+      line-height: 15px;
+      font-size: 0.800rem;
+      margin: 0;
+      padding: 4px;
+      background: rgba(220, 220, 220, 0.35);
+    }
+  }
+
+  .tb-resize-container {
+    overflow-y: auto;
+    height: 100%;
+    width: 100%;
+    position: relative;
+
+    .ace_editor {
+      height: 100%;
+    }
+  }
+
+}
diff --git a/ui/src/app/rulechain/script/node-script-test.service.js b/ui/src/app/rulechain/script/node-script-test.service.js
new file mode 100644
index 0000000..c685634
--- /dev/null
+++ b/ui/src/app/rulechain/script/node-script-test.service.js
@@ -0,0 +1,81 @@
+/*
+ * Copyright © 2016-2018 The Thingsboard Authors
+ *
+ * Licensed 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.
+ */
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import nodeScriptTestTemplate from './node-script-test.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function NodeScriptTest($q, $mdDialog, $document) {
+
+    var service = {
+        testNodeScript: testNodeScript
+    };
+
+    return service;
+
+    function testNodeScript($event, script, scriptType, functionTitle, functionName, argNames, msg, metadata, msgType) {
+        var deferred = $q.defer();
+
+        if ($event) {
+            $event.stopPropagation();
+        }
+        var onShowingCallback = {
+            onShowed: () => {
+            }
+        };
+
+        var inputParams = {
+            script: script,
+            scriptType: scriptType,
+            functionName: functionName,
+            argNames: argNames
+        };
+
+        $mdDialog.show({
+            controller: 'NodeScriptTestController',
+            controllerAs: 'vm',
+            templateUrl: nodeScriptTestTemplate,
+            parent: angular.element($document[0].body),
+            locals: {
+                msg: msg,
+                metadata: metadata,
+                msgType: msgType,
+                functionTitle: functionTitle,
+                inputParams: inputParams,
+                onShowingCallback: onShowingCallback
+            },
+            fullscreen: true,
+            skipHide: true,
+            targetEvent: $event,
+            onComplete: () => {
+                onShowingCallback.onShowed();
+            }
+        }).then(
+            (script) => {
+                deferred.resolve(script);
+            },
+            () => {
+                deferred.reject();
+            }
+        );
+
+        return deferred.promise;
+    }
+
+}
\ No newline at end of file
diff --git a/ui/src/app/rulechain/script/node-script-test.tpl.html b/ui/src/app/rulechain/script/node-script-test.tpl.html
new file mode 100644
index 0000000..9337240
--- /dev/null
+++ b/ui/src/app/rulechain/script/node-script-test.tpl.html
@@ -0,0 +1,119 @@
+<!--
+
+    Copyright © 2016-2018 The Thingsboard Authors
+
+    Licensed 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.
+
+-->
+<md-dialog class="tb-node-script-test-dialog"
+           aria-label="{{ 'rulenode.test-script-function' | translate }}" style="width: 800px;">
+    <form flex name="theForm" ng-submit="vm.save()">
+        <md-toolbar>
+            <div class="md-toolbar-tools">
+                <h2>{{ 'rulenode.test-script-function' | translate }}</h2>
+                <span flex></span>
+                <md-button class="md-icon-button" ng-click="vm.cancel()">
+                    <ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
+                </md-button>
+            </div>
+        </md-toolbar>
+        <md-dialog-content flex style="position: relative;">
+            <div class="tb-absolute-fill">
+                <div id="top_panel" class="tb-split tb-split-vertical">
+                    <div id="top_left_panel" class="tb-split tb-content">
+                        <div class="tb-resize-container">
+                            <div class="tb-editor-area-title-panel">
+                                <label translate>rulenode.message</label>
+                            </div>
+                            <ng-form name="payloadForm">
+                                <div layout="column" style="height: 100%;">
+                                    <div layout="row">
+                                        <md-input-container class="md-block" style="margin-bottom: 0px; min-width: 200px;">
+                                            <label translate>rulenode.message-type</label>
+                                            <input required name="msgType" ng-model="vm.inputParams.msgType">
+                                            <div ng-messages="payloadForm.msgType.$error">
+                                                <div translate ng-message="required">rulenode.message-type-required</div>
+                                            </div>
+                                        </md-input-container>
+                                    </div>
+                                    <tb-json-content flex
+                                                     ng-model="vm.inputParams.msg"
+                                                     label="{{ 'rulenode.message' | translate }}"
+                                                     content-type="vm.types.contentType.JSON.value"
+                                                     validate-content="true"
+                                                     validation-trigger-arg="validatePayload"
+                                                     fill-height="true">
+                                    </tb-json-content>
+                                </div>
+                            </ng-form>
+                        </div>
+                    </div>
+                    <div id="top_right_panel" class="tb-split tb-content">
+                        <div class="tb-resize-container" id="metadata-panel">
+                            <div class="tb-editor-area-title-panel">
+                                <label translate>rulenode.metadata</label>
+                            </div>
+                            <ng-form name="metadataForm">
+                                <tb-key-val-map title-text="rulenode.metadata" ng-disabled="$root.loading"
+                                                key-val-map="vm.inputParams.metadata"></tb-key-val-map>
+                            </ng-form>
+                        </div>
+                    </div>
+                </div>
+                <div id="bottom_panel" class="tb-split tb-split-vertical">
+                    <div id="bottom_left_panel" class="tb-split tb-content">
+                        <div class="tb-resize-container">
+                            <div class="tb-editor-area-title-panel">
+                                <label>{{ vm.functionTitle }}</label>
+                            </div>
+                            <ng-form name="funcBodyForm">
+                                <tb-js-func id="funcBodyInput" ng-model="vm.inputParams.script"
+                                            function-name="{{vm.inputParams.functionName}}"
+                                            function-args="{{ vm.inputParams.argNames }}"
+                                            validation-args="{{ [[vm.inputParams.msg, vm.inputParams.metadata, vm.inputParams.msgType]] }}"
+                                            validation-trigger-arg="validateFuncBody"
+                                            result-type="object"
+                                            fill-height="true">
+                                </tb-js-func>
+                            </ng-form>
+                        </div>
+                    </div>
+                    <div id="bottom_right_panel" class="tb-split tb-content">
+                        <div class="tb-resize-container">
+                            <div class="tb-editor-area-title-panel">
+                                <label translate>rulenode.output</label>
+                            </div>
+                            <tb-json-content ng-model="vm.output"
+                                             label="{{ 'rulenode.output' | translate }}"
+                                             content-type="vm.types.contentType.JSON.value"
+                                             validate-content="false"
+                                             ng-readonly="true"
+                                             fill-height="true">
+                            </tb-json-content>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </md-dialog-content>
+        <md-dialog-actions layout="row">
+            <md-button ng-disabled="$root.loading" ng-click="vm.test()" class="md-raised md-primary">
+                {{ 'rulenode.test' | translate }}
+            </md-button>
+            <span flex></span>
+            <md-button ng-disabled="$root.loading || theForm.funcBodyForm.$invalid || !theForm.funcBodyForm.$dirty" type="submit" class="md-raised md-primary">
+                {{ 'action.save' | translate }}
+            </md-button>
+            <md-button ng-disabled="$root.loading" ng-click="vm.cancel()" style="margin-right:20px;">{{ 'action.cancel' | translate }}</md-button>
+        </md-dialog-actions>
+    </form>
+</md-dialog>
diff --git a/ui/src/app/services/item-buffer.service.js b/ui/src/app/services/item-buffer.service.js
index 9fce811..a9fe348 100644
--- a/ui/src/app/services/item-buffer.service.js
+++ b/ui/src/app/services/item-buffer.service.js
@@ -24,10 +24,11 @@ export default angular.module('thingsboard.itembuffer', [angularStorage])
     .name;
 
 /*@ngInject*/
-function ItemBuffer($q, bufferStore, types, utils, dashboardUtils) {
+function ItemBuffer($q, bufferStore, types, utils, dashboardUtils, ruleChainService) {
 
     const WIDGET_ITEM = "widget_item";
     const WIDGET_REFERENCE = "widget_reference";
+    const RULE_NODES = "rule_nodes";
 
     var service = {
         prepareWidgetItem: prepareWidgetItem,
@@ -37,7 +38,10 @@ function ItemBuffer($q, bufferStore, types, utils, dashboardUtils) {
         canPasteWidgetReference: canPasteWidgetReference,
         pasteWidget: pasteWidget,
         pasteWidgetReference: pasteWidgetReference,
-        addWidgetToDashboard: addWidgetToDashboard
+        addWidgetToDashboard: addWidgetToDashboard,
+        copyRuleNodes: copyRuleNodes,
+        hasRuleNodes: hasRuleNodes,
+        pasteRuleNodes: pasteRuleNodes
     }
 
     return service;
@@ -151,6 +155,81 @@ function ItemBuffer($q, bufferStore, types, utils, dashboardUtils) {
         };
     }
 
+    function copyRuleNodes(nodes, connections) {
+        var ruleNodes = {
+            nodes: [],
+            connections: []
+        };
+        var top = -1, left = -1, bottom = -1, right = -1;
+        for (var i=0;i<nodes.length;i++) {
+            var origNode = nodes[i];
+            var node = {
+                additionalInfo: origNode.additionalInfo,
+                configuration: origNode.configuration,
+                debugMode: origNode.debugMode,
+                x: origNode.x,
+                y: origNode.y,
+                name: origNode.name,
+                componentClazz: origNode.component.clazz,
+            };
+            if (origNode.targetRuleChainId) {
+                node.targetRuleChainId = origNode.targetRuleChainId;
+            }
+            if (origNode.error) {
+                node.error = origNode.error;
+            }
+            ruleNodes.nodes.push(node);
+            if (i==0) {
+                top = node.y;
+                left = node.x;
+                bottom = node.y + 50;
+                right = node.x + 170;
+            } else {
+                top = Math.min(top, node.y);
+                left = Math.min(left, node.x);
+                bottom = Math.max(bottom, node.y + 50);
+                right = Math.max(right, node.x + 170);
+            }
+        }
+        ruleNodes.originX = left + (right-left)/2;
+        ruleNodes.originY = top + (bottom-top)/2;
+        for (i=0;i<connections.length;i++) {
+            var connection = connections[i];
+            ruleNodes.connections.push(connection);
+        }
+        bufferStore.set(RULE_NODES, angular.toJson(ruleNodes));
+    }
+
+    function hasRuleNodes() {
+        return bufferStore.get(RULE_NODES);
+    }
+
+    function pasteRuleNodes(x, y) {
+        var ruleNodesJson = bufferStore.get(RULE_NODES);
+        if (ruleNodesJson) {
+            var ruleNodes = angular.fromJson(ruleNodesJson);
+            var deltaX = x - ruleNodes.originX;
+            var deltaY = y - ruleNodes.originY;
+            for (var i=0;i<ruleNodes.nodes.length;i++) {
+                var node = ruleNodes.nodes[i];
+                var component = ruleChainService.getRuleNodeComponentByClazz(node.componentClazz);
+                if (component) {
+                    delete node.componentClazz;
+                    node.component = component;
+                    node.nodeClass = types.ruleNodeType[component.type].nodeClass;
+                    node.icon = types.ruleNodeType[component.type].icon;
+                    node.connectors = [];
+                    node.x = Math.round(node.x + deltaX);
+                    node.y = Math.round(node.y + deltaY);
+                } else {
+                    return null;
+                }
+            }
+            return ruleNodes;
+        }
+        return null;
+    }
+
     function copyWidget(dashboard, sourceState, sourceLayout, widget) {
         var widgetItem = prepareWidgetItem(dashboard, sourceState, sourceLayout, widget);
         bufferStore.set(WIDGET_ITEM, angular.toJson(widgetItem));
diff --git a/ui/src/app/services/menu.service.js b/ui/src/app/services/menu.service.js
index 9dbddd9..5d97ea6 100644
--- a/ui/src/app/services/menu.service.js
+++ b/ui/src/app/services/menu.service.js
@@ -79,6 +79,12 @@ function Menu(userService, $state, $rootScope) {
                             icon: 'settings_ethernet'
                         },
                         {
+                            name: 'rulechain.rulechains',
+                            type: 'link',
+                            state: 'home.ruleChains',
+                            icon: 'settings_ethernet'
+                        },
+                        {
                             name: 'tenant.tenants',
                             type: 'link',
                             state: 'home.tenants',
@@ -128,6 +134,16 @@ function Menu(userService, $state, $rootScope) {
                             ]
                         },
                         {
+                            name: 'rulechain.management',
+                            places: [
+                                {
+                                    name: 'rulechain.rulechains',
+                                    icon: 'settings_ethernet',
+                                    state: 'home.ruleChains'
+                                }
+                            ]
+                        },
+                        {
                             name: 'tenant.management',
                             places: [
                                 {
@@ -183,6 +199,12 @@ function Menu(userService, $state, $rootScope) {
                             icon: 'settings_ethernet'
                         },
                         {
+                            name: 'rulechain.rulechains',
+                            type: 'link',
+                            state: 'home.ruleChains',
+                            icon: 'settings_ethernet'
+                        },
+                        {
                             name: 'customer.customers',
                             type: 'link',
                             state: 'home.customers',
@@ -236,6 +258,16 @@ function Menu(userService, $state, $rootScope) {
                             ]
                         },
                         {
+                            name: 'rulechain.management',
+                            places: [
+                                {
+                                    name: 'rulechain.rulechains',
+                                    icon: 'settings_ethernet',
+                                    state: 'home.ruleChains'
+                                }
+                            ]
+                        },
+                        {
                             name: 'customer.management',
                             places: [
                                 {
diff --git a/ui/src/scss/main.scss b/ui/src/scss/main.scss
index 93ff320..6aa662c 100644
--- a/ui/src/scss/main.scss
+++ b/ui/src/scss/main.scss
@@ -203,6 +203,12 @@ md-sidenav {
  * THINGSBOARD SPECIFIC
  ***********************/
 
+$swift-ease-out-duration: 0.4s !default;
+$swift-ease-out-timing-function: cubic-bezier(0.25, 0.8, 0.25, 1) !default;
+
+$input-label-float-offset: 6px !default;
+$input-label-float-scale: 0.75 !default;
+
 label {
   &.tb-title {
     pointer-events: none;
@@ -213,6 +219,18 @@ label {
     &.no-padding {
       padding-bottom: 0px;
     }
+    &.tb-required:after {
+      content: ' *';
+      font-size: 13px;
+      vertical-align: top;
+      color: rgba(0,0,0,0.54);
+    }
+    &.tb-error {
+      color: rgb(221,44,0);
+      &.tb-required:after {
+        color: rgb(221,44,0);
+      }
+    }
   }
 }