diff --git a/pages/docs/dsl.md b/pages/docs/dsl.md
index 19125a893a07aa3dda53a34ddd7f46537675bbb8..e8b6b2bb11c67168bafd33efa48b505248ea9b37 100644
--- a/pages/docs/dsl.md
+++ b/pages/docs/dsl.md
@@ -13,7 +13,7 @@ The kind of the element determines, whether an endpoint for it can be receiving,
 To declare a new endpoints, use the following syntax:
 
 ```
-("send"|"receive") ["indexed"] ["with add"] <Non-Terminal>[.<Target>["()"]] ["using" <Mapping-Name> (, <Mapping-Name>)*] ";"
+("send"|"receive") ["indexed"] ["with add"] <Non-Terminal>[.<Target>["(<AttributeType>)"]] ["using" <Mapping-Name> (, <Mapping-Name>)*] ";"
 ```
 
 A breakdown of the parts of that syntax:
@@ -28,10 +28,12 @@ A breakdown of the parts of that syntax:
 - The second optional keyword `with add` can also be used only for receiving endpoints targeting a list children.
   As described above, it can be combined with `indexed`.
   If used on its own, the incoming data is interpreted as a complete list and its elements will be appended to the current list.
-- The `<Non-Terminal>[.<Target>["()"]]` notation describes the actual affected node.
+- The `<Non-Terminal>[.<Target>["(<AttributeType>)"]]` notation describes the actual affected node.
   - If the target is omitted, all nodes of that non-terminal type can be connected, irrespective of their context. This is a context-free endpoint definition.
   - The target can be any child on the right-hand side of a production rule, a role of a relation, or an attribute.
