diff --git a/source/RobotAPI/components/ObjectPoseObserver/ObjectPose.cpp b/source/RobotAPI/components/ObjectPoseObserver/ObjectPose.cpp
index 6ca0338ce6c4c8edae44bb3248d9d4e8b1ea6c03..19bb9d5aac9be101f1c532279977f58a262eab72 100644
--- a/source/RobotAPI/components/ObjectPoseObserver/ObjectPose.cpp
+++ b/source/RobotAPI/components/ObjectPoseObserver/ObjectPose.cpp
@@ -56,6 +56,8 @@ namespace armarx::objpose
         robotConfig = ice.robotConfig;
         robotPose = armarx::objpose::toEigen(ice.robotPose);
 
+        objpose::fromIce(ice.attachment, this->attachment);
+
         confidence = ice.confidence;
         timestamp = IceUtil::Time::microSeconds(ice.timestampMicroSeconds);
 
@@ -83,6 +85,8 @@ namespace armarx::objpose
         ice.robotConfig = robotConfig;
         ice.robotPose = new Pose(robotPose);
 
+        objpose::toIce(ice.attachment, this->attachment);
+
         ice.confidence = confidence;
         ice.timestampMicroSeconds = timestamp.toMicroSeconds();
 
@@ -107,6 +111,8 @@ namespace armarx::objpose
         robotConfig = robot->getConfig()->getRobotNodeJointValueMap();
         robotPose = robot->getGlobalPose();
 
+        attachment = std::nullopt;
+
         confidence = provided.confidence;
         timestamp = IceUtil::Time::microSeconds(provided.timestampMicroSeconds);
 
