diff --git a/applications/test/vtmWriter/Make/files b/applications/test/vtmWriter/Make/files
new file mode 100644
index 0000000000000000000000000000000000000000..4a16ec021696d6f9178cb8107131bef8f6d59662
--- /dev/null
+++ b/applications/test/vtmWriter/Make/files
@@ -0,0 +1,3 @@
+Test-vtmWriter.C
+
+EXE = $(FOAM_APPBIN)/Test-vtmWriter
diff --git a/applications/test/vtmWriter/Make/options b/applications/test/vtmWriter/Make/options
new file mode 100644
index 0000000000000000000000000000000000000000..7ce182425d9bee4bef91ba2a36b6b8475fc1aeb2
--- /dev/null
+++ b/applications/test/vtmWriter/Make/options
@@ -0,0 +1,5 @@
+EXE_INC = \
+    -I$(LIB_SRC)/fileFormats/lnInclude
+
+EXE_LIBS = \
+    -lfileFormats
diff --git a/applications/test/vtmWriter/Test-vtmWriter.C b/applications/test/vtmWriter/Test-vtmWriter.C
new file mode 100644
index 0000000000000000000000000000000000000000..234268d779fbb329a2174dca704c77879de9b670
--- /dev/null
+++ b/applications/test/vtmWriter/Test-vtmWriter.C
@@ -0,0 +1,159 @@
+/*---------------------------------------------------------------------------*\
+  =========                 |
+  \\      /  F ield         | OpenFOAM: The Open Source CFD Toolbox
+   \\    /   O peration     |
+    \\  /    A nd           | Copyright (C) 2018 OpenCFD Ltd.
+     \\/     M anipulation  |
+-------------------------------------------------------------------------------
+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/>.
+
+Application
+    Test-vtmWriter
+
+Description
+    Basic functionality tests for vtk::vtmWriter
+
+\*---------------------------------------------------------------------------*/
+
+#include "foamVtmWriter.H"
+
+using namespace Foam;
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+int main(int argc, char *argv[])
+{
+    vtk::vtmWriter writer1;
+    {
+        fileName base = "region1_0001";
+
+        writer1.beginBlock("internal");
+        writer1.append_vtu
+        (
+            base/"internal"
+        );
+        writer1.endBlock("internal");
+
+        {
+            writer1.beginBlock("boundary");
+            writer1.append_vtp
+            (
+                base/"patch0"
+            );
+            writer1.append("");  // bad entry
+            writer1.append_vtp
+            (
+                base/"patch1"
+            );
+            writer1.append_vtp
+            (
+                base/"patch2"
+            );
+        }
+
+        writer1.endBlock("boundary");
+
+        {
+            writer1.beginBlock("empty");
+            writer1.endBlock("empty");
+        }
+        {
+            writer1.beginBlock("dangling1");
+            writer1.beginBlock("dangling2");
+        }
+    }
+
+    Info<< nl << "vtm information" << nl;
+    writer1.dump(Info),
+    Info<< nl;
+
+//    writer1.repair();
+//
+//   Info<< nl << "vtm information - after repair" << nl;
+//    writer1.dump(Info),
+//    Info<< nl;
+
+    writer1.repair(true);
+
+//    Info<< nl << "vtm information - after repair(collapse)" << nl;
+//    writer1.dump(Info),
+//    Info<< nl;
+//
+//    Info<< nl << "vtm information - after repair(collapse)" << nl;
+//    writer1.dump(Info),
+//    Info<< nl;
+
+    Info<< nl << "Write to file" << nl;
+    writer1.write("vtmWriter1.vtm");
+
+
+    vtk::vtmWriter writer2;
+    {
+        fileName base = "region2_0001";
+
+        writer2.beginBlock("internal");
+        writer2.append_vtu
+        (
+            base/"internal"
+        );
+        writer2.endBlock("internal");
+
+        {
+            writer2.beginBlock("boundary");
+            writer2.append_vtp
+            (
+                base/"patch0"
+            );
+            writer2.append("");  // bad entry
+            writer2.append_vtp
+            (
+                base/"patch1"
+            );
+            writer2.append_vtp
+            (
+                base/"patch2"
+            );
+        }
+
+        writer2.endBlock("boundary");
+
+        // These should be automatically skiped
+        writer2.endBlock();
+        writer2.endBlock();
+        writer2.endBlock();
+        writer2.endBlock();
+    }
+
+    Info<< nl << "vtm information" << nl;
+    writer2.dump(Info);
+
+    writer2.repair(true);
+
+
+    vtk::vtmWriter writer3;
+
+    writer3.add("some-region1", writer1);
+    writer3.add("some-region2", writer2);
+
+    Info<< nl << "Combined:" << nl;
+    writer3.dump(Info);
+
+    return 0;
+}
+
+
+// ************************************************************************* //
diff --git a/src/fileFormats/Make/files b/src/fileFormats/Make/files
index 678c9c418ee58f37c6fb651391858e8483a02c2d..640d98882bf31900f33ff30ca6f13cf793f55e11 100644
--- a/src/fileFormats/Make/files
+++ b/src/fileFormats/Make/files
@@ -17,6 +17,7 @@ stl/STLAsciiParseManual.C
 stl/STLAsciiParseRagel.C
 
 vtk/file/foamVtkFileWriter.C