-    The brackets `()` after the target must be used in case of an attribute, and only then.
+    The brackets `(<AttributeType>)` after the target must be used in case of an attribute, and only then.
+    Here, the return type of the attribute has to be specified, as aspect files are not parsed by RagConnect.
+    Hence, RagConnect can not and will not verify the existence of the attribute, and the possible non-existence of an attribute will be found by the Java compiler.
 - Optionally, an endpoint can use one or more [mappings](#mappings).
   They will be applied before sending, or after receiving a message.
   Mappings will always be applied in the order they are listed after `using`.
diff --git a/pages/docs/inner-workings.md b/pages/docs/inner-workings.md
index bd9986a9c7c81c034e30e57e364d243393a8e39a..1cb22a597804d2a67a6f05c0181f4b7c359644ee 100644
--- a/pages/docs/inner-workings.md
+++ b/pages/docs/inner-workings.md
@@ -1,4 +1,4 @@
-# Inner workings of `RagConnect`
+# Inner Workings of `RagConnect`
 
 Please see [API documentation](ragdoc/index.html) for more details.
 
@@ -20,11 +20,11 @@ The other main aspect (which is currently not really used) is `IntermediateToYAM
 This is used to generate a YAML file containing the data used by mustache.
 It can be used by the default mustache implementation together with the templates.
 
-# Implementation details
+# Implementation Details
 
 In the following, details for special implementation topics are discussed.
 
-## forwarding
+## Forwarding
 
 When a nonterminal is used in a send endpoints, it needs an implicit forwarding attribute to work, because only _computed elements_ can be sent.
 Since the nonterminal itself should be sent, the generated attribute simply returns this nonterminal.
@@ -33,3 +33,29 @@ However, changing any token within the whole subtree or changing the structure o
 This way, the dependency tracking registers a dependency between structure and tokens to the attribute.
 
 The attribute (as well as any other generated element) is prefixed with `_ragconnect_` to avoid potential name conflicts with user-specified elements.
+
+# Implementation Hints
+
+## Debugging Tests and Finding Bugs
+
+To help with finding errors/bugs when tests fail, there are several things to find the correct spot.
+
+- **Look closely**. Analyze the error message closely, and possible any previous error message(s) that could have caused the test to fail.
+- **Focus on single error**
+  - To only inspect one test, mark them with `@Tag("New")` and use the gradle task "newTests".
+  - Use `Assumptions.assumeTrue(false);` to abort unneeded test cases early.
+  - When editing RagConnect itself and force recreating source for the affected test, e.g., `compileForwardingIncremental.outputs.upToDateWhen { false }`
+  - _Remember to undo all changes, once the bug is fixed._
+- **Activate logs**. Add the following to the `ragconnect` specification of the compile-task of the affected test:
+  ```
+  logReads = true
+  logWrites = true
+  logIncremental = true
+  ```
+  _Remember to remove those lines, once the bug is fixed._
+- **Trace incremental events**. Add the following right after create the root node (named `model` here):
+  ```java
+  model.trace().setReceiver(TestUtils::logEvent);
+  ```
+  This will output every event fired by the incremental evaluation engine. _Remember to remove this line, once the bug is fixed._
+- **Add log statements**. As there will be quite some log output, add some identifying log statement (i.e., using `logger.fatal("---")`) right before the suspicious statement to inspect only the relevant log message after that.
diff --git a/ragconnect.base/src/main/jastadd/Analysis.jrag b/ragconnect.base/src/main/jastadd/Analysis.jrag
index 85ff38d40d88ddbc415d3bd3ea39867c00d1e2fd..d4e166a34a2b62150d76dfa0decf3c0a827bb649 100644
--- a/ragconnect.base/src/main/jastadd/Analysis.jrag
+++ b/ragconnect.base/src/main/jastadd/Analysis.jrag
@@ -2,6 +2,19 @@ aspect Analysis {
   // --- isAlreadyDefined ---
   syn boolean EndpointDefinition.isAlreadyDefined() = getEndpointTarget().isAlreadyDefined();
   syn boolean EndpointTarget.isAlreadyDefined();
+  eq AttributeEndpointTarget.isAlreadyDefined() {
+    // define lookup here, as not used elsewhere
+    int numberOfSameDefs = 0;
+    for (EndpointTarget target : ragconnect().givenEndpointTargetList()) {
+      if (target.isAttributeEndpointTarget()) {
+        AttributeEndpointTarget other = target.asAttributeEndpointTarget();
+        if (other.getParentTypeDecl().equals(this.getParentTypeDecl()) && other.getName().equals(this.getName())) {
+          numberOfSameDefs += 1;
+        }
+      }
+    }
+    return numberOfSameDefs > 1;
+  }
   eq TokenEndpointTarget.isAlreadyDefined() {
     return lookupTokenEndpointDefinitions(getToken()).stream()
         .filter(containingEndpointDefinition()::matchesType)
@@ -63,13 +76,17 @@ aspect Analysis {
     }
   }
 
-  syn boolean EndpointTarget.entityIsNormalAttribute();
-  eq TokenEndpointTarget.entityIsNormalAttribute() = !getToken().getNTA();
-  eq TypeEndpointTarget.entityIsNormalAttribute() = !getType().getNTA();
-  eq ContextFreeTypeEndpointTarget.entityIsNormalAttribute() = false;
+  syn boolean EndpointTarget.hasAttributeResetMethod();
+  eq AttributeEndpointTarget.hasAttributeResetMethod() = false;
+  eq TokenEndpointTarget.hasAttributeResetMethod() = getToken().getNTA();
+  eq TypeEndpointTarget.hasAttributeResetMethod() = getType().getNTA();
+  eq ContextFreeTypeEndpointTarget.hasAttributeResetMethod() = false;
 
   // --- needProxyToken ---
-  syn boolean TokenComponent.needProxyToken() = !getDependencySourceDefinitionList().isEmpty() || getTokenEndpointTargetList().stream().map(EndpointTarget::containingEndpointDefinition).anyMatch(EndpointDefinition::shouldSendValue);
+  syn boolean TokenComponent.needProxyToken() = !getDependencySourceDefinitionList().isEmpty() ||
+          getTokenEndpointTargetList().stream()
+                  .map(EndpointTarget::containingEndpointDefinition)
+                  .anyMatch(EndpointDefinition::shouldNotResetValue);
 
   // --- effectiveUsedAt ---
   coll Set<EndpointDefinition> MappingDefinition.effectiveUsedAt()
diff --git a/ragconnect.base/src/main/jastadd/Intermediate.jadd b/ragconnect.base/src/main/jastadd/Intermediate.jadd
index bcd4cc4da4242ccd317f3d23bb66bb70a62f3599..47f3aa473e0959ece3a24b684b98b59893c58413 100644
--- a/ragconnect.base/src/main/jastadd/Intermediate.jadd
+++ b/ragconnect.base/src/main/jastadd/Intermediate.jadd
@@ -168,6 +168,10 @@ aspect MustacheMappingApplicationAndDefinition {
   syn String MEndpointDefinition.preemptiveReturn();
   syn String MEndpointDefinition.firstInputVarName();
 
+  eq MAttributeSendDefinition.firstInputVarName() = getterMethodCall();
+  eq MAttributeSendDefinition.preemptiveExpectedValue() = lastValueGetterCall();
+  eq MAttributeSendDefinition.preemptiveReturn() = "return false;";
+
   eq MTokenReceiveDefinition.firstInputVarName() = "message";
   eq MTokenReceiveDefinition.preemptiveExpectedValue() = getterMethodCall();
   eq MTokenReceiveDefinition.preemptiveReturn() = "return;";
@@ -323,7 +327,7 @@ aspect MustacheRagConnect {
 
 aspect MustacheReceiveAndSendAndHandleUri {
   // === EndpointDefinition ===
-  syn String EndpointDefinition.connectMethodName() = "connect" + entityName();
+  syn String EndpointDefinition.connectMethodName() = "connect" + capitalize(entityName());
 
   syn String EndpointDefinition.connectParameterName() = "uriString";
 
@@ -348,7 +352,7 @@ aspect MustacheReceiveAndSendAndHandleUri {
     } else {
       extra = "";
     }
-    return "disconnect" + extra + entityName();
+    return "disconnect" + extra + capitalize(entityName());
   }
 
   syn String EndpointDefinition.entityName() = getEndpointTarget().entityName();
@@ -371,6 +375,10 @@ aspect MustacheReceiveAndSendAndHandleUri {
   syn String EndpointTarget.parentTypeName();
   syn String EndpointTarget.entityName();
 
+  eq AttributeEndpointTarget.getterMethodName() = getName();
+  eq AttributeEndpointTarget.parentTypeName() = getParentTypeDecl().getName();
+  eq AttributeEndpointTarget.entityName() = getName();
+
   eq TokenEndpointTarget.getterMethodName() = "get" + getToken().getName();
   eq TokenEndpointTarget.parentTypeName() = getToken().containingTypeDecl().getName();
   eq TokenEndpointTarget.entityName() = getToken().getName();
@@ -429,7 +437,7 @@ aspect MustacheSendDefinition {
 
   syn String EndpointDefinition.senderName() = getEndpointTarget().senderName();
 
-  syn boolean EndpointDefinition.shouldSendValue() = getSend() && getEndpointTarget().entityIsNormalAttribute();
+  syn boolean EndpointDefinition.shouldNotResetValue() = getSend() && !getEndpointTarget().hasAttributeResetMethod();
 
   syn String EndpointDefinition.tokenResetMethodName() = getterMethodName() + "_reset";
 
@@ -456,14 +464,15 @@ aspect MustacheSendDefinition {
           getTypeDecl().getName() :
           ragconnect().configJastAddList() + "<" + getTypeDecl().getName() + ">";
 
-  syn String EndpointTarget.senderName();
-  eq TokenEndpointTarget.senderName() = ragconnect().internalRagConnectPrefix() + "_sender_" + getToken().getName();
-  eq TypeEndpointTarget.senderName() = ragconnect().internalRagConnectPrefix() + "_sender_" + getType().getName();
+  syn String EndpointTarget.senderName() = ragconnect().internalRagConnectPrefix() + "_sender_" + entityName();
   eq ContextFreeTypeEndpointTarget.senderName() = null;
 
   syn String MEndpointDefinition.updateMethodName();
   syn String MEndpointDefinition.writeMethodName();
 
+  eq MAttributeSendDefinition.updateMethodName() = ragconnect().internalRagConnectPrefix() + "_update_attr_" + getEndpointDefinition().entityName();
+  eq MAttributeSendDefinition.writeMethodName() = ragconnect().internalRagConnectPrefix() + "_writeLastValue_attr_" + getEndpointDefinition().entityName();
+
   eq MTokenReceiveDefinition.updateMethodName() = null;
   eq MTokenReceiveDefinition.writeMethodName() = null;
 
@@ -497,7 +506,7 @@ aspect MustacheTokenComponent {
 
   syn EndpointDefinition TokenComponent.normalTokenSendDef() {
     for (EndpointTarget target : getTokenEndpointTargetList()) {
-      if (target.isTokenEndpointTarget() && target.containingEndpointDefinition().shouldSendValue()) {
+      if (target.isTokenEndpointTarget() && target.containingEndpointDefinition().shouldNotResetValue()) {
         return target.containingEndpointDefinition();
       }
     }
@@ -580,6 +589,12 @@ aspect AttributesForMustache {
     return result;
   }
   abstract MEndpointDefinition EndpointTarget.createMEndpointDefinition(boolean isSend);
+  MEndpointDefinition AttributeEndpointTarget.createMEndpointDefinition(boolean isSend) {
+    if (!isSend) {
+      throw new IllegalArgumentException("AttributeEndpointTarget can only be sent!");
+    }
+    return new MAttributeSendDefinition();
+  }
   MEndpointDefinition TokenEndpointTarget.createMEndpointDefinition(boolean isSend) {
     return isSend ? new MTokenSendDefinition() : new MTokenReceiveDefinition();
   }
diff --git a/ragconnect.base/src/main/jastadd/Intermediate.relast b/ragconnect.base/src/main/jastadd/Intermediate.relast
index 79097503ef32b417be179ccb5f679886a6297582..486158a4a4094291a756cf8c7715ed6104f5e252 100644
--- a/ragconnect.base/src/main/jastadd/Intermediate.relast
+++ b/ragconnect.base/src/main/jastadd/Intermediate.relast
@@ -1,6 +1,7 @@
 abstract MEndpointDefinition ::= InnerMappingDefinition:MInnerMappingDefinition*;
 rel MEndpointDefinition.EndpointDefinition -> EndpointDefinition;
 
+MAttributeSendDefinition : MEndpointDefinition;
 abstract MTokenEndpointDefinition : MEndpointDefinition;
 MTokenReceiveDefinition : MTokenEndpointDefinition;
 MTokenSendDefinition : MTokenEndpointDefinition;
diff --git a/ragconnect.base/src/main/jastadd/IntermediateToYAML.jrag b/ragconnect.base/src/main/jastadd/IntermediateToYAML.jrag
index fb198ce9cfe34db3234cc5a7feff5ddfb210dc91..49c2693696a02de14852e252f3aadd3a722dfb95 100644
--- a/ragconnect.base/src/main/jastadd/IntermediateToYAML.jrag
+++ b/ragconnect.base/src/main/jastadd/IntermediateToYAML.jrag
@@ -103,7 +103,7 @@ aspect IntermediateToYAML {
       result.put("lastValueGetterCall" , lastValueGetterCall());
       result.put("lastValueSetter" , lastValueSetter());
       result.put("senderName" , senderName());
-      result.put("shouldSendValue" , shouldSendValue());
+      result.put("shouldNotResetValue" , shouldNotResetValue());
       result.put("tokenResetMethodName" , tokenResetMethodName());
       result.put("updateMethodName" , updateMethodName());
       result.put("writeMethodName" , writeMethodName());
diff --git a/ragconnect.base/src/main/jastadd/Mappings.jrag b/ragconnect.base/src/main/jastadd/Mappings.jrag
index 738028b3a264a0c7dcf976618aa0b63f35be62c5..aa070695a83e31f819d62308b094679b27125a8e 100644
--- a/ragconnect.base/src/main/jastadd/Mappings.jrag
+++ b/ragconnect.base/src/main/jastadd/Mappings.jrag
@@ -191,13 +191,6 @@ aspect Mappings {
 
   // --- suitableReceiveDefaultMapping ---
   syn DefaultMappingDefinition EndpointDefinition.suitableReceiveDefaultMapping() {
-    if (getEndpointTarget().isTypeEndpointTarget()) {
-      try {
-        TypeDecl typeDecl = program().resolveTypeDecl(targetTypeName());
-        return typeIsList() && !getIndexBasedListAccess() ? ragconnect().defaultBytesToListTreeMapping(typeDecl.getName()) : ragconnect().defaultBytesToTreeMapping(typeDecl.getName());
-      } catch (Exception ignore) {
-      }
-    }
     switch (targetTypeName()) {
       case "boolean":
       case "Boolean":
@@ -225,8 +218,7 @@ aspect Mappings {
       default:
         try {
           TypeDecl typeDecl = program().resolveTypeDecl(targetTypeName());
-          // TODO: also support list-types, if list is first type
-          return ragconnect().defaultBytesToTreeMapping(typeDecl.getName());
+          return getEndpointTarget().isTypeEndpointTarget() && typeIsList() && !getIndexBasedListAccess() ? ragconnect().defaultBytesToListTreeMapping(typeDecl.getName()) : ragconnect().defaultBytesToTreeMapping(typeDecl.getName());
         } catch (Exception ignore) {
         }
         System.err.println("Could not find suitable default mapping for " + targetTypeName() + " on " + this);
@@ -236,9 +228,6 @@ aspect Mappings {
 
   // --- suitableSendDefaultMapping ---
   syn DefaultMappingDefinition EndpointDefinition.suitableSendDefaultMapping() {
-    if (getEndpointTarget().isTypeEndpointTarget() && typeIsList() && !getIndexBasedListAccess()) {
-      return ragconnect().defaultListTreeToBytesMapping();
-    }
     switch (targetTypeName()) {
       case "boolean":
       case "Boolean":
@@ -264,6 +253,9 @@ aspect Mappings {
       case "String":
         return ragconnect().defaultStringToBytesMapping();
       default:
+        if (getEndpointTarget().isTypeEndpointTarget() && typeIsList() && !getIndexBasedListAccess()) {
+          return ragconnect().defaultListTreeToBytesMapping();
+        }
         try {
           TypeDecl typeDecl = program().resolveTypeDecl(targetTypeName());
           return ragconnect().defaultTreeToBytesMapping(typeDecl.getName());
@@ -288,6 +280,7 @@ aspect Mappings {
     }
   }
   syn String EndpointTarget.targetTypeName();
+  eq AttributeEndpointTarget.targetTypeName() = getTypeName();
   eq TokenEndpointTarget.targetTypeName() = getToken().effectiveJavaTypeUse().getName();
   eq TypeEndpointTarget.targetTypeName() = getType().getTypeDecl().getName();
   eq ContextFreeTypeEndpointTarget.targetTypeName() = getTypeDecl().getName();
diff --git a/ragconnect.base/src/main/jastadd/Navigation.jrag b/ragconnect.base/src/main/jastadd/Navigation.jrag
index a65ab72fa946bcec3351bb496f6632c012bac678..dac193142b1382f01b61e696a0f9d5b02dd8b038 100644
--- a/ragconnect.base/src/main/jastadd/Navigation.jrag
+++ b/ragconnect.base/src/main/jastadd/Navigation.jrag
@@ -24,6 +24,12 @@ aspect NewStuff {
   syn boolean EndpointTarget.isUntypedEndpointTarget() = false;
   eq UntypedEndpointTarget.isUntypedEndpointTarget() = true;
 
+  /** Tests if EndpointTarget is a AttributeEndpointTarget.
+  *  @return 'true' if this is a AttributeEndpointTarget, otherwise 'false'
+  */
+  syn boolean EndpointTarget.isAttributeEndpointTarget() = false;
+  eq AttributeEndpointTarget.isAttributeEndpointTarget() = true;
+
   /** casts a EndpointTarget into a TokenEndpointTarget if possible.
    *  @return 'this' cast to a TokenEndpointTarget or 'null'
    */
@@ -51,6 +57,13 @@ aspect NewStuff {
   syn UntypedEndpointTarget EndpointTarget.asUntypedEndpointTarget();
   eq EndpointTarget.asUntypedEndpointTarget() = null;
   eq UntypedEndpointTarget.asUntypedEndpointTarget() = this;
+
+  /** casts a EndpointTarget into a AttributeEndpointTarget if possible.
+   *  @return 'this' cast to a AttributeEndpointTarget or 'null'
+   */
+  syn AttributeEndpointTarget EndpointTarget.asAttributeEndpointTarget();
+  eq EndpointTarget.asAttributeEndpointTarget() = null;
+  eq AttributeEndpointTarget.asAttributeEndpointTarget() = this;
 }
 aspect RagConnectNavigation {
 
diff --git a/ragconnect.base/src/main/jastadd/RagConnect.relast b/ragconnect.base/src/main/jastadd/RagConnect.relast
index 01a0f2f884a2f0824bf2787eaf42d7d2d5797c41..f6cf0dd68708157aa291b6b0b50077b2fe72034c 100644
--- a/ragconnect.base/src/main/jastadd/RagConnect.relast
+++ b/ragconnect.base/src/main/jastadd/RagConnect.relast
@@ -13,9 +13,10 @@ TypeEndpointTarget : EndpointTarget;
 rel TypeEndpointTarget.Type <-> TypeComponent.TypeEndpointTarget*;
 ContextFreeTypeEndpointTarget : EndpointTarget;
 rel ContextFreeTypeEndpointTarget.TypeDecl <-> TypeDecl.ContextFreeTypeEndpointTarget*;
-UntypedEndpointTarget : EndpointTarget ::= <TypeName> <ChildName>;  // only used by parser
+UntypedEndpointTarget : EndpointTarget ::= <TypeName> <ChildName> <IsAttribute:boolean>;  // only used by parser
 // to be integrated:
-//AttributeEndpointTarget : EndpointTarget ::= <Name> ;
+AttributeEndpointTarget : EndpointTarget ::= <Name> <TypeName> ;
+rel AttributeEndpointTarget.ParentTypeDecl <-> TypeDecl.AttributeEndpointTarget*;
 //RelationEndpointTarget : EndpointTarget ;
 //rel RelationEndpointTarget.Role <-> Role.RelationEndpointTarget* ;
 
diff --git a/ragconnect.base/src/main/jastadd/Util.jadd b/ragconnect.base/src/main/jastadd/Util.jadd
index 81b820c0b4610e17f82aa31f97c44c97405443e0..31b15ba693b320a4cb502ef3034ea036979bf05b 100644
--- a/ragconnect.base/src/main/jastadd/Util.jadd
+++ b/ragconnect.base/src/main/jastadd/Util.jadd
@@ -1,5 +1,7 @@
 aspect Util {
   static String ASTNode.capitalize(String s) {
+    if (s == null) return null;
+    if (s.isEmpty()) return "";
     return Character.toUpperCase(s.charAt(0)) + s.substring(1);
   }
   protected T JastAddList.firstChild() { return getChild(0); }
diff --git a/ragconnect.base/src/main/jastadd/parser/ParserRewrites.jrag b/ragconnect.base/src/main/jastadd/parser/ParserRewrites.jrag
index f4a8912fe7c2affa7034d3eb8651f9d210bbba54..04e8cadc459653830e5f29e2d189b1aa2c6c0ad5 100644
--- a/ragconnect.base/src/main/jastadd/parser/ParserRewrites.jrag
+++ b/ragconnect.base/src/main/jastadd/parser/ParserRewrites.jrag
@@ -23,6 +23,19 @@ aspect ParserRewrites {
       result.setTypeDecl(TypeDecl.createRef(getTypeName()));
       return result;
     }
+
+    when (getIsAttribute())
+    to AttributeEndpointTarget {
+      AttributeEndpointTarget result = new AttributeEndpointTarget();
+      String[] tokens = this.getChildName().split(":");
+      String attributeName = tokens[0];
+      String attributeTypeName = tokens[1];
+      result.copyOtherValuesFrom(this);
+      result.setName(attributeName);
+      result.setTypeName(attributeTypeName);
+      result.setParentTypeDecl(TypeDecl.createRef(getTypeName()));
+      return result;
+    }
   }
 
   syn String UntypedEndpointTarget.combinedName() = getTypeName() + "." + getChildName();
@@ -37,7 +50,7 @@ aspect ParserRewrites {
   eq UntypedEndpointTarget.parentTypeName() = "<untyped.parentTypeName>";
   eq UntypedEndpointTarget.entityName() = "<untyped.entityName>";
   eq UntypedEndpointTarget.isAlreadyDefined() = false;
-  eq UntypedEndpointTarget.entityIsNormalAttribute() = false;
+  eq UntypedEndpointTarget.hasAttributeResetMethod() = false;
   eq UntypedEndpointTarget.targetTypeName() = "<untyped.targetTypeName>";
   eq UntypedEndpointTarget.isTypeEndpointTarget() = false;
 }
diff --git a/ragconnect.base/src/main/jastadd/parser/RagConnect.parser b/ragconnect.base/src/main/jastadd/parser/RagConnect.parser
index 54a388e51d35bc52296fd4f60e220d325be00674..f822bf01139341080c65f5026bd5661124cafd4e 100644
--- a/ragconnect.base/src/main/jastadd/parser/RagConnect.parser
+++ b/ragconnect.base/src/main/jastadd/parser/RagConnect.parser
@@ -62,8 +62,10 @@ EndpointDefinition endpoint_definition_type
 ;
 
 EndpointTarget endpoint_target
-  = ID.type_name DOT ID.child_name    {: return new UntypedEndpointTarget(type_name, child_name); :}
-  | ID.type_name                      {: return new UntypedEndpointTarget(type_name, ""); :}
+  = ID.type_name DOT ID.child_name    {: return new UntypedEndpointTarget(type_name, child_name, false); :}
+  | ID.type_name DOT ID.child_name BRACKET_LEFT ID.attribute_type_name BRACKET_RIGHT
+     {: return new UntypedEndpointTarget(type_name, child_name + ":" + attribute_type_name, true); :}
+  | ID.type_name                      {: return new UntypedEndpointTarget(type_name, "", false); :}
 ;
 
 ArrayList string_list
diff --git a/ragconnect.base/src/main/jastadd/scanner/Keywords.flex b/ragconnect.base/src/main/jastadd/scanner/Keywords.flex
index 8a1eaec1e8d95c0d8786f9f44168d1f060b27cfa..cda59f5c346fa12f5b83723ed82a0c8849d639b8 100644
--- a/ragconnect.base/src/main/jastadd/scanner/Keywords.flex
+++ b/ragconnect.base/src/main/jastadd/scanner/Keywords.flex
@@ -8,3 +8,5 @@
 "with"       { return sym(Terminals.WITH); }
 "indexed"    { return sym(Terminals.INDEXED); }
 "add"        { return sym(Terminals.ADD); }
+"("          { return sym(Terminals.BRACKET_LEFT); }
+")"          { return sym(Terminals.BRACKET_RIGHT); }
diff --git a/ragconnect.base/src/main/resources/handler.mustache b/ragconnect.base/src/main/resources/handler.mustache
index a7ef5977683a236275a14eb37b9775d24f31526d..0d368731fd37454d5ea3982ebd2a3e2d03c8f047 100644
--- a/ragconnect.base/src/main/resources/handler.mustache
+++ b/ragconnect.base/src/main/resources/handler.mustache
@@ -110,6 +110,10 @@ aspect RagConnectHandler {
       senders.forEach(Runnable::run);
     }
 
+    void run(RagConnectToken token) {
+      tokenToSender.get(token).run();
+    }
+
     byte[] getLastValue() {
       return lastValue;
     }
@@ -150,6 +154,10 @@ aspect RagConnectHandler {
       java.util.Optional.ofNullable(publishers.get(index)).ifPresent(RagConnectPublisher::run);
     }
 
+    void run(int index, RagConnectToken token) {
+      java.util.Optional.ofNullable(publishers.get(index)).ifPresent(publisher -> publisher.run(token));
+    }
+
     byte[] getLastValue(int index) {
       RagConnectPublisher publisher = publishers.get(index);
       if (publisher == null) {
diff --git a/ragconnect.base/src/main/resources/ragconnect.mustache b/ragconnect.base/src/main/resources/ragconnect.mustache
index fbe3dd572676b60fdb826ec01b098d8a6aa7252a..dc6b839a87050d3993cd1bf6f19ffe3694d16242 100644
--- a/ragconnect.base/src/main/resources/ragconnect.mustache
+++ b/ragconnect.base/src/main/resources/ragconnect.mustache
@@ -87,22 +87,33 @@ aspect RagConnectObserver {
   class RagConnectObserver implements ASTState.Trace.Receiver {
 
     class RagConnectObserverEntry {
-      final RagConnectToken connectToken;
       final ASTNode node;
       final String attributeString;
       final boolean compareParams;
       final Object params;
       final Runnable attributeCall;
+      final java.util.List<RagConnectToken> connectList = new java.util.ArrayList<>();
 
-      RagConnectObserverEntry(RagConnectToken connectToken, ASTNode node, String attributeString,
+      RagConnectObserverEntry(ASTNode node, String attributeString,
                               boolean compareParams, Object params, Runnable attributeCall) {
-        this.connectToken = connectToken;
         this.node = node;
         this.attributeString = attributeString;
         this.compareParams = compareParams;
         this.params = params;
         this.attributeCall = attributeCall;
       }
+
+      boolean baseMembersEqualTo(RagConnectObserverEntry other) {
+        return baseMembersEqualTo(other.node, other.attributeString, other.compareParams, other.params);
+      }
+
+      boolean baseMembersEqualTo(ASTNode otherNode, String otherAttributeString,
+          boolean otherCompareParams, Object otherParams) {
+        return this.node.equals(otherNode) &&
+            this.attributeString.equals(otherAttributeString) &&
+            this.compareParams == otherCompareParams &&
+            (!this.compareParams || java.util.Objects.equals(this.params, otherParams));
+      }
     }
 
 {{#configExperimentalJastAdd329}}
@@ -124,7 +135,7 @@ aspect RagConnectObserver {
 
 {{#configExperimentalJastAdd329}}
     java.util.Set<RagConnectObserverEntry> entryQueue = new java.util.HashSet<>();
-    RagConnectObserverStartEntry startEntry = null;
+    java.util.Deque<RagConnectObserverStartEntry> startEntries = new java.util.LinkedList<>();
 {{/configExperimentalJastAdd329}}
 
     RagConnectObserver(ASTNode node) {
@@ -145,42 +156,73 @@ aspect RagConnectObserver {
       {{#configLoggingEnabledForIncremental}}
       System.out.println("** observer add: " + node + " on " + attributeString + (compareParams ? " (parameterized)" : ""));
       {{/configLoggingEnabledForIncremental}}
-      observedNodes.add(new RagConnectObserverEntry(connectToken, node, attributeString,
-                                                    compareParams, params, attributeCall));
+      // either add to an existing entry (with same node, attribute) or create new entry
+      boolean needNewEntry = true;
+      for (RagConnectObserverEntry entry : observedNodes) {
+        if (entry.baseMembersEqualTo(node, attributeString, compareParams, params)) {
+          entry.connectList.add(connectToken);
+          needNewEntry = false;
+          break;
+        }
+      }
+      if (needNewEntry) {
+        RagConnectObserverEntry newEntry = new RagConnectObserverEntry(node, attributeString,
+            compareParams, params, attributeCall);
+        newEntry.connectList.add(connectToken);
+        observedNodes.add(newEntry);
+      }
     }
 
     void remove(RagConnectToken connectToken) {
-      observedNodes.removeIf(entry -> entry.connectToken.equals(connectToken));
+      RagConnectObserverEntry entryToDelete = null;
+      for (RagConnectObserverEntry entry : observedNodes) {
+        entry.connectList.remove(connectToken);
+        if (entry.connectList.isEmpty()) {
+          entryToDelete = entry;
+        }
+      }
+      if (entryToDelete != null) {
+        observedNodes.remove(entryToDelete);
+      }
     }
+
     @Override
     public void accept(ASTState.Trace.Event event, ASTNode node, String attribute, Object params, Object value) {
       oldReceiver.accept(event, node, attribute, params, value);
 {{#configExperimentalJastAdd329}}
       // react to INC_FLUSH_START and remember entry
-      if (event == ASTState.Trace.Event.INC_FLUSH_START && startEntry == null) {
+      if (event == ASTState.Trace.Event.INC_FLUSH_START) {
         {{#configLoggingEnabledForIncremental}}
         System.out.println("** observer start: " + node + " on " + attribute);
         {{/configLoggingEnabledForIncremental}}
-        startEntry = new RagConnectObserverStartEntry(node, attribute, value);
+        startEntries.addFirst(new RagConnectObserverStartEntry(node, attribute, value));
         return;
       }
 
       // react to INC_FLUSH_END and process queued entries, if it matches start entry
-      if (event == ASTState.Trace.Event.INC_FLUSH_END &&
-            node == startEntry.node &&
+      if (event == ASTState.Trace.Event.INC_FLUSH_END) {
+        if (startEntries.isEmpty()) {
+          {{#configLoggingEnabledForIncremental}}
+          System.out.println("** observer end without start! for " + node + " on " + attribute);
+          {{/configLoggingEnabledForIncremental}}
+          return;
+        }
+        RagConnectObserverStartEntry startEntry = startEntries.peekFirst();
+        if (node == startEntry.node &&
             attribute == startEntry.attributeString &&
             value == startEntry.flushIncToken) {
-        // create a copy of the queue to avoid entering this again causing an endless recursion
-        RagConnectObserverEntry[] entriesToProcess = entryQueue.toArray(new RagConnectObserverEntry[entryQueue.size()]);
-        entryQueue.clear();
-        startEntry = null;
-        {{#configLoggingEnabledForIncremental}}
-        System.out.println("** observer process (entries: " + entriesToProcess.length + "): " + node + " on " + attribute);
-        {{/configLoggingEnabledForIncremental}}
-        for (RagConnectObserverEntry entry : entriesToProcess) {
-          entry.attributeCall.run();
+          // create a copy of the queue to avoid entering this again causing an endless recursion
+          RagConnectObserverEntry[] entriesToProcess = entryQueue.toArray(new RagConnectObserverEntry[entryQueue.size()]);
+          entryQueue.clear();
+          startEntries.removeFirst();
+          {{#configLoggingEnabledForIncremental}}
+          System.out.println("** observer process (entries: " + entriesToProcess.length + "): " + node + " on " + attribute);
+          {{/configLoggingEnabledForIncremental}}
+          for (RagConnectObserverEntry entry : entriesToProcess) {
+            entry.attributeCall.run();
+          }
+          return;
         }
-        return;
       }
 
 {{/configExperimentalJastAdd329}}
diff --git a/ragconnect.base/src/main/resources/sendDefinition.mustache b/ragconnect.base/src/main/resources/sendDefinition.mustache
index 293b37c65f2159687fbbe81c8eb112789dd952e9..18d48cafc978318d3e06b32af8e9f315e75c31a4 100644
--- a/ragconnect.base/src/main/resources/sendDefinition.mustache
+++ b/ragconnect.base/src/main/resources/sendDefinition.mustache
@@ -18,7 +18,7 @@ public boolean {{parentTypeName}}.{{connectMethodName}}(String {{connectParamete
         }{{#IndexBasedListAccess}}, index{{/IndexBasedListAccess}}, connectToken);
       {{updateMethodName}}({{#IndexBasedListAccess}}index{{/IndexBasedListAccess}});
       if (writeCurrentValue) {
-        {{writeMethodName}}({{#IndexBasedListAccess}}index{{/IndexBasedListAccess}});
+        {{writeMethodName}}({{#IndexBasedListAccess}}index, {{/IndexBasedListAccess}}connectToken);
       }
       success = true;
       break;
@@ -97,9 +97,9 @@ public boolean {{parentTypeName}}.{{disconnectMethodName}}(String {{connectParam
 }
 
 protected boolean {{parentTypeName}}.{{updateMethodName}}({{#IndexBasedListAccess}}int index{{/IndexBasedListAccess}}) {
-  {{^shouldSendValue}}
+  {{^shouldNotResetValue}}
   {{tokenResetMethodName}}();
-  {{/shouldSendValue}}
+  {{/shouldNotResetValue}}
   {{> mappingApplication}}
   {{lastValueSetter}}({{#IndexBasedListAccess}}index, {{/IndexBasedListAccess}}{{lastResult}});
   // normally we would return true here. unless no connect method was called so far to initialize {{senderName}} yet
@@ -110,6 +110,10 @@ protected void {{parentTypeName}}.{{writeMethodName}}({{#IndexBasedListAccess}}i
   {{senderName}}.run({{#IndexBasedListAccess}}index{{/IndexBasedListAccess}});
 }
 
+protected void {{parentTypeName}}.{{writeMethodName}}({{#IndexBasedListAccess}}int index, {{/IndexBasedListAccess}}RagConnectToken token) {
+  {{senderName}}.run({{#IndexBasedListAccess}}index, {{/IndexBasedListAccess}}token);
+}
+
 {{#needForwardingNTA}}
 syn {{{forwardingNTA_Type}}} {{parentTypeName}}.{{forwardingNTA_Name}}({{#IndexBasedListAccess}}int index{{/IndexBasedListAccess}}) = {{realGetterMethodCall}}.{{touchedTerminalsMethodName}}();
 {{/needForwardingNTA}}
diff --git a/ragconnect.tests/build.gradle b/ragconnect.tests/build.gradle
index 8d073b55ac6b14ebdf6b295bd993dda64dbbd701..8bc120a2d071c77105a2560ac5b8a8b204883d1d 100644
--- a/ragconnect.tests/build.gradle
+++ b/ragconnect.tests/build.gradle
@@ -370,7 +370,6 @@ task compileTreeIncremental(type: RagConnectTest) {
         inputFiles = [file('src/test/01-input/tree/Test.relast'),
                       file('src/test/01-input/tree/Test.connect')]
         rootNode = 'Root'
-        logWrites = true
     }
     relast {
         useJastAddNames = true
@@ -413,7 +412,6 @@ task compileTreeAllowedTokensIncremental(type: RagConnectTest) {
         inputFiles = [file('src/test/01-input/treeAllowedTokens/Test.relast'),
                       file('src/test/01-input/treeAllowedTokens/Test.connect')]
         rootNode = 'Root'
-        logWrites = true
     }
     relast {
         useJastAddNames = true
@@ -604,9 +602,6 @@ task compileIndexedSendIncremental(type: RagConnectTest, dependsOn: ':ragconnect
         inputFiles = [file('src/test/01-input/indexedSend/Test.relast'),
                       file('src/test/01-input/indexedSend/Test.connect')]
         rootNode = 'Root'
-        logWrites = true
-        logReads = true
-        logIncremental = true
         extraOptions = ['--experimental-jastadd-329']
     }
     relast {
@@ -621,3 +616,25 @@ task compileIndexedSendIncremental(type: RagConnectTest, dependsOn: ':ragconnect
         extraOptions = JASTADD_INCREMENTAL_OPTIONS_TRACING_FULL
     }
 }
+
+// --- Test: attribute-incremental ---
+task compileAttributeIncremental(type: RagConnectTest, dependsOn: ':ragconnect.base:jar') {
+    ragconnect {
+        outputDir = file('src/test/02-after-ragconnect/attributeInc')
+        inputFiles = [file('src/test/01-input/attribute/Test.relast'),
+                      file('src/test/01-input/attribute/Test.connect')]
+        rootNode = 'Root'
+        extraOptions = ['--experimental-jastadd-329']
+    }
+    relast {
+        useJastAddNames = true
+        grammarName = 'src/test/03-after-relast/attributeInc/attributeInc'
+        serializer = 'jackson'
+    }
+    jastadd {
+        jastAddList = 'JastAddList'
+        packageName = 'attributeInc.ast'
+        inputFiles = [file('src/test/01-input/attribute/Test.jadd')]
+        extraOptions = JASTADD_INCREMENTAL_OPTIONS_TRACING_FULL
+    }
+}
diff --git a/ragconnect.tests/src/test/01-input/attribute/README.md b/ragconnect.tests/src/test/01-input/attribute/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..08481b7a25582721dfa8435ac199b94acb205de6
--- /dev/null
+++ b/ragconnect.tests/src/test/01-input/attribute/README.md
@@ -0,0 +1,3 @@
+# Attribute
+
+Idea: Use send definitions for attributes.
diff --git a/ragconnect.tests/src/test/01-input/attribute/Test.connect b/ragconnect.tests/src/test/01-input/attribute/Test.connect
new file mode 100644
index 0000000000000000000000000000000000000000..03688a96bdb327076e974498d3ce62c9497f89be
--- /dev/null
+++ b/ragconnect.tests/src/test/01-input/attribute/Test.connect
@@ -0,0 +1,31 @@
+send SenderRoot.basic(String) ;
+send SenderRoot.simple(String) ;
+send SenderRoot.transformed(int) ;
+send SenderRoot.toReferenceType(A) ;
+send SenderRoot.toNTA(A) ;
+
+AddSuffix maps A a to A {:
+  A result = new A();
+  String changedValue = a.getValue() + "post";
+  result.setValue(changedValue);
+  result.setInner(new Inner("inner" + a.getInner().getInnerValue()));
+  return result;
+:}
+
+AddStringSuffix maps String s to String {:
+  return s + "post";
+:}
+
+AddPlusOne maps int i to int {:
+  return i + 1;
+:}
+
+receive ReceiverRoot.FromBasic;
+receive ReceiverRoot.FromSimpleNoMapping;
+receive ReceiverRoot.FromSimpleWithMapping using AddStringSuffix;
+receive ReceiverRoot.FromTransformedNoMapping;
+receive ReceiverRoot.FromTransformedWithMapping using AddPlusOne;
+receive ReceiverRoot.FromReferenceTypeNoMapping;
+receive ReceiverRoot.FromReferenceTypeWithMapping using AddSuffix;
+receive ReceiverRoot.FromNTANoMapping;
+receive ReceiverRoot.FromNTAWithMapping using AddSuffix;
diff --git a/ragconnect.tests/src/test/01-input/attribute/Test.jadd b/ragconnect.tests/src/test/01-input/attribute/Test.jadd
new file mode 100644
index 0000000000000000000000000000000000000000..34b1b45e7a181d0368df2da491c25c197cc009b8
--- /dev/null
+++ b/ragconnect.tests/src/test/01-input/attribute/Test.jadd
@@ -0,0 +1,39 @@
+aspect Computation {
+  syn String SenderRoot.basic() = getInput();
+  syn String SenderRoot.simple() = getInput() + "Post";
+  syn int SenderRoot.transformed() = Integer.parseInt(getInput());
+  syn A SenderRoot.toReferenceType() {
+    A result = new A();
+    result.setValue(getInput());
+    Inner inner = new Inner();
+    inner.setInnerValue("1");
+    result.setInner(inner);
+    return result;
+  }
+  syn nta A SenderRoot.toNTA() {
+    A result = new A();
+    result.setValue(getInput());
+    Inner inner = new Inner();
+    inner.setInnerValue("2");
+    result.setInner(inner);
+    return result;
+  }
+}
+aspect MakeCodeCompile {
+
+}
+aspect MakeCodeWork {
+
+}
+aspect NameResolution {
+  // overriding customID guarantees to produce the same JSON representation for equal lists
+  // otherwise, the value for id is different each time
+  @Override
+  protected String A.customID() {
+    return getClass().getSimpleName() + getValue();
+  }
+  @Override
+  protected String Inner.customID() {
+    return getClass().getSimpleName() + getInnerValue();
+  }
+}
diff --git a/ragconnect.tests/src/test/01-input/attribute/Test.relast b/ragconnect.tests/src/test/01-input/attribute/Test.relast
new file mode 100644
index 0000000000000000000000000000000000000000..7afc9b7745c0da1cab4af5140fdddb2e5981f3b1
--- /dev/null
+++ b/ragconnect.tests/src/test/01-input/attribute/Test.relast
@@ -0,0 +1,14 @@
+Root ::= SenderRoot* ReceiverRoot;
+SenderRoot ::= <Input> ;
+ReceiverRoot ::=
+    <FromBasic>
+    <FromSimpleNoMapping>
+    <FromSimpleWithMapping>
+    <FromTransformedNoMapping:int>
+    <FromTransformedWithMapping:int>
+    FromReferenceTypeNoMapping:A
+    FromReferenceTypeWithMapping:A
+    FromNTANoMapping:A
+    FromNTAWithMapping:A ;
+A ::= <Value> Inner ;
+Inner ::= <InnerValue> ;
diff --git a/ragconnect.tests/src/test/01-input/errors/Standard.connect b/ragconnect.tests/src/test/01-input/errors/Standard.connect
index 6224df4184a2f7cc21d913934785cd022b14f5b0..fb2c7cb2400d1b8ca44903a9aeb3bb0bfcf28537 100644
--- a/ragconnect.tests/src/test/01-input/errors/Standard.connect
+++ b/ragconnect.tests/src/test/01-input/errors/Standard.connect
@@ -60,6 +60,15 @@ D.SourceDoubledValue canDependOn D.TargetDoubledValue as DoubledValue ;
 D.SourceDoubledValue canDependOn D.TargetDoubledValue as DoubledValue ;
 send D.TargetDoubledValue;
 
+// non-existence of attributes is not checked by RagConnect
+send A.nonExistingAttribute(int);
+
+// Already defined endpoints for attributes will be reported, however
+send A.nonExistingAttribute(int);
+
+// mappings are not checked, here string would not match
+send A.anotherIntAttribute(int) using StringToString;
+
 // --- mapping definitions ---
 ListToList maps java.util.List<String> list to java.util.List<String> {:
   return list;
diff --git a/ragconnect.tests/src/test/01-input/errors/Standard.expected b/ragconnect.tests/src/test/01-input/errors/Standard.expected
index 9b3d23ac98118e0857ffb9964e1a04d592b1dea1..1f2b02da133cc26bc9660b466112cb73421ab529 100644
--- a/ragconnect.tests/src/test/01-input/errors/Standard.expected
+++ b/ragconnect.tests/src/test/01-input/errors/Standard.expected
@@ -11,3 +11,5 @@ Standard.connect Line 43, column 1: Endpoint definition already defined for C.Do
 Standard.connect Line 44, column 1: Endpoint definition already defined for C.DoubledValue
 Standard.connect Line 55, column 1: The name of a dependency definition must not be equal to a list-node on the source
 Standard.connect Line 60, column 1: Dependency definition already defined for D with name DoubledValue
+Standard.connect Line 64, column 1: Endpoint definition already defined for A.nonExistingAttribute
+Standard.connect Line 67, column 1: Endpoint definition already defined for A.nonExistingAttribute
diff --git a/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/AttributeTest.java b/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/AttributeTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..2e7e0c7d79bd7d20e938eff6257008379306bc8d
--- /dev/null
+++ b/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/AttributeTest.java
@@ -0,0 +1,251 @@
+package org.jastadd.ragconnect.tests;
+
+import attributeInc.ast.*;
+import org.junit.jupiter.api.Tag;
+
+import java.io.IOException;
+import java.util.Objects;
+import java.util.concurrent.Callable;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Predicate;
+import java.util.function.Supplier;
+
+import static java.util.function.Predicate.isEqual;
+import static org.jastadd.ragconnect.tests.TestUtils.*;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Test case "attribute".
+ *
+ * @author rschoene - Initial contribution
+ */
+@Tag("Incremental")
+public class AttributeTest extends AbstractMqttTest {
+
+  private static final String TOPIC_WILDCARD = "attr/#";
+  private static final String TOPIC_BASIC = "attr/string/basic";
+  private static final String TOPIC_SIMPLE_NO_MAPPING = "attr/string/simple/plain";
+  private static final String TOPIC_SIMPLE_WITH_MAPPING = "attr/string/simple/mapped";
+  private static final String TOPIC_TRANSFORMED_NO_MAPPING = "attr/int/transformed/plain";
+  private static final String TOPIC_TRANSFORMED_WITH_MAPPING = "attr/int/transformed/mapped";
+  private static final String TOPIC_REFERENCE_TYPE_NO_MAPPING = "attr/a/ref/plain";
+  private static final String TOPIC_REFERENCE_TYPE_WITH_MAPPING = "attr/a/ref/mapped";
+  private static final String TOPIC_NTA_NO_MAPPING = "attr/a/nta/plain";
+  private static final String TOPIC_NTA_WITH_MAPPING = "attr/a/nta/mapped";
+
+  private static final String INITIAL_STRING = "initial";
+  private static final String INITIAL_STRING_FOR_INT = "1";
+
+  private MqttHandler handler;
+  private ReceiverData data;
+
+  private Root model;
+  private SenderRoot senderString;
+  private SenderRoot senderInt;
+  private SenderRoot senderA;
+  private ReceiverRoot receiverRoot;
+
+  @Override
+  protected void createModel() {
+    model = new Root();
+//    model.trace().setReceiver(TestUtils::logEvent);
+    senderString = new SenderRoot().setInput(INITIAL_STRING);
+    senderInt = new SenderRoot().setInput(INITIAL_STRING_FOR_INT);
+    senderA = new SenderRoot().setInput(INITIAL_STRING);
+    receiverRoot = new ReceiverRoot();
+    model.addSenderRoot(senderString);
+    model.addSenderRoot(senderInt);
+    model.addSenderRoot(senderA);
+    model.setReceiverRoot(receiverRoot);
+  }
+
+  @Override
+  protected void setupReceiverAndConnect() throws IOException, InterruptedException {
+    model.ragconnectSetupMqttWaitUntilReady(2, TimeUnit.SECONDS);
+    handler = new MqttHandler().setHost(TestUtils.getMqttHost()).dontSendWelcomeMessage();
+    assertTrue(handler.waitUntilReady(2, TimeUnit.SECONDS));
+
+    data = new ReceiverData();
+    assertTrue(handler.newConnection(TOPIC_WILDCARD, bytes -> data.numberOfValues += 1));
+
+    // connect receive
+    assertTrue(receiverRoot.connectFromBasic(mqttUri(TOPIC_BASIC)));
+    assertTrue(receiverRoot.connectFromSimpleNoMapping(mqttUri(TOPIC_SIMPLE_NO_MAPPING)));
+    assertTrue(receiverRoot.connectFromSimpleWithMapping(mqttUri(TOPIC_SIMPLE_WITH_MAPPING)));
+    assertTrue(receiverRoot.connectFromTransformedNoMapping(mqttUri(TOPIC_TRANSFORMED_NO_MAPPING)));
+    assertTrue(receiverRoot.connectFromTransformedWithMapping(mqttUri(TOPIC_TRANSFORMED_WITH_MAPPING)));
+    assertTrue(receiverRoot.connectFromReferenceTypeNoMapping(mqttUri(TOPIC_REFERENCE_TYPE_NO_MAPPING)));
+    assertTrue(receiverRoot.connectFromReferenceTypeWithMapping(mqttUri(TOPIC_REFERENCE_TYPE_WITH_MAPPING)));
+    assertTrue(receiverRoot.connectFromNTANoMapping(mqttUri(TOPIC_NTA_NO_MAPPING)));
+    assertTrue(receiverRoot.connectFromNTAWithMapping(mqttUri(TOPIC_NTA_WITH_MAPPING)));
+
+    // connect send, and wait to receive (if writeCurrentValue is set)
+    assertTrue(senderString.connectBasic(mqttUri(TOPIC_BASIC), isWriteCurrentValue()));
+    assertTrue(senderString.connectSimple(mqttUri(TOPIC_SIMPLE_NO_MAPPING), isWriteCurrentValue()));
+    assertTrue(senderString.connectSimple(mqttUri(TOPIC_SIMPLE_WITH_MAPPING), isWriteCurrentValue()));
+
+    assertTrue(senderInt.connectTransformed(mqttUri(TOPIC_TRANSFORMED_NO_MAPPING), isWriteCurrentValue()));
+    assertTrue(senderInt.connectTransformed(mqttUri(TOPIC_TRANSFORMED_WITH_MAPPING), isWriteCurrentValue()));
+
+    assertTrue(senderA.connectToReferenceType(mqttUri(TOPIC_REFERENCE_TYPE_NO_MAPPING), isWriteCurrentValue()));
+    assertTrue(senderA.connectToReferenceType(mqttUri(TOPIC_REFERENCE_TYPE_WITH_MAPPING), isWriteCurrentValue()));
+    assertTrue(senderA.connectToNTA(mqttUri(TOPIC_NTA_NO_MAPPING), isWriteCurrentValue()));
+    assertTrue(senderA.connectToNTA(mqttUri(TOPIC_NTA_WITH_MAPPING), isWriteCurrentValue()));
+
+    waitForValue(senderString.basic(), receiverRoot::getFromBasic);
+    waitForValue(senderString.simple(), receiverRoot::getFromSimpleNoMapping);
+    waitForValue(senderInt.transformed(), receiverRoot::getFromTransformedNoMapping);
+    waitForNonNull(receiverRoot::getFromReferenceTypeNoMapping);
+    waitForNonNull(receiverRoot::getFromNTANoMapping);
+  }
+
+  private <T> void waitForValue(T expectedValue, Callable<T> callable) {
+    if (isWriteCurrentValue()) {
+      awaitMqtt().until(callable, isEqual(expectedValue));
+    }
+  }
+
+  private <T> void waitForNonNull(Callable<T> callable) {
+    if (isWriteCurrentValue()) {
+      awaitMqtt().until(callable, Predicate.not(isEqual(null)));
+    }
+  }
+
+  @Override
+  protected void communicateSendInitialValue() throws IOException, InterruptedException {
+    // basic, simple(2)    <-- senderString
+    // transformed(2)      <-- senderInt
+    // ref-type(2), nta(2) <-- senderA
+    check(9, INITIAL_STRING, INITIAL_STRING + "Post", INITIAL_STRING_FOR_INT, INITIAL_STRING, INITIAL_STRING);
+
+    senderString.setInput("test-01");
+    check(12, "test-01", "test-01Post", INITIAL_STRING_FOR_INT, INITIAL_STRING, INITIAL_STRING);
+
+    senderString.setInput("test-01");
+    check(12, "test-01", "test-01Post", INITIAL_STRING_FOR_INT, INITIAL_STRING, INITIAL_STRING);
+
+    senderInt.setInput("20");
+    check(14, "test-01", "test-01Post", "20", INITIAL_STRING, INITIAL_STRING);
+
+    senderA.setInput("test-03");
+    check(18, "test-01", "test-01Post", "20", "test-03", "test-03");
+
+    assertTrue(senderString.disconnectSimple(mqttUri(TOPIC_SIMPLE_NO_MAPPING)));
+    assertTrue(senderString.disconnectSimple(mqttUri(TOPIC_SIMPLE_WITH_MAPPING)));
+    senderString.setInput("test-04");
+    check(19, "test-04", "test-01Post", "20", "test-03", "test-03");
+
+    assertTrue(senderA.disconnectToNTA(mqttUri(TOPIC_NTA_NO_MAPPING)));
+    senderA.setInput("test-05");
+    check(22, "test-04", "test-01Post", "20", "test-05", "test-03");
+  }
+
+  @Override
+  protected void communicateOnlyUpdatedValue() throws IOException, InterruptedException {
+    waitForMqtt();
+    // basic, simple(2)    <-- senderString
+    // transformed(2)      <-- senderInt
+    // ref-type(2), nta(2) <-- senderA
+    check(0, null, null, null, null, null);
+
+    senderString.setInput("test-01");
+    check(3, "test-01", "test-01Post", null, null, null);
+
+    senderString.setInput("test-01");
+    check(3, "test-01", "test-01Post", null, null, null);
+
+    senderInt.setInput("20");
+    check(5, "test-01", "test-01Post", "20", null, null);
+
+    senderA.setInput("test-03");
+    check(9, "test-01", "test-01Post", "20", "test-03", "test-03");
+
+    assertTrue(senderString.disconnectSimple(mqttUri(TOPIC_SIMPLE_NO_MAPPING)));
+    assertTrue(senderString.disconnectSimple(mqttUri(TOPIC_SIMPLE_WITH_MAPPING)));
+    senderString.setInput("test-04");
+    check(10, "test-04", "test-01Post", "20", "test-03", "test-03");
+
+    assertTrue(senderA.disconnectToNTA(mqttUri(TOPIC_NTA_NO_MAPPING)));
+    senderA.setInput("test-05");
+    check(13, "test-04", "test-01Post", "20", "test-05", "test-03");
+  }
+
+  private void check(int numberOfValues, String basic, String simple, String transformed,
+                     String a, String ntaNoMapping) {
+    awaitEquals(numberOfValues, () -> data.numberOfValues, "numberOfValues");
+
+    awaitEquals(Objects.requireNonNullElse(basic, ""),
+            receiverRoot::getFromBasic, "basic");
+
+    if (simple != null) {
+      awaitEquals(simple,
+              receiverRoot::getFromSimpleNoMapping, "simple");
+      awaitEquals(simple + "post",
+              receiverRoot::getFromSimpleWithMapping, "simple mapped");
+    } else {
+      awaitEquals("",
+              receiverRoot::getFromSimpleNoMapping, "simple null");
+      awaitEquals("",
+              receiverRoot::getFromSimpleWithMapping, "simple mapped null");
+    }
+
+    if (transformed != null) {
+      awaitEquals(Integer.parseInt(transformed),
+              receiverRoot::getFromTransformedNoMapping, "transformed");
+      awaitEquals(Integer.parseInt(transformed) + 1,
+              receiverRoot::getFromTransformedWithMapping, "transformed mapped");
+    } else {
+      awaitEquals(0,
+              receiverRoot::getFromTransformedNoMapping, "transformed null");
+      awaitEquals(0,
+              receiverRoot::getFromTransformedWithMapping, "transformed mapped null");
+    }
+
+    if (a != null) {
+      awaitA(a, "1",
+              receiverRoot.getFromReferenceTypeNoMapping(), "ref-type");
+      awaitA(a + "post", "inner1",
+              receiverRoot.getFromReferenceTypeWithMapping(), "ref-type mapped");
+      awaitA(a + "post", "inner2",
+              receiverRoot.getFromNTAWithMapping(), "nta mapped");
+    } else {
+      awaitNull(receiverRoot::getFromReferenceTypeNoMapping, "manual ref-type null");
+      awaitNull(receiverRoot::getFromReferenceTypeWithMapping, "ref-type mapped null");
+      awaitNull(receiverRoot::getFromNTAWithMapping, "nta mapped null");
+    }
+
+    if (ntaNoMapping != null) {
+      awaitA(ntaNoMapping, "2",
+              receiverRoot.getFromNTANoMapping(), "nta");
+    } else {
+      awaitNull(receiverRoot::getFromNTANoMapping, "nta null");
+    }
+  }
+
+  private void awaitNull(Supplier<A> actual, String alias) {
+    awaitMqtt().alias(alias).until(() -> actual.get() == null);
+  }
+
+  private <T> void awaitEquals(T expected, Callable<T> actual, String alias) {
+    awaitMqtt().alias(alias).until(actual, isEqual(expected));
+  }
+
+  private void awaitA(String expectedValue, String expectedInner, A actual, String message) {
+    awaitEquals(expectedValue, actual::getValue, message + " value");
+    awaitEquals(expectedInner, actual.getInner()::getInnerValue, message + " inner");
+  }
+
+  @Override
+  protected void closeConnections() {
+    if (handler != null) {
+      handler.close();
+    }
+    if (model != null) {
+      model.ragconnectCloseConnections();
+    }
+  }
+
+  private static class ReceiverData {
+    int numberOfValues = 0;
+  }
+}
diff --git a/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/ForwardingTest.java b/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/ForwardingTest.java
index 64430eb40800d7bea28e38e63d0cb73728f5856f..570c4d04742d78501f2a206aa4149d1d79d3b9f5 100644
--- a/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/ForwardingTest.java
+++ b/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/ForwardingTest.java
@@ -1,8 +1,6 @@
 package org.jastadd.ragconnect.tests;
 
 import forwardingInc.ast.*;
-import org.apache.logging.log4j.LogManager;
-import org.apache.logging.log4j.Logger;
 import org.assertj.core.groups.Tuple;
 import org.junit.jupiter.api.Tag;
 
@@ -14,9 +12,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.groups.Tuple.tuple;
-import static org.awaitility.Awaitility.await;
-import static org.jastadd.ragconnect.tests.TestUtils.mqttUri;
-import static org.jastadd.ragconnect.tests.TestUtils.waitForMqtt;
+import static org.jastadd.ragconnect.tests.TestUtils.*;
 import static org.junit.jupiter.api.Assertions.*;
 
 /**
@@ -183,7 +179,7 @@ public class ForwardingTest extends AbstractMqttTest {
 
   private void waitForValue() {
     if (isWriteCurrentValue()) {
-      await().until(() -> data.valueSentSinceLastCheck.getAndSet(false));
+      awaitMqtt().until(() -> data.valueSentSinceLastCheck.getAndSet(false));
     }
   }
 
@@ -524,9 +520,7 @@ public class ForwardingTest extends AbstractMqttTest {
     if (model != null) {
       model.ragconnectCloseConnections();
     }
-    if (observer != null) {
-      observer.init();
-    }
+    observer.init();
   }
 
   static class Values<T> {
diff --git a/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/IndexedSendTest.java b/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/IndexedSendTest.java
index a4575feafc716fe6dbd95f43f2b2212436a843d0..75d7d16b18c64f7b5fd39c71acba81f92223a2af 100644
--- a/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/IndexedSendTest.java
+++ b/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/IndexedSendTest.java
@@ -11,9 +11,7 @@ import java.util.concurrent.TimeUnit;
 import java.util.function.Predicate;
 
 import static org.assertj.core.groups.Tuple.tuple;
-import static org.awaitility.Awaitility.await;
-import static org.jastadd.ragconnect.tests.TestUtils.mqttUri;
-import static org.jastadd.ragconnect.tests.TestUtils.waitForMqtt;
+import static org.jastadd.ragconnect.tests.TestUtils.*;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
@@ -99,7 +97,7 @@ public class IndexedSendTest extends AbstractMqttTest {
 
   private void waitForValue(Callable<Integer> callable, int expectedValue) {
     if (isWriteCurrentValue()) {
-      await().until(callable, Predicate.isEqual(expectedValue));
+      awaitMqtt().until(callable, Predicate.isEqual(expectedValue));
     }
   }
 
diff --git a/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/TestUtils.java b/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/TestUtils.java
index 937c930d4b4a9e75003dbf74cdc42f12bc85ca0e..9a6c22bd962940b5d0dc0e942dcc91a3ca1fbf19 100644
--- a/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/TestUtils.java
+++ b/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/TestUtils.java
@@ -5,6 +5,8 @@ import com.fasterxml.jackson.core.JsonFactory;
 import com.fasterxml.jackson.core.JsonGenerator;
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
+import org.awaitility.Awaitility;
+import org.awaitility.core.ConditionFactory;
 import org.jastadd.ragconnect.compiler.Compiler;
 import org.junit.jupiter.api.Assertions;
 
@@ -24,7 +26,6 @@ import java.util.stream.Collectors;
 import static java.util.Collections.addAll;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.util.Lists.newArrayList;
-import static org.awaitility.Awaitility.await;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.junit.jupiter.api.Assertions.fail;
 
@@ -145,6 +146,10 @@ public class TestUtils {
     TimeUnit.MILLISECONDS.sleep(1500);
   }
 
+  public static ConditionFactory awaitMqtt() {
+    return Awaitility.await().atMost(1500, TimeUnit.MILLISECONDS);
+  }
+
   static <T_Event, T_ASTNode> void logEvent(T_Event event, T_ASTNode node, String attribute, Object params, Object value) {
     logger.info("event: {}, node: {}, attribute: {}, params: {}, value: {}",
             event, node, attribute, params, value);
@@ -388,7 +393,7 @@ public class TestUtils {
     }
 
     void awaitChange() {
-      await().until(hasChanged);
+      awaitMqtt().until(hasChanged);
       updatePrevious();
     }
 
diff --git a/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/singleList/AbstractSingleListTest.java b/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/singleList/AbstractSingleListTest.java
index 832ba76d7df935164b7ebd5ac90af1bbb3e854e9..cdcb41c72e1af5b9b53f9afba3fd78eefbe06bd6 100644
--- a/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/singleList/AbstractSingleListTest.java
+++ b/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/singleList/AbstractSingleListTest.java
@@ -14,10 +14,8 @@ import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.function.Function;
 
-import static org.awaitility.Awaitility.await;
+import static org.jastadd.ragconnect.tests.TestUtils.*;
 import static org.jastadd.ragconnect.tests.TestUtils.IntList.list;
-import static org.jastadd.ragconnect.tests.TestUtils.mqttUri;
-import static org.jastadd.ragconnect.tests.TestUtils.testJaddContainReferenceToJackson;
 import static org.junit.jupiter.api.Assertions.*;
 
 /**
@@ -167,7 +165,7 @@ public abstract class AbstractSingleListTest extends AbstractMqttTest {
 
   private void waitForValue() {
     if (isWriteCurrentValue()) {
-      await().until(() -> data.valueSentSinceLastCheck.getAndSet(false));
+      awaitMqtt().until(() -> data.valueSentSinceLastCheck.getAndSet(false));
     }
   }