@@ -137,6 +143,70 @@ namespace armarx::objpose
 namespace armarx
 {
 
+    void objpose::fromIce(const data::ObjectAttachmentInfo& ice, ObjectAttachmentInfo& attachment)
+    {
+        attachment.agentName = ice.agentName;
+        attachment.frameName = ice.frameName;
+        attachment.poseInFrame = toEigen(ice.poseInFrame);
+    }
+
+    void objpose::fromIce(const data::ObjectAttachmentInfoPtr& ice, std::optional<ObjectAttachmentInfo>& attachment)
+    {
+        if (ice)
+        {
+            attachment = ObjectAttachmentInfo();
+            fromIce(*ice, *attachment);
+        }
+        else
+        {
+            attachment = std::nullopt;
+        }
+    }
+
+    std::optional<objpose::ObjectAttachmentInfo> objpose::fromIce(const data::ObjectAttachmentInfoPtr& ice)
+    {
+        if (!ice)
+        {
+            return std::nullopt;
+        }
+        objpose::ObjectAttachmentInfo info;
+        fromIce(*ice, info);
+        return info;
+    }
+
+
+    void objpose::toIce(data::ObjectAttachmentInfo& ice, const ObjectAttachmentInfo& attachment)
+    {
+        ice.agentName = attachment.agentName;
+        ice.frameName = attachment.frameName;
+        ice.poseInFrame = new Pose(attachment.poseInFrame);
+    }
+
+    void objpose::toIce(data::ObjectAttachmentInfoPtr& ice, const std::optional<ObjectAttachmentInfo>& attachment)
+    {
+        if (attachment)
+        {
+            ice = new objpose::data::ObjectAttachmentInfo();
+            toIce(*ice, *attachment);
+        }
+        else
+        {
+            ice = nullptr;
+        }
+    }
+
+    objpose::data::ObjectAttachmentInfoPtr objpose::toIce(const std::optional<ObjectAttachmentInfo>& attachment)
+    {
+        if (!attachment)
+        {
+            return nullptr;
+        }
+        objpose::data::ObjectAttachmentInfoPtr ice = new objpose::data::ObjectAttachmentInfo();
+        toIce(*ice, *attachment);
+        return ice;
+    }
+
+
     void objpose::fromIce(const data::ObjectPose& ice, ObjectPose& pose)
     {
         pose.fromIce(ice);
@@ -190,7 +260,3 @@ namespace armarx
 
 }
 
-
-
-
-
diff --git a/source/RobotAPI/components/ObjectPoseObserver/ObjectPose.h b/source/RobotAPI/components/ObjectPoseObserver/ObjectPose.h
index d00e548b82bc9bdd72a46aeb997a6eb297e89ba4..cdfd1cfdece0570cf77162549de5da7d443c0e26 100644
--- a/source/RobotAPI/components/ObjectPoseObserver/ObjectPose.h
+++ b/source/RobotAPI/components/ObjectPoseObserver/ObjectPose.h
@@ -16,6 +16,14 @@
 namespace armarx::objpose
 {
 
+    struct ObjectAttachmentInfo
+    {
+        std::string frameName;
+        std::string agentName;
+        Eigen::Matrix4f poseInFrame;
+    };
+
+
     /**
      * @brief An object pose as stored by the ObjectPoseObserver.
      */
@@ -48,6 +56,8 @@ namespace armarx::objpose
         std::map<std::string, float> robotConfig;
         Eigen::Matrix4f robotPose;
 
+        std::optional<ObjectAttachmentInfo> attachment;
+
         /// Confidence in [0, 1] (1 = full, 0 = none).
         float confidence = 0;
         /// Source timestamp.
@@ -67,6 +77,10 @@ namespace armarx::objpose
     using ObjectPoseSeq = std::vector<ObjectPose>;
 
 
+    void fromIce(const data::ObjectAttachmentInfo& ice, ObjectAttachmentInfo& attachment);
+    void fromIce(const data::ObjectAttachmentInfoPtr& ice, std::optional<ObjectAttachmentInfo>& attachment);
+    std::optional<ObjectAttachmentInfo> fromIce(const data::ObjectAttachmentInfoPtr& ice);
+
     void fromIce(const data::ObjectPose& ice, ObjectPose& pose);
     ObjectPose fromIce(const data::ObjectPose& ice);
 
@@ -74,6 +88,10 @@ namespace armarx::objpose
     ObjectPoseSeq fromIce(const data::ObjectPoseSeq& ice);
 
 
+    void toIce(data::ObjectAttachmentInfo& ice, const ObjectAttachmentInfo& attachment);
+    void toIce(data::ObjectAttachmentInfoPtr& ice, const std::optional<ObjectAttachmentInfo>& attachment);
+    data::ObjectAttachmentInfoPtr toIce(const std::optional<ObjectAttachmentInfo>& ice);
+
     void toIce(data::ObjectPose& ice, const ObjectPose& pose);
     data::ObjectPose toIce(const ObjectPose& pose);
 
diff --git a/source/RobotAPI/components/ObjectPoseObserver/ObjectPoseObserver.cpp b/source/RobotAPI/components/ObjectPoseObserver/ObjectPoseObserver.cpp
index 892094431698411a2ad66189986a7fa0f2cbb6ef..a6b0afcdd5b61d39917be0b440c440472f31af79 100644
--- a/source/RobotAPI/components/ObjectPoseObserver/ObjectPoseObserver.cpp
+++ b/source/RobotAPI/components/ObjectPoseObserver/ObjectPoseObserver.cpp
@@ -156,6 +156,7 @@ namespace armarx
         updateProviderInfo(providerName, info);
     }
 
+
     void ObjectPoseObserver::reportObjectPoses(
         const std::string& providerName, const objpose::data::ProvidedObjectPoseSeq& providedPoses, const Ice::Current&)
     {
@@ -204,7 +205,7 @@ namespace armarx
     {
         if (!info.proxy)
         {
-            ARMARX_WARNING << "Received availability signal from provider '" << providerName << "' "
+            ARMARX_WARNING << "Received availability signal by provider '" << providerName << "' "
                            << "with invalid provider proxy.\nIgnoring provider '" << providerName << "'.";
             return;
         }
@@ -240,13 +241,11 @@ namespace armarx
         std::scoped_lock lock(dataMutex);
         // ARMARX_INFO << "Locking took " << (IceUtil::Time::now() - start).toMilliSeconds() << " ms";
         // start = IceUtil::Time::now();
+        bool synchronized = false;
         objpose::data::ObjectPoseSeq result;
-        for (const auto& [name, poses] : objectPoses)
+        for (auto& [name, poses] : objectPoses)
         {
-            for (const auto& pose : poses)
-            {
-                result.push_back(pose.toIce());
-            }
+            toIceWithAttachments(poses, robot, result, synchronized);
         }
         // ARMARX_INFO << "getObjectPoses() took " << (IceUtil::Time::now() - start).toMilliSeconds() << " ms";
         return result;
@@ -255,7 +254,10 @@ namespace armarx
     objpose::data::ObjectPoseSeq ObjectPoseObserver::getObjectPosesByProvider(const std::string& providerName, const Ice::Current&)
     {
         std::scoped_lock lock(dataMutex);
-        return objpose::toIce(objectPoses.at(providerName));
+        bool synchronized = false;
+        objpose::data::ObjectPoseSeq result;
+        toIceWithAttachments(objectPoses.at(providerName), robot, result, synchronized);
+        return result;
     }
 
 
@@ -316,7 +318,7 @@ namespace armarx
         {
             if (objpose::ObjectPoseProviderPrx proxy = proxies.at(providerName); proxy)
             {
-                ARMARX_INFO << "Requesting " << request.objectIDs.size() << " objects from provider '"
+                ARMARX_INFO << "Requesting " << request.objectIDs.size() << " objects by provider '"
                             << providerName << "' for " << request.relativeTimeoutMS << " ms.";
                 objpose::provider::RequestObjectsOutput providerOutput = proxy->requestObjects(request);
 
@@ -334,7 +336,6 @@ namespace armarx
         return output;
     }
 
-
     objpose::ProviderInfoMap ObjectPoseObserver::getAvailableProvidersInfo(const Ice::Current&)
     {
         std::scoped_lock lock(dataMutex);
@@ -373,6 +374,175 @@ namespace armarx
     }
 
 
+
+    objpose::AttachObjectToRobotNodeOutput ObjectPoseObserver::attachObjectToRobotNode(
+        const objpose::AttachObjectToRobotNodeInput& input, const Ice::Current&)
+    {
+        std::scoped_lock lock(dataMutex);
+
+        objpose::AttachObjectToRobotNodeOutput output;
+        output.success = false;  // We are not successful until proven otherwise.
+
+        ObjectID objectID = armarx::fromIce(input.objectID);
+
+        if (input.agentName != "" && input.agentName != robot->getName())
+        {
+            ARMARX_WARNING << "Tried to attach object " << objectID << " to unknown agent '" << input.agentName << "'."
+                           << "\n(You can leave the agent name empty if there is only one agent.)\n"
+                           << "\nKnown agents: " << std::vector<std::string> {robot->getName()};
+            return output;
+        }
+        VirtualRobot::RobotPtr agent = this->robot;
+
+        if (!agent->hasRobotNode(input.frameName))
+        {
+            ARMARX_WARNING << "Tried to attach object " << objectID << " to unknown node '" << input.frameName
+                           << "' of agent '" << agent->getName() << "'.";
+            return output;
+        }
+        std::string frameName = input.frameName;
+
+
+        // Find object pose.
+        objpose::ObjectPose* currentObjectPose = findObjectPose(input.providerName, objectID);
+        if (!currentObjectPose)
+        {
+            ARMARX_WARNING << "Tried to attach object " << objectID << " to node '" << frameName
+                           << "' of agent '" << agent->getName() << "', but object is currently not provided.";
+            return output;
+        }
+
+        objpose::ObjectAttachmentInfo info;
+        info.agentName = agent->getName();
+        info.frameName = frameName;
+
+        if (input.poseInFrame)
+        {
+            info.poseInFrame = PosePtr::dynamicCast(input.poseInFrame)->toEigen();
+        }
+        else
+        {
+            RobotState::synchronizeLocalClone(robot);
+
+            armarx::FramedPose framed(currentObjectPose->objectPoseGlobal, armarx::GlobalFrame, agent->getName());
+            if (frameName == armarx::GlobalFrame)
+            {
+                info.poseInFrame = framed.toGlobalEigen(robot);
+            }
+            else
+            {
+                framed.changeFrame(robot, info.frameName);
+                info.poseInFrame = framed.toEigen();
+            }
+        }
+        attachments[std::make_pair(currentObjectPose->providerName, objectID)] = info;
+
+        ARMARX_INFO << "Attached object " << objectID << " by provider '" << currentObjectPose->providerName << "' "
+                    << "to node '" << info.frameName << "' of agent '" << info.agentName << "'.\n"
+                    << "Object pose in frame: \n" << info.poseInFrame;
+
+        output.success = true;
+        output.attachment = new objpose::data::ObjectAttachmentInfo();
+        output.attachment->frameName = info.frameName;
+        output.attachment->agentName = info.agentName;
+        output.attachment->poseInFrame = new Pose(info.poseInFrame);
+
+        return output;
+    }
+
+    objpose::DetachObjectFromRobotNodeOutput ObjectPoseObserver::detachObjectFromRobotNode(
+        const objpose::DetachObjectFromRobotNodeInput& input, const Ice::Current&)
+    {
+        objpose::DetachObjectFromRobotNodeOutput output;
+
+        ObjectID objectID = armarx::fromIce(input.objectID);
+        std::string providerName = input.providerName;
+
+        std::optional<objpose::ObjectAttachmentInfo> attachment;
+        {
+            std::scoped_lock lock(dataMutex);
+
+            // Remove from latest pose (if it was cached).
+            objpose::ObjectPose* objectPose = findObjectPose(input.providerName, objectID);
+            if (objectPose)
+            {
+                objectPose->attachment = std::nullopt;
+            }
+
+            if (providerName.empty() && objectPose)
+            {
+                providerName = objectPose->providerName;
+            }
+            // Remove from attachment map.
+            if (input.providerName.size() > 0)
+            {
+                auto it = attachments.find(std::make_pair(input.providerName, objectID));
+                if (it != attachments.end())
+                {
+                    attachment = it->second;
+                    attachments.erase(it);
+                }
+            }
+            else
+            {
+                // Search for entry with matching object ID.
+                for (auto it = attachments.begin(); it != attachments.end(); ++it)
+                {
+                    const ObjectID& id = it->first.second;
+                    if (id == objectID)
+                    {
+                        attachment = it->second;
+                        attachments.erase(it);
+                        break;
+                    }
+                }
+            }
+        }
+
+        if (attachment)
+        {
+            ARMARX_INFO << "Detached object " << objectID << " by provider '" << providerName << "' from robot node '"
+                        << attachment->frameName << "' of agent '" << attachment->agentName << "'.";
+            output.wasAttached = true;
+        }
+        else
+        {
+            ARMARX_INFO << "Tried to detach object " << objectID << " by provider '" << providerName << "' "
+                        << "from robot node, but it was not attached.";
+        }
+
+        return output;
+    }
+
+    objpose::DetachAllObjectsFromRobotNodesOutput ObjectPoseObserver::detachAllObjectsFromRobotNodes(const Ice::Current&)
+    {
+        objpose::DetachAllObjectsFromRobotNodesOutput output;
+        output.numDetached = 0;
+
+        {
+            std::scoped_lock lock(dataMutex);
+
+            // Clear attachment map.
+            size_t num = attachments.size();
+            attachments.clear();
+            output.numDetached = int(num);
+
+            // Remove from poses (if it was cached).
+            for (auto& [prov, poses] : objectPoses)
+            {
+                for (auto& pose : poses)
+                {
+                    pose.attachment = std::nullopt;
+                }
+            }
+        }
+        ARMARX_INFO << "Detached all objects (" << output.numDetached << ") from robot nodes.";
+
+        return output;
+    }
+
+
+
     void ObjectPoseObserver::handleProviderUpdate(const std::string& providerName)
     {
         // Initialized to 0 on first access.
@@ -400,12 +570,30 @@ namespace armarx
     {
         viz::Layer layer = arviz.layer(providerName);
 
-        for (const objpose::ObjectPose& objectPose : objectPoses.at(providerName))
+        bool synchronized = false;
+
+        for (objpose::ObjectPose& objectPose : objectPoses.at(providerName))
         {
             const armarx::ObjectID id = objectPose.objectID;
             std::string key = id.str();
 
-            Eigen::Matrix4f pose = visu.inGlobalFrame ? objectPose.objectPoseGlobal : objectPose.objectPoseRobot;
+            Eigen::Matrix4f pose = Eigen::Matrix4f::Identity();
+
+            if (isAttachedCached(objectPose))
+            {
+                if (!synchronized)
+                {
+                    RobotState::synchronizeLocalClone(robot);
+                    synchronized = true;
+                }
+                objpose::ObjectPose updated = applyAttachment(objectPose, robot);
+                pose = visu.inGlobalFrame ? updated.objectPoseGlobal : updated.objectPoseRobot;
+            }
+            else
+            {
+                pose = visu.inGlobalFrame ? objectPose.objectPoseGlobal : objectPose.objectPoseRobot;
+            }
+
             {
                 viz::Object object = viz::Object(key).pose(pose);
                 if (std::optional<ObjectInfo> objectInfo = objectFinder.findObject(id))
@@ -427,7 +615,7 @@ namespace armarx
             {
                 const simox::OrientedBoxf& oobb = *objectPose.localOOBB;
                 layer.add(viz::Box(key + " OOBB")
-                          .pose(pose * simox::math::pose(oobb.center(), oobb.rotation()))
+                          .pose(pose * oobb.transformation())
                           .size(oobb.dimensions()).color(simox::Color::lime(255, 64)));
             }
             if (visu.objectFrames)
@@ -470,5 +658,97 @@ namespace armarx
         return oobb;
     }
 
+    objpose::ObjectPose* ObjectPoseObserver::findObjectPose(const std::string& providerName, ObjectID& objectID)
+    {
+        objpose::ObjectPose* pose = nullptr;
+        if (!providerName.empty())
+        {
+            pose = findObjectPoseByID(objectPoses.at(providerName), objectID);
+        }
+        else
+        {
+            for (auto& [_, poses] : objectPoses)
+            {
+                pose = findObjectPoseByID(poses, objectID);
+                if (pose)
+                {
+                    break;
+                }
+            }
+        }
+        return pose;
+    }
+
+    objpose::ObjectPose* ObjectPoseObserver::findObjectPoseByID(objpose::ObjectPoseSeq& objectPoses, const ObjectID& id)
+    {
+        for (objpose::ObjectPose& pose : objectPoses)
+        {
+            if (pose.objectID == id)
+            {
+                return &pose;
+            }
+        }
+        return nullptr;
+    }
+
+    bool ObjectPoseObserver::isAttachedCached(objpose::ObjectPose& objectPose) const
+    {
+        if (!objectPose.attachment)
+        {
+            auto it = attachments.find(std::make_pair(objectPose.providerName, objectPose.objectID));
+            if (it != attachments.end())
+            {
+                objectPose.attachment = it->second;
+            }
+        }
+        return bool(objectPose.attachment);
+    }
+
+    objpose::ObjectPose ObjectPoseObserver::applyAttachment(
+        const objpose::ObjectPose& objectPose, VirtualRobot::RobotPtr agent)
+    {
+        ARMARX_CHECK(objectPose.attachment);
+
+        objpose::ObjectPose updated = objectPose;
+        const objpose::ObjectAttachmentInfo& attachment = *updated.attachment;
+
+        ARMARX_CHECK_EQUAL(attachment.agentName, agent->getName());
+
+        VirtualRobot::RobotNodePtr robotNode = agent->getRobotNode(attachment.frameName);
+        ARMARX_CHECK_NOT_NULL(robotNode);
+
+        // Overwrite object pose
+        updated.objectPoseRobot = robotNode->getPoseInRootFrame() * attachment.poseInFrame;
+        updated.objectPoseGlobal = agent->getGlobalPose() * updated.objectPoseRobot;
+
+        updated.robotPose = agent->getGlobalPose();
+        updated.robotConfig = agent->getConfig()->getRobotNodeJointValueMap();
+
+        return updated;
+    }
+
+
+    void ObjectPoseObserver::toIceWithAttachments(
+        objpose::ObjectPoseSeq& objectPoses, VirtualRobot::RobotPtr agent,
+        objpose::data::ObjectPoseSeq& result, bool& synchronized)
+    {
+        for (objpose::ObjectPose& pose : objectPoses)
+        {
+            if (isAttachedCached(pose))
+            {
+                if (!synchronized)  // Synchronize only once.
+                {
+                    RobotState::synchronizeLocalClone(agent);
+                    synchronized = true;
+                }
+                result.push_back(applyAttachment(pose, agent).toIce());
+            }
+            else
+            {
+                result.push_back(pose.toIce());
+            }
+        }
+    }
+
 }
 
diff --git a/source/RobotAPI/components/ObjectPoseObserver/ObjectPoseObserver.h b/source/RobotAPI/components/ObjectPoseObserver/ObjectPoseObserver.h
index 687db8ec03dfc84ab83a9e57da6e443d59e23dc5..ad7d2e3de5016685a58e2a65f1921b59cf16d2c7 100644
--- a/source/RobotAPI/components/ObjectPoseObserver/ObjectPoseObserver.h
+++ b/source/RobotAPI/components/ObjectPoseObserver/ObjectPoseObserver.h
@@ -88,17 +88,26 @@ namespace armarx
         // ObjectPoseObserverInterface interface
     public:
 
+        // READING
+
         objpose::data::ObjectPoseSeq getObjectPoses(ICE_CURRENT_ARG) override;
         objpose::data::ObjectPoseSeq getObjectPosesByProvider(const std::string& providerName, ICE_CURRENT_ARG) override;
 
-        objpose::observer::RequestObjectsOutput requestObjects(const objpose::observer::RequestObjectsInput& input, ICE_CURRENT_ARG) override;
-
         Ice::StringSeq getAvailableProviderNames(ICE_CURRENT_ARG) override;
         objpose::ProviderInfo getProviderInfo(const std::string& providerName, ICE_CURRENT_ARG) override;
         objpose::ProviderInfoMap getAvailableProvidersInfo(ICE_CURRENT_ARG) override;
         bool hasProvider(const std::string& providerName, ICE_CURRENT_ARG) override;
 
 
+        // MODIFICATION
+
+        objpose::observer::RequestObjectsOutput requestObjects(const objpose::observer::RequestObjectsInput& input, ICE_CURRENT_ARG) override;
+
+        objpose::AttachObjectToRobotNodeOutput attachObjectToRobotNode(const objpose::AttachObjectToRobotNodeInput& input, ICE_CURRENT_ARG) override;
+        objpose::DetachObjectFromRobotNodeOutput detachObjectFromRobotNode(const objpose::DetachObjectFromRobotNodeInput& input, ICE_CURRENT_ARG) override;
+        objpose::DetachAllObjectsFromRobotNodesOutput detachAllObjectsFromRobotNodes(ICE_CURRENT_ARG) override;
+
+
         //Ice::Int getUpdateCounterByProvider(const std::string& providerName, ICE_CURRENT_ARG) override;
         //StringIntDictionary getAllUpdateCounters(ICE_CURRENT_ARG) override;
 
@@ -129,18 +138,31 @@ namespace armarx
         std::optional<simox::OrientedBoxf> getObjectOOBB(const ObjectID& id);
 
 
+        objpose::ObjectPose* findObjectPose(const std::string& providerName, ObjectID& objectID);
+        static objpose::ObjectPose* findObjectPoseByID(objpose::ObjectPoseSeq& objectPoses, const ObjectID& id);
+
+
+        bool isAttachedCached(objpose::ObjectPose& objectPose) const;
+        static objpose::ObjectPose applyAttachment(const objpose::ObjectPose& objectPose, VirtualRobot::RobotPtr robot);
+
+        void toIceWithAttachments(objpose::ObjectPoseSeq& objectPoses, VirtualRobot::RobotPtr agent,
+                                  objpose::data::ObjectPoseSeq& result, bool& synchronized);
+
+
     private:
 
-        std::mutex robotMutex;
+        std::mutex dataMutex;
+
         VirtualRobot::RobotPtr robot;
 
-        std::mutex dataMutex;
 
         objpose::ProviderInfoMap providers;
 
         std::map<std::string, objpose::ObjectPoseSeq> objectPoses;
         std::map<std::string, int> updateCounters;
 
+        std::map<std::pair<std::string, ObjectID>, objpose::ObjectAttachmentInfo> attachments;
+
 
         ObjectFinder objectFinder;
         /// Caches results of attempts to retrieve the OOBB from ArmarXObjects.
diff --git a/source/RobotAPI/gui-plugins/ObjectPoseGui/ObjectPoseGuiWidget.ui b/source/RobotAPI/gui-plugins/ObjectPoseGui/ObjectPoseGuiWidget.ui
index 00677428b7b88ead1fbf7dbd1fb380ffad0a94d9..b6e436bb8e4805a0b0c4ac6d40615b404447333c 100644
--- a/source/RobotAPI/gui-plugins/ObjectPoseGui/ObjectPoseGuiWidget.ui
+++ b/source/RobotAPI/gui-plugins/ObjectPoseGui/ObjectPoseGuiWidget.ui
@@ -6,8 +6,8 @@
    <rect>
     <x>0</x>
     <y>0</y>
-    <width>621</width>
-    <height>350</height>
+    <width>736</width>
+    <height>391</height>
    </rect>
   </property>
   <property name="windowTitle">
@@ -48,7 +48,7 @@
    <item>
     <widget class="QTabWidget" name="tabWidget">
      <property name="currentIndex">
-      <number>1</number>
+      <number>0</number>
      </property>
      <widget class="QWidget" name="tabObjects">
       <attribute name="title">
@@ -90,23 +90,23 @@
            <string>Timestamp</string>
           </property>
          </column>
+         <column>
+          <property name="text">
+           <string>Attached</string>
+          </property>
+         </column>
          <item>
           <property name="text">
-           <string>Provider2</string>
+           <string>Provider1</string>
           </property>
           <item>
            <property name="text">
-            <string>Dataset1/Object2</string>
+            <string>Dataset1/Object1</string>
            </property>
            <property name="text">
-            <string>Provider2</string>
+            <string>Provider1</string>
            </property>
           </item>
-         </item>
-         <item>
-          <property name="text">
-           <string>Provider1</string>
-          </property>
           <item>
            <property name="text">
             <string>Dataset2/Object2/instance1</string>
@@ -115,12 +115,17 @@
             <string>Provider1</string>
            </property>
           </item>
+         </item>
+         <item>
+          <property name="text">
+           <string>Provider2</string>
+          </property>
           <item>
            <property name="text">
-            <string>Dataset1/Object1</string>
+            <string>Dataset1/Object2</string>
            </property>
            <property name="text">
-            <string>Provider1</string>
+            <string>Provider2</string>
            </property>
           </item>
          </item>
diff --git a/source/RobotAPI/gui-plugins/ObjectPoseGui/ObjectPoseGuiWidgetController.cpp b/source/RobotAPI/gui-plugins/ObjectPoseGui/ObjectPoseGuiWidgetController.cpp
index 828d0fb5b2d1ea32a288bbd6c2fd6ed00ba2bc47..4fc35cf54815b585866146d2ce0f8f30ef784285 100644
--- a/source/RobotAPI/gui-plugins/ObjectPoseGui/ObjectPoseGuiWidgetController.cpp
+++ b/source/RobotAPI/gui-plugins/ObjectPoseGui/ObjectPoseGuiWidgetController.cpp
@@ -156,10 +156,10 @@ namespace armarx
         }
 
         IceUtil::Time start = IceUtil::Time::now();
-        ARMARX_INFO << "Getting object poses...";
+        ARMARX_VERBOSE << "Getting object poses...";
         const objpose::data::ObjectPoseSeq objectPosesIce = objectPoseObserver->getObjectPoses();
-        ARMARX_INFO << "Got " << objectPosesIce.size() << " object poses. "
-                    << "(Took " << (IceUtil::Time::now() - start).toMilliSecondsDouble() << " ms.)";
+        ARMARX_VERBOSE << "Got " << objectPosesIce.size() << " object poses. "
+                       << "(Took " << (IceUtil::Time::now() - start).toMilliSecondsDouble() << " ms.)";
 
         const objpose::ObjectPoseSeq objectPoses = objpose::fromIce(objectPosesIce);
 
@@ -200,20 +200,31 @@ namespace armarx
                 item->setText(col++, QString::fromStdString(pose.providerName));
                 item->setText(col++, QString::fromStdString(objpose::ObjectTypeEnumNames.to_name(pose.objectType)));
 
-                std::stringstream ss;
-                if (pose.localOOBB)
                 {
-                    static const Eigen::IOFormat iof(5, 0, "", " x ", "", "", "", "");
-                    ss << pose.localOOBB->dimensions().format(iof);
+                    std::stringstream ss;
+                    if (pose.localOOBB)
+                    {
+                        static const Eigen::IOFormat iof(5, 0, "", " x ", "", "", "", "");
+                        ss << pose.localOOBB->dimensions().format(iof);
+                    }
+                    else
+                    {
+                        ss << "None";
+                    }
+                    item->setText(col++, QString::fromStdString(ss.str()));
                 }
-                else
-                {
-                    ss << "None";
-                }
-                item->setText(col++, QString::fromStdString(ss.str()));
                 item->setText(col++, QString::number(double(pose.confidence), 'g', 2));
                 item->setText(col++, QString::fromStdString(TimeUtil::toStringDateTime(pose.timestamp)));
 
+                {
+                    std::stringstream ss;
+                    if (pose.attachment)
+                    {
+                        ss << pose.attachment->agentName << "/" << pose.attachment->frameName;
+                    }
+                    item->setText(col++, QString::fromStdString(ss.str()));
+                }
+
                 return true;
             });
             builder.updateTree(item, objectPoses);
@@ -227,7 +238,7 @@ namespace armarx
         });
         builder.updateTree(tree, objectPosesByProvider);
 
-        ARMARX_INFO << "Gui update took " << (IceUtil::Time::now() - start).toMilliSecondsDouble() << " ms.";
+        ARMARX_VERBOSE << "Gui update took " << (IceUtil::Time::now() - start).toMilliSecondsDouble() << " ms.";
     }
 
 
