diff --git a/src/fileFormats/ensight/file/ensightFile.C b/src/fileFormats/ensight/file/ensightFile.C
index dca789e4c15d0f4a59757cb466a1b0ffbd90fec7..6ee8f464baf4340391d4c300e13fe03274cafb91 100644
--- a/src/fileFormats/ensight/file/ensightFile.C
+++ b/src/fileFormats/ensight/file/ensightFile.C
@@ -6,7 +6,7 @@
      \\/     M anipulation  |
 -------------------------------------------------------------------------------
     Copyright (C) 2011-2015 OpenFOAM Foundation
-    Copyright (C) 2016-2023 OpenCFD Ltd.
+    Copyright (C) 2016-2024 OpenCFD Ltd.
 -------------------------------------------------------------------------------
 License
     This file is part of OpenFOAM.
@@ -221,7 +221,7 @@ Foam::Ostream& Foam::ensightFile::write
 }
 
 
-Foam::Ostream& Foam::ensightFile::write(const int32_t val)
+void Foam::ensightFile::writeInt(const int32_t val, const int fieldWidth)
 {
     if (format() == IOstreamOption::BINARY)
     {
@@ -233,24 +233,22 @@ Foam::Ostream& Foam::ensightFile::write(const int32_t val)
     }
     else
     {
-        stdStream().width(10);
+        stdStream().width(fieldWidth);
         stdStream() << val;
         syncState();
     }
-
-    return *this;
 }
 
 
-Foam::Ostream& Foam::ensightFile::write(const int64_t val)
+void Foam::ensightFile::writeInt(const int64_t val, const int fieldWidth)
 {
-    int32_t ivalue(narrowInt32(val));
+    int32_t work(narrowInt32(val));
 
-    return write(ivalue);
+    writeInt(work, fieldWidth);
 }
 
 
-Foam::Ostream& Foam::ensightFile::write(const float val)
+void Foam::ensightFile::writeFloat(const float val, const int fieldWidth)
 {
     if (format() == IOstreamOption::BINARY)
     {
@@ -262,40 +260,45 @@ Foam::Ostream& Foam::ensightFile::write(const float val)
     }
     else
     {
-        stdStream().width(12);
+        stdStream().width(fieldWidth);
         stdStream() << val;
         syncState();
     }
+}
 
-    return *this;
+
+void Foam::ensightFile::writeFloat(const double val, const int fieldWidth)
+{
+    float work(narrowFloat(val));
+
+    writeFloat(work, fieldWidth);
 }
 
 
-Foam::Ostream& Foam::ensightFile::write(const double val)
+Foam::Ostream& Foam::ensightFile::write(const int32_t val)
 {
-    float fvalue(narrowFloat(val));
+    writeInt(val, 10);
+    return *this;
+}
+
 
-    return write(fvalue);
+Foam::Ostream& Foam::ensightFile::write(const int64_t val)
+{
+    writeInt(val, 10);
+    return *this;
 }
 
 
-Foam::Ostream& Foam::ensightFile::write
-(
-    const label value,
-    const label fieldWidth
-)
+Foam::Ostream& Foam::ensightFile::write(const float val)
 {
-    if (format() == IOstreamOption::BINARY)
-    {
-        write(value);
-    }
-    else
-    {
-        stdStream().width(fieldWidth);
-        stdStream() << value;
-        syncState();
-    }
+    writeFloat(val, 12);
+    return *this;
+}
+
 
+Foam::Ostream& Foam::ensightFile::write(const double val)
+{
+    writeFloat(val, 12);
     return *this;
 }
 
@@ -376,7 +379,7 @@ void Foam::ensightFile::beginParticleCoordinates(const label nparticles)
 {
     writeString("particle coordinates");
     newline();
-    write(nparticles, 8); // unusual width
+    writeInt(nparticles, 8);  // Warning: unusual width
     newline();
 }
 
diff --git a/src/fileFormats/ensight/file/ensightFile.H b/src/fileFormats/ensight/file/ensightFile.H
index 2e16f0e95d291c80f0214a7276060d3adb0f7b9e..79ed2f0a7b5b1a4f113832287c247b1456437e9b 100644
--- a/src/fileFormats/ensight/file/ensightFile.H
+++ b/src/fileFormats/ensight/file/ensightFile.H
@@ -163,6 +163,18 @@ public:
         //- Write string as "%79s" or as binary (max 80 chars)
         void writeString(const std::string& str);
 
+        //- Write integer value with specified width or as binary
+        void writeInt(const int32_t val, const int fieldWidth);
+
+        //- Write (narrowed) integer value with specified width or as binary
+        void writeInt(const int64_t val, const int fieldWidth);
+
+        //- Write floating-point with specified width or as binary
+        void writeFloat(const float val, const int fieldWidth);
+
+        //- Write (narrowed) floating-point with specified width or as binary
+        void writeFloat(const double val, const int fieldWidth);
+
         //- Write undef value
         void writeUndef();
 
@@ -197,15 +209,12 @@ public:
         //- Write string, uses writeString()
         virtual Ostream& write(const std::string& str) override;
 
-        //- Write integer as "%10d" or as binary
+        //- Write integer value as "%10d" or as binary
         virtual Ostream& write(const int32_t val) override;
 
-        //- Write integer as "%10d" or as binary
+        //- Write integer value as "%10d" or as binary (narrowed to int32_t)
         virtual Ostream& write(const int64_t val) override;
 
-        //- Write integer with specified width or as binary
-        Ostream& write(const label value, const label fieldWidth);
-
         //- Write floating-point as "%12.5e" or as binary
         virtual Ostream& write(const float val) override;
 
diff --git a/src/fileFormats/ensight/mesh/ensightMesh.C b/src/fileFormats/ensight/mesh/ensightMesh.C
index 8681c14abbcb1ba9eaeba5c8c2b38ce46e09fb19..d57478fa2d82a44342354cc4d015f8d40c2ae96d 100644
--- a/src/fileFormats/ensight/mesh/ensightMesh.C
+++ b/src/fileFormats/ensight/mesh/ensightMesh.C
@@ -6,7 +6,7 @@
      \\/     M anipulation  |
 -------------------------------------------------------------------------------
     Copyright (C) 2011-2016 OpenFOAM Foundation
-    Copyright (C) 2016-2022 OpenCFD Ltd.
+    Copyright (C) 2016-2024 OpenCFD Ltd.
 -------------------------------------------------------------------------------
 License
     This file is part of OpenFOAM.
@@ -449,6 +449,19 @@ void Foam::ensightMesh::write
     {
         faceZoneParts_[id].write(os, mesh_, parallel);
     }
+
+    // No geometry parts written?
+    // - with lagrangian-only output the VTK EnsightReader still
+    //   needs a volume geometry, and ensight usually does too
+    if
+    (
+        cellZoneParts_.empty()
+     && boundaryParts_.empty()
+     && faceZoneParts_.empty()
+    )
+    {
+        ensightCells::writeBox(os, mesh_.bounds());
+    }
 }
 
 
diff --git a/src/fileFormats/ensight/mesh/ensightMesh.H b/src/fileFormats/ensight/mesh/ensightMesh.H
index fb61d716ebd2efce8df315a32ed0ab17138ebb03..a0cc1cbf54da6d55af6e542da23d0928afc15ddd 100644
--- a/src/fileFormats/ensight/mesh/ensightMesh.H
+++ b/src/fileFormats/ensight/mesh/ensightMesh.H
@@ -6,7 +6,7 @@
      \\/     M anipulation  |
 -------------------------------------------------------------------------------
     Copyright (C) 2011-2016 OpenFOAM Foundation
-    Copyright (C) 2016-2022 OpenCFD Ltd.
+    Copyright (C) 2016-2024 OpenCFD Ltd.
 -------------------------------------------------------------------------------
 License
     This file is part of OpenFOAM.
@@ -216,18 +216,22 @@ public:
 
     // Output
 
-        //- Write geometry to file. Normally in parallel
+        //- Write geometry to file (normally in parallel).
+        //  If all geometry is disabled, it will simply writes the mesh
+        //  bounding box (to ensure that the geometry file is non-empty)
         void write
         (
             ensightGeoFile& os,
-            bool parallel = Pstream::parRun()
+            bool parallel = UPstream::parRun()
         ) const;
 