+vtk/file/foamVtmWriter.C
 vtk/core/foamVtkCore.C
 vtk/core/foamVtkPTraits.C
 
diff --git a/src/fileFormats/vtk/file/foamVtmWriter.C b/src/fileFormats/vtk/file/foamVtmWriter.C
new file mode 100644
index 0000000000000000000000000000000000000000..d935c22c762dd45b175fc057501eaa3ed5f2c592
--- /dev/null
+++ b/src/fileFormats/vtk/file/foamVtmWriter.C
@@ -0,0 +1,694 @@
+/*---------------------------------------------------------------------------*\
+  =========                 |
+  \\      /  F ield         | OpenFOAM: The Open Source CFD Toolbox
+   \\    /   O peration     |
+    \\  /    A nd           | Copyright (C) 2018 OpenCFD Ltd.
+     \\/     M anipulation  |
+-------------------------------------------------------------------------------
+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 <fstream>
+#include "foamVtmWriter.H"
+#include "Time.H"
+#include "OSspecific.H"
+
+// * * * * * * * * * * * * * * * * Local Class * * * * * * * * * * * * * * * //
+
+void Foam::vtk::vtmWriter::vtmEntry::clear()
+{
+    type_ = NONE;
+    name_.clear();
+    file_.clear();
+}
+
+
+bool Foam::vtk::vtmWriter::vtmEntry::good() const
+{
+    return
+    (
+        type_ == vtmEntry::BEGIN_BLOCK
+     || type_ == vtmEntry::END_BLOCK
+     || (type_ == vtmEntry::DATA && file_.size())
+    );
+}
+
+
+bool Foam::vtk::vtmWriter::vtmEntry::write(vtk::formatter& format) const
+{
+    if (type_ == vtmEntry::BEGIN_BLOCK)
+    {
+        format.openTag(vtk::fileTag::BLOCK);
+        if (name_.size())
+        {
+            format.xmlAttr("name", name_);
+        }
+        format.closeTag();
+
+        return true;
+    }
+    else if (type_ == vtmEntry::END_BLOCK)
+    {
+        format.endBlock();
+        return true;
+    }
+    else if (type_ == vtmEntry::DATA && file_.size())
+    {
+        format.openTag(vtk::fileTag::DATA_SET);
+
+        if (name_.size())
+        {
+            format.xmlAttr("name", name_);
+        }
+
+        format.xmlAttr("file", file_);
+
+        format.closeTag(true);  // Empty tag. ie, <DataSet ... />
+        return true;
+    }
+
+    return false;
+}
+
+
+// * * * * * * * * * * * * * Private Member Functions  * * * * * * * * * * * //
+
+bool Foam::vtk::vtmWriter::pruneEmpty()
+{
+    const label nEntries = entries_.size();
+
+    label dst=0;
+
+    for (label src=0; src < nEntries; ++src)
+    {
+        if (entries_[src].good())
+        {
+            if (dst != src)
+            {
+                entries_[dst] = std::move(entries_[src]);
+            }
+            ++dst;
+        }
+    }
+
+    const bool changed = (dst != nEntries);
+    entries_.resize(dst);
+
+    return changed;
+}
+
+
+bool Foam::vtk::vtmWriter::pruneEmptyBlocks()
+{
+    bool pruned = false;
+
+    const label nEntries = entries_.size();
+
+    while (true)
+    {
+        bool changed = false;
+
+        for (label i=0; i < nEntries; ++i)
+        {
+            vtmEntry& e = entries_[i];
+
+            if (e.isType(vtmEntry::BEGIN_BLOCK))
+            {
+                for (label j=i+1; j < nEntries; ++j)
+                {
+                    if (entries_[j].isType(vtmEntry::END_BLOCK))
+                    {
+                        e.clear();
+                        entries_[j].clear();
+
+                        changed = true;
+                        break;
+                    }
+                    else if (!entries_[j].isType(vtmEntry::NONE))
+                    {
+                        break;
+                    }
+                }
+            }
+        }
+
+        if (changed)
+        {
+            pruned = true;
+        }
+        else
+        {
+            break;
+        }
+    }
+
+    // Collapse single-entry blocks when the names allow it
+
+    // Transcribe, removing NONE entries
+    pruneEmpty();
+
+    return pruned;
+}
+
+
+bool Foam::vtk::vtmWriter::collapseBlocks()
+{
+    bool collapsed = false;
+
+    const label nEntries = entries_.size();
+
+    for (label i=0; i < nEntries-2; ++i)
+    {
+        vtmEntry& b = entries_[i];    // begin
+        vtmEntry& d = entries_[i+1];  // data
+        vtmEntry& e = entries_[i+2];  // end
+
+        if
+        (
+            b.isType(vtmEntry::BEGIN_BLOCK)
+         && e.isType(vtmEntry::END_BLOCK)
+         && d.isType(vtmEntry::DATA)
+         && (d.name_.empty() || d.name_ == b.name_)
+        )
+        {
+            d.name_ = std::move(b.name_);
+
+            b.clear();
+            e.clear();
+
+            collapsed = true;
+        }
+    }
+
+    pruneEmpty();
+
+    return collapsed;
+}
+
+
+void Foam::vtk::vtmWriter::repair(bool collapse)
+{
+    // Add or remove END_BLOCK
+
+    label depth = 0;
+    label nEntries = 0;
+
+    for (vtmEntry& e : entries_)
+    {
+        if (e.isType(vtmEntry::BEGIN_BLOCK))
+        {
+            ++depth;
+        }
+        else if (e.isType(vtmEntry::END_BLOCK))
+        {
+            --depth;
+
+            if (depth < 0)
+            {
+                // Truncate now and exit
+                entries_.resize(nEntries);
+                break;
+            }
+        }
+        else if (e.isType(vtmEntry::DATA))
+        {
+            if (e.file_.empty())
+            {
+                // Bad entry - reset to NONE
+                e.clear();
+            }
+        }
+
+        ++nEntries;
+    }
+
+    // Close any dangling blocks
+    while (depth--)
+    {
+        entries_.append(vtmEntry::endblock());
+    }
+
+    blocks_.clear();
+    pruneEmpty();
+
+    if (collapse)
+    {
+        pruneEmptyBlocks();
+        collapseBlocks();
+    }
+}
+
+
+void Foam::vtk::vtmWriter::dump(Ostream& os) const
+{
+    label depth = 0;
+
+    // Output format is a mix of dictionary and JSON
+    // the only purpose being for diagnostics
+
+    for (const vtmEntry& e : entries_)
+    {
+        switch (e.type_)
+        {
+            case vtmEntry::NONE:
+            {
+                os.indent();
+                os  << "none" << nl;
+                break;
+            }
+            case vtmEntry::DATA:
+            {
+                os.indent();
+                os  << "{ \"name\" : " << e.name_
+                    << ", \"file\" : " << e.file_ << " }" << nl;
+                break;
+            }
+            case vtmEntry::BEGIN_BLOCK:
+            {
+                ++depth;
+                os.beginBlock(e.name_);
+                break;
+            }
+            case vtmEntry::END_BLOCK:
+            {
+                --depth;
+                os.endBlock();
+                os << nl;
+                break;
+            }
+        }
+    }
+
+    for (label i=0; i < depth; ++i)
+    {
+        os.decrIndent();
+    }
+
+    if (depth > 0)
+    {
+        os << "# Had " << depth << " unclosed blocks" << nl;
+    }
+    if (depth < 0)
+    {
+        os << "# Had " << (-depth) << " too many end blocks" << nl;
+    }
+}
+
+
+// * * * * * * * * * * * * * * * * Constructors  * * * * * * * * * * * * * * //
+
+Foam::vtk::vtmWriter::vtmWriter()
+:
+    vtmWriter(true, false)
+{}
+
+
+Foam::vtk::vtmWriter::vtmWriter(bool autoName)
+:
+    vtmWriter(autoName, false)
+{}
+
+
+Foam::vtk::vtmWriter::vtmWriter(bool autoName, bool autoCollapse)
+:
+    autoName_(autoName),
+    autoCollapse_(autoCollapse),
+    hasTime_(false),
+    entries_(),
+    blocks_(),
+    timeValue_(Zero)
+{}
+
+
+// * * * * * * * * * * * * * * * Member Functions  * * * * * * * * * * * * * //
+
+void Foam::vtk::vtmWriter::clear()
+{
+    entries_.clear();
+    blocks_.clear();
+
+    timeValue_ = Zero;
+    hasTime_ = false;
+}
+
+
+bool Foam::vtk::vtmWriter::empty() const
+{
+    for (const auto& e : entries_)
+    {
+        if (e.isType(vtmEntry::DATA) && e.name_.size())
+        {
+            return false;
+        }
+    }
+
+    return true;
+}
+
+
+Foam::label Foam::vtk::vtmWriter::size() const
+{
+    label ndata = 0;
+
+    for (const auto& e : entries_)
+    {
+        if (e.isType(vtmEntry::DATA) && e.file_.size())
+        {
+            ++ndata;
+        }
+    }
+
+    return ndata;
+}
+
+
+void Foam::vtk::vtmWriter::setTime(scalar timeValue)
+{
+    timeValue_ = timeValue;
+    hasTime_ = true;
+}
+
+
+void Foam::vtk::vtmWriter::setTime(const Time& t)
+{
+    timeValue_ = t.value();
+    hasTime_ = true;
+}
+
+
+Foam::label Foam::vtk::vtmWriter::beginBlock(const word& blockName)
+{
+    entries_.append(vtmEntry::block(blockName));
+    blocks_.append(blockName);
+
+    return blocks_.size();
+}
+
+
+Foam::label Foam::vtk::vtmWriter::endBlock(const word& blockName)
+{
+    label nblock = blocks_.size();
+
+    if (nblock)
+    {
+        const word curr(blocks_.remove());
+
+        // Verify expected end tag
+        if (!blockName.empty() && blockName != curr)
+        {
+            WarningInFunction
+                << "expecting to end block '" << blockName
+                << "' but found '" << curr << "' instead"
+                << endl;
+        }
+
+        entries_.append(vtmEntry::endblock());
+    }
+
+    return blocks_.size();
+}
+
+
+bool Foam::vtk::vtmWriter::append(const fileName& file)
+{
+    if (autoName_)
+    {
+        return append(fileName::nameLessExt(file), file);
+    }
+    else
+    {
+        return append(word::null, file);
+    }
+}
+
+
+bool Foam::vtk::vtmWriter::append
+(
+    const fileName& file,
+    vtk::fileTag contentType
+)
+{
+    if (autoName_)
+    {
+        return append(fileName::nameLessExt(file), file, contentType);
+    }
+    else
+    {
+        return append(word::null, file, contentType);
+    }
+}
+
+
+bool Foam::vtk::vtmWriter::append
+(
+    const word& name,
+    const fileName& file
+)
+{
+    if (file.empty())
+    {
+        return false;
+    }
+
+    entries_.append(vtmEntry::entry(name, file));
+    return true;
+}
+
+
+bool Foam::vtk::vtmWriter::append
+(
+    const word& name,
+    const fileName& file,
+    vtk::fileTag contentType
+)
+{
+    if (file.empty())
+    {
+        return false;
+    }
+
+    if (file.hasExt(vtk::fileExtension[contentType]))
+    {
+        entries_.append(vtmEntry::entry(name, file));
+    }
+    else
+    {
+        entries_.append
+        (
+            vtmEntry::entry
+            (
+                name,
+                file + "." + vtk::fileExtension[contentType]
+            )
+        );
+    }
+
+    return true;
+}
+
+
+void Foam::vtk::vtmWriter::add
+(
+    const word& blockName,
+    const fileName& prefix,
+    const vtmWriter& other
+)
+{
+    // Standard sanity repair (block ending), prune empty entries
+    repair();
+
+    beginBlock(blockName);
+
+    label depth = 0;
+    bool good = true;
+
+    for (const vtmEntry& e : other.entries_)
+    {
+        switch (e.type_)
+        {
+            case vtmEntry::NONE:
+            {
+                break;
+            }
+            case vtmEntry::DATA:
+            {
+                if (e.good())
+                {
+                    entries_.append(e);
+
+                    if (prefix.size())
+                    {
+                        fileName& f = entries_.last().file_;
+
+                        f = prefix/f;
+                    }
+                }
+
+                break;
+            }
+            case vtmEntry::BEGIN_BLOCK:
+            {
+                ++depth;
+                entries_.append(e);
+                break;
+            }
+            case vtmEntry::END_BLOCK:
+            {
+                good = (depth > 0);
+                --depth;
+                if (good)
+                {
+                    entries_.append(e);
+                }
+                break;
+            }
+        }
+
+        if (!good) break;
+    }
+
+    while (depth--)
+    {
+        entries_.append(vtmEntry::endblock());
+    }
+
+    entries_.append(vtmEntry::endblock());
+
+    if (!hasTime_ && other.hasTime_)
+    {
+        hasTime_ = true;
+        timeValue_ = other.timeValue_;
+    }
+}
+
+
+void Foam::vtk::vtmWriter::add
+(
+    const word& blockName,
+    const vtmWriter& other
+)
+{
+    add(blockName, fileName::null, other);
+}
+
+
+Foam::label Foam::vtk::vtmWriter::write(const fileName& file)
+{
+    std::ofstream os_;
+
+    mkDir(file.path());
+
+    if (file.hasExt(ext()))
+    {
+        os_.open(file);
+    }
+    else
+    {
+        os_.open(file + "." + ext());
+    }
+
+    auto format = vtk::newFormatter(os_, formatType::INLINE_ASCII);
+
+
+    // Contents Header
+    {
+        format().xmlHeader();
+
+        if (hasTime_)
+        {
+            format().xmlComment
+            (
+                "time='" + Foam::name(timeValue_) + "'"
+            );
+        }
+
+        format().beginVTKFile<vtk::fileTag::MULTI_BLOCK>();
+    }
+
+
+    // Walk the block and dataset contents
+
+    label depth = 0;
+    label ndata = 0;
+
+    for (const vtmEntry& e : entries_)
+    {
+        switch (e.type_)
+        {
+            case vtmEntry::DATA:
+            {
+                if (e.file_.empty())
+                {
+                    continue;  // Empty dataset is junk - skip
+                }
+                ++ndata;
+                break;
+            }
+            case vtmEntry::BEGIN_BLOCK:
+            {
+                ++depth;
+                break;
+            }
+            case vtmEntry::END_BLOCK:
+            {
+                --depth;
+                break;
+            }
+            default:
+            {
+                continue;
+                break;
+            }
+        }
+
+        if (depth < 0)
+        {
+            // Too many end blocks - stop output now. Should we warn?
+            break;
+        }
+        e.write(format());
+    }
+
+    // Close any dangling blocks
+    while (depth--)
+    {
+        format().endBlock();
+    }
+
+    format().endTag(vtk::fileTag::MULTI_BLOCK);
+
+
+    // FieldData for TimeValue
+    if (hasTime_)
+    {
+        format()
+            .beginFieldData()
+            .writeTimeValue(timeValue_)
+            .endFieldData();
+    }
+
+    format().endVTKFile();
+
+    format.clear();
+    os_.close();
+
+    return ndata;
+}
+
+
+// ************************************************************************* //
diff --git a/src/fileFormats/vtk/file/foamVtmWriter.H b/src/fileFormats/vtk/file/foamVtmWriter.H
new file mode 100644
index 0000000000000000000000000000000000000000..24b9bed0fd880f7567412cdffadc87664f6688f8
--- /dev/null
+++ b/src/fileFormats/vtk/file/foamVtmWriter.H
@@ -0,0 +1,342 @@
+/*---------------------------------------------------------------------------*\
+  =========                 |
+  \\      /  F ield         | OpenFOAM: The Open Source CFD Toolbox
+   \\    /   O peration     |
+    \\  /    A nd           | Copyright (C) 2018 OpenCFD Ltd.
+     \\/     M anipulation  |
+-------------------------------------------------------------------------------
+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::vtk::vtmWriter
+
+Description
+    Provides a means of accumulating file entries for generating
+    a vtkMultiBlockDataSet (.vtm) file.
+
+    For example, to generate the following content:
+    \verbatim
+    <?xml version='1.0'?>
+    <VTKFile type='vtkMultiBlockDataSet' ...>
+      <vtkMultiBlockDataSet>
+        <DataSet name='internal' file='internal.vtu' />
+        <Block name='boundary'>
+          <DataSet name='inlet' file='boundary/inlet.vtp' />
+          <DataSet name='outlet' file='boundary/outlet.vtp' />
+        </Block>
+      </vtkMultiBlockDataSet>
+      <FieldData>
+        <DataArray type='Float32' Name='TimeValue' ...>
+           12.345
+        </DataArray>
+      </FieldData>
+    </VTKFile>
+    \endverbatim
+
+    The following code would be used:
+    \code
+        vtm.clear();
+        vtm.setTime(12.345);
+
+        vtm.append("internal", "internal.vtu");
+
+        vtm.beginBlock("boundary");
+        vtm.append("boundary/inlet.vtp");
+        vtm.append("boundary/outlet.vtp");
+
+        vtm.write("outputName");
+    \endcode
+
+SourceFiles
+    foamVtmWriter.C
+
+\*---------------------------------------------------------------------------*/
+
+#ifndef foamVtmWriter_H
+#define foamVtmWriter_H
+
+#include "foamVtkOutputOptions.H"
+#include "DynamicList.H"
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+namespace Foam
+{
+
+// Forward declarations
+class Time;
+
+namespace vtk
+{
+
+/*---------------------------------------------------------------------------*\
+                       Class vtk::vtmWriter Declaration
+\*---------------------------------------------------------------------------*/
+
+class vtmWriter
+{
+    //- Simple structure for containing entries
+    struct vtmEntry
+    {
+        enum Type
+        {
+            NONE = 0,
+            DATA = 'D',
+            BEGIN_BLOCK = '{',
+            END_BLOCK = '}'
+        };
+
+        //- The entry type
+        int type_;
+
+        //- The 'name' entry (to describe block or data)
+        string name_;
+
+        //- The 'file' entry (data only)
+        fileName file_;
+
+        // Constructors
+
+            vtmEntry(const vtmEntry&) = default;
+            vtmEntry(vtmEntry&&) = default;
+            vtmEntry& operator=(const vtmEntry&) = default;
+            vtmEntry& operator=(vtmEntry&&) = default;
+
+            //- Construct null
+            vtmEntry()
+            :
+                type_(NONE)
+            {}
+
+            //- Construct from components
+            vtmEntry(int what, const string& name, const fileName& file)
+            :
+                type_(what), name_(name), file_(file)
+            {}
+
+
+        // Factory Methods
+
+            static vtmEntry block(const string& name)
+            {
+                return vtmEntry(BEGIN_BLOCK, name, "");
+            }
+
+            static vtmEntry endblock()
+            {
+                return vtmEntry(END_BLOCK, "", "");
+            }
+
+            static vtmEntry entry(const fileName& file)
+            {
+                return vtmEntry(DATA, "", file);
+            }
+
+            static vtmEntry entry(const string& name, const fileName& file)
+            {
+                return vtmEntry(DATA, name, file);
+            }
+
+
+        // Member Functions
+
+            //- Test the type
+            bool isType(Type what) const
+            {
+               return type_ == what;
+            }
+
+            //- Reset to NONE
+            void clear();
+
+            //- True if the entry is good.
+            bool good() const;
+
+            //- Output valid entry as XML
+            bool write(vtk::formatter& format) const;
+    };
+
+
+    // Private Member Data
+
+        //- Auto-generate names from 'file' entry?
+        bool autoName_;
+
+        //- Collapse empty blocks and combine block/dataset etc.
+        bool autoCollapse_;
+
+        //- Has a TimeValue for FieldData?
+        bool hasTime_;
+
+        //- A vtm file entry: begin/end block, dataset
+        DynamicList<vtmEntry> entries_;
+
+        //- LIFO stack of current block names
+        DynamicList<word> blocks_;
+
+        //- TimeValue for FieldData
+        scalar timeValue_;
+
+
+    // Private Member Functions
+
+        //- Remove NONE entries
+        bool pruneEmpty();
+
+        //- Remove empty blocks
+        bool pruneEmptyBlocks();
+
+        //- Collapse block if it has a single dataset and the names allow it
+        bool collapseBlocks();
+
+
+public:
+
+    // Constructors
+
+        //- Construct null, with autoName on
+        vtmWriter();
+
+        //- Construct with specified behaviour for autoName
+        explicit vtmWriter(bool autoName);
+
+        //- Construct with specified behaviour for autoName, autoCollapse
+        vtmWriter(bool autoName, bool autoCollapse);
+
+
+    //- Destructor
+    ~vtmWriter() = default;
+
+
+    // Member Functions
+
+        //- File extension (always "vtm")
+        inline static word ext();
+
+        //- If there are no data sets
+        bool empty() const;
+
+        //- The number of data sets
+        label size() const;
+
+
+    // Content Management
+
+        //- Clear all entries and reset output
+        void clear();
+
+        //- Define "TimeValue" for FieldData (name as per Catalyst output)
+        void setTime(scalar timeValue);
+
+        //- Define "TimeValue" for FieldData (name as per Catalyst output)
+        void setTime(const Time& t);
+
+
+        //- Start a new block, optionally with a name
+        //  \return block depth
+        label beginBlock(const word& blockName = word::null);
+
+        //- End the previous block, optionally with name checking
+        //  \return block depth
+        label endBlock(const word& blockName = word::null);
+
+
+        //- Add a file. The name is either empty or created with autoName
+        //  \return True if file is non-empty
+        bool append(const fileName& file);
+
+        //- Add a file with name
+        //  \return True if file is non-empty
+        bool append(const word& name, const fileName& file);
+
+        //- Add a file with given contentType extension
+        //- The name is either empty or created with autoName
+        //  \return True if file is non-empty
+        bool append(const fileName& file, vtk::fileTag contentType);
+
+        //- Add a file with name, with given contentType extension
+        //  \return True if file is non-empty
+        bool append
+        (
+            const word& name,
+            const fileName& file,
+            vtk::fileTag contentType
+        );
+
+        //- Add a (.vtp) file
+        //  \return True if file is non-empty
+        inline bool append_vtp(const fileName& file);
+
+        //- Add a (.vtp) file with name
+        //  \return True if file is non-empty
+        inline bool append_vtp(const word& name, const fileName& file);
+
+        //- Add a (.vtu) file
+        //  \return True if file is non-empty
+        inline bool append_vtu(const fileName& file);
+
+        //- Add a (.vtu) file with name
+        //  \return True if file is non-empty
+        inline bool append_vtu(const word& name, const fileName& file);
+
+
+    // Content Management
+
+        //- Sanity fixes on the data
+        void repair(bool collapse=false);
+
+        //- Add in content from another vtm and place under the given block
+        //- name.
+        void add(const word& blockName, const vtmWriter& other);
+
+        //- Add in content from another vtm and place under the given block
+        //- name. Adjust the added 'file' entries to include the given prefix.
+        void add
+        (
+            const word& blockName,
+            const fileName& prefix,
+            const vtmWriter& other
+        );
+
+    // Writing
+
+        //- Open file for writing (creates parent directory) and write the
+        //- blocks and TimeValue.
+        //  The file name is with/without an extension.
+        //  \return number of data sets
+        label write(const fileName& file);
+
+        //- Print debug view of block and dataset contents
+        void dump(Ostream& os) const;
+};
+
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+} // End namespace vtk
+} // End namespace Foam
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+#include "foamVtmWriterI.H"
+
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+#endif
+
+// ************************************************************************* //
diff --git a/src/fileFormats/vtk/file/foamVtmWriterI.H b/src/fileFormats/vtk/file/foamVtmWriterI.H
new file mode 100644
index 0000000000000000000000000000000000000000..90351d70eefe418fc606542ba27e7a5ddcbf003a
--- /dev/null
+++ b/src/fileFormats/vtk/file/foamVtmWriterI.H
@@ -0,0 +1,66 @@
+/*---------------------------------------------------------------------------*\
+  =========                 |
+  \\      /  F ield         | OpenFOAM: The Open Source CFD Toolbox
+   \\    /   O peration     |
+    \\  /    A nd           | Copyright (C) 2018 OpenCFD Ltd.
+     \\/     M anipulation  |
+-------------------------------------------------------------------------------
+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/>.
+
+\*---------------------------------------------------------------------------*/
+
+// * * * * * * * * * * * * * * * Member Functions  * * * * * * * * * * * * * //
+
+inline Foam::word Foam::vtk::vtmWriter::ext()
+{
+    return vtk::fileExtension[vtk::fileTag::MULTI_BLOCK];
+}
+
+
+inline bool Foam::vtk::vtmWriter::append_vtp(const fileName& file)
+{
+    return append(file, vtk::fileTag::POLY_DATA);
+}
+
+
+inline bool Foam::vtk::vtmWriter::append_vtp
+(
+    const word& name,
+    const fileName& file
+)
+{
+    return append(name, file, vtk::fileTag::POLY_DATA);
+}
+
+
+inline bool Foam::vtk::vtmWriter::append_vtu(const fileName& file)
+{
+    return append(file, vtk::fileTag::UNSTRUCTURED_GRID);
+}
+
+
+inline bool Foam::vtk::vtmWriter::append_vtu
+(
+    const word& name,
+    const fileName& file
+)
+{
+    return append(name, file, vtk::fileTag::UNSTRUCTURED_GRID);
+}
+
+
+// ************************************************************************* //