@@ -241,8 +252,8 @@ namespace armarx
 
         IceUtil::Time start = IceUtil::Time::now();
         objpose::ProviderInfoMap availableProvidersInfo = objectPoseObserver->getAvailableProvidersInfo();
-        ARMARX_INFO << "Got infos of " << availableProvidersInfo.size() << " object pose providers. "
-                    << "(Took " << (IceUtil::Time::now() - start).toMilliSecondsDouble() << " ms.)";
+        ARMARX_VERBOSE << "Got infos of " << availableProvidersInfo.size() << " object pose providers. "
+                       << "(Took " << (IceUtil::Time::now() - start).toMilliSecondsDouble() << " ms.)";
 
 
         // Restructure data.
@@ -300,7 +311,7 @@ namespace armarx
         });
         builder.updateTree(tree, data);
 
-        ARMARX_INFO << "Gui update took " << (IceUtil::Time::now() - start).toMilliSecondsDouble() << " ms.";
+        ARMARX_VERBOSE << "Gui update took " << (IceUtil::Time::now() - start).toMilliSecondsDouble() << " ms.";
     }
 
     void ObjectPoseGuiWidgetController::requestSelectedObjects()
diff --git a/source/RobotAPI/interface/objectpose/ObjectPoseObserver.ice b/source/RobotAPI/interface/objectpose/ObjectPoseObserver.ice
index 0333c21c51055c711e29f06a34570d341987a226..9004c2afcc900c2f0a35d1134f1d100d551ff0ad 100644
--- a/source/RobotAPI/interface/objectpose/ObjectPoseObserver.ice
+++ b/source/RobotAPI/interface/objectpose/ObjectPoseObserver.ice
@@ -59,6 +59,44 @@ module armarx
             };
         };
 