-        //- Write geometry to file. Normally in parallel
+        //- Write geometry to file (normally in parallel).
+        //  If all geometry is disabled, it will simply writes the mesh
+        //  bounding box (to ensure that the geometry file is non-empty)
         inline void write
         (
             autoPtr<ensightGeoFile>& os,
-            bool parallel = Pstream::parRun()
+            bool parallel = UPstream::parRun()
         ) const;
 };
 
diff --git a/src/fileFormats/ensight/part/cells/ensightCells.H b/src/fileFormats/ensight/part/cells/ensightCells.H
index ac64cd0d81c6fa3f7aec5b420e3825d332df6a56..5b2197a20690ed3b698c738baa4e5ffa1be9a723 100644
--- a/src/fileFormats/ensight/part/cells/ensightCells.H
+++ b/src/fileFormats/ensight/part/cells/ensightCells.H
@@ -5,7 +5,7 @@
     \\  /    A nd           | www.openfoam.com
      \\/     M anipulation  |
 -------------------------------------------------------------------------------
-    Copyright (C) 2016-2022 OpenCFD Ltd.
+    Copyright (C) 2016-2024 OpenCFD Ltd.
 -------------------------------------------------------------------------------
 License
     This file is part of OpenFOAM.
@@ -46,6 +46,7 @@ namespace Foam
 
 // Forward Declarations
 class bitSet;
+class boundBox;
 class polyMesh;
 template<class T> class InfoProxy;
 
@@ -279,6 +280,16 @@ public:
             const polyMesh& mesh,
             bool parallel
         ) const;
+
+        //- Write bounding box geometry.
+        //- All parameters are only relevant on master
+        static void writeBox
+        (
+            ensightGeoFile& os,
+            const boundBox& bb,
+            const label partIndex = 0,
+            const word& partName = "geometry-box"
+        );
 };
 
 
diff --git a/src/fileFormats/ensight/part/cells/ensightCellsIO.C b/src/fileFormats/ensight/part/cells/ensightCellsIO.C
index f45f70a4176983ec5e246b833d2a6836331569d3..2223861df624a51b668cc99db7fec5ff3625b312 100644
--- a/src/fileFormats/ensight/part/cells/ensightCellsIO.C
+++ b/src/fileFormats/ensight/part/cells/ensightCellsIO.C
@@ -5,7 +5,7 @@
     \\  /    A nd           | www.openfoam.com
      \\/     M anipulation  |
 -------------------------------------------------------------------------------
-    Copyright (C) 2020 OpenCFD Ltd.
+    Copyright (C) 2020-2024 OpenCFD Ltd.
 -------------------------------------------------------------------------------
 License
     This file is part of OpenFOAM.
@@ -28,7 +28,9 @@ License
 #include "ensightCells.H"
 #include "ensightOutput.H"
 #include "InfoProxy.H"
+#include "boundBox.H"
 #include "polyMesh.H"
+#include "cellModel.H"
 #include "globalIndex.H"
 #include "globalMeshData.H"
 #include "manifoldCellsMeshObject.H"
@@ -330,6 +332,44 @@ void Foam::ensightCells::write
 }
 
 
+void Foam::ensightCells::writeBox
+(
+    ensightGeoFile& os,
+    const boundBox& bb,
+    const label partIndex,
+    const word& partName
+)
+{
+    pointField points;
+    cellShapeList shapes;
+
+    if (UPstream::master())
+    {
+        points = bb.hexCorners();
+        shapes.emplace_back(cellModel::HEX, identity(8));
+    }
+
+    ensightOutput::Detail::writeCoordinates
+    (
+        os,
+        partIndex,
+        partName,
+        8,          // nPoints (global)
+        points,
+        false       // serial only! (parallel=false)
+    );
+
+    if (UPstream::master())
+    {
+        os.writeKeyword(ensightCells::key(ensightCells::elemType::HEXA8));
+        os.write(shapes.size());  // one cell (global)
+        os.newline();
+
+        ensightOutput::writeCellShapes(os, shapes);
+    }
+}
+
+
 // * * * * * * * * * * * * * * * Ostream Operator  * * * * * * * * * * * * * //
 
 template<>
diff --git a/src/finiteArea/output/ensight/ensightFaMesh.H b/src/finiteArea/output/ensight/ensightFaMesh.H
index 394b4d5463eaff73c661d63fa0a4f17be24b5608..ce3deb7534a5307dc37a111154ac09f02110b91f 100644
--- a/src/finiteArea/output/ensight/ensightFaMesh.H
+++ b/src/finiteArea/output/ensight/ensightFaMesh.H
@@ -148,14 +148,14 @@ public:
         void write
         (
             ensightGeoFile& os,
-            bool parallel = Pstream::parRun()
+            bool parallel = UPstream::parRun()
         ) const;
 
         //- Write geometry to file. Normally in parallel
         inline void write
         (
             autoPtr<ensightGeoFile>& os,
-            bool parallel = Pstream::parRun()
+            bool parallel = UPstream::parRun()
         ) const;
 };
 
diff --git a/src/functionObjects/lagrangian/Make/files b/src/functionObjects/lagrangian/Make/files
index e349d4f5454fbc240faf2ceb27ef1a052c0ea176..51e5c3b1128b7d52fb022c2488b0628c5cc3740a 100644
--- a/src/functionObjects/lagrangian/Make/files
+++ b/src/functionObjects/lagrangian/Make/files
@@ -5,5 +5,6 @@ icoUncoupledKinematicCloud/icoUncoupledKinematicCloud.C
 dsmcFields/dsmcFields.C
 
 vtkCloud/vtkCloud.C
+ensightCloud/ensightCloudWriteObject.cxx
 
 LIB = $(FOAM_LIBBIN)/liblagrangianFunctionObjects
diff --git a/src/functionObjects/lagrangian/dataCloud/dataCloud.C b/src/functionObjects/lagrangian/dataCloud/dataCloud.C
index e34be5bc705d6697564e9131e2fe63d2a06c9831..8ff65489f56bccbc022631dff7b9be9f9dccf44d 100644
--- a/src/functionObjects/lagrangian/dataCloud/dataCloud.C
+++ b/src/functionObjects/lagrangian/dataCloud/dataCloud.C
@@ -5,7 +5,7 @@
     \\  /    A nd           | www.openfoam.com
      \\/     M anipulation  |
 -------------------------------------------------------------------------------
-    Copyright (C) 2018-2022 OpenCFD Ltd.
+    Copyright (C) 2018-2024 OpenCFD Ltd.
 -------------------------------------------------------------------------------
 License
     This file is part of OpenFOAM.
@@ -52,12 +52,16 @@ bool Foam::functionObjects::dataCloud::writeCloud
     const word& cloudName
 )
 {
-    const auto* objPtr = mesh_.findObject<cloud>(cloudName);
-    if (!objPtr)
+    applyFilter_ = false;
+
+    const auto* cloudPtr = mesh_.findObject<cloud>(cloudName);
+    if (!cloudPtr)
     {
         return false;
     }
 
+    const auto& currCloud = *cloudPtr;
+
     objectRegistry obrTmp
     (
         IOobject
@@ -71,7 +75,7 @@ bool Foam::functionObjects::dataCloud::writeCloud
         )
     );
 
-    objPtr->writeObjects(obrTmp);
+    currCloud.writeObjects(obrTmp);
 
     const auto* pointsPtr = cloud::findIOPosition(obrTmp);
 
@@ -86,7 +90,10 @@ bool Foam::functionObjects::dataCloud::writeCloud
 
 
     // Number of parcels (locally)
-    label nParcels = (applyFilter_ ? parcelAddr_.count() : pointsPtr->size());
+    const label nParcels
+    (
+        applyFilter_ ? parcelAddr_.count() : pointsPtr->size()
+    );
 
     // Total number of parcels on all processes
     const label nTotParcels = returnReduce(nParcels, sumOp<label>());
@@ -104,9 +111,9 @@ bool Foam::functionObjects::dataCloud::writeCloud
         return false;
     }
 
-    if (Pstream::master())
+    if (UPstream::master())
     {
-        mkDir(outputName.path());
+        Foam::mkDir(outputName.path());
     }
 
     return
@@ -163,12 +170,15 @@ bool Foam::functionObjects::dataCloud::read(const dictionary& dict)
 
     selectClouds_.clear();
     dict.readIfPresent("clouds", selectClouds_);
