diff --git a/lib/shared/dm-ops/dm-op-utils.js b/lib/shared/dm-ops/dm-op-utils.js
--- a/lib/shared/dm-ops/dm-op-utils.js
+++ b/lib/shared/dm-ops/dm-op-utils.js
@@ -107,7 +107,8 @@
   | InboundDMOperationSpecification;
 
 async function createMessagesToPeersFromDMOp(
-  operation: OutboundDMOperationSpecification,
+  operation: DMOperation,
+  recipients: OutboundDMOperationSpecificationRecipients,
   allPeerUserIDAndDeviceIDs: $ReadOnlyArray<{
     +userID: string,
     +deviceID: string,
@@ -120,25 +121,25 @@
   }
 
   let peerUserIDAndDeviceIDs = allPeerUserIDAndDeviceIDs;
-  if (operation.recipients.type === 'self_devices') {
+  if (recipients.type === 'self_devices') {
     peerUserIDAndDeviceIDs = allPeerUserIDAndDeviceIDs.filter(
       peer => peer.userID === currentUserInfo.id,
     );
-  } else if (operation.recipients.type === 'some_users') {
-    const userIDs = new Set(operation.recipients.userIDs);
+  } else if (recipients.type === 'some_users') {
+    const userIDs = new Set(recipients.userIDs);
     peerUserIDAndDeviceIDs = allPeerUserIDAndDeviceIDs.filter(peer =>
       userIDs.has(peer.userID),
     );
-  } else if (operation.recipients.type === 'all_thread_members') {
-    const members = threadInfos[operation.recipients.threadID]?.members ?? [];
+  } else if (recipients.type === 'all_thread_members') {
+    const members = threadInfos[recipients.threadID]?.members ?? [];
     const memberIDs = members.map(member => member.id);
 
     const userIDs = new Set(memberIDs);
     peerUserIDAndDeviceIDs = allPeerUserIDAndDeviceIDs.filter(peer =>
       userIDs.has(peer.userID),
     );
-  } else if (operation.recipients.type === 'some_devices') {
-    const deviceIDs = new Set(operation.recipients.deviceIDs);
+  } else if (recipients.type === 'some_devices') {
+    const deviceIDs = new Set(recipients.deviceIDs);
     peerUserIDAndDeviceIDs = allPeerUserIDAndDeviceIDs.filter(peer =>
       deviceIDs.has(peer.deviceID),
     );
@@ -148,7 +149,7 @@
   const targetPeers = peerUserIDAndDeviceIDs.filter(
     peer => peer.deviceID !== thisDeviceID,
   );
-  return generateMessagesToPeers(operation.op, targetPeers);
+  return generateMessagesToPeers(operation, targetPeers);
 }
 
 function getCreateThickRawThreadInfoInputFromThreadInfo(
diff --git a/lib/shared/dm-ops/process-dm-ops.js b/lib/shared/dm-ops/process-dm-ops.js
--- a/lib/shared/dm-ops/process-dm-ops.js
+++ b/lib/shared/dm-ops/process-dm-ops.js
@@ -5,14 +5,14 @@
 import * as React from 'react';
 import uuid from 'uuid';
 
+import type { ProcessDMOperationUtilities } from './dm-op-spec.js';
 import { dmOpSpecs } from './dm-op-specs.js';
-import type {
-  OutboundDMOperationSpecification,
-  DMOperationSpecification,
-} from './dm-op-utils.js';
 import {
+  type OutboundDMOperationSpecification,
+  type DMOperationSpecification,
   createMessagesToPeersFromDMOp,
   dmOperationSpecificationTypes,
+  type OutboundComposableDMOperationSpecification,
 } from './dm-op-utils.js';
 import { processNewUserIDsActionType } from '../../actions/user-actions.js';
 import { useLoggedInUserInfo } from '../../hooks/account-hooks.js';
@@ -40,21 +40,24 @@
 import { messageSpecs } from '../messages/message-specs.js';
 import { updateSpecs } from '../updates/update-specs.js';
 
-function useProcessDMOperation(): (
-  dmOperationSpecification: DMOperationSpecification,
-  dmOpID: ?string,
-) => Promise<void> {
+function useSendDMOperationUtils(): ProcessDMOperationUtilities {
   const fetchMessage = useGetLatestMessageEdit();
   const threadInfos = useSelector(state => state.threadStore.threadInfos);
-
-  const utilities = React.useMemo(
+  return React.useMemo(
     () => ({
       fetchMessage,
       threadInfos,
     }),
     [fetchMessage, threadInfos],
   );
+}
 
+function useProcessDMOperation(): (
+  dmOperationSpecification: DMOperationSpecification,
+  dmOpID: ?string,
+) => Promise<void> {
+  const threadInfos = useSelector(state => state.threadStore.threadInfos);
+  const utilities = useSendDMOperationUtils();
   const dispatchWithMetadata = useDispatchWithMetadata();
   const loggedInUserInfo = useLoggedInUserInfo();
   const viewerID = loggedInUserInfo?.id;
@@ -80,7 +83,8 @@
         dmOperationSpecification.type === dmOperationSpecificationTypes.OUTBOUND
       ) {
         outboundP2PMessages = await createMessagesToPeersFromDMOp(
-          dmOperationSpecification,
+          dmOp,
+          dmOperationSpecification.recipients,
           allPeerUserIDAndDeviceIDs,
           currentUserInfo,
           threadInfos,
@@ -374,8 +378,68 @@
   );
 }
 
+function useSendComposableDMOperation(): (
+  dmOperationSpecification: OutboundComposableDMOperationSpecification,
+) => Promise<$ReadOnlyArray<string>> {
+  const threadInfos = useSelector(state => state.threadStore.threadInfos);
+  const { getDMOpsSendingPromise } = usePeerToPeerCommunication();
+  const dispatchWithMetadata = useDispatchWithMetadata();
+  const allPeerUserIDAndDeviceIDs = useSelector(getAllPeerUserIDAndDeviceIDs);
+  const currentUserInfo = useSelector(state => state.currentUserInfo);
+  const utilities = useSendDMOperationUtils();
+
+  return React.useCallback(
+    async (
+      dmOperationSpecification: OutboundComposableDMOperationSpecification,
+    ): Promise<$ReadOnlyArray<string>> => {
+      const { promise, dmOpID } = getDMOpsSendingPromise();
+
+      const { op, composableMessageID, recipients } = dmOperationSpecification;
+
+      const outboundP2PMessages = await createMessagesToPeersFromDMOp(
+        op,
+        recipients,
+        allPeerUserIDAndDeviceIDs,
+        currentUserInfo,
+        threadInfos,
+      );
+
+      const notificationsCreationData = await dmOpSpecs[
+        op.type
+      ].notificationsCreationData?.(op, utilities);
+
+      dispatchWithMetadata(
+        {
+          type: processDMOpsActionType,
+          payload: {
+            rawMessageInfos: [],
+            updateInfos: [],
+            outboundP2PMessages,
+            composableMessageID,
+            notificationsCreationData,
+          },
+        },
+        {
+          dmOpID,
+        },
+      );
+
+      return promise;
+    },
+    [
+      allPeerUserIDAndDeviceIDs,
+      currentUserInfo,
+      dispatchWithMetadata,
+      getDMOpsSendingPromise,
+      threadInfos,
+      utilities,
+    ],
+  );
+}
+
 export {
   useProcessDMOperation,
   useProcessAndSendDMOperation,
   useRetrySendDMOperation,
+  useSendComposableDMOperation,
 };