+        struct AttachObjectToRobotNodeInput
+        {
+            string providerName;
+            armarx::data::ObjectID objectID;
+
+            /// The frame (robot node) to attach to.
+            string frameName;
+            /// The agent's name.
+            string agentName;
+
+            /**
+             * If given, specifies the object's pose in the frame.
+             * If not given, the current object's pose is used.
+             */
+            PoseBase poseInFrame;
+        };
+        struct AttachObjectToRobotNodeOutput
+        {
+            bool success;
+            data::ObjectAttachmentInfo attachment;
+        };
+
+        struct DetachObjectFromRobotNodeInput
+        {
+            string providerName;
+            armarx::data::ObjectID objectID;
+        };
+        struct DetachObjectFromRobotNodeOutput
+        {
+            /// Whether the object was attached before.
+            bool wasAttached;
+        };
+        struct DetachAllObjectsFromRobotNodesOutput
+        {
+            /// Number of objects that have been detached.
+            int numDetached;
+        };
+
         interface ObjectPoseObserverInterface extends ObserverInterface, ObjectPoseTopic
         {
             // Reading
@@ -74,6 +112,13 @@ module armarx
             // Modifying
             observer::RequestObjectsOutput requestObjects(observer::RequestObjectsInput input);
 
+            /// Attach an object to a robot node.
+            AttachObjectToRobotNodeOutput attachObjectToRobotNode(AttachObjectToRobotNodeInput input);
+            /// Detach an attached object from a robot node.
+            DetachObjectFromRobotNodeOutput detachObjectFromRobotNode(DetachObjectFromRobotNodeInput input);
+            /// Detach all objects from robot nodes.
+            DetachAllObjectsFromRobotNodesOutput detachAllObjectsFromRobotNodes();
+
         };
     };
 