+    selectClouds_.uniq();
 
     if (selectClouds_.empty())
     {
-        selectClouds_.resize(1);
-        selectClouds_.first() =
-            dict.getOrDefault<word>("cloud", cloud::defaultName);
+        word cloudName;
+        if (dict.readIfPresent("cloud", cloudName))
+        {
+            selectClouds_.push_back(std::move(cloudName));
+        }
     }
 
     dict.readEntry("field", fieldName_);
@@ -209,7 +219,12 @@ bool Foam::functionObjects::dataCloud::execute()
 
 bool Foam::functionObjects::dataCloud::write()
 {
-    const wordList cloudNames(mesh_.sortedNames<cloud>(selectClouds_));
+    const wordList cloudNames
+    (
+        selectClouds_.empty()
+      ? mesh_.sortedNames<cloud>()
+      : mesh_.sortedNames<cloud>(selectClouds_)
+    );
 
     if (cloudNames.empty())
     {
diff --git a/src/functionObjects/lagrangian/dataCloud/dataCloud.H b/src/functionObjects/lagrangian/dataCloud/dataCloud.H
index 4943ccf1c7040c6637314b31429556a0db05c9cd..33b2791b6bb9a6a6d8861eb0abddb7cb72623bcc 100644
--- a/src/functionObjects/lagrangian/dataCloud/dataCloud.H
+++ b/src/functionObjects/lagrangian/dataCloud/dataCloud.H
@@ -5,7 +5,7 @@
     \\  /    A nd           | www.openfoam.com
      \\/     M anipulation  |
 -------------------------------------------------------------------------------
-    Copyright (C) 2018-2020 OpenCFD Ltd.
+    Copyright (C) 2018-2024 OpenCFD Ltd.
 -------------------------------------------------------------------------------
 License
     This file is part of OpenFOAM.
@@ -48,10 +48,10 @@ Description
     \heading Basic Usage
     \table
         Property     | Description                      | Required | Default
-        type         | Type name: dataCloud             | yes   |
+        type         | Type name: dataCloud             | yes |
         clouds       | List of clouds (name or regex)   | no  |
-        cloud        | Cloud name                       | no  | defaultCloud
-        field        | Name of the field                | yes   |
+        cloud        | Cloud name                       | no  |
+        field        | Name of the field                | yes |
         selection    | Parcel selection control         | no  | empty-dict
     \endtable
 
@@ -103,7 +103,7 @@ class dataCloud
     public fvMeshFunctionObject,
     public Foam::Detail::parcelSelection
 {
-    // Private data
+    // Private Data
 
         //- The printf format for zero-padding names
         string printf_;
@@ -177,7 +177,7 @@ class dataCloud
         bool writeField
         (
             const fileName& outputName,
-            const objectRegistry& obrTmp
+            const objectRegistry& obr
         ) const;
 
 
diff --git a/src/functionObjects/lagrangian/dataCloud/dataCloudTemplates.C b/src/functionObjects/lagrangian/dataCloud/dataCloudTemplates.C
index d2b6e16b1304d3d497c3eafa8438acfb18d8a669..67319c08efeb2b610ce61016d06be549316ef987 100644
--- a/src/functionObjects/lagrangian/dataCloud/dataCloudTemplates.C
+++ b/src/functionObjects/lagrangian/dataCloud/dataCloudTemplates.C
@@ -171,10 +171,10 @@ template<class Type>
 bool Foam::functionObjects::dataCloud::writeField
 (
     const fileName& outputName,
-    const objectRegistry& obrTmp
+    const objectRegistry& obr
 ) const
 {
-    const auto* pointsPtr = cloud::findIOPosition(obrTmp);
+    const auto* pointsPtr = cloud::findIOPosition(obr);
 
     if (!pointsPtr)
     {
@@ -185,8 +185,8 @@ bool Foam::functionObjects::dataCloud::writeField
     // Fields are not always on all processors (eg, multi-component parcels).
     // Thus need to resolve between all processors.
 
-    const List<Type>* fldPtr = obrTmp.findObject<IOField<Type>>(fieldName_);
-    const List<Type>& values = (fldPtr ? *fldPtr : List<Type>());
+    const List<Type>* fldPtr = obr.findObject<IOField<Type>>(fieldName_);
+    const List<Type>& values = (fldPtr ? *fldPtr : List<Type>::null());
 
     if (!returnReduceOr(fldPtr != nullptr))
     {
diff --git a/src/functionObjects/lagrangian/ensightCloud/ensightCloudWriteObject.H b/src/functionObjects/lagrangian/ensightCloud/ensightCloudWriteObject.H
new file mode 100644
index 0000000000000000000000000000000000000000..ea10e6bcd748ef0258ccea902eac08d0f705690f
--- /dev/null
+++ b/src/functionObjects/lagrangian/ensightCloud/ensightCloudWriteObject.H
@@ -0,0 +1,266 @@
+/*---------------------------------------------------------------------------*\
+  =========                 |
+  \\      /  F ield         | OpenFOAM: The Open Source CFD Toolbox
+   \\    /   O peration     |
+    \\  /    A nd           | www.openfoam.com
+     \\/     M anipulation  |
+-------------------------------------------------------------------------------
+    Copyright (C) 2024 OpenCFD Ltd.
+-------------------------------------------------------------------------------
+License
+    This file is part of OpenFOAM.
+
+    OpenFOAM is free software: you can redistribute it and/or modify it
+    under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    OpenFOAM is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+    for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with OpenFOAM.  If not, see <http://www.gnu.org/licenses/>.
+
+Class
+    Foam::functionObjects::ensightCloudWriteObject
+
+Group
+    grpLagrangianFunctionObjects
+
+Description
+    This functionObject writes cloud(s) in ensight format
+
+    Example of function object specification:
+    \verbatim
+    cloudWrite1
+    {
+        type            ensightCloud;
+        libs            (lagrangianFunctionObjects);
+        writeControl    writeTime;
+        writeInterval   1;
+        format          ascii;
+
+        timeFormat      scientific;
+        timePrecision   5;
+
+        cloud           myCloud;
+        fields          (T U rho);
+        width           4;  // file-padding
+
+        selection
+        {
+            stride
+            {
+                // every 10th parcelId
+                action  add;
+                source  stride;
+                stride  10;
+            }
+            Umin
+            {
+                // Remove slow parcels
+                action  subtract;
+                source  field;
+                field   U;
+                accept  (less 1e-3);
+            }
+            diam
+            {
+                // Only particular diameter ranges
+                action  subset;
+                source  field;
+                field   d;
+                accept  (greater 1e-3) and (less 1e-3);
+            }
+        }
+    }
+    \endverbatim
+
+    \heading Basic Usage
+    \table
+        Property     | Description                      | Required | Default
+        type         | Type name: ensightCloud          | yes |
+        clouds       | List of clouds (name or regex)   | no  |
+        cloud        | Cloud name                       | no  |
+        fields       | List of fields (name or regex)   | no  |
+        selection    | Parcel selection control         | no  | empty-dict
+    \endtable
+
+    \heading Output Options
+    \table
+        Property     | Description                      | Required | Default
+        format       | Format as ascii or binary        | no  | binary
+        width        | Mask width for \c data/XXXX      | no  | 8
+        directory    | The output directory name    | no | postProcessing/NAME
+        overwrite    | Remove existing directory        | no  | false
+        consecutive  | Consecutive output numbering     | no  | false
+        width        | Padding width for file name      | no  | 8
+        prune        | Suppress writing of empty clouds | no  | false
+        timeFormat   | Time format (ensight case)       | no  | scientific
+        timePrecision | Time precision (ensight case)   | no  | 5
+        writeControl | Output control                   | recommended | timeStep
+    \endtable
+
+    The output filename and fields are added to the functionObjectProperties
+    information. For the previous example specification:
+
+    \verbatim
+    cloudWrite1
+    {
+        myCloud
+        {
+            file    "<case>/simulation.case";
+            fields  (T U rho);
+        }
+    }
+    \endverbatim
+
+Note
+    The selection dictionary can be used for finer control of the parcel
+    output. It contains a set of (add,subtract,subset,clear,invert)
+    selection actions and sources.
+    Omitting the selection dictionary is the same as specifying the
+    conversion of all parcels (in the selected clouds).
+    More syntax details are to be found in the corresponding
+    Foam::Detail::parcelSelection class.
+
+See also
+    Foam::Detail::parcelSelection
+    Foam::functionObjects::vtkCloud
+    Foam::functionObjects::ensightWrite
+    Foam::functionObjects::fvMeshFunctionObject
+    Foam::functionObjects::timeControl
+
+SourceFiles
+    ensightCloudWriteObject.cxx
+    ensightCloudWriteObjectImpl.cxx
+
+\*---------------------------------------------------------------------------*/
+
+#ifndef functionObjects_ensightCloudWriteObject_H
+#define functionObjects_ensightCloudWriteObject_H
+
+#include "fvMeshFunctionObject.H"
+#include "ensightCase.H"
+#include "globalIndex.H"
+#include "parcelSelectionDetail.H"
+#include "wordRes.H"
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+namespace Foam
+{
+namespace functionObjects
+{
+
+/*---------------------------------------------------------------------------*\
+                   Class ensightCloudWriteObject Declaration
+\*---------------------------------------------------------------------------*/
+
+class ensightCloudWriteObject
+:
+    public fvMeshFunctionObject,
+    public Foam::Detail::parcelSelection
+{
+    // Private Data
+
+        //- Ensight output options
+        ensightCase::options caseOpts_;
+
+        //- Output directory
+        fileName outputDir_;
+
+        //- Consecutive output numbering
+        bool consecutive_;
+
+        //- Suppress writing of empty clouds
+        bool pruneEmpty_;
+
+        //- Apply output filter (for the current cloud)
+        bool applyFilter_;
+
+        //- Sizing of selected parcels (including any filtering)
+        globalIndex procAddr_;
+
+        //- Requested names of clouds to process
+        wordRes selectClouds_;
+
+        //- Subset of cloud fields to process
+        wordRes selectFields_;
+
+        //- Ensight case handler
+        autoPtr<ensightCase> ensCase_;
+
+
+    // Private Member Functions
+
+        //- Ensight case handler
+        ensightCase& ensCase() { return *ensCase_; }
+
+        //- Write a cloud to disk (creates parent directory),
+        //- and record on the cloud OutputProperties.
+        //  \param file is the output file name, with extension.
+        bool writeCloud(const word& cloudName);
+
+        //- Write fields of IOField<Type>
+        template<class Type>
+        wordList writeFields
+        (
+            const word& cloudName,
+            const objectRegistry& obrTmp
+        );
+
+
+        //- No copy construct
+        ensightCloudWriteObject(const ensightCloudWriteObject&) = delete;
+
+        //- No copy assignment
+        void operator=(const ensightCloudWriteObject&) = delete;
+
+
+public:
+
+    //- Runtime type information
+    TypeName("ensightCloud");
+
+
+    // Constructors
+
+        //- Construct from Time and dictionary
+        ensightCloudWriteObject
+        (
+            const word& name,
+            const Time& runTime,
+            const dictionary& dict
+        );
+
+
+    //- Destructor
+    virtual ~ensightCloudWriteObject() = default;
+
+
+    // Member Functions
+
+        //- Read the ensightCloud specification
+        virtual bool read(const dictionary& dict);
+
+        //- Execute, currently does nothing
+        virtual bool execute();
+
+        //- Write fields
+        virtual bool write();
+};
+
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+} // End namespace functionObjects
+} // End namespace Foam
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+#endif
+
+// ************************************************************************* //
diff --git a/src/functionObjects/lagrangian/ensightCloud/ensightCloudWriteObject.cxx b/src/functionObjects/lagrangian/ensightCloud/ensightCloudWriteObject.cxx
new file mode 100644
index 0000000000000000000000000000000000000000..01b6cdb083dd0f66fa80817d51961c3e819ff54a
--- /dev/null
+++ b/src/functionObjects/lagrangian/ensightCloud/ensightCloudWriteObject.cxx
@@ -0,0 +1,425 @@
+/*---------------------------------------------------------------------------*\
+  =========                 |
+  \\      /  F ield         | OpenFOAM: The Open Source CFD Toolbox
+   \\    /   O peration     |
+    \\  /    A nd           | www.openfoam.com
+     \\/     M anipulation  |
+-------------------------------------------------------------------------------
+    Copyright (C) 2024 OpenCFD Ltd.
+-------------------------------------------------------------------------------
+License
+    This file is part of OpenFOAM.
+
+    OpenFOAM is free software: you can redistribute it and/or modify it
+    under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    OpenFOAM is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+    for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with OpenFOAM.  If not, see <http://www.gnu.org/licenses/>.
+
+\*---------------------------------------------------------------------------*/
+
+#include "ensightCloudWriteObject.H"
+#include "ensightCells.H"
+#include "Cloud.H"
+#include "dictionary.H"
+#include "fvMesh.H"
+#include "ensightOutputCloud.H"
+#include "addToRunTimeSelectionTable.H"
+#include "pointList.H"
+#include "stringOps.H"
+
+// * * * * * * * * * * * * * * Static Data Members * * * * * * * * * * * * * //
+
+namespace Foam
+{
+namespace functionObjects
+{
+    defineTypeNameAndDebug(ensightCloudWriteObject, 0);
+
+    addToRunTimeSelectionTable
+    (
+        functionObject,
+        ensightCloudWriteObject,
+        dictionary
+    );
+}
+}
+
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+// Implementation
+#include "ensightCloudWriteObjectImpl.cxx"
+
+
+// * * * * * * * * * * * * * Private Member Functions  * * * * * * * * * * * //
+
+bool Foam::functionObjects::ensightCloudWriteObject::writeCloud
+(
+    const word& cloudName
+)
+{
+    applyFilter_ = false;
+    procAddr_.clear();
+
+    const auto* cloudPtr = mesh_.cfindObject<cloud>(cloudName);
+    if (!cloudPtr)
+    {
+        return false;
+    }
+
+    const auto& currCloud = *cloudPtr;
+
+    objectRegistry obrTmp
+    (
+        IOobject
+        (
+            "ensight::ensightCloud::" + cloudName,
+            mesh_.time().constant(),
+            mesh_,
+            IOobject::NO_READ,
+            IOobject::NO_WRITE,
+            IOobject::NO_REGISTER
+        )
+    );
+
+    currCloud.writeObjects(obrTmp);
+
+    const auto* pointsPtr = cloud::findIOPosition(obrTmp);
+
+    if (!pointsPtr)
+    {
+        // This should be impossible
+        return false;
+    }
+
+    applyFilter_ = calculateFilter(obrTmp, log);
+    Pstream::reduceOr(applyFilter_);
+
+    // Number of parcels (locally)
+    const label nParcels =
+    (
+        applyFilter_ ? parcelAddr_.count() : pointsPtr->size()
+    );
+
+    // Gather sizes (offsets irrelevant)
+    procAddr_.reset(globalIndex::gatherOnly{}, nParcels);
+
+    bool noCloud(!procAddr_.totalSize());
+    Pstream::broadcast(noCloud);
+
+    if (applyFilter_)
+    {
+        // Report filtered/unfiltered count
+        Log << "After filtering using "
+            << procAddr_.totalSize() << '/'
+            << (returnReduce(pointsPtr->size(), sumOp<label>()))
+            << " parcels" << nl;
+    }
+
+    if (pruneEmpty_ && noCloud)
+    {
+        return false;
+    }
+
+
+    // Copy positions (for simplicity and for filtering).
+    // Store as floatVector, since that is what Ensight will write anyhow
+
+    DynamicList<floatVector> positions;
+    positions.reserve(UPstream::master() ? procAddr_.maxSize() : nParcels);
+
+    {
+        const auto& points = *pointsPtr;
+
+        positions.resize_nocopy(nParcels);
+
+        auto iter = positions.begin();
+
+        if (applyFilter_)
+        {
+            if (std::is_same<float, vector::cmptType>::value)
+            {
+                for (const label idx : parcelAddr_)
+                {
+                    *iter = points[idx];
+                    ++iter;
+                }
+            }
+            else
+            {
+                for (const label idx : parcelAddr_)
+                {
+                    const auto& pos = points[idx];
+
+                    (*iter).x() = narrowFloat(pos.x());
+                    (*iter).y() = narrowFloat(pos.y());
+                    (*iter).z() = narrowFloat(pos.z());
+                    ++iter;
+                }
+            }
+        }
+        else
+        {
+            if (std::is_same<float, vector::cmptType>::value)
+            {
+                for (const auto& pos : points)
+                {
+                    *iter = pos;
+                    ++iter;
+                }
+            }
+            else
+            {
+                for (const auto& pos : points)
+                {
+                    (*iter).x() = narrowFloat(pos.x());
+                    (*iter).y() = narrowFloat(pos.y());
+                    (*iter).z() = narrowFloat(pos.z());
+                    ++iter;
+                }
+            }
+        }
+    }
+
+
+    // Write positions
+    {
+        autoPtr<ensightFile> os = ensCase().newCloud(cloudName);
+
+        ensightOutput::writeCloudPositions
+        (
+            os.ref(),
+            positions,
+            procAddr_
+        );
+    }
+
+    // Prevent any possible conversion of positions as a field
+    obrTmp.filterKeys
+    (
+        [](const word& k)
+        {
+            return k.starts_with("position") || k.starts_with("coordinate");
+        },
+        true  // prune
+    );
+
+
+    // Write fields
+
+    DynamicList<word> written(obrTmp.size() + currCloud.objectRegistry::size());
+
+    written.push_back
+    (
+        writeFields<label>(cloudName, obrTmp)
+    );
+    written.push_back
+    (
+        writeFields<scalar>(cloudName, obrTmp)
+    );
+    written.push_back
+    (
+        writeFields<vector>(cloudName, obrTmp)
+    );
+
+    // Any cloudFunctions results
+    written.push_back
+    (
+        writeFields<scalar>(cloudName, currCloud)
+    );
+
+    // Record information into the state (all processors)
+    //
+    // foName
+    // {
+    //     cloudName
+    //     {
+    //         file   "<case>/postProcessing/name/casename.case";
+    //         fields (U T rho);
+    //     }
+    // }
+
+    const fileName& file = ensCase().path();
+
+    // Case-local file name with "<case>" to make relocatable
+    dictionary propsDict;
+    propsDict.add
+    (
+        "file",
+        time_.relativePath(file, true)
+    );
+    propsDict.add("fields", written);
+
+    setObjectProperty(name(), cloudName, propsDict);
+
+    return true;
+}
+
+
+// * * * * * * * * * * * * * * * * Constructors  * * * * * * * * * * * * * * //
+
+Foam::functionObjects::ensightCloudWriteObject::ensightCloudWriteObject
+(
+    const word& name,
+    const Time& runTime,
+    const dictionary& dict
+)
+:
+    fvMeshFunctionObject(name, runTime, dict),
+    caseOpts_("format", dict, IOstreamOption::BINARY),
+    outputDir_(),
+    consecutive_(false),
+    pruneEmpty_(false),
+    applyFilter_(false),
+    procAddr_()
+{
+    // May still want this?
+    // if (postProcess)
+    // {
+    //     // Disable for post-process mode.
+    //     // Emit as FatalError for the try/catch in the caller.
+    //     FatalError
+    //         << type() << " disabled in post-process mode"
+    //         << exit(FatalError);
+    // }
+
+    read(dict);
+}
+
+
+// * * * * * * * * * * * * * * * Member Functions  * * * * * * * * * * * * * //
+
+bool Foam::functionObjects::ensightCloudWriteObject::read
+(
+    const dictionary& dict
+)
+{
+    fvMeshFunctionObject::read(dict);
+
+    // Case/writer options
+    consecutive_ = dict.getOrDefault("consecutive", false);
+
+    caseOpts_.width(dict.getOrDefault<label>("width", 8));
+    caseOpts_.overwrite(dict.getOrDefault("overwrite", false));
+
+    caseOpts_.timeFormat("timeFormat", dict);
+    caseOpts_.timePrecision("timePrecision", dict);
+
+
+    pruneEmpty_ = dict.getOrDefault("prune", false);
+
+    selectClouds_.clear();
+    dict.readIfPresent("clouds", selectClouds_);
+    selectClouds_.uniq();
+    if (selectClouds_.empty())
+    {
+        word cloudName;
+        if (dict.readIfPresent("cloud", cloudName))
+        {
+            selectClouds_.push_back(std::move(cloudName));
+        }
+    }
+
+    selectFields_.clear();
+    dict.readIfPresent("fields", selectFields_);
+    selectFields_.uniq();
+
+    // Actions to define selection
+    parcelSelect_ = dict.subOrEmptyDict("selection");
+
+
+    // Output directory
+
+    outputDir_.clear();
+    dict.readIfPresent("directory", outputDir_);
+
+    if (outputDir_.size())
+    {
+        // User-defined output directory
+        outputDir_.expand();
+        if (!outputDir_.isAbsolute())
+        {
+            outputDir_ = time_.globalPath()/outputDir_;
+        }
+    }
+    else
+    {
+        // Standard postProcessing/ naming
+        outputDir_ = time_.globalPath()/functionObject::outputPrefix/name();
+    }
+    outputDir_.clean();  // Remove unneeded ".."
+
+    return true;
+}
+
+
+bool Foam::functionObjects::ensightCloudWriteObject::execute()
+{
+    return true;
+}
+
+
+bool Foam::functionObjects::ensightCloudWriteObject::write()
+{
+    const wordList cloudNames
+    (
+        selectClouds_.empty()
+      ? mesh_.sortedNames<cloud>()
+      : mesh_.sortedNames<cloud>(selectClouds_)
+    );
+
+    if (cloudNames.empty())
+    {
+        return true;  // skip - nothing available
+    }
+
+    if (!ensCase_)
+    {
+        ensCase_.reset
+        (
+            new ensightCase(outputDir_, time_.globalCaseName(), caseOpts_)
+        );
+
+        // Generate a (non-moving) dummy geometry
+        // - ParaView ensight-reader needs this, and usually ensight does too
+        autoPtr<ensightGeoFile> os = ensCase().newGeometry(false);
+        ensightCells::writeBox(os.ref(), mesh_.bounds());
+    }
+
+    if (consecutive_)
+    {
+        ensCase().nextTime(time_.value());
+    }
+    else
+    {
+        ensCase().setTime(time_.value(), time_.timeIndex());
+    }
+
+    Log << type() << ' ' << name() << " write" << nl;
+
+    // Each cloud separately
+    for (const word& cloudName : cloudNames)
+    {
+        // writeCloud() includes mkDir (on master)
+
+        if (writeCloud(cloudName))
+        {
+            Log << "    cloud  : " << endl;
+        }
+    }
+
+    ensCase().write();  // Flush case information
+
+    return true;
+}
+
+
+// ************************************************************************* //
diff --git a/src/functionObjects/lagrangian/ensightCloud/ensightCloudWriteObjectImpl.cxx b/src/functionObjects/lagrangian/ensightCloud/ensightCloudWriteObjectImpl.cxx
new file mode 100644
index 0000000000000000000000000000000000000000..01fcf08b14891f9d8347309bd2db1ebf6663e88f
--- /dev/null
+++ b/src/functionObjects/lagrangian/ensightCloud/ensightCloudWriteObjectImpl.cxx
@@ -0,0 +1,106 @@
+/*---------------------------------------------------------------------------*\
+  =========                 |
+  \\      /  F ield         | OpenFOAM: The Open Source CFD Toolbox
+   \\    /   O peration     |
+    \\  /    A nd           | www.openfoam.com
+     \\/     M anipulation  |
+-------------------------------------------------------------------------------
+    Copyright (C) 2024 OpenCFD Ltd.
+-------------------------------------------------------------------------------
+License
+    This file is part of OpenFOAM.
+
+    OpenFOAM is free software: you can redistribute it and/or modify it
+    under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    OpenFOAM is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+    for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with OpenFOAM.  If not, see <http://www.gnu.org/licenses/>.
+
+\*---------------------------------------------------------------------------*/
+
+#include "IOField.H"
+#include "ensightOutputCloud.H"
+
+// * * * * * * * * * * * * * Private Member Functions  * * * * * * * * * * * //
+
+template<class Type>
+Foam::wordList Foam::functionObjects::ensightCloudWriteObject::writeFields
+(
+    const word& cloudName,
+    const objectRegistry& obrTmp
+)
+{
+    static_assert
+    (
+        (
+            std::is_same<label, typename pTraits<Type>::cmptType>::value
+         || std::is_floating_point<typename pTraits<Type>::cmptType>::value
+        ),
+        "Label and Floating-point vector space only"
+    );
+
+    // Other integral types (eg, bool etc) would need cast/convert to label.
+    // Similarly for labelVector etc.
+
+
+    // Fields are not always on all processors (eg, multi-component parcels).
+    // Thus need to resolve names between all processors.
+
+    wordList fieldNames =
+    (
+        selectFields_.size()
+      ? obrTmp.names<IOField<Type>>(selectFields_)
+      : obrTmp.names<IOField<Type>>()
+    );
+
+    Pstream::combineReduce(fieldNames, ListOps::uniqueEqOp<word>());
+    Foam::sort(fieldNames);  // Consistent order
+
+    DynamicList<Type> scratch;
+
+    for (const word& fieldName : fieldNames)
+    {
+        const List<Type>* fldPtr = obrTmp.findObject<IOField<Type>>(fieldName);
+        const List<Type>& values = (fldPtr ? *fldPtr : List<Type>::null());
+
+        autoPtr<ensightFile> os =
+            ensCase().newCloudData<Type>(cloudName, fieldName);
+
+        if (applyFilter_)
+        {
+            scratch.resize_nocopy(parcelAddr_.count());
+
+            auto iter = scratch.begin();
+
+            for (const label idx : parcelAddr_)
+            {
+                *iter = values[idx];
+                ++iter;
+            }
+
+            // TBD:
+            // recalculate globalIndex instead of relying on procAddr_ ?
+
+            ensightOutput::writeCloudField(os.ref(), scratch, procAddr_);
+        }
+        else
+        {
+            // TBD:
+            // recalculate globalIndex instead of relying on procAddr_ ?
+
+            ensightOutput::writeCloudField(os.ref(), values, procAddr_);
+        }
+    }
+
+    return fieldNames;
+}
+
+
+// ************************************************************************* //
diff --git a/src/functionObjects/lagrangian/vtkCloud/vtkCloud.C b/src/functionObjects/lagrangian/vtkCloud/vtkCloud.C
index 08557fd4014a2dc2a7ff875280ad46d4e72f0cfa..223fa2feab4f4d391e6f907b997bf1606c2dd91e 100644
--- a/src/functionObjects/lagrangian/vtkCloud/vtkCloud.C
+++ b/src/functionObjects/lagrangian/vtkCloud/vtkCloud.C
@@ -5,7 +5,7 @@
     \\  /    A nd           | www.openfoam.com
      \\/     M anipulation  |
 -------------------------------------------------------------------------------
-    Copyright (C) 2018-2022 OpenCFD Ltd.
+    Copyright (C) 2018-2024 OpenCFD Ltd.
 -------------------------------------------------------------------------------
 License
     This file is part of OpenFOAM.
@@ -101,12 +101,16 @@ bool Foam::functionObjects::vtkCloud::writeCloud
     const word& cloudName
 )
 {
-    const auto* objPtr = mesh_.findObject<cloud>(cloudName);
-    if (!objPtr)
+    applyFilter_ = false;
+
+    const auto* cloudPtr = mesh_.cfindObject<cloud>(cloudName);
+    if (!cloudPtr)
     {
         return false;
     }
 
+    const auto& currCloud = *cloudPtr;
+
     objectRegistry obrTmp
     (
         IOobject
@@ -120,7 +124,7 @@ bool Foam::functionObjects::vtkCloud::writeCloud
         )
     );
 
-    objPtr->writeObjects(obrTmp);
+    currCloud.writeObjects(obrTmp);
 
     const auto* pointsPtr = cloud::findIOPosition(obrTmp);
 
@@ -135,7 +139,10 @@ bool Foam::functionObjects::vtkCloud::writeCloud
 
 
     // Number of parcels (locally)
-    label nParcels = (applyFilter_ ? parcelAddr_.count() : pointsPtr->size());
+    const label nParcels
+    (
+        applyFilter_ ? parcelAddr_.count() : pointsPtr->size()
+    );
 
     // Total number of parcels on all processes
     const label nTotParcels = returnReduce(nParcels, sumOp<label>());
@@ -163,9 +170,9 @@ bool Foam::functionObjects::vtkCloud::writeCloud
             << exit(FatalError);
     }
 
-    if (Pstream::master())
+    if (UPstream::master())
     {
-        mkDir(file.path());
+        Foam::mkDir(file.path());
         os.open(file);
 
         format = writeOpts_.newFormatter(os);
@@ -238,7 +245,7 @@ bool Foam::functionObjects::vtkCloud::writeCloud
     }
 
 
-    if (Pstream::master())
+    if (UPstream::master())
     {
         format().flush();
         format().endDataArray();
@@ -270,7 +277,7 @@ bool Foam::functionObjects::vtkCloud::writeCloud
 
     // Write fields
 
-    if (Pstream::master())
+    if (UPstream::master())
     {
         if (useVerts_)
         {
@@ -282,13 +289,28 @@ bool Foam::functionObjects::vtkCloud::writeCloud
         }
     }
 
-    DynamicList<word> written(obrTmp.size());
+    DynamicList<word> written(obrTmp.size() + currCloud.objectRegistry::size());
 
-    written.append(writeFields<label>(format, obrTmp, nTotParcels));
-    written.append(writeFields<scalar>(format, obrTmp, nTotParcels));
-    written.append(writeFields<vector>(format, obrTmp, nTotParcels));
+    written.push_back
+    (
+        writeFields<label>(format, obrTmp, nTotParcels)
+    );
+    written.push_back
+    (
+        writeFields<scalar>(format, obrTmp, nTotParcels)
+    );
+    written.push_back
+    (
+        writeFields<vector>(format, obrTmp, nTotParcels)
+    );
+
+    // Any cloudFunctions results
+    written.push_back
+    (
+        writeFields<scalar>(format, currCloud, nTotParcels)
+    );
 
-    if (Pstream::master())
+    if (UPstream::master())
     {
         if (useVerts_)
         {
@@ -415,12 +437,15 @@ bool Foam::functionObjects::vtkCloud::read(const dictionary& dict)
 
     selectClouds_.clear();
     dict.readIfPresent("clouds", selectClouds_);
+    selectClouds_.uniq();
 
     if (selectClouds_.empty())
     {
-        selectClouds_.resize(1);
-        selectClouds_.first() =
-            dict.getOrDefault<word>("cloud", cloud::defaultName);
+        word cloudName;
+        if (dict.readIfPresent("cloud", cloudName))
+        {
+            selectClouds_.push_back(std::move(cloudName));
+        }
     }
 
     selectFields_.clear();
@@ -463,7 +488,12 @@ bool Foam::functionObjects::vtkCloud::execute()
 
 bool Foam::functionObjects::vtkCloud::write()
 {
-    const wordList cloudNames(mesh_.sortedNames<cloud>(selectClouds_));
+    const wordList cloudNames
+    (
+        selectClouds_.empty()
+      ? mesh_.sortedNames<cloud>()
+      : mesh_.sortedNames<cloud>(selectClouds_)
+    );
 
     if (cloudNames.empty())
     {
@@ -498,7 +528,7 @@ bool Foam::functionObjects::vtkCloud::write()
             Log << "    cloud  : "
                 << time_.relativePath(outputName) << endl;
 
-            if (Pstream::master())
+            if (UPstream::master())
             {
                 // Add to file-series and emit as JSON
                 fileName seriesName(vtk::seriesWriter::base(outputName));
diff --git a/src/functionObjects/lagrangian/vtkCloud/vtkCloud.H b/src/functionObjects/lagrangian/vtkCloud/vtkCloud.H
index 9511de0db04920d6f56f9baf26240ec60b9c7191..648414eb2c388a3e005d9641e0b53d1111af3623 100644
--- a/src/functionObjects/lagrangian/vtkCloud/vtkCloud.H
+++ b/src/functionObjects/lagrangian/vtkCloud/vtkCloud.H
@@ -5,7 +5,7 @@
     \\  /    A nd           | www.openfoam.com
      \\/     M anipulation  |
 -------------------------------------------------------------------------------
-    Copyright (C) 2018-2020 OpenCFD Ltd.
+    Copyright (C) 2018-2024 OpenCFD Ltd.
 -------------------------------------------------------------------------------
 License
     This file is part of OpenFOAM.
@@ -81,7 +81,7 @@ Description
         Property     | Description                      | Required | Default
         type         | Type name: vtkCloud              | yes |
         clouds       | List of clouds (name or regex)   | no  |
-        cloud        | Cloud name                       | no  | defaultCloud
+        cloud        | Cloud name                       | no  |
         fields       | List of fields (name or regex)   | no  |
         selection    | Parcel selection control         | no  | empty-dict
     \endtable
@@ -160,7 +160,7 @@ class vtkCloud
     public fvMeshFunctionObject,
     public Foam::Detail::parcelSelection
 {
-    // Private data
+    // Private Data
 
         //- Writer options
         vtk::outputOptions writeOpts_;
@@ -209,7 +209,7 @@ class vtkCloud
         wordList writeFields
         (
             autoPtr<vtk::formatter>& format,
-            const objectRegistry& obrTmp,
+            const objectRegistry& obr,
             const label nTotParcels
         ) const;
 
diff --git a/src/functionObjects/lagrangian/vtkCloud/vtkCloudTemplates.C b/src/functionObjects/lagrangian/vtkCloud/vtkCloudTemplates.C
index c28bc1fe4c7a062b0afb21e07bcdf3c8f33ffd8b..f61c1de41e6679de0c8c88179ba00f9280bc353c 100644
--- a/src/functionObjects/lagrangian/vtkCloud/vtkCloudTemplates.C
+++ b/src/functionObjects/lagrangian/vtkCloud/vtkCloudTemplates.C
@@ -5,7 +5,7 @@
     \\  /    A nd           | www.openfoam.com
      \\/     M anipulation  |
 -------------------------------------------------------------------------------
-    Copyright (C) 2018-2022 OpenCFD Ltd.
+    Copyright (C) 2018-2024 OpenCFD Ltd.
 -------------------------------------------------------------------------------
 License
     This file is part of OpenFOAM.
@@ -33,7 +33,7 @@ template<class Type>
 Foam::wordList Foam::functionObjects::vtkCloud::writeFields
 (
     autoPtr<vtk::formatter>& format,
-    const objectRegistry& obrTmp,
+    const objectRegistry& obr,
     const label nTotParcels
 ) const
 {
@@ -55,16 +55,22 @@ Foam::wordList Foam::functionObjects::vtkCloud::writeFields
     // Fields are not always on all processors (eg, multi-component parcels).
     // Thus need to resolve names between all processors.
 
-    wordList fieldNames(obrTmp.names<IOField<Type>>());
+    wordList fieldNames =
+    (
+        selectFields_.size()
+      ? obr.names<IOField<Type>>(selectFields_)
+      : obr.names<IOField<Type>>()
+    );
+
     Pstream::combineReduce(fieldNames, ListOps::uniqueEqOp<word>());
     Foam::sort(fieldNames);  // Consistent order
 
     for (const word& fieldName : fieldNames)
     {
-        const List<Type>* fldPtr = obrTmp.findObject<IOField<Type>>(fieldName);
-        const List<Type>& values = (fldPtr ? *fldPtr : List<Type>());
+        const List<Type>* fldPtr = obr.findObject<IOField<Type>>(fieldName);
+        const List<Type>& values = (fldPtr ? *fldPtr : List<Type>::null());
 
-        if (Pstream::master())
+        if (UPstream::master())
         {
             if (std::is_same<label, typename pTraits<Type>::cmptType>::value)
             {
@@ -93,7 +99,7 @@ Foam::wordList Foam::functionObjects::vtkCloud::writeFields
             vtk::writeListParallel(format.ref(), values);
         }
 
-        if (Pstream::master())
+        if (UPstream::master())
         {
             // Non-legacy
             format().flush();
diff --git a/src/lagrangian/intermediate/conversion/ensight/ensightOutputCloud.C b/src/lagrangian/intermediate/conversion/ensight/ensightOutputCloud.C
index d81b259444bb9c813aa19fb64312f365acb6a765..720ad68a457ed125b8e89bdeeef5738ea342e63e 100644
--- a/src/lagrangian/intermediate/conversion/ensight/ensightOutputCloud.C
+++ b/src/lagrangian/intermediate/conversion/ensight/ensightOutputCloud.C
@@ -5,7 +5,7 @@
     \\  /    A nd           | www.openfoam.com
      \\/     M anipulation  |
 -------------------------------------------------------------------------------
-    Copyright (C) 2016-2022 OpenCFD Ltd.
+    Copyright (C) 2016-2024 OpenCFD Ltd.
 -------------------------------------------------------------------------------
 License
     This file is part of OpenFOAM.
@@ -43,7 +43,7 @@ static inline void writeMeasured_binary
     const UList<floatVector>& points
 )
 {
-    for (const floatVector& p : points)
+    for (const auto& p : points)
     {
         os.write(p.x());
         os.write(p.y());
@@ -59,9 +59,9 @@ static inline label writeMeasured_ascii
     const UList<floatVector>& points
 )
 {
-    for (const floatVector& p : points)
+    for (const auto& p : points)
     {
-        os.write(++pointId, 8); // 1-index and an unusual width
+        os.writeInt(++pointId, 8);  // 1-index and an unusual width
         os.write(p.x());
         os.write(p.y());
         os.write(p.z());
@@ -79,75 +79,24 @@ static inline label writeMeasured_ascii
 bool Foam::ensightOutput::writeCloudPositions
 (
     ensightFile& os,
-    const fvMesh& mesh,
-    const word& cloudName,
-    bool exists
+    DynamicList<floatVector>& positions,
+    const globalIndex& procAddr
 )
 {
-    label nLocalParcels(0);
-    autoPtr<Cloud<passiveParticle>> parcelsPtr;
+    // Total number of parcels across all ranks
+    const label nTotParcels = procAddr.totalSize();
 
-    if (exists)
-    {
-        parcelsPtr.reset(new Cloud<passiveParticle>(mesh, cloudName, false));
-        nLocalParcels = parcelsPtr().size();
-    }
-
-    // Total number of parcels on all processes
-    const label nTotParcels = returnReduce(nLocalParcels, sumOp<label>());
+    bool noCloud(!procAddr.totalSize());
+    Pstream::broadcast(noCloud);
 
     if (UPstream::master())
     {
         os.beginParticleCoordinates(nTotParcels);
     }
 
-    if (!nTotParcels)
-    {
-        return false;  // DONE
-    }
-
-
-    // Gather sizes (offsets irrelevant)
-    const globalIndex procAddr(globalIndex::gatherOnly{}, nLocalParcels);
-
-
-    DynamicList<floatVector> positions;
-    positions.reserve(UPstream::master() ? procAddr.maxSize() : nLocalParcels);
-
-    // Extract positions from parcel.
-    // Store as floatVector, since that is what Ensight will write anyhow
-
-    if (parcelsPtr)
+    if (noCloud)
     {
-        const auto& parcels = *parcelsPtr;
-
-        positions.resize_nocopy(parcels.size());  // same as nLocalParcels
-
-        auto outIter = positions.begin();
-
-        if (std::is_same<float, vector::cmptType>::value)
-        {
-            for (const passiveParticle& p : parcels)
-            {
-                *outIter = p.position();
-                ++outIter;
-            }
-        }
-        else
-        {
-            for (const passiveParticle& p : parcels)
-            {
-                vector pos(p.position());
-
-                (*outIter).x() = narrowFloat(pos.x());
-                (*outIter).y() = narrowFloat(pos.y());
-                (*outIter).z() = narrowFloat(pos.z());
-
-                ++outIter;
-            }
-        }
-
-        parcelsPtr.reset(nullptr);
+        return false;  // All empty
     }
 
     if (UPstream::master())
@@ -178,6 +127,9 @@ bool Foam::ensightOutput::writeCloudPositions
         }
 
 
+        positions.clear();
+        positions.reserve_nocopy(procAddr.maxNonLocalSize());
+
         // Receive and write
         for (const label proci : procAddr.subProcs())
         {
@@ -186,6 +138,7 @@ bool Foam::ensightOutput::writeCloudPositions
             if (procSize)
             {
                 positions.resize_nocopy(procSize);
+
                 UIPstream::read
                 (
                     UPstream::commsTypes::scheduled,
@@ -205,7 +158,7 @@ bool Foam::ensightOutput::writeCloudPositions
             }
         }
     }
-    else
+    else if (UPstream::is_subrank())
     {
         if (positions.size())
         {
@@ -223,4 +176,86 @@ bool Foam::ensightOutput::writeCloudPositions
 }
 
 
+bool Foam::ensightOutput::writeCloudPositions
+(
+    ensightFile& os,
+    DynamicList<floatVector>& positions
+)
+{
+    return ensightOutput::writeCloudPositions
+    (
+        os,
+        positions,
+        // Gather sizes (offsets irrelevant)
+        globalIndex(globalIndex::gatherOnly{}, positions.size())
+    );
+}
+
+
+bool Foam::ensightOutput::writeCloudPositions
+(
+    ensightFile& os,
+    const fvMesh& mesh,
+    const word& cloudName,
+    bool exists
+)
+{
+    autoPtr<Cloud<passiveParticle>> parcelsPtr;
+
+    if (exists)
+    {
+        parcelsPtr.reset(new Cloud<passiveParticle>(mesh, cloudName, false));
+    }
+
+    const label nLocalParcels
+    (
+        parcelsPtr ? parcelsPtr->size() : 0
+    );
+
+    // Gather sizes (offsets irrelevant)
+    // and total number of parcels (all processes)
+    const globalIndex procAddr(globalIndex::gatherOnly{}, nLocalParcels);
+
+    // Extract positions from parcel.
+    // Store as floatVector, since that is what Ensight will write anyhow
+
+    DynamicList<floatVector> positions;
+    positions.reserve(UPstream::master() ? procAddr.maxSize() : nLocalParcels);
+
+    if (parcelsPtr)
+    {
+        const auto& parcels = *parcelsPtr;
+
+        positions.resize_nocopy(parcels.size());  // same as nLocalParcels
+
+        auto iter = positions.begin();
+
+        if (std::is_same<float, vector::cmptType>::value)
+        {
+            for (const auto& p : parcels)
+            {
+                *iter = p.position();
+                ++iter;
+            }
+        }
+        else
+        {
+            for (const auto& p : parcels)
+            {
+                const vector pos(p.position());
+
+                (*iter).x() = narrowFloat(pos.x());
+                (*iter).y() = narrowFloat(pos.y());
+                (*iter).z() = narrowFloat(pos.z());
+                ++iter;
+            }
+        }
+
+        parcelsPtr.reset(nullptr);
+    }
+
+    return ensightOutput::writeCloudPositions(os, positions, procAddr);
+}
+
+
 // ************************************************************************* //
diff --git a/src/lagrangian/intermediate/conversion/ensight/ensightOutputCloud.H b/src/lagrangian/intermediate/conversion/ensight/ensightOutputCloud.H
index e83fbecec0cbf7e582206856ddf079135b4ab1b6..323296eb261c59da3de3156a1605708f8c833732 100644
--- a/src/lagrangian/intermediate/conversion/ensight/ensightOutputCloud.H
+++ b/src/lagrangian/intermediate/conversion/ensight/ensightOutputCloud.H
@@ -5,7 +5,7 @@
     \\  /    A nd           | www.openfoam.com
      \\/     M anipulation  |
 -------------------------------------------------------------------------------
-    Copyright (C) 2016-2022 OpenCFD Ltd.
+    Copyright (C) 2016-2024 OpenCFD Ltd.
 -------------------------------------------------------------------------------
 License
     This file is part of OpenFOAM.
@@ -40,6 +40,8 @@ SourceFiles
 
 #include "ensightFile.H"
 #include "IOField.H"
+#include "DynamicList.H"
+#include "vector.H"
 
 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
 
@@ -48,6 +50,7 @@ namespace Foam
 
 // Forward Declarations
 class fvMesh;
+class globalIndex;
 
 namespace ensightOutput
 {
@@ -55,6 +58,34 @@ namespace ensightOutput
 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
 
 
+//- Write cloud positions
+bool writeCloudPositions
+(
+    //! Output file (must be valid on master)
+    ensightFile& os,
+
+    //! The positions (measured data) to write.
+    //! Also used as intermediate buffer (on master)
+    DynamicList<floatVector>& positions,
+
+    //! The global sizes of \p positions (must be valid on master)
+    //! and consistent with \p positions dimensions
+    const globalIndex& procAddr
+);
+
+
+//- Write cloud positions
+bool writeCloudPositions
+(
+    //! Output file (must be valid on master)
+    ensightFile& os,
+
+    //! The positions (measured data) to write.
+    //! Also used as intermediate buffer (on master)
+    DynamicList<floatVector>& positions
+);
+
+
 //- Write cloud positions
 bool writeCloudPositions
 (
@@ -80,7 +111,23 @@ bool writeCloudField
     ensightFile& os,
 
     //! The cloud field
-    const IOField<Type>& field
+    const UList<Type>& field,
+
+    //! The global sizes of \p field (must be valid on master)
+    //! and consistent with \p field dimensions
+    const globalIndex& procAddr
+);
+
+
+//- Write cloud field, returning true if the field is non-empty.
+template<class Type>
+bool writeCloudField
+(
+    //! Output file (must be valid on master)
+    ensightFile& os,
+
+    //! The cloud field
+    const UList<Type>& field
 );
 
 
diff --git a/src/lagrangian/intermediate/conversion/ensight/ensightOutputCloudTemplates.C b/src/lagrangian/intermediate/conversion/ensight/ensightOutputCloudTemplates.C
index cc865cc7d3bd0a21b7916f782a25d42993d3f3d4..85b5d8a898a51c65a5973819648f1b896ef029a0 100644
--- a/src/lagrangian/intermediate/conversion/ensight/ensightOutputCloudTemplates.C
+++ b/src/lagrangian/intermediate/conversion/ensight/ensightOutputCloudTemplates.C
@@ -5,7 +5,7 @@
     \\  /    A nd           | www.openfoam.com
      \\/     M anipulation  |
 -------------------------------------------------------------------------------
-    Copyright (C) 2016-2022 OpenCFD Ltd.
+    Copyright (C) 2016-2024 OpenCFD Ltd.
 -------------------------------------------------------------------------------
 License
     This file is part of OpenFOAM.
@@ -39,7 +39,6 @@ Foam::label Foam::ensightOutput::Detail::writeCloudFieldContent
     label count
 )
 {
-    // Write master data
     for (Type val : field)          // <-- working on a copy!
     {
         if (mag(val) < 1e-90)       // approximately root(ROOTVSMALL)
@@ -70,18 +69,20 @@ template<class Type>
 bool Foam::ensightOutput::writeCloudField
 (
     ensightFile& os,
-    const IOField<Type>& field
+    const UList<Type>& field,
+    const globalIndex& procAddr
 )
 {
-    if (returnReduceAnd(field.empty()))
+    bool allEmpty(!procAddr.totalSize());
+    Pstream::broadcast(allEmpty);
+
+    if (allEmpty)
     {
-        return false;
+        return false;  // All empty
     }
 
-    // Gather sizes (offsets irrelevant)
-    const globalIndex procAddr(globalIndex::gatherOnly{}, field.size());
 
-    if (Pstream::master())
+    if (UPstream::master())
     {
         // 6 values per line
         label count = 0;
@@ -128,7 +129,7 @@ bool Foam::ensightOutput::writeCloudField
             os.newline();
         }
     }
-    else
+    else if (UPstream::is_subrank())
     {
         if (field.size())
         {
@@ -146,6 +147,23 @@ bool Foam::ensightOutput::writeCloudField
 }
 
 
+template<class Type>
+bool Foam::ensightOutput::writeCloudField
+(
+    ensightFile& os,
+    const UList<Type>& field
+)
+{
+    return ensightOutput::writeCloudField
+    (
+        os,
+        field,
+        // Gather sizes (offsets irrelevant)
+        globalIndex(globalIndex::gatherOnly{}, field.size())
+    );
+}
+
+
 template<class Type>
 bool Foam::ensightOutput::readWriteCloudField
 (
@@ -162,10 +180,11 @@ bool Foam::ensightOutput::readWriteCloudField
 
         IOobject io(fieldObject);
         io.readOpt(IOobject::READ_IF_PRESENT);
+        io.registerObject(IOobject::NO_REGISTER);
 
         IOField<Type> field(io);
 
-        writeCloudField(os, field);
+        ensightOutput::writeCloudField(os, field);
     }
 
     return true;