diff --git a/source/RobotAPI/interface/objectpose/object_pose_types.ice b/source/RobotAPI/interface/objectpose/object_pose_types.ice
index d6b4d7a6eb93a681068f5d3583c9e80e5d0fe91b..66618485d73ea91819fca88d6951875fba892bcd 100644
--- a/source/RobotAPI/interface/objectpose/object_pose_types.ice
+++ b/source/RobotAPI/interface/objectpose/object_pose_types.ice
@@ -83,6 +83,8 @@ module armarx
             sequence<ProvidedObjectPose> ProvidedObjectPoseSeq;
 
 
+            class ObjectAttachmentInfo;
+
             /// An object pose as stored by the ObjectPoseObserver.
             struct ObjectPose
             {
@@ -102,6 +104,8 @@ module armarx
                 StringFloatDictionary robotConfig;
                 PoseBase robotPose;
 
+                ObjectAttachmentInfo attachment;
+
                 /// Confidence in [0, 1] (1 = full, 0 = none).
                 float confidence = 0;
                 /// Source timestamp.
@@ -111,6 +115,15 @@ module armarx
                 Box localOOBB;
             };
             sequence<ObjectPose> ObjectPoseSeq;
+
+
+            class ObjectAttachmentInfo
+            {
+                string frameName;
+                string agentName;
+
+                PoseBase poseInFrame;
+            };
         }
 
     };