diff --git a/src/functionObjects/utilities/Make/files b/src/functionObjects/utilities/Make/files
index aed6a6fa70dd76862b514715b14d950ddc2789c8..9da1341dfa4c24e926fd98462203b5aec976a3d3 100644
--- a/src/functionObjects/utilities/Make/files
+++ b/src/functionObjects/utilities/Make/files
@@ -9,6 +9,17 @@ areaWrite/areaWrite.C
 ensightWrite/ensightWrite.C
 ensightWrite/ensightWriteUpdate.C
 
+foamReport/foamReport.C
+foamReport/substitutionModels/substitutionModel/substitutionModel.C
+foamReport/substitutionModels/substitutionModel/substitutionModelNew.C
+foamReport/substitutionModels/dictionaryValue/dictionaryValue.C
+foamReport/substitutionModels/functionObjectValue/functionObjectValue.C
+foamReport/substitutionModels/fileRegEx/fileRegEx.C
+foamReport/substitutionModels/environmentVariable/environmentVariable.C
+foamReport/substitutionModels/userValue/userValue.C
+
+graphFunctionObject/graphFunctionObject.C
+
 vtkWrite/vtkWrite.C
 vtkWrite/vtkWriteUpdate.C
 
diff --git a/src/functionObjects/utilities/foamReport/foamReport.C b/src/functionObjects/utilities/foamReport/foamReport.C
new file mode 100644
index 0000000000000000000000000000000000000000..349fbf777bed5ea7d048a43d5f2a8019d01bf07d
--- /dev/null
+++ b/src/functionObjects/utilities/foamReport/foamReport.C
@@ -0,0 +1,350 @@
+/*---------------------------------------------------------------------------*\
+  =========                 |
+  \\      /  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 "foamReport.H"
+#include "addToRunTimeSelectionTable.H"
+#include "argList.H"
+#include "clock.H"
+#include "cloud.H"
+#include "foamVersion.H"
+#include "fvMesh.H"
+#include "IFstream.H"
+#include "stringOps.H"
+#include "substitutionModel.H"
+
+// * * * * * * * * * * * * * * Static Data Members * * * * * * * * * * * * * //
+
+namespace Foam
+{
+namespace functionObjects
+{
+    defineTypeNameAndDebug(foamReport, 0);
+    addToRunTimeSelectionTable(functionObject, foamReport, dictionary);
+}
+}
+
+
+// * * * * * * * * * * * * Protected Member Functions  * * * * * * * * * * * //
+
+void Foam::functionObjects::foamReport::setStaticBuiltins()
+{
+    substitutionModel::addBuiltinStr("OF_HOST", Foam::hostName());
+    substitutionModel::addBuiltinStr
+    (
+        "OF_PROC_ZERO_DIR",
+        Pstream::parRun() ? "processor0" : ""
+    );
+
+    substitutionModel::addBuiltin("OF_API", foamVersion::api);
+    substitutionModel::addBuiltinStr("OF_PATCH", foamVersion::patch);
+    substitutionModel::addBuiltinStr("OF_BUILD", foamVersion::build);
+    substitutionModel::addBuiltinStr("OF_BUILD_ARCH", foamVersion::buildArch);
+    substitutionModel::addBuiltinStr("OF_VERSION", foamVersion::version);
+
+    substitutionModel::addBuiltinStr("OF_DATE_START", clock::date());
+    substitutionModel::addBuiltinStr("OF_CLOCK_START", clock::clockTime());
+
+    substitutionModel::addBuiltinStr("OF_EXECUTABLE", argList::envExecutable());
+    substitutionModel::addBuiltinStr("OF_CASE_PATH", argList::envGlobalPath());
+    substitutionModel::addBuiltinStr("OF_CASE_NAME", time().globalCaseName());
+
+    substitutionModel::addBuiltin("OF_NPROCS", Pstream::nProcs());
+
+    // Set mesh builtins when there is only 1 mesh
+    const auto meshes = time_.lookupClass<fvMesh>();
+    if (meshes.size() == 1)
+    {
+        const auto& mesh = *(meshes.begin().val());
+        substitutionModel::addBuiltin("OF_MESH_NCELLS", mesh.nCells());
+        substitutionModel::addBuiltin("OF_MESH_NFACES", mesh.nFaces());
+        substitutionModel::addBuiltin("OF_MESH_NEDGES", mesh.nEdges());
+        substitutionModel::addBuiltin("OF_MESH_NPOINTS", mesh.nPoints());
+        substitutionModel::addBuiltin
+        (
+            "OF_MESH_NINTERNALFACES",
+            mesh.nInternalFaces()
+        );
+        substitutionModel::addBuiltin
+        (
+            "OF_MESH_NBOUNDARYFACES",
+            mesh.nBoundaryFaces()
+        );
+        substitutionModel::addBuiltin
+        (
+            "OF_MESH_NPATCHES",
+            mesh.boundaryMesh().nNonProcessor()
+        );
+        substitutionModel::addBuiltin
+        (
+            "OF_MESH_BOUNDS_MIN",
+            mesh.bounds().min()
+        );
+        substitutionModel::addBuiltin
+        (
+            "OF_MESH_BOUNDS_MAX",
+            mesh.bounds().max()
+        );
+    }
+}
+
+
+void Foam::functionObjects::foamReport::setDynamicBuiltins()
+{
+    // Overwrite existing entries
+    substitutionModel::setBuiltinStr("OF_TIME", time().timeName());
+    substitutionModel::setBuiltin("OF_NTIMES", time().times().size());
+    substitutionModel::setBuiltin("OF_TIME_INDEX", time().timeIndex());
+    substitutionModel::setBuiltin("OF_TIME_DELTAT", time().deltaTValue());
+
+    substitutionModel::setBuiltinStr("OF_DATE_NOW", clock::date());
+    substitutionModel::setBuiltinStr("OF_CLOCK_NOW", clock::clockTime());
+
+    substitutionModel::setBuiltin("OF_NREGIONS", time().names<fvMesh>().size());
+    substitutionModel::setBuiltin("OF_NCLOUDS", time().names<cloud>().size());
+}
+
+
+bool Foam::functionObjects::foamReport::parseTemplate(const fileName& fName)
+{
+    Info<< "    Reading template from " << fName << endl;
+
+    IFstream is(fName);
+
+    if (!is.good())
+    {
+        FatalErrorInFunction
+            << "Unable to open file " << fName << endl;
+    }
+
+    DynamicList<string> contents;
+    string buffer;
+
+    label lineNo = 0;
+    while (is.good())
+    {
+        is.getLine(buffer);
+
+        // Collect keys for this line and clean the buffer
+        const wordList keys(substitutionModel::getKeys(buffer));
+
+        Tuple2<label, DynamicList<label>> nullValue(-1, DynamicList<label>());
+
+        // Assemble table of keyword and lines where the keyword appears
+        for (const word& key : keys)
+        {
+            if (modelKeys_.insert(key, nullValue))
+            {
+                // Set substitution model responsible for this keyword
+                label modeli = -1;
+                forAll(substitutions_, i)
+                {
+                    if (substitutions_[i].valid(key))
+                    {
+                        modeli = i;
+                        break;
+                    }
+                }
+
+                // Note: cannot check that key/model is set here
+                // - dynamic builtins not ready yet...
+
+                modelKeys_[key].first() = modeli;
+            }
+
+            DynamicList<label>& lineNos = modelKeys_[key].second();
+            lineNos.push_back(lineNo);
+        }
+
+        contents.push_back(buffer);
+
+        ++lineNo;
+    }
+
+    templateContents_.transfer(contents);
+
+    return templateContents_.size() > 0;
+}
+
+
+bool Foam::functionObjects::foamReport::apply(Ostream& os) const
+{
+    List<string> out(templateContents_);
+
+    forAllConstIters(modelKeys_, iter)
+    {
+        const word& key = iter.key();
+        const label modeli = iter.val().first();
+        const DynamicList<label>& lineNos = iter.val().second();
+
+        DebugInfo<< "key:" << key << endl;
+
+        for (const label linei : lineNos)
+        {
+            if (modeli == -1)
+            {
+                if (!substitutionModel::replaceBuiltin(key, out[linei]))
+                {
+                    WarningInFunction
+                        << "Unable to find substitution for " << key
+                        << " on line " << linei << endl;
+                }
+            }
+            else
+            {
+                substitutions_[modeli].apply(key, out[linei]);
+            }
+        }
+    }
+
+    for (const auto& line : out)
+    {
+        os  << line.c_str() << nl;
+    }
+
+    return true;
+}
+
+
+// * * * * * * * * * * * * * * * * Constructors  * * * * * * * * * * * * * * //
+
+Foam::functionObjects::foamReport::foamReport
+(
+    const word& name,
+    const Time& runTime,
+    const dictionary& dict
+)
+:
+    stateFunctionObject(name, runTime),
+    writeFile(runTime, name, typeName, dict),
+    templateFile_(),
+    modelKeys_(),
+    substitutions_(),
+    debugKeys_(dict.getOrDefault<bool>("debugKeys", false))
+{
+    read(dict);
+
+    setStaticBuiltins();
+}
+
+
+// * * * * * * * * * * * * * * * Member Functions  * * * * * * * * * * * * * //
+
+bool Foam::functionObjects::foamReport::read(const dictionary& dict)
+{
+    if (stateFunctionObject::read(dict))
+    {
+        Info<< type() << " " << name() << ":" << nl;
+
+        dict.readEntry("template", templateFile_);
+
+        Info<< "    Template: " << templateFile_ << endl;
+
+        const word ext = templateFile_.ext();
+
+        if (ext.size())
+        {
+            setExt("." + ext);
+        }
+        else
+        {
+            setExt(ext);
+        }
+
+        Info<< "    Reading substitutions" << endl;
+
+        const dictionary& subsDict = dict.subDict("substitutions");
+
+        substitutions_.resize(subsDict.size());
+
+        label i = 0;
+        for (const entry& e : subsDict)
+        {
+            if (!e.isDict())
+            {
+                FatalIOErrorInFunction(subsDict)
+                    << "Substitution models must be provided in dictionary "
+                    << "format"
+                    << exit(FatalIOError);
+            }
+
+            substitutions_.set(i++, substitutionModel::New(e.dict(), time()));
+        }
+
+        parseTemplate(templateFile_.expand());
+
+        Info<< endl;
+
+        return true;
+    }
+
+    return false;
+}
+
+
+bool Foam::functionObjects::foamReport::execute()
+{
+    for (auto& sub : substitutions_)
+    {
+        sub.update();
+    }
+
+    return true;
+}
+
+
+bool Foam::functionObjects::foamReport::write()
+{
+    if (!Pstream::master()) return true;
+
+    setDynamicBuiltins();
+
+    auto filePtr = newFileAtTime(name(), time().value());
+    auto& os = filePtr();
+
+    // Reset stream width (by default assumes fixed width tabular output)
+    os.width(0);
+
+    // Perform the substitutions
+    apply(os);
+
+    if (debugKeys_)
+    {
+        os  << "Model keys:" << nl;
+        for (const auto& model : substitutions_)
+        {
+            os  << model.type() << ":" << model.keys() << nl;
+        }
+
+        os  << "Builtins:" << nl;
+        substitutionModel::writeBuiltins(os);
+    }
+
+    return true;
+}
+
+
+// ************************************************************************* //
diff --git a/src/functionObjects/utilities/foamReport/foamReport.H b/src/functionObjects/utilities/foamReport/foamReport.H
new file mode 100644
index 0000000000000000000000000000000000000000..52651560cf3de5493d33138cad390c990a3c62c3
--- /dev/null
+++ b/src/functionObjects/utilities/foamReport/foamReport.H
@@ -0,0 +1,273 @@
+/*---------------------------------------------------------------------------*\
+  =========                 |
+  \\      /  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::foamReport
+
+Group
+    grpUtilitiesFunctionObjects
+
+Description
+    Replaces user-supplied keywords by run-time computed values in a text file.
+
+    Operands:
+    \table
+      Operand      | Type                | Location
+      input        | -                   | -
+      output file  | TBA                 <!--
+               --> | postProcessing/\<FO\>/\<time\>/\<file\>(s)
+    \endtable
+
+Usage
+    Example using \c system/controlDict.functions:
+
+    \verbatim
+    foamReport1
+    {
+        // Mandatory entries
+        type            foamReport;
+        libs            (utilityFunctionObjects);
+
+        template        "<system>/myTemplate.md";
+
+        substitutions
+        {
+            divSchemes1
+            {
+                type        dictionaryValue;
+                object      fvSchemes;
+
+                entries
+                {
+                    divSchemes      "divSchemes";
+                }
+            }
+            fvSolution1
+            {
+                type        dictionaryValue;
+                path        "<system>/fvSolution";
+
+                entries
+                {
+                    solver_p        "solvers/p/solver";
+                    solver_p_tol    "solvers/p/tolerance";
+                    solver_p_reltol "solvers/p/relTol";
+                    solver_U        "solvers/U/solver";
+                    solver_U_tol    "solvers/U/tolerance";
+                    solver_U_reltol "solvers/U/relTol";
+                }
+            }
+            controlDict1
+            {
+                type        dictionaryValue;
+                path        "<system>/controlDict";
+
+                entries
+                {
+                    initial_deltaT       "deltaT";
+                }
+            }
+            continuityErrors
+            {
+                type        functionObjectValue;
+                functionObject continuityError1;
+
+                entries
+                {
+                    cont_error_local    local;
+                    cont_error_global   global;
+                    cont_error_cumulative cumulative;
+                }
+            }
+        }
+
+        // Optional entries
+        debugKeys           <bool>;
+
+        // Inherited entries
+        ...
+    }
+    \endverbatim
+
+    where the entries mean:
+    \table
+      Property     | Description                          | Type | Reqd | Deflt
+      type         | Type name: foamReport                | word | yes  | -
+      libs         | Library name: utilityFunctionObjects | word | yes  | -
+      template     | Path to user-supplied text template  | string | yes | -
+      substitutions | Dictionary of substitution models   | dictionary | yes | -
+      debugKeys    | Flag to write all known keys         | bool | no   | false
+    \endtable
+
+    The \c entries sections typically define a dictionary of keys (to use in
+    your template) and method to set the key value, e.g. for a dictionaryValue
+    model used to set values from the \c fvSolution file:
+
+    \verbatim
+        type        dictionaryValue;
+        path        "<system>/fvSolution";
+
+        entries
+        {
+            solver_p        "solvers/p/solver";
+            solver_p_tol    "solvers/p/tolerance";
+        }
+    \endverbatim
+
+    The inherited entries are elaborated in:
+      - \link substitutionModel.H \endlink
+      - \link stateFunctionObject.H \endlink
+      - \link writeFile.H \endlink
+
+See also
+  - Foam::functionObject
+  - Foam::functionObjects::stateFunctionObject
+  - Foam::functionObjects::writeFile
+  - Foam::substitutionModel
+
+SourceFiles
+    foamReport.C
+    foamReportTemplates.C
+
+\*---------------------------------------------------------------------------*/
+
+#ifndef functionObjects_foamReport_H
+#define functionObjects_foamReport_H
+
+#include "stateFunctionObject.H"
+#include "writeFile.H"
+#include "Tuple2.H"
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+namespace Foam
+{
+
+class substitutionModel;
+
+namespace functionObjects
+{
+
+/*---------------------------------------------------------------------------*\
+                         Class foamReport Declaration
+\*---------------------------------------------------------------------------*/
+
+class foamReport
+:
+    public stateFunctionObject,
+    public writeFile
+{
+
+protected:
+
+    // Protected Data
+
+        //- Path to user-supplied template
+        fileName templateFile_;
+
+        //- Mapping from keyword to substitution model index and line
+        //- numbers of template file where keyword is used
+        HashTable<Tuple2<label, DynamicList<label>>> modelKeys_;
+
+        //- Template file contents split into lines
+        List<string> templateContents_;
+
+        //- List of substitution models
+        PtrList<substitutionModel> substitutions_;
+
+        //- Debug flag to write all known keys
+        //  Helps when assembling template file...
+        bool debugKeys_;
+
+
+    // Protected Member Functions
+
+        //- Parse the template and collect keyword information
+        bool parseTemplate(const fileName& fName);
+
+        //- Set static builtin entries
+        void setStaticBuiltins();
+
+        //- Set dynamic (potentially changing per execution step) builtin
+        //- entries
+        void setDynamicBuiltins();
+
+        //- Apply the substitution models to the template
+        bool apply(Ostream& os) const;
+
+
+    // Generated Methods
+
+        //- No copy construct
+        foamReport(const foamReport&) = delete;
+
+        //- No copy assignment
+        void operator=(const foamReport&) = delete;
+
+
+public:
+
+    //- Runtime type information
+    TypeName("foamReport");
+
+
+    // Constructors
+
+        //- Construct from Time and dictionary
+        foamReport
+        (
+            const word& name,
+            const Time& runTime,
+            const dictionary& dict
+        );
+
+
+    //- Destructor
+    virtual ~foamReport() = default;
+
+
+    // Member Functions
+
+        //- Read foamReport settings
+        virtual bool read(const dictionary&);
+
+        //- Execute foamReport
+        virtual bool execute();
+
+        //- Write foamReport results
+        virtual bool write();
+};
+
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+} // End namespace functionObjects
+} // End namespace Foam
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+#endif
+
+// ************************************************************************* //
diff --git a/src/functionObjects/utilities/foamReport/substitutionModels/dictionaryValue/dictionaryValue.C b/src/functionObjects/utilities/foamReport/substitutionModels/dictionaryValue/dictionaryValue.C
new file mode 100644
index 0000000000000000000000000000000000000000..d754ff598c4574d5d6606bbc0332411326389328
--- /dev/null
+++ b/src/functionObjects/utilities/foamReport/substitutionModels/dictionaryValue/dictionaryValue.C
@@ -0,0 +1,219 @@
+/*---------------------------------------------------------------------------*\
+  =========                |
+  \      /  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 "dictionaryValue.H"
+#include "addToRunTimeSelectionTable.H"
+#include "IFstream.H"
+#include "polyMesh.H"
+
+// * * * * * * * * * * * * * * Static Data Members * * * * * * * * * * * * * //
+
+namespace Foam
+{
+namespace substitutionModels
+{
+    defineTypeNameAndDebug(dictionaryValue, 0);
+    addToRunTimeSelectionTable(substitutionModel, dictionaryValue, dictionary);
+}
+}
+
+
+// * * * * * * * * * * * * Protected Member Functions  * * * * * * * * * * * //
+
+bool Foam::substitutionModels::dictionaryValue::processDict
+(
+    const dictionary& dict,
+    const word& key,
+    string& buffer
+) const
+{
+    const string& lookup = entries_[key];
+
+    OStringStream oss;
+    if (lookup.empty())
+    {
+        // Add complete dictionary
+        oss << dict;
+    }
+    else
+    {
+        const entry* ePtr = dict.findScoped(lookup);
+
+        if (!ePtr)
+        {
+            WarningInFunction
+                << "Unable to find entry " << lookup
+                << endl;
+            return false;
+        }
+
+        if (ePtr->isDict())
+        {
+            const dictionary& de = ePtr->dict();
+
+            // Write dictionary contents
+            oss << de.dictName() << de;
+        }
+        else
+        {
+            for (const auto& t : ePtr->stream())
+            {
+                if (oss.count()) oss << separator_;
+                oss << t;
+            }
+        }
+    }
+
+    buffer.replaceAll(keyify(key), oss.str());
+
+    return true;
+}
+
+
+// * * * * * * * * * * * * * * * * Constructors  * * * * * * * * * * * * * * //
+
+Foam::substitutionModels::dictionaryValue::dictionaryValue
+(
+    const dictionary& dict,
+    const Time& time
+)
+:
+    substitutionModel(dict, time),
+    object_(),
+    region_(polyMesh::defaultRegion),
+    path_(),
+    separator_(dict.getOrDefault<word>("separator", " ")),
+    entries_()
+{
+    const auto* oPtr = dict.findEntry("object");
+    const auto* pPtr = dict.findEntry("path");
+
+    if (oPtr && pPtr)
+    {
+        FatalIOErrorInFunction(dict)
+            << "Specify either 'object' or 'path' but not both"
+            << exit(FatalIOError);
+    }
+
+    if (oPtr)
+    {
+        // Optionally read the region
+        dict.readIfPresent<word>("region", region_);
+
+        // Must read the object name to look up
+        object_ = dict.get<word>("object");
+    }
+
+    if (pPtr)
+    {
+        path_ = dict.get<fileName>("path").expand();
+    }
+
+    // Populate entries
+    const dictionary& entriesDict = dict.subDict("entries");
+    for (const auto& e : entriesDict)
+    {
+        entries_.insert(cleanKey(e.keyword()), string(e.stream()));
+    }
+}
+
+
+// * * * * * * * * * * * * * * * Member Functions  * * * * * * * * * * * * * //
+
+bool Foam::substitutionModels::dictionaryValue::valid(const word& keyName) const
+{
+    return entries_.found(keyName);
+}
+
+
+bool Foam::substitutionModels::dictionaryValue::apply
+(
+    const word& key,
+    string& buffer
+) const
+{
+    if (!valid(key)) return false;
+
+    if (path_.size())
+    {
+        fileName path(path_);
+        if (replaceBuiltin(path))
+        {
+            path.clean();
+        }
+
+        IFstream is(path);
+
+        if (!is.good())
+        {
+            WarningInFunction
+                << "Unable to find dictionary at " << path
+                << ". Deactivating." << endl;
+
+            return false;
+        }
+
+        return processDict(dictionary(is), key, buffer);
+    }
+    else
+    {
+        const auto* obrPtr = time_.cfindObject<objectRegistry>(region_);
+
+        if (!obrPtr)
+        {
+            WarningInFunction
+                << "Unable to find region " << region_
+                << ". Deactivating." << endl;
+
+            return false;
+        }
+
+        // Find object; recursive lookup into parent
+        const auto* dictPtr = obrPtr->cfindObject<IOdictionary>(object_, true);
+
+        if (!dictPtr)
+        {
+            WarningInFunction
+                << "Unable find dictionary " << object_
+                << " on region " << region_
+                << ". Deactivating." << endl;
+
+            return false;
+        }
+
+        return processDict(*dictPtr, key, buffer);
+    }
+}
+
+
+Foam::wordList Foam::substitutionModels::dictionaryValue::keys() const
+{
+    return entries_.sortedToc();
+}
+
+
+// ************************************************************************* //
\ No newline at end of file
diff --git a/src/functionObjects/utilities/foamReport/substitutionModels/dictionaryValue/dictionaryValue.H b/src/functionObjects/utilities/foamReport/substitutionModels/dictionaryValue/dictionaryValue.H
new file mode 100644
index 0000000000000000000000000000000000000000..e1cd47506a623f857311cf8c6050ef5d38724ccf
--- /dev/null
+++ b/src/functionObjects/utilities/foamReport/substitutionModels/dictionaryValue/dictionaryValue.H
@@ -0,0 +1,182 @@
+/*---------------------------------------------------------------------------*\
+  =========                |
+  \      /  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::substitutionModels::dictionaryValue
+
+Description
+    The \c dictionaryValue substitution model. Dictionaries can be retrieved
+    from an object registry, e.g. time, mesh, or from file.
+
+    The example below shows how the keywords \c p_solver and \c u_solver are set
+    by retrieving values from the \c fvSolution dictionary.
+
+    \verbatim
+    dictionaryValues1
+    {
+        // Mandatory entries
+        type        dictionaryValue;
+
+        entries
+        {
+            p_solver    "solvers/p/solver";
+            u_solver    "solvers/u/solver";
+        }
+
+        // Conditional entries
+
+            // Option-1
+            object      "fvSolution";  // registry-based retrieval
+            // region      "fluidMesh";
+
+            // Option-2
+            // path        "<system>/fvSolution"; // file-based retrieval
+
+
+        // Optional entries
+        separator       <word>;
+
+        // Inherited entries
+        ...
+    }
+    \endverbatim
+
+    The entries mean:
+    \table
+      Property     | Description                        | Type | Reqd  | Deflt
+      type         | Type name: dictionaryValue         | word |  yes  | -
+      entries      | keyword lookup pairs               | dictionary | yes | -
+      object       | Name of registered dictionary      | string | no  | -
+      region       | Name of mesh region                | word | no  | region0
+      path         | Path to dictionary file            | string | no  | -
+      separator | Sep. when lookup value has multiple tokens | word | no | -
+    \endtable
+
+    The inherited entries are elaborated in:
+      - \link substitutionModel.H \endlink
+
+SourceFiles
+    dictionaryValue.C
+
+---------------------------------------------------------------------------*/
+
+#ifndef Foam_substitutionModels_dictionaryValue_H
+#define Foam_substitutionModels_dictionaryValue_H
+
+#include "substitutionModel.H"
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+namespace Foam
+{
+
+namespace substitutionModels
+{
+
+/*---------------------------------------------------------------------------*\
+                       Class dictionaryValue Declaration
+\*---------------------------------------------------------------------------*/
+
+class dictionaryValue
+:
+    public substitutionModel
+{
+    // Private Data
+
+        //- Dictionary name for registry-based lookup
+        word object_;
+
+        //- Region name for registry-based lookup
+        word region_;
+
+        //- Path to dictionary for file-based lookup
+        fileName path_;
+
+        //- Separator when lookup value has multiple tokens
+        const word separator_;
+
+        //- Hash table for key and entry-lookup pairs
+        HashTable<string> entries_;
+
+
+    // Private Functions
+
+        //- No copy construct
+        dictionaryValue(const dictionaryValue&) = delete;
+
+        //- No copy assignment
+        void operator=(const dictionaryValue&) = delete;
+
+
+protected:
+
+    // Protected Member Functions
+
+        //- Main function to process the dictionary
+        bool processDict
+        (
+            const dictionary& dict,
+            const word& key,
+            string& buffer
+        ) const;
+
+
+public:
+
+    //- Runtime type information
+    TypeName("dictionaryValue");
+
+
+    //- Constructor
+    dictionaryValue(const dictionary& dict, const Time& time);
+
+
+    //- Destructor
+    virtual ~dictionaryValue() = default;
+
+
+    // Member Functions
+
+        //- Return true of model applies to this keyName
+        virtual bool valid(const word& keyName) const;
+
+        //- Apply substitutions to this string buffer
+        virtual bool apply(const word& key, string& buffer) const;
+
+        //- Return a word list of the keys
+        virtual wordList keys() const;
+};
+
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+} // End namespace substitutionModels
+} // End namespace Foam
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+#endif
+
+// ************************************************************************* //
\ No newline at end of file
diff --git a/src/functionObjects/utilities/foamReport/substitutionModels/environmentVariable/environmentVariable.C b/src/functionObjects/utilities/foamReport/substitutionModels/environmentVariable/environmentVariable.C
new file mode 100644
index 0000000000000000000000000000000000000000..49e1951e02ae586b20e856b542f434baf753185d
--- /dev/null
+++ b/src/functionObjects/utilities/foamReport/substitutionModels/environmentVariable/environmentVariable.C
@@ -0,0 +1,101 @@
+/*---------------------------------------------------------------------------*\
+  =========                |
+  \      /  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 "environmentVariable.H"
+#include "addToRunTimeSelectionTable.H"
+
+// * * * * * * * * * * * * * * Static Data Members * * * * * * * * * * * * * //
+
+namespace Foam
+{
+namespace substitutionModels
+{
+    defineTypeNameAndDebug(environmentVariable, 0);
+    addToRunTimeSelectionTable
+    (
+        substitutionModel,
+        environmentVariable,
+        dictionary
+    );
+}
+}
+
+
+// * * * * * * * * * * * * * * * * Constructors  * * * * * * * * * * * * * * //
+
+Foam::substitutionModels::environmentVariable::environmentVariable
+(
+    const dictionary& dict,
+    const Time& time
+)
+:
+    substitutionModel(dict, time),
+    entries_()
+{
+    // Populate entries
+    const dictionary& entriesDict = dict.subDict("entries");
+    for (const auto& e : entriesDict)
+    {
+        entries_.insert(cleanKey(e.keyword()), string(e.stream()));
+    }
+}
+
+
+// * * * * * * * * * * * * * * * Member Functions  * * * * * * * * * * * * * //
+
+bool Foam::substitutionModels::environmentVariable::valid
+(
+    const word& keyName
+) const
+{
+    return entries_.found(keyName);
+}
+
+
+bool Foam::substitutionModels::environmentVariable::apply
+(
+    const word& key,
+    string& buffer
+) const
+{
+    if (!valid(key)) return false;
+
+    const string env(Foam::getEnv(entries_[key]));
+
+    buffer.replaceAll(keyify(key), env);
+
+    return true;
+}
+
+
+Foam::wordList Foam::substitutionModels::environmentVariable::keys() const
+{
+    return entries_.sortedToc();
+}
+
+
+// ************************************************************************* //
\ No newline at end of file
diff --git a/src/functionObjects/utilities/foamReport/substitutionModels/environmentVariable/environmentVariable.H b/src/functionObjects/utilities/foamReport/substitutionModels/environmentVariable/environmentVariable.H
new file mode 100644
index 0000000000000000000000000000000000000000..d72088e823b4219b27e19741fe0e3a1f6a8672bf
--- /dev/null
+++ b/src/functionObjects/utilities/foamReport/substitutionModels/environmentVariable/environmentVariable.H
@@ -0,0 +1,136 @@
+/*---------------------------------------------------------------------------*\
+  =========                |
+  \      /  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::substitutionModels::environmentVariable
+
+Description
+    The \c environmentVariable substitution model.
+
+    \verbatim
+    environmentVariables1
+    {
+        // Mandatory entries
+        type        environmentVariable;
+
+        entries
+        {
+            home    "HOME";
+            ldpath  "LD_LIBRARY_PATH";
+        }
+
+        // Inherited entries
+        ...
+    }
+    \endverbatim
+
+    The entries mean:
+    \table
+      Property     | Description                        | Type | Reqd  | Deflt
+      type         | Type name: environmentVariable     | word |  yes  | -
+      entries      | Keyword lookup pairs               | dictionary | yes | -
+    \endtable
+
+    The inherited entries are elaborated in:
+      - \link substitutionModel.H \endlink
+
+SourceFiles
+    environmentVariable.C
+
+---------------------------------------------------------------------------*/
+
+#ifndef Foam_substitutionModels_environmentVariable_H
+#define Foam_substitutionModels_environmentVariable_H
+
+#include "substitutionModel.H"
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+namespace Foam
+{
+
+namespace substitutionModels
+{
+
+/*---------------------------------------------------------------------------*\
+                  Class environmentVariable Declaration
+\*---------------------------------------------------------------------------*/
+
+class environmentVariable
+:
+    public substitutionModel
+{
+    // Private Data
+
+        //- Hash table for key and environment variable pairs
+        HashTable<string> entries_;
+
+
+    // Private Functions
+
+        //- No copy construct
+        environmentVariable(const environmentVariable&) = delete;
+
+        //- No copy assignment
+        void operator=(const environmentVariable&) = delete;
+
+
+public:
+
+    //- Runtime type information
+    TypeName("environmentVariable");
+
+
+    //- Constructor
+    environmentVariable(const dictionary& dict, const Time& time);
+
+
+    //- Destructor
+    virtual ~environmentVariable() = default;
+
+
+    // Member Functions
+
+        //- Return true of model applies to this keyName
+        virtual bool valid(const word& keyName) const;
+
+        //- Apply substitutions to this string buffer
+        virtual bool apply(const word& key, string& buffer) const;
+
+        //- Return a word list of the keys
+        virtual wordList keys() const;
+};
+
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+} // End namespace substitutionModels
+} // End namespace Foam
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+#endif
+
+// ************************************************************************* //
\ No newline at end of file
diff --git a/src/functionObjects/utilities/foamReport/substitutionModels/fileRegEx/fileRegEx.C b/src/functionObjects/utilities/foamReport/substitutionModels/fileRegEx/fileRegEx.C
new file mode 100644
index 0000000000000000000000000000000000000000..37fc3f0d68c9dbc76f873adec166d5d1b8251ce2
--- /dev/null
+++ b/src/functionObjects/utilities/foamReport/substitutionModels/fileRegEx/fileRegEx.C
@@ -0,0 +1,163 @@
+/*---------------------------------------------------------------------------*\
+  =========                |
+  \      /  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 "fileRegEx.H"
+#include "addToRunTimeSelectionTable.H"
+#include "IFstream.H"
+
+// * * * * * * * * * * * * * * Static Data Members * * * * * * * * * * * * * //
+
+namespace Foam
+{
+namespace substitutionModels
+{
+    defineTypeNameAndDebug(fileRegEx, 0);
+    addToRunTimeSelectionTable(substitutionModel, fileRegEx, dictionary);
+}
+}
+
+// * * * * * * * * * * * * * * * * Constructors  * * * * * * * * * * * * * * //
+
+Foam::substitutionModels::fileRegEx::fileRegEx
+(
+    const dictionary& dict,
+    const Time& time
+)
+:
+    substitutionModel(dict, time),
+    path_(dict.get<fileName>("path")),
+    entries_(),
+    sectionSeparator_
+    (
+        dict.getOrDefault<string>
+        (
+            "sectionSeparator",
+            "Time ="
+        )
+    ),
+    matchSeparator_(dict.getOrDefault<string>("matchSeparator", " ")),
+    lastMatch_(dict.getOrDefault<bool>("lastMatch", true))
+{
+    // Populate entries
+    const dictionary& entriesDict = dict.subDict("entries");
+    for (const auto& e : entriesDict)
+    {
+        entries_.insert(cleanKey(e.keyword()), string(e.stream()));
+    }
+}
+
+
+// * * * * * * * * * * * * * * * Member Functions  * * * * * * * * * * * * * //
+
+bool Foam::substitutionModels::fileRegEx::valid(const word& keyName) const
+{
+    return entries_.found(keyName);
+}
+
+
+bool Foam::substitutionModels::fileRegEx::apply
+(
+    const word& key,
+    string& buffer
+) const
+{
+    if (!valid(key)) return false;
+
+    fileName path(path_);
+    replaceBuiltin(path);
+    IFstream is(path);
+
+    if (!is.good())
+    {
+        WarningInFunction
+            << "Unable to find file at " << path_
+            << ". Deactivating." << endl;
+
+        return false;
+    }
+
+    Info<< "Scanning for sections beginning with "
+        << sectionSeparator_ << endl;
+
+    // For log files containing multiple time steps
+    // - put strings for last time step into a string list
+    DynamicList<string> lines(96);
+    string line;
+    bool started = sectionSeparator_.empty() ? true : false;
+    while (is.good())
+    {
+        is.getLine(line);
+        if (line.starts_with(sectionSeparator_))
+        {
+            started = true;
+            lines.clear();
+        }
+        if (started)
+        {
+            lines.append(line);
+        }
+    }
+
+    Info<< "Cached " << lines.size() << " lines" << endl;
+
+    OStringStream oss;
+    regExp re(entries_[key].c_str());
+
+    for (const string& data : lines)
+    {
+        regExp::results_type match;
+        if (re.match(data, match))
+        {
+            oss.reset();
+
+            for (size_t i = 1; i < match.size(); ++i)
+            {
+                if (i > 1) oss << matchSeparator_;
+                oss << match[i].str().c_str();
+            }
+
+            if (!lastMatch_) break;
+        }
+    }
+
+    if (oss.count())
+    {
+        buffer.replaceAll(keyify(key), oss.str());
+        return true;
+    }
+
+    return false;
+}
+
+
+Foam::wordList Foam::substitutionModels::fileRegEx::keys() const
+{
+    return entries_.sortedToc();
+}
+
+
+// ************************************************************************* //
\ No newline at end of file
diff --git a/src/functionObjects/utilities/foamReport/substitutionModels/fileRegEx/fileRegEx.H b/src/functionObjects/utilities/foamReport/substitutionModels/fileRegEx/fileRegEx.H
new file mode 100644
index 0000000000000000000000000000000000000000..891a1d6858f27fd5551322afb3b4997e0246ef37
--- /dev/null
+++ b/src/functionObjects/utilities/foamReport/substitutionModels/fileRegEx/fileRegEx.H
@@ -0,0 +1,162 @@
+/*---------------------------------------------------------------------------*\
+  =========                |
+  \      /  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::substitutionModels::fileRegEx
+
+Description
+    The \c fileRegEx substitution model.
+
+    The example below shows how the keyword \c executionTime is set by
+    applying a regular expression (string) to a log file.
+
+    \verbatim
+    fileRegEx1
+    {
+        // Mandatory entries
+        type        fileRegEx;
+        path        "log.simpleFoam";
+
+        entries
+        {
+            executionTime    "ExecutionTime = (.*) s  Clock.*";
+        }
+
+        // Optional entries
+        sectionSeparator    <string>;
+        matchSeparator      <string>;
+        lastMatch           <bool>;
+
+        // Inherited entries
+        ...
+    }
+    \endverbatim
+
+    The entries mean:
+    \table
+      Property     | Description                        | Type | Reqd  | Deflt
+      type         | Type name: functionObjectValue     | word |  yes  | -
+      path         | Path to file                       | string |  yes  | -
+      entries      | Keyword regular-expression pairs   | dictionary | yes | -
+      sectionSeparator | Marker used to separate files into sections  <!--
+            --!>   | string | no | "Time ="
+      matchSeparator | Separator used to join multiple values <!--
+            --!>   | string | no | " "
+      lastMatch    | Flag to use last file section      | bool | no    | yes
+    \endtable
+
+    The inherited entries are elaborated in:
+      - \link substitutionModel.H \endlink
+
+SourceFiles
+    fileRegEx.C
+
+---------------------------------------------------------------------------*/
+
+#ifndef Foam_substitutionModels_fileRegEx_H
+#define Foam_substitutionModels_fileRegEx_H
+
+#include "substitutionModel.H"
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+namespace Foam
+{
+
+namespace substitutionModels
+{
+
+/*---------------------------------------------------------------------------*\
+                       Class fileRegEx Declaration
+\*---------------------------------------------------------------------------*/
+
+class fileRegEx
+:
+    public substitutionModel
+{
+    // Private Data
+
+        //- Path to dictionary
+        const fileName path_;
+
+        //- Hash table for key and regular expression pairs
+        HashTable<string> entries_;
+
+        //- Section separator to dive log files, e.g. into time step info
+        const string sectionSeparator_;
+
+        //- Separator to apply between (multiple) matches
+        const string matchSeparator_;
+
+        //- Last match wins flag
+        bool lastMatch_;
+
+
+    // Private Functions
+
+        //- No copy construct
+        fileRegEx(const fileRegEx&) = delete;
+
+        //- No copy assignment
+        void operator=(const fileRegEx&) = delete;
+
+
+public:
+
+    //- Runtime type information
+    TypeName("fileRegEx");
+
+
+    //- Constructor
+    fileRegEx(const dictionary& dict, const Time& time);
+
+
+    //- Destructor
+    virtual ~fileRegEx() = default;
+
+
+    // Member Functions
+
+        //- Return true of model applies to this keyName
+        virtual bool valid(const word& keyName) const;
+
+        //- Apply substitutions to this string buffer
+        virtual bool apply(const word& key, string& buffer) const;
+
+        //- Return a word list of the keys
+        virtual wordList keys() const;
+};
+
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+} // End namespace substitutionModels
+} // End namespace Foam
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+#endif
+
+// ************************************************************************* //
\ No newline at end of file
diff --git a/src/functionObjects/utilities/foamReport/substitutionModels/functionObjectValue/functionObjectValue.C b/src/functionObjects/utilities/foamReport/substitutionModels/functionObjectValue/functionObjectValue.C
new file mode 100644
index 0000000000000000000000000000000000000000..34f5188331581d43a6d755bc923f424c9a829aa3
--- /dev/null
+++ b/src/functionObjects/utilities/foamReport/substitutionModels/functionObjectValue/functionObjectValue.C
@@ -0,0 +1,149 @@
+/*---------------------------------------------------------------------------*\
+  =========                |
+  \      /  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 "functionObjectValue.H"
+#include "addToRunTimeSelectionTable.H"
+#include "IFstream.H"
+
+// * * * * * * * * * * * * * * Static Data Members * * * * * * * * * * * * * //
+
+namespace Foam
+{
+namespace substitutionModels
+{
+    defineTypeNameAndDebug(functionObjectValue, 0);
+    addToRunTimeSelectionTable
+    (
+        substitutionModel,
+        functionObjectValue,
+        dictionary
+    );
+}
+}
+
+// * * * * * * * * * * * * Protected Member Functions  * * * * * * * * * * * //
+
+template<class Type>
+bool Foam::substitutionModels::functionObjectValue::getValue
+(
+    OStringStream& oss,
+    const word& lookup
+) const
+{
+    const auto& foProps = time_.functionObjects().propsDict();
+
+    Type result;
+    if (foProps.getObjectResult(functionObject_, lookup, result))
+    {
+        oss << result;
+        return true;
+    }
+
+    return false;
+}
+
+
+// * * * * * * * * * * * * * * * * Constructors  * * * * * * * * * * * * * * //
+
+Foam::substitutionModels::functionObjectValue::functionObjectValue
+(
+    const dictionary& dict,
+    const Time& time
+)
+:
+    substitutionModel(dict, time),
+    functionObject_(dict.get<word>("functionObject")),
+    entries_(),
+    debugValues_(dict.getOrDefault<bool>("debugValues", false))
+{
+    // Populate entries
+    const dictionary& entriesDict = dict.subDict("entries");
+    for (const auto& e : entriesDict)
+    {
+        entries_.insert(cleanKey(e.keyword()), word(e.stream()));
+    }
+}
+
+
+// * * * * * * * * * * * * * * * Member Functions  * * * * * * * * * * * * * //
+
+bool Foam::substitutionModels::functionObjectValue::update()
+{
+    if (debugValues_)
+    {
+        Info<< nl << "Function object results:" << nl;
+        time_.functionObjects().propsDict().writeAllResultEntries(Info);
+    }
+
+    return true;
+}
+
+
+bool Foam::substitutionModels::functionObjectValue::valid
+(
+    const word& keyName
+) const
+{
+    return entries_.found(keyName);
+}
+
+
+bool Foam::substitutionModels::functionObjectValue::apply
+(
+    const word& key,
+    string& buffer
+) const
+{
+    if (!valid(key)) return false;
+
+    OStringStream oss;
+
+    const word& lookup = entries_[key];
+
+    bool ok =
+        getValue<label>(oss, lookup)
+     || getValue<scalar>(oss, lookup)
+     || getValue<vector>(oss, lookup)
+     || getValue<sphericalTensor>(oss, lookup)
+     || getValue<symmTensor>(oss, lookup)
+     || getValue<tensor>(oss, lookup);
+
+    if (!ok) return false;
+
+    buffer.replaceAll(keyify(key), oss.str());
+
+    return true;
+}
+
+
+Foam::wordList Foam::substitutionModels::functionObjectValue::keys() const
+{
+    return entries_.sortedToc();
+}
+
+
+// ************************************************************************* //
\ No newline at end of file
diff --git a/src/functionObjects/utilities/foamReport/substitutionModels/functionObjectValue/functionObjectValue.H b/src/functionObjects/utilities/foamReport/substitutionModels/functionObjectValue/functionObjectValue.H
new file mode 100644
index 0000000000000000000000000000000000000000..024ef6774c50e7234bcad368ab41432d8472f13e
--- /dev/null
+++ b/src/functionObjects/utilities/foamReport/substitutionModels/functionObjectValue/functionObjectValue.H
@@ -0,0 +1,167 @@
+/*---------------------------------------------------------------------------*\
+  =========                |
+  \      /  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::substitutionModels::functionObjectValue
+
+Description
+    functionObjectValue substitution model.
+
+Usage
+    The \c functionObjectValue substitution model.
+
+    The example below shows how the keywords \c cont_error_* are set by
+    retrieving the values \c local, \c global, \c cumulative from the function
+    object \c functionObjectValue.
+
+    \verbatim
+    functionObjectValue1
+    {
+        // Mandatory entries
+        type        functionObjectValue;
+        functionObject continuityError1;
+
+        entries
+        {
+            cont_error_local    local;
+            cont_error_global   global;
+            cont_error_cumulative cumulative;
+        }
+
+        // Optional entries
+        debugValues   <bool>;
+
+        // Inherited entries
+        ...
+    }
+    \endverbatim
+
+    The entries mean:
+    \table
+      Property     | Description                        | Type | Reqd  | Deflt
+      type         | Type name: functionObjectValue     | word |  yes  | -
+      functionObject | Name of function object to query | word |  yes  | -
+      entries      | Keyword-lookup pairs               | dictionary | yes | -
+      debugValues  | Flag to show available function values | bool | no | false
+    \endtable
+
+    The inherited entries are elaborated in:
+      - \link substitutionModel.H \endlink
+
+SourceFiles
+    functionObjectValue.C
+
+---------------------------------------------------------------------------*/
+
+#ifndef Foam_substitutionModels_functionObjectValue_H
+#define Foam_substitutionModels_functionObjectValue_H
+
+#include "substitutionModel.H"
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+namespace Foam
+{
+
+namespace substitutionModels
+{
+
+/*---------------------------------------------------------------------------*\
+                       Class functionObjectValue Declaration
+\*---------------------------------------------------------------------------*/
+
+class functionObjectValue
+:
+    public substitutionModel
+{
+    // Private Data
+
+        //- Name of function object
+        const word functionObject_;
+
+        //- Hash table for key and entry-lookup pairs
+        HashTable<word> entries_;
+
+        //- Debug - shows available function values
+        bool debugValues_;
+
+
+    // Private Functions
+
+        //- Get the result value from the function object
+        template<class Type>
+        bool getValue(OStringStream& oss, const word& lookup) const;
+
+        //- No copy construct
+        functionObjectValue(const functionObjectValue&) = delete;
+
+        //- No copy assignment
+        void operator=(const functionObjectValue&) = delete;
+
+
+public:
+
+    //- Runtime type information
+    TypeName("functionObjectValue");
+
+
+    //- Constructor
+    functionObjectValue
+    (
+        const dictionary& dict,
+        const Time& time
+    );
+
+
+    //- Destructor
+    virtual ~functionObjectValue() = default;
+
+
+    // Member Functions
+
+        //- Update model local data
+        virtual bool update();
+
+        //- Return true of model applies to this keyName
+        virtual bool valid(const word& keyName) const;
+
+        //- Apply substitutions to this string buffer
+        virtual bool apply(const word& key, string& buffer) const;
+
+        //- Return a word list of the keys
+        virtual wordList keys() const;
+};
+
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+} // End namespace substitutionModels
+} // End namespace Foam
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+#endif
+
+// ************************************************************************* //
\ No newline at end of file
diff --git a/src/functionObjects/utilities/foamReport/substitutionModels/substitutionModel/substitutionModel.C b/src/functionObjects/utilities/foamReport/substitutionModels/substitutionModel/substitutionModel.C
new file mode 100644
index 0000000000000000000000000000000000000000..bac232c9516473341f34ff89750da8498f07bd43
--- /dev/null
+++ b/src/functionObjects/utilities/foamReport/substitutionModels/substitutionModel/substitutionModel.C
@@ -0,0 +1,167 @@
+/*---------------------------------------------------------------------------*\
+  =========                |
+  \      /  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 "substitutionModel.H"
+#include "stringOps.H"
+
+// * * * * * * * * * * * * * * Static Data Members * * * * * * * * * * * * * //
+
+namespace Foam
+{
+    defineTypeNameAndDebug(substitutionModel, 0);
+    defineRunTimeSelectionTable(substitutionModel, dictionary);
+}
+
+const Foam::word Foam::substitutionModel::KEY_BEGIN = "{{";
+const Foam::word Foam::substitutionModel::KEY_END = "}}";
+Foam::HashTable<Foam::string> Foam::substitutionModel::builtin_;
+
+// * * * * * * * * * * * * * Static Member Functions * * * * * * * * * * * * //
+
+Foam::string Foam::substitutionModel::keyify(const word& w)
+{
+    return KEY_BEGIN + w + KEY_END;
+}
+
+
+Foam::word Foam::substitutionModel::cleanKey(const string& str)
+{
+    return stringOps::upper(stringOps::trim(str));
+};
+
+
+Foam::wordList Foam::substitutionModel::getKeys(string& buffer)
+{
+    const label lBegin = KEY_BEGIN.length();
+    const label lEnd = KEY_END.length();
+
+    wordHashSet keys;
+
+    size_t pos0 = 0;
+    size_t pos = 0;
+    string cleanedBuffer = "";
+    while (((pos = buffer.find(KEY_BEGIN, pos)) != string::npos))
+    {
+        cleanedBuffer += buffer.substr(pos0, pos-pos0);
+
+        size_t posEnd = buffer.find(KEY_END, pos);
+
+        if (posEnd != string::npos)
+        {
+            const word k(cleanKey(buffer.substr(pos+lBegin, posEnd-pos-lEnd)));
+            keys.insert(k);
+            cleanedBuffer += keyify(k);
+        }
+
+        pos = posEnd + lEnd;
+        pos0 = pos;
+    }
+
+    cleanedBuffer += buffer.substr(pos0, buffer.length() - pos0);
+    buffer = cleanedBuffer;
+
+    return keys.toc();
+}
+
+
+void Foam::substitutionModel::addBuiltinStr
+(
+    const word& key,
+    const string& value
+)
+{
+    builtin_.insert(cleanKey(key), value.c_str());
+}
+
+
+bool Foam::substitutionModel::containsBuiltin(const word& key)
+{
+    return builtin_.contains(key);
+}
+
+
+void Foam::substitutionModel::setBuiltinStr
+(
+    const word& key,
+    const string& value
+)
+{
+    builtin_.set(cleanKey(key), value.c_str());
+}
+
+
+bool Foam::substitutionModel::replaceBuiltin(const word& key, string& str)
+{
+    if (builtin_.found(key))
+    {
+        str.replaceAll(keyify(key), builtin_[key].c_str());
+        return true;
+    }
+
+    return false;
+}
+
+
+bool Foam::substitutionModel::replaceBuiltin(string& str)
+{
+    const string str0 = str;
+
+    // Quick exit if there are no keys in the string
+    if (str.find(KEY_BEGIN) == string::npos) return false;
+
+    forAllConstIters(builtin_, iter)
+    {
+        str.replaceAll(keyify(iter.key()), iter.val().c_str());
+    }
+
+    return str != str0;
+}
+
+
+void Foam::substitutionModel::writeBuiltins(Ostream& os)
+{
+    for (const auto& iter : builtin_.csorted())
+    {
+        os  << keyify(iter.key()).c_str() << " : " << iter.val() << nl;
+    }
+}
+
+
+// * * * * * * * * * * * * * * * * Constructors  * * * * * * * * * * * * * * //
+
+Foam::substitutionModel::substitutionModel
+(
+    const dictionary& dict,
+    const Time& time
+)
+:
+    dict_(dict),
+    time_(time)
+{}
+
+
+// ************************************************************************* //
\ No newline at end of file
diff --git a/src/functionObjects/utilities/foamReport/substitutionModels/substitutionModel/substitutionModel.H b/src/functionObjects/utilities/foamReport/substitutionModels/substitutionModel/substitutionModel.H
new file mode 100644
index 0000000000000000000000000000000000000000..d4c474416bba3df2798ac03fecad09a5aae11e87
--- /dev/null
+++ b/src/functionObjects/utilities/foamReport/substitutionModels/substitutionModel/substitutionModel.H
@@ -0,0 +1,206 @@
+/*---------------------------------------------------------------------------*\
+  =========                |
+  \      /  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::substitutionModel
+
+Description
+    Base class for substitution models.
+
+    Provides a static hash table for builtin keyword-value pairs and functions
+    to manipulate/interact.
+
+SourceFiles
+    substitutionModel.C
+    substitutionModelNew.C
+
+---------------------------------------------------------------------------*/
+
+#ifndef Foam_substitutionModel_H
+#define Foam_substitutionModel_H
+
+#include "runTimeSelectionTables.H"
+#include "dictionary.H"
+#include "Time.H"
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+namespace Foam
+{
+
+/*---------------------------------------------------------------------------*\
+                      Class substitutionModel Declaration
+\*---------------------------------------------------------------------------*/
+
+class substitutionModel
+{
+public:
+
+    // Static Data Members
+
+        //- Keyword starting characters
+        static const word KEY_BEGIN;
+
+        //- Keyword ending characters
+        static const word KEY_END;
+
+        //- Built-in substitutions
+        static HashTable<string> builtin_;
+
+
+    // Static Member Functions
+
+        //- Return a key representation from a word
+        static string keyify(const word& w);
+
+        //- Clean the key text
+        static word cleanKey(const string& str);
+
+        //- Return all keys from a string buffer
+        //  Also cleans the key strings in the buffer
+        static wordList getKeys(string& buffer);
+
+        //- Add a builtin to the hash table - does not overwrite
+        static void addBuiltinStr(const word& key, const string& value);
+
+        //- Add a builtin to the hash table - does not overwrite
+        template<class Type>
+        static void addBuiltin(const word& key, const Type& value);
+
+        //- Return true if key is builtin
+        static bool containsBuiltin(const word& key);
+
+        //- Set a builtin to the hash table
+        static void setBuiltinStr(const word& key, const string& value);
+
+        //- Set a builtin to the hash table
+        template<class Type>
+        static void setBuiltin(const word& key, const Type& value);
+
+        //- Replace key in string
+        static bool replaceBuiltin(const word& key, string& str);
+
+        //- Replace all occurrences of key in string
+        static bool replaceBuiltin(string& str);
+
+        //- Write all builtins to stream
+        static void writeBuiltins(Ostream& os);
+
+
+private:
+
+    // Private Functions
+
+        //- No copy construct
+        substitutionModel(const substitutionModel&) = delete;
+
+        //- No copy assignment
+        void operator=(const substitutionModel&) = delete;
+
+
+protected:
+
+    // Protected Data
+
+        //- Construction dictionary
+        const dictionary dict_;
+
+        //- Reference to the time database
+        const Time& time_;
+
+
+public:
+
+    //- Runtime type information
+    TypeName("substitutionModel");
+
+    // Declare run-time constructor selection table
+
+        declareRunTimeSelectionTable
+        (
+            autoPtr,
+            substitutionModel,
+            dictionary,
+            (
+                const dictionary& dict,
+                const Time& time
+            ),
+            (dict, time)
+        );
+
+
+    // Selectors
+
+        //- Return a reference to the selected substitution model
+        static autoPtr<substitutionModel> New
+        (
+            const dictionary& dict,
+            const Time& time
+        );
+
+
+    //- Constructor
+    substitutionModel
+    (
+        const dictionary& dict,
+        const Time& time
+    );
+
+
+    //- Destructor
+    virtual ~substitutionModel() = default;
+
+
+    // Member Functions
+
+        //- Update model local data
+        virtual bool update() { return true; }
+
+        //- Return true of model applies to this keyName
+        virtual bool valid(const word& keyName) const = 0;
+
+        //- Apply substitutions to this string buffer
+        virtual bool apply(const word& key, string& buffer) const = 0;
+
+        //- Return a word list of the keys
+        virtual wordList keys() const = 0;
+};
+
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+} // End namespace Foam
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+#ifdef NoRepository
+    #include "substitutionModelTemplates.C"
+#endif
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+#endif
+
+// ************************************************************************* //
\ No newline at end of file
diff --git a/src/functionObjects/utilities/foamReport/substitutionModels/substitutionModel/substitutionModelNew.C b/src/functionObjects/utilities/foamReport/substitutionModels/substitutionModel/substitutionModelNew.C
new file mode 100644
index 0000000000000000000000000000000000000000..c284136c373d29e39bd032209b7696deaabb0798
--- /dev/null
+++ b/src/functionObjects/utilities/foamReport/substitutionModels/substitutionModel/substitutionModelNew.C
@@ -0,0 +1,60 @@
+/*---------------------------------------------------------------------------*\
+  =========                |
+  \      /  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 "substitutionModel.H"
+#include "error.H"
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+Foam::autoPtr<Foam::substitutionModel> Foam::substitutionModel::New
+(
+    const dictionary& dict,
+    const Time& time
+)
+{
+    const word modelType(dict.get<word>("type"));
+
+    Info<< "        Selecting substitution model " << modelType << endl;
+
+    auto* ctorPtr = dictionaryConstructorTable(modelType);
+
+    if (!ctorPtr)
+    {
+        FatalIOErrorInLookup
+        (
+            dict,
+            "substitutionModel",
+            modelType,
+            *dictionaryConstructorTablePtr_
+        ) << exit(FatalIOError);
+    }
+
+    return autoPtr<substitutionModel>(ctorPtr(dict, time));
+}
+
+
+// ************************************************************************* //
diff --git a/src/functionObjects/utilities/foamReport/substitutionModels/substitutionModel/substitutionModelTemplates.C b/src/functionObjects/utilities/foamReport/substitutionModels/substitutionModel/substitutionModelTemplates.C
new file mode 100644
index 0000000000000000000000000000000000000000..71a4a90b83a73e8e021e45cdd5cc1cfaaa963d38
--- /dev/null
+++ b/src/functionObjects/utilities/foamReport/substitutionModels/substitutionModel/substitutionModelTemplates.C
@@ -0,0 +1,47 @@
+/*---------------------------------------------------------------------------*\
+  =========                |
+  \      /  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/>.
+
+\*---------------------------------------------------------------------------*/
+
+template<class Type>
+void Foam::substitutionModel::addBuiltin(const word& key, const Type& value)
+{
+    OStringStream oss;
+    oss << value;
+    addBuiltinStr(key, oss.str());
+}
+
+
+template<class Type>
+void Foam::substitutionModel::setBuiltin(const word& key, const Type& value)
+{
+    OStringStream oss;
+    oss << value;
+
+    setBuiltinStr(key, oss.str());
+}
+
+
+// ************************************************************************* //
diff --git a/src/functionObjects/utilities/foamReport/substitutionModels/userValue/userValue.C b/src/functionObjects/utilities/foamReport/substitutionModels/userValue/userValue.C
new file mode 100644
index 0000000000000000000000000000000000000000..4c05164c32f3692b880c810bb5bfdece26bfb96c
--- /dev/null
+++ b/src/functionObjects/utilities/foamReport/substitutionModels/userValue/userValue.C
@@ -0,0 +1,99 @@
+/*---------------------------------------------------------------------------*\
+  =========                |
+  \      /  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 "userValue.H"
+#include "addToRunTimeSelectionTable.H"
+
+// * * * * * * * * * * * * * * Static Data Members * * * * * * * * * * * * * //
+
+namespace Foam
+{
+namespace substitutionModels
+{
+    defineTypeNameAndDebug(userValue, 0);
+    addToRunTimeSelectionTable
+    (
+        substitutionModel,
+        userValue,
+        dictionary
+    );
+}
+}
+
+
+// * * * * * * * * * * * * * * * * Constructors  * * * * * * * * * * * * * * //
+
+Foam::substitutionModels::userValue::userValue
+(
+    const dictionary& dict,
+    const Time& time
+)
+:
+    substitutionModel(dict, time),
+    entries_()
+{
+    // Populate entries
+    const dictionary& entriesDict = dict.subDict("entries");
+    for (const auto& e : entriesDict)
+    {
+        entries_.insert(cleanKey(e.keyword()), string(e.stream()));
+    }
+}
+
+
+// * * * * * * * * * * * * * * * Member Functions  * * * * * * * * * * * * * //
+
+bool Foam::substitutionModels::userValue::valid
+(
+    const word& keyName
+) const
+{
+    return entries_.found(keyName);
+}
+
+
+bool Foam::substitutionModels::userValue::apply
+(
+    const word& key,
+    string& buffer
+) const
+{
+    if (!valid(key)) return false;
+
+    buffer.replaceAll(keyify(key), entries_[key]);
+
+    return true;
+}
+
+
+Foam::wordList Foam::substitutionModels::userValue::keys() const
+{
+    return entries_.sortedToc();
+}
+
+
+// ************************************************************************* //
\ No newline at end of file
diff --git a/src/functionObjects/utilities/foamReport/substitutionModels/userValue/userValue.H b/src/functionObjects/utilities/foamReport/substitutionModels/userValue/userValue.H
new file mode 100644
index 0000000000000000000000000000000000000000..78ec3a48183d533cbafed4d645c2bf40448f4d40
--- /dev/null
+++ b/src/functionObjects/utilities/foamReport/substitutionModels/userValue/userValue.H
@@ -0,0 +1,137 @@
+/*---------------------------------------------------------------------------*\
+  =========                |
+  \      /  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::substitutionModels::userValue
+
+Description
+    The \c userValue substitution model. Dictionaries can be retrieved from
+    an object registry, e.g. time, mesh, or from a file.
+
+    \verbatim
+    userValues1
+    {
+        // Mandatory entries
+        type        userValue;
+
+        entries
+        {
+            my_keyword1 "My local string value";
+            my_keyword2 "My local string value";
+        }
+
+        // Inherited entries
+        ...
+    }
+    \endverbatim
+
+    The entries mean:
+    \table
+      Property     | Description                    | Type | Reqd  | Deflt
+      type         | Type name: userValue           | word |  yes  | -
+      entries      | Keyword lookup pairs           | dictionary | yes | -
+    \endtable
+
+    The inherited entries are elaborated in:
+      - \link substitutionModel.H \endlink
+
+SourceFiles
+    userValue.C
+
+---------------------------------------------------------------------------*/
+
+#ifndef Foam_substitutionModels_userValue_H
+#define Foam_substitutionModels_userValue_H
+
+#include "substitutionModel.H"
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+namespace Foam
+{
+
+namespace substitutionModels
+{
+
+/*---------------------------------------------------------------------------*\
+                          Class userValue Declaration
+\*---------------------------------------------------------------------------*/
+
+class userValue
+:
+    public substitutionModel
+{
+    // Private Data
+
+        //- Hash table for key and environment variable pairs
+        HashTable<string> entries_;
+
+
+    // Private Functions
+
+        //- No copy construct
+        userValue(const userValue&) = delete;
+
+        //- No copy assignment
+        void operator=(const userValue&) = delete;
+
+
+public:
+
+    //- Runtime type information
+    TypeName("userValue");
+
+
+    //- Constructor
+    userValue(const dictionary& dict, const Time& time);
+
+
+    //- Destructor
+    virtual ~userValue() = default;
+
+
+    // Member Functions
+
+        //- Return true of model applies to this keyName
+        virtual bool valid(const word& keyName) const;
+
+        //- Apply substitutions to this string buffer
+        virtual bool apply(const word& key, string& buffer) const;
+
+        //- Return a word list of the keys
+        virtual wordList keys() const;
+};
+
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+} // End namespace substitutionModels
+} // End namespace Foam
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+#endif
+
+// ************************************************************************* //
\ No newline at end of file
diff --git a/src/functionObjects/utilities/graphFunctionObject/SVGTools.H b/src/functionObjects/utilities/graphFunctionObject/SVGTools.H
new file mode 100644
index 0000000000000000000000000000000000000000..18ec80537bf16377e63970da41a284fc77a0165c
--- /dev/null
+++ b/src/functionObjects/utilities/graphFunctionObject/SVGTools.H
@@ -0,0 +1,238 @@
+/*---------------------------------------------------------------------------*\
+  =========                |
+  \      /  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/>.
+
+Namespace
+    Foam::SVG
+
+Description
+    Collection of tools to generate SVG strings
+
+SourceFiles
+    SVGTools.H
+
+\*---------------------------------------------------------------------------*/
+
+#ifndef Foam_SVGTools_H
+#define Foam_SVGTools_H
+
+#include "Ostream.H"
+#include "OStringStream.H"
+#include "List.H"
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+namespace Foam
+{
+
+/*---------------------------------------------------------------------------*\
+                           Namespace SVG Declaration
+\*---------------------------------------------------------------------------*/
+
+namespace SVG
+{
+    typedef std::pair<const char*, string> entryType;
+
+    struct element;
+    Ostream& operator<<(Ostream& os, const element& e);
+
+    // Base SVG element
+    struct element
+    {
+        const word key_;
+        DynamicList<entryType> styles_;
+        DynamicList<entryType> elems_;
+
+        element
+        (
+            const word& key,
+            const std::initializer_list<entryType>& styles  = {},
+            const std::initializer_list<entryType>& elems = {}
+        )
+        :
+            key_(key),
+            styles_(styles),
+            elems_(elems)
+        {}
+
+        template<class Type>
+        void addAttr(const char* key, const Type& value)
+        {
+            OStringStream oss;
+            oss << value;
+            elems_.push_back(entryType(key, oss.str().c_str()));
+        }
+
+        void addAttrStr(const char* key, const string& str)
+        {
+            elems_.push_back(entryType(key, str.c_str()));
+        }
+
+        friend Ostream& operator<<(Ostream& os, const element& ele)
+        {
+            os  << "<" << ele.key_;
+
+            for (const auto& e : ele.elems_)
+            {
+                os << " " << e.first << "=" << e.second;
+            }
+
+            os  << " style=\"";
+            for (const auto& s : ele.styles_)
+            {
+                os << s.first << ":" << s.second.c_str() << ";";
+            }
+
+            os << "\">";
+
+            return os;
+        }
+
+        const word end = "</" + key_ + ">";
+    };
+
+
+    struct text;
+    Ostream& operator<<(Ostream& os, const text& t);
+
+    // Text
+    struct text
+    :
+        element
+    {
+        const string text_;
+
+        text
+        (
+            const string text,
+            const label left,
+            const label top,
+            const std::initializer_list<entryType>& styles = {},
+            const word anchor = "middle",
+            const std::initializer_list<entryType>& elems = {}
+        )
+        :
+            element("text", styles, elems),
+            text_(text)
+        {
+            elems_.push_back(entryType("x", Foam::name(left)));
+            elems_.push_back(entryType("y", Foam::name(top)));
+            elems_.push_back(entryType("text-anchor", anchor));
+            elems_.push_back
+            (
+                entryType("font-family", "Arial, Helvetica, sans-serif")
+            );
+        }
+
+
+        friend Ostream& operator<<(Ostream& os, const text& t)
+        {
+            // element::operator<<(os, t);
+            os  << static_cast<const element&>(t);
+
+            os  << t.text_.c_str();
+
+            os  << t.end;
+
+            return os;
+        }
+    };
+
+
+    struct line;
+    Ostream& operator<<(Ostream& os, const line& l);
+
+    // Line
+    struct line
+    :
+        element
+    {
+        line
+        (
+            const label x1,
+            const label y1,
+            const label x2,
+            const label y2,
+            const std::initializer_list<entryType>& styles = {},
+            const std::initializer_list<entryType>& elems = {}
+        )
+        :
+            element("line", styles, elems)
+        {
+            elems_.push_back(entryType("x1", Foam::name(x1)));
+            elems_.push_back(entryType("y1", Foam::name(y1)));
+            elems_.push_back(entryType("x2", Foam::name(x2)));
+            elems_.push_back(entryType("y2", Foam::name(y2)));
+        }
+
+
+        friend Ostream& operator<<(Ostream& os, const line& l)
+        {
+            // element::operator<<(os, l);
+            os  << static_cast<const element&>(l);
+            os  << l.end;
+
+            return os;
+        }
+    };
+
+    struct header;
+    Ostream& operator<<(Ostream& os, const header& h);
+
+    // Header
+    struct header
+    {
+        label width_;
+        label height_;
+
+        header(const label width, const label height)
+        :
+            width_(width),
+            height_(height)
+        {}
+
+        friend Ostream& operator<<(Ostream& os, const header& h)
+        {
+            os  << "<svg viewBox=\"0 0 " << h.width_ << ' ' << h.height_ << "\""
+                << " xmlns=\"http://www.w3.org/2000/svg\""
+                << " xmlns:xlink=\"http://www.w3.org/1999/xlink\""
+                << " xmlns:bx=\"https://www.boxy-svg.com/bx\">";
+
+            return os;
+        }
+    };
+
+    // Close SVG element
+    const char* end  = "</svg>";
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+} // End namespace SVG
+} // End namespace Foam
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+#endif
+
+// ************************************************************************* //
\ No newline at end of file
diff --git a/src/functionObjects/utilities/graphFunctionObject/graphFunctionObject.C b/src/functionObjects/utilities/graphFunctionObject/graphFunctionObject.C
new file mode 100644
index 0000000000000000000000000000000000000000..1627dbf6f26e4718a2050f246f4c9f0244bec989
--- /dev/null
+++ b/src/functionObjects/utilities/graphFunctionObject/graphFunctionObject.C
@@ -0,0 +1,654 @@
+/*---------------------------------------------------------------------------*\
+  =========                 |
+  \      /  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 "graphFunctionObject.H"
+#include "addToRunTimeSelectionTable.H"
+#include "OFstream.H"
+#include "labelVector.H"
+#include "FlatOutput.H"
+#include "SVGTools.H"
+
+// * * * * * * * * * * * * * * Static Data Members * * * * * * * * * * * * * //
+
+namespace Foam
+{
+namespace functionObjects
+{
+    defineTypeNameAndDebug(graphFunctionObject, 0);
+    addToRunTimeSelectionTable
+    (
+        functionObject,
+        graphFunctionObject,
+        dictionary
+    );
+}
+}
+
+// 'Muted' colour scheme from https://personal.sron.nl/~pault/ (12.07.24)
+Foam::wordList Foam::functionObjects::graphFunctionObject::defaultColours
+({
+    "#CC6677",
+    "#332288",
+    "#DDCC77",
+    "#117733",
+    "#88CCEE",
+    "#882255",
+    "#44AA99",
+    "#999933",
+    "#AA4499"
+});
+
+
+// * * * * * * * * * * * * Protected Member Functions  * * * * * * * * * * * //
+
+template<class Type>
+bool Foam::functionObjects::graphFunctionObject::getValue
+(
+    const label objecti,
+    label& valuei
+)
+{
+    const word& object = objects_[objecti];
+    const word& entry = entries_[objecti];
+
+    Type result;
+    if (!this->getObjectResult(object, entry, result))
+    {
+        return false;
+    }
+
+    auto& cols = objectToCol_[objecti];
+    if (cols.empty())
+    {
+        for (direction d = 0; d < pTraits<Type>::nComponents; ++d)
+        {
+            cols.push_back(valuei++);
+            values_.push_back(DynamicList<scalar>());
+        }
+    }
+
+    for (direction d = 0; d < pTraits<Type>::nComponents; ++d)
+    {
+        scalar v = component(result, d);
+
+        if (logScaleY_)
+        {
+            v = (v < SMALL) ? 1 : log10(v);
+        }
+
+        values_[cols[d]].push_back(v);
+    }
+
+    return true;
+}
+
+
+Foam::label Foam::functionObjects::graphFunctionObject::setAxisProps
+(
+    const bool logScale,
+    scalar& xmin,
+    scalar& xmax,
+    scalar& xtick
+) const
+{
+    DebugInfo
+        << "1 -- xmin:" << xmin << " xmax:" << xmax
+        << " xtick:" << xtick << endl;
+
+    /*
+    Divisions Based on (12.07.24):
+    https://peltiertech.com/calculate-nice-axis-scales-in-your-excel-worksheet
+    */
+
+    const scalar range = xmax - xmin;
+    const scalar eps = 0.01*range;
+
+    // Extend xmin and xmax by eps
+    if (mag(xmin) < SMALL)
+    {
+        xmin = 0;
+    }
+    else
+    {
+        xmin = (xmin > 0) ? max(0, xmin - eps) : xmin - eps;
+    }
+
+    if (mag(xmax) < SMALL)
+    {
+        xmax = mag(xmin) < SMALL ? 1 : 0;
+    }
+    else
+    {
+        xmax = (xmax < 0) ? min(0, xmax + eps) : xmax + eps;
+    }
+
+    DebugInfo
+        << "2 -- xmin:" << xmin << " xmax:" << xmax
+        << " xtick:" << xtick << endl;
+
+    auto lookup = [](const scalar x) -> scalar
+    {
+        if (x < 2.5) { return 0.2; }
+        if (x < 5.0) { return 0.5; }
+        if (x < 10.0) { return 2.0; }
+        return 10.0;
+    };
+
+    const scalar power = log10(range);
+    const scalar factor = pow(10, power - floor(power));
+
+    xtick = lookup(factor)*pow(10, floor(power));
+    xmin = xtick*floor(xmin/xtick);
+    xmax = xtick*(floor(xmax/xtick) + 1);
+
+    // Convert ticks to integer powers of 10 for log scales
+    if (logScale)
+    {
+        xmin = floor(xmin);
+        xmax = ceil(xmax);
+        xtick = 1;
+    }
+
+    DebugInfo
+        << "power:" << power << " factor:" << factor
+        << " xmin:" << xmin << " xmax:" << xmax
+        << " xtick:" << xtick << endl;
+
+    return round((xmax - xmin)/xtick);
+}
+
+
+// * * * * * * * * * * * * * * * * Constructors  * * * * * * * * * * * * * * //
+
+Foam::functionObjects::graphFunctionObject::graphFunctionObject
+(
+    const word& name,
+    const Time& runTime,
+    const dictionary& dict
+)
+:
+    stateFunctionObject(name, runTime),
+    writeFile(runTime, name, typeName, dict, true, ".svg"),
+    objects_(),
+    entries_(),
+    titles_(),
+    colours_(),
+    dashes_(),
+    times_(),
+    values_(),
+    objectToCol_(),
+    xMin_(dict.getOrDefault<scalar>("xMin", GREAT)),
+    xMax_(dict.getOrDefault<scalar>("xMax", GREAT)),
+    yMin_(dict.getOrDefault<scalar>("yMin", GREAT)),
+    yMax_(dict.getOrDefault<scalar>("yMax", GREAT)),
+    xlabel_(dict.getOrDefault<string>("xlabel", "Iteration/Time")),
+    ylabel_(dict.getOrDefault<string>("ylabel", "Property")),
+    width_(dict.getOrDefault<label>("width", 800)),
+    height_(dict.getOrDefault<label>("height", 600)),
+    strokeWidth_(dict.getOrDefault<label>("strokeWidth", 2)),
+    logScaleX_(dict.getOrDefault<bool>("logScaleX", false)),
+    logScaleY_(dict.getOrDefault<bool>("logScaleY", false)),
+    drawGrid_(dict.getOrDefault<bool>("drawGrid", true))
+{
+    const dictionary& functions = dict.subDict("functions");
+    objects_.setSize(functions.size());
+    entries_.setSize(functions.size());
+    titles_.setSize(functions.size());
+    colours_.setSize(functions.size());
+    dashes_.setSize(functions.size());
+    objectToCol_.setSize(functions.size());
+
+    label defaultColouri = 0;
+    label entryi = 0;
+
+    for (const auto& e : functions)
+    {
+        if (!e.isDict())
+        {
+            FatalIOErrorInFunction(functions)
+                << "Functions must be provided in dictionary format"
+                << exit(FatalIOError);
+        }
+
+        const dictionary& d = e.dict();
+        objects_[entryi] = d.get<word>("object");
+        entries_[entryi] = d.get<word>("entry");
+        titles_[entryi] = d.getOrDefault<string>("title", e.keyword());
+
+        labelVector colour;
+        if (d.readIfPresent("colour", colour))
+        {
+            // Warn/error if outside 0-255 range?
+            colour[0] = min(255, max(0, colour[0]));
+            colour[1] = min(255, max(0, colour[1]));
+            colour[2] = min(255, max(0, colour[2]));
+
+            OStringStream oss;
+            oss << "rgb" << flatOutput(colour, FlatOutput::ParenComma{});
+            colours_[entryi] = oss.str();
+        }
+        else
+        {
+            colours_[entryi] = defaultColours[defaultColouri++];
+            if (defaultColouri == defaultColours.size())
+            {
+                // Lots of lines to plot - exhausted list of default colours.
+                // Restarting ...
+                defaultColouri = 0;
+            }
+        }
+
+        {
+            labelList dashes;
+            if (d.readIfPresent("dashes", dashes))
+            {
+                OStringStream oss;
+                oss << flatOutput(dashes, FlatOutput::BareSpace{});
+                dashes_[entryi] = oss.str();
+            }
+            else
+            {
+                // Solid line
+                dashes_[entryi] = "0";
+            }
+        }
+
+        ++entryi;
+    }
+}
+
+
+// * * * * * * * * * * * * * * * Member Functions  * * * * * * * * * * * * * //
+
+bool Foam::functionObjects::graphFunctionObject::execute()
+{
+    if (!Pstream::master()) return true;
+
+    scalar& graphTime = times_.emplace_back(time_.timeOutputValue());
+
+    if (logScaleX_)
+    {
+        graphTime = log10(max(graphTime, SMALL));
+    }
+
+    label valuei = 0;
+    forAll(objects_, objecti)
+    {
+        bool ok =
+            getValue<label>(objecti, valuei)
+         || getValue<scalar>(objecti, valuei)
+         || getValue<vector>(objecti, valuei)
+         || getValue<sphericalTensor>(objecti, valuei)
+         || getValue<symmTensor>(objecti, valuei)
+         || getValue<tensor>(objecti, valuei);
+
+        if (!ok)
+        {
+            // Entry not found
+            Log << type() << " " << name() << " execute: "
+                << "Unable to get value for object:" << objects_[objecti]
+                << " entry:" << entries_[objecti] << endl;
+        }
+    }
+
+    return true;
+}
+
+
+bool Foam::functionObjects::graphFunctionObject::write()
+{
+    if (!Pstream::master()) return true;
+    // DebugVar(values_);
+
+    auto filePtr = newFileAtTime(name(), time().value());
+    auto& os = filePtr();
+
+    scalar ymin = GREAT;
+    scalar ymax = -GREAT;
+
+    bool valid = false;
+    for (const auto& data : values_)
+    {
+        for (const auto& value : data)
+        {
+            ymin = min(ymin, value);
+            ymax = max(ymax, value);
+            valid = true;
+        }
+    }
+
+    // Early exit if there is no data
+    if (!valid)
+    {
+        Log << type() << " " << name() << " write:" << nl
+            << "    No data to plot - skipping" << nl;
+
+        // Empty graph
+        os  << SVG::header(width_, height_) << nl << SVG::end << endl;
+
+        return false;
+    }
+
+    auto applyLimits = [](const scalar val, const scalar lim, const bool lg)
+    {
+        if (lim < 0.99*GREAT)
+        {
+            return lg ? log10(lim) : lim;
+        }
+
+        return val;
+    };
+
+
+    // Set y axis limits if user-supplied
+    ymin = applyLimits(ymin, yMin_, logScaleY_);
+    ymax = applyLimits(ymax, yMax_, logScaleY_);
+
+
+    scalar ytick = 0;
+    const label ny = setAxisProps(logScaleY_, ymin, ymax, ytick);
+
+    const scalar border = 0.1;
+    const scalar w = width_*(1.0 - 2*border);
+    const scalar h = height_*(1.0 - 2*border);
+
+    // Set x axis limits if user-supplied
+    scalar xmin = applyLimits(0, xMin_, logScaleX_);
+    scalar xmax = applyLimits(max(times_), xMax_, logScaleX_);
+
+    // Set x axis properties; return the number of tic divisions
+    scalar xtick = 0;
+    const label nx = setAxisProps(logScaleX_, xmin, xmax, xtick);
+
+    // Top pixel co-ordinate
+    auto top = [=](const scalar y)
+    {
+        const scalar ratio = (y - ymin)/(ymax - ymin + ROOTVSMALL);
+        return round(height_ - ratio*h - border*height_);
+    };
+
+    // Left pixel co-ordinate
+    auto left = [=](const scalar x)
+    {
+        return round(x/(xmax - xmin + ROOTVSMALL)*w + border*width_);
+    };
+
+    const scalar fontpx = min(20, h/(2*values_.size()));
+    const scalar fontdy = 1.5*fontpx;
+
+    // Legend - top right: text (right aligned), coloured line (fixed positions)
+    const label legendLineRight = border*width_ + w - fontpx;
+    const label legendLineLeft = legendLineRight - 0.5*border*width_;
+    const label legendLabelRight = legendLineLeft - 0.5*fontpx;
+
+    // Graph box and tick colour
+    const word colour = "rgb(105,105,105)";
+
+    os  << SVG::header(width_, height_) << nl;
+
+    // Graph bounding box
+    SVG::element bounds("rect", {{"fill", "none"}, {"stroke", colour}});
+    bounds.addAttr("x", round(border*width_));
+    bounds.addAttr("y", round(border*height_));
+    bounds.addAttr("width", round(w));
+    bounds.addAttr("height", round(h));
+    os  << bounds << bounds.end << nl;
+
+
+    // X axis label
+    os  << SVG::text
+        (
+            xlabel_,
+            0.5*width_,
+            height_ - 0.5*(border*height_) + fontpx,
+            {{"font-size", Foam::name(1.2*fontpx)}},
+            "middle"
+        )
+        << nl;
+
+    // Y axis label - text rotated
+    SVG::text ytext
+    (
+        ylabel_,
+        0,
+        0,
+        {{"font-size", Foam::name(1.2*fontpx)}},
+        "middle"
+    );
+    ytext.addAttr("alignment-baseline", "middle");
+    ytext.addAttrStr
+    (
+        "transform",
+        "translate(" + Foam::name(left(xmin) - 3*fontpx) + ","
+      + Foam::name(0.5*height_) + ") rotate(270)"
+    );
+    os  << ytext << nl;
+
+    const label dTick = 0.2*fontpx;
+
+    // Background grid
+    if (drawGrid_)
+    {
+        const word colourGrid = "rgb(200,200,200)";
+
+        for (label i = 1; i < nx; ++i)
+        {
+            const label x = left(xmin + i*xtick);
+            const label y1 = top(ymin);
+            const label y2 = top(ymax);
+
+            // Dashed grid lines
+            os  << SVG::line
+                (
+                    x,
+                    y1,
+                    x,
+                    y2,
+                    {
+                        {"stroke", colourGrid},
+                        {"stroke-width", "1"},
+                        {"stroke-dasharray", "4"}
+                    }
+                ) << nl;
+        }
+
+        for (label i = 1; i < ny; ++i)
+        {
+            const label y = top(ymin + i*ytick);
+            const label x1 = left(xmin);
+            const label x2 = left(xmax);
+
+            // Dashed grid lines
+            os  << SVG::line
+                (
+                    x1,
+                    y,
+                    x2,
+                    y,
+                    {
+                        {"stroke", colourGrid},
+                        {"stroke-width", "1"},
+                        {"stroke-dasharray", "4"}
+                    }
+                ) << nl;
+        }
+    }
+
+    // Axis labels
+    for (label i = 0; i <= nx; ++i)
+    {
+        const scalar v = xmin + i*xtick;
+        const label x = left(v);
+        const scalar y0 = ymin;
+        const label y1 = top(y0);
+        const label y2 = y1 + dTick;
+        const string tickLabel = logScaleX_
+           ? "<tspan>10<tspan style=\"font-size:"
+           + Foam::name(label(0.75*fontpx))
+           + "px\" dy=\"" + Foam::name(-0.4*fontpx) + "\">"
+           + Foam::name(v)
+           + "</tspan></tspan>"
+           : Foam::name(v);
+
+        // Ticks
+        os  << SVG::line
+            (
+                x,
+                y1,
+                x,
+                y2,
+                {
+                    {"stroke", colour},
+                    {"stroke-width", Foam::name(strokeWidth_)}
+                }
+            ) << nl;
+
+        // Labels
+        os  << SVG::text
+            (
+                tickLabel,
+                x,
+                y2 + 1.25*fontpx,
+                {{"font-size", Foam::name(fontpx)}},
+                "middle"
+            ) << nl;
+    }
+    for (label i = 0; i <= ny; ++i)
+    {
+        const scalar v = ymin + i*ytick;
+        const label y = top(v);
+        const label y2 = y + 0.4*fontpx;
+        const scalar x0 = xmin;
+        const label x1 = left(x0);
+        const label x2 = x1 - dTick;
+        const string tickLabel = logScaleY_
+          ? "<tspan>10<tspan style=\"font-size:"
+          + Foam::name(label(0.6*fontpx))
+          + "px\" dy=\"" + Foam::name(-0.4*fontpx) + "\">"
+          + Foam::name(v)
+          + "</tspan></tspan>"
+          : Foam::name(v);
+
+        // Ticks
+        os  << SVG::line
+            (
+                x1,
+                y,
+                x2,
+                y,
+                {{"stroke", colour},{"stroke-width",  "1"}}
+            ) << nl;
+
+        // Labels
+        os  << SVG::text
+            (
+                tickLabel,
+                x2 - 0.5*fontpx,
+                y2,
+                {{"font-size", Foam::name(fontpx)}},
+                "end"
+            ) << nl;
+    }
+
+
+    forAll(objects_, objecti)
+    {
+        const word& colour = colours_[objecti];
+
+        const auto& cols = objectToCol_[objecti];
+        for (const label c : cols)
+        {
+            const word cmpt = cols.size() > 1 ? Foam::name(c) : "";
+
+            label legendTop = border*height_ + fontdy*(c+1);
+            os  << SVG::text
+                (
+                    titles_[objecti] + cmpt,
+                    legendLabelRight,
+                    legendTop,
+                    {{"font-size", Foam::name(fontpx)}},
+                    "end"
+                ) << nl;
+
+            os  << SVG::line
+                (
+                    legendLineLeft,
+                    legendTop - 0.5*fontpx,
+                    legendLineRight,
+                    legendTop - 0.5*fontpx,
+                    {{"stroke", colour},{"stroke-width", "2"}},
+                    {{"stroke-dasharray", dashes_[objecti]}}
+                ) << nl;
+
+
+            os  << "<path d=\"";
+            const auto& data = values_[c];
+            bool firstPoint = true;
+            forAll(data, i)
+            {
+                const scalar t = times_[i];
+                const scalar v = data[i];
+
+                if ((v > ymin) && (v < ymax))
+                {
+                    if (firstPoint)
+                    {
+                        os  << " M ";
+                    }
+                    else
+                    {
+                        os  << " L ";
+                    }
+
+                    os  << left(t) << ' ' << top(v);
+
+                    firstPoint = false;
+                }
+                else
+                {
+                    firstPoint = true;
+                }
+            }
+
+            os  << "\""
+                << " style=\"stroke:" << colour << ";"
+                << " fill:none; stroke-width:2;\""
+                << " stroke-dasharray=\"" << dashes_[objecti].c_str() << "\" />"
+                << nl;
+        }
+    }
+
+    os  << SVG::end << endl;
+
+    Log << type() << " " << name() <<  " write:" << nl
+        << "    Written file " << os.name() << nl << endl;
+
+    return true;
+}
+
+
+// ************************************************************************* //
\ No newline at end of file
diff --git a/src/functionObjects/utilities/graphFunctionObject/graphFunctionObject.H b/src/functionObjects/utilities/graphFunctionObject/graphFunctionObject.H
new file mode 100644
index 0000000000000000000000000000000000000000..73ddbac9d934e96ba7456027beaa07f94ea78749
--- /dev/null
+++ b/src/functionObjects/utilities/graphFunctionObject/graphFunctionObject.H
@@ -0,0 +1,300 @@
+/*---------------------------------------------------------------------------*\
+  =========                |
+  \      /  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::graphFunctionObject
+
+Description
+    Accumulates function object result values and renders into a graph in
+    SVG format.
+
+    Operands:
+    \table
+    Operand      | Type                                 | Location
+    input        | Function object results              | Memory; <!--
+    --> $FOAM_CASE/\<time\>/uniform/functionObjects/functionObjectProperties
+    output file  | SVG                                  | <!--
+    --> $FOAM_CASE/postProcessing/<functionObject>/<time>/<functionObject>.svg
+    \endtable
+
+Usage
+    Minimal example by using \c system/controlDict.functions to plot the
+    residuals from the \c solverInfo function object:
+
+    \verbatim
+    residualGraph
+    {
+        // Mandatory entries
+        type            graphFunctionObject;
+        libs            (utilityFunctionObjects);
+
+        functions
+        {
+            entry
+            {
+                // Mandatory entries
+                object      <word>;
+                entry       <word>;
+
+                // Optional entries
+                title       <string>;
+                colour      <labelVector>;
+                dashes      <labelList>;
+            }
+            line1
+            {
+                object      solverInfo1;
+                entry       Ux_initial;
+            }
+            line2
+            {
+                object      solverInfo1;
+                entry       p_initial;
+            }
+        }
+
+        // Optional entries
+        xMin            <scalar>;
+        xMax            <scalar>;
+        yMin            <scalar>;
+        yMax            <scalar>;
+        xlabel          <string>;  // "Iteration";
+        ylabel          <string>;  // "log10(Initial residual)";
+        width           <label>;
+        height          <label>;
+        strokeWidth     <label>;
+        logScaleX       <bool>;
+        logScaleY       <bool>;
+        drawGrid        <bool>;
+
+        // Inherited entries
+        ...
+    }
+    \endverbatim
+
+    where the entries mean:
+    \table
+    Property     | Description                        | Type | Reqd | Deflt
+    type         | Type name: graphFunctionObject     | word |  yes  | -
+    libs         | Library name: utilityFunctionObjects | word | yes | -
+    functions    | Dictionary of lines to draw        | dictionary | yes | -
+    width        | Output SVG width in pixel          | label| no    | 800
+    height       | Output SVG height in pixel         | label| no    | 600
+    xMin         | User defined minimum x axis limit  | scalar | no | calculated
+    xMax         | User defined maximum x axis limit  | scalar | no | calculated
+    yMin         | User defined minimum y axis limit  | scalar | no | calculated
+    yMax         | User defined maximum y axis limit  | scalar | no | calculated
+    xLabel       | X axis label                 | string | no  | Iteration/Time
+    yLabel       | Y axis label                 | string | no  | Property
+    strokeWidth  | Line stroke width in pixel         | label  | no  | 2
+    logScaleX    | Use log scale for x axis           | bool   | no  | false
+    logScaleY    | Use log scale for y axis           | bool   | no  | false
+    drawGrid     | Draw background grid               | bool   | no  | true
+    \endtable
+
+    The inherited entries are elaborated in:
+      - \link stateFunctionObject.H \endlink
+      - \link writeFile.H \endlink
+
+    Each line corresponds to the history of function object result values, e.g.
+
+    \verbatim
+    line1
+    {
+        object      solverInfo1;
+        entry       Ux_initial;
+        colour      (255 0 0);
+        dashes      (4 1);
+        title       Ux;
+    }
+    \endverbatim
+
+    where the entries mean:
+    \table
+    Property     | Description                        | Type | Reqd  | Deflt
+    object       | Function object name               | word |  yes  | -
+    entry        | Function object result entry name  | word |  yes  | -
+    colour       | Line colour                        | label vector | no | auto
+    dashes       | Line dash array                    | label vector | no | auto
+    title        | Title                              | string | no | dict name
+    \endtable
+
+See also
+    - Foam::functionObjects::solverInfo
+
+SourceFiles
+    graphFunctionObject.C
+
+\*---------------------------------------------------------------------------*/
+
+#ifndef Foam_functionObjects_graphFunctionObject_H
+#define Foam_functionObjects_graphFunctionObject_H
+
+#include "stateFunctionObject.H"
+#include "writeFile.H"
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+namespace Foam
+{
+
+namespace functionObjects
+{
+
+/*---------------------------------------------------------------------------*\
+                     Class graphFunctionObject Declaration
+\*---------------------------------------------------------------------------*/
+
+class graphFunctionObject
+:
+    public stateFunctionObject,
+    public writeFile
+{
+    // Private Data
+
+        //- List of default curve colours
+        static wordList defaultColours;
+
+        //- Names of function objects
+        List<word> objects_;
+
+        //- Function object entries
+        List<word> entries_;
+
+        //- Line titles
+        List<string> titles_;
+
+        //- Line colours - hex string
+        List<word> colours_;
+
+        //- Line dash array
+        List<string> dashes_;
+
+        //- Times
+        DynamicList<scalar> times_;
+
+        //- Time vs. flattened values
+        DynamicList<DynamicList<scalar>> values_;
+
+        //- Mapping from object to column index in values_
+        List<DynamicList<label>> objectToCol_;
+
+        //- User-supplied minimum x value
+        scalar xMin_;
+
+        //- User-supplied maximum x value
+        scalar xMax_;
+
+        //- User-supplied minimum y value
+        scalar yMin_;
+
+        //- User-supplied maximum y value
+        scalar yMax_;
+
+        //- X axis label
+        const string xlabel_;
+
+        //- Y axis label
+        const string ylabel_;
+
+        //- Width in px
+        const label width_;
+
+        //- Height in px
+        const label height_;
+
+        //- Line width in px
+        const label strokeWidth_;
+
+        //- Flag to use log scale on x-axis
+        bool logScaleX_;
+
+        //- Flag to use log scale on y-axis
+        bool logScaleY_;
+
+        //- Draw background grid
+        const bool drawGrid_;
+
+
+    // Private Functions
+
+        //- Get the result value from the function object
+        //  \returns true if the value was found
+        template<class Type>
+        bool getValue(const label objecti, label& valuei);
+
+        //- Set axis min, max, tick
+        //  \returns number of ticks
+        label setAxisProps
+        (
+            const bool logScale,
+            scalar& xmin,
+            scalar& xmax,
+            scalar& xtick
+        ) const;
+
+        //- No copy construct
+        graphFunctionObject(const graphFunctionObject&) = delete;
+
+        //- No copy assignment
+        void operator=(const graphFunctionObject&) = delete;
+
+
+public:
+
+    //- Runtime type information
+    TypeName("graphFunctionObject");
+
+    //- Construct from Time and dictionary
+    graphFunctionObject
+    (
+        const word& name,
+        const Time& runTime,
+        const dictionary& dict
+    );
+
+    //- Destructor
+    virtual ~graphFunctionObject() = default;
+
+
+    // Member Functions
+
+        //- Execute
+        virtual bool execute();
+
+        //- Write
+        virtual bool write();
+};
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+} // End namespace functionObjects
+} // End namespace Foam
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+#endif
+
+// ************************************************************************* //
\ No newline at end of file
diff --git a/tutorials/incompressible/simpleFoam/motorBike/system/controlDict b/tutorials/incompressible/simpleFoam/motorBike/system/controlDict
index 6a42118c719acefc87b101e6a3b491494806f159..2e17d5d2c6ad8bf9e70372eaa456b86bb1c75e53 100644
--- a/tutorials/incompressible/simpleFoam/motorBike/system/controlDict
+++ b/tutorials/incompressible/simpleFoam/motorBike/system/controlDict
@@ -51,6 +51,10 @@ functions
     #include "cuttingPlane"
     #include "forceCoeffs"
     #include "ensightWrite"
+
+    #include "solverInfo"
+    #include "graphFunctionObject"
+    #include "foamReport"
 }
 
 
diff --git a/tutorials/incompressible/simpleFoam/motorBike/system/foamReport b/tutorials/incompressible/simpleFoam/motorBike/system/foamReport
new file mode 100644
index 0000000000000000000000000000000000000000..f90a2f0683950dce6208530a4ca46dd067b86e7f
--- /dev/null
+++ b/tutorials/incompressible/simpleFoam/motorBike/system/foamReport
@@ -0,0 +1,59 @@
+foamReport1
+{
+    type            foamReport;
+    libs            (utilityFunctionObjects);
+
+    writeControl    writeTime;
+
+    template        "<system>/myReportTemplate.md";
+
+    substitutions
+    {
+        timing1
+        {
+            type        fileRegEx;
+            path        "log.simpleFoam";
+
+            entries
+            {
+                executionTime "ExecutionTime = (.*) s  Clock.*";
+            }
+        }
+        divSchemes1
+        {
+            type        dictionaryValue;
+            path        "<system>/fvSchemes";
+
+            entries
+            {
+                divSchemes      "divSchemes";
+            }
+        }
+        fvSolution1
+        {
+            type        dictionaryValue;
+            path        "<system>/fvSolution";
+
+            entries
+            {
+                solver_p        "solvers/p/solver";
+                solver_p_tol    "solvers/p/tolerance";
+                solver_p_reltol "solvers/p/relTol";
+                solver_U        "solvers/U/solver";
+                solver_U_tol    "solvers/U/tolerance";
+                solver_U_reltol "solvers/U/relTol";
+            }
+        }
+        controlDict1
+        {
+            type        dictionaryValue;
+            path        "<system>/controlDict";
+
+            entries
+            {
+                initial_deltaT       "deltaT";
+            }
+        }
+    }
+}
+
diff --git a/tutorials/incompressible/simpleFoam/motorBike/system/graphFunctionObject b/tutorials/incompressible/simpleFoam/motorBike/system/graphFunctionObject
new file mode 100644
index 0000000000000000000000000000000000000000..ba4f9a4a6716c67f135e5ab386f96c664a521204
--- /dev/null
+++ b/tutorials/incompressible/simpleFoam/motorBike/system/graphFunctionObject
@@ -0,0 +1,142 @@
+/*--------------------------------*- C++ -*----------------------------------*\
+| =========                 |                                                 |
+| \\      /  F ield         | OpenFOAM: The Open Source CFD Toolbox           |
+|  \\    /   O peration     | Version:  v2406                                 |
+|   \\  /    A nd           | Website:  www.openfoam.com                      |
+|    \\/     M anipulation  |                                                 |
+\*---------------------------------------------------------------------------*/
+
+residualGraph1
+{
+    // Mandatory entries
+    type            graphFunctionObject;
+    libs            (utilityFunctionObjects);
+
+    functions
+    {
+        Ux
+        {
+            // Mandatory entries
+            object      solverInfo1;
+            entry       Ux_initial;
+
+            // Optional entries
+            // title       <string>;
+            // colour      <labelVector>;
+            // dashes      <labelList>;
+        }
+        Uy
+        {
+            object      solverInfo1;
+            entry       Uy_initial;
+        }
+        Uz
+        {
+            object      solverInfo1;
+            entry       Uz_initial;
+        }
+        p
+        {
+            object      solverInfo1;
+            entry       p_initial;
+        }
+    }
+
+    // Optional entries
+    logScaleX       no;
+    logScaleY       yes;
+    xlabel          "Iteration";
+    ylabel          "log10(Initial residual)";
+    // xMin            <scalar>;
+    // xMax            <scalar>;
+    // yMin            <scalar>;
+    // yMax            <scalar>;
+    // width           <label>;
+    // height          <label>;
+    // strokeWidth     <label>;
+    // drawGrid        <bool>;
+
+    // Inherited entries
+    writePrecision   6;
+    writeToFile      true;
+    useUserTime      true;
+
+    region          region0;
+    enabled         true;
+    log             true;
+    timeStart       0;
+    timeEnd         1000;
+    executeControl  timeStep;
+    executeInterval 1;
+    writeControl    writeTime;
+    writeInterval   -1;
+}
+
+
+forceCoeffsGraph1
+{
+    type            graphFunctionObject;
+    libs            (utilityFunctionObjects);
+    writeControl    writeTime;
+
+    logScaleX       no;
+    logScaleY       no;
+
+    xlabel          "Iteration";
+    ylabel          "Coefficient";
+
+    yMin            -1;
+    yMax            1;
+
+    functions
+    {
+        Cd
+        {
+            object      forceCoeffs1;
+            entry       Cd;
+        }
+        // CdMean
+        // {
+        //     object      valueAverage1;
+        //     entry       CdMean;
+        // }
+        Cd(f)
+        {
+            object      forceCoeffs1;
+            entry       Cd(f);
+        }
+        Cd(r)
+        {
+            object      forceCoeffs1;
+            entry       Cd(r);
+        }
+        Cl
+        {
+            object      forceCoeffs1;
+            entry       Cl;
+            //colour      (0, 0, 0);
+        }
+        // ClMean
+        // {
+        //     object      valueAverage1;
+        //     entry       ClMean;
+        // }
+        Cl(f)
+        {
+            object      forceCoeffs1;
+            entry       Cl(f);
+            //colour      (0, 0, 0);
+            title       Cl(f);
+        }
+        Cl(r)
+        {
+            object      forceCoeffs1;
+            entry       Cl(r);
+            //colour      (0, 0, 0);
+            title       Cl(r);
+        }
+    }
+}
+
+
+// ************************************************************************* //
diff --git a/tutorials/incompressible/simpleFoam/motorBike/system/myReportTemplate.md b/tutorials/incompressible/simpleFoam/motorBike/system/myReportTemplate.md
new file mode 100644
index 0000000000000000000000000000000000000000..5cdb747cd42fdc0ad5da2cf5cd08a12c8a04fef5
--- /dev/null
+++ b/tutorials/incompressible/simpleFoam/motorBike/system/myReportTemplate.md
@@ -0,0 +1,104 @@
+---
+marp: true
+paginate: true
+---
+
+<style>
+:root {
+    font-size: 20px;
+}
+td {
+    width: 1000px;
+}
+table {
+    width: 100%;
+}
+img {
+    display: block;
+    margin-left: auto;
+    margin-right: auto;
+    width: 60%;
+}
+</style>
+
+# {{OF_EXECUTABLE}} : {{OF_CASE_NAME}} tutorial
+
+- Case: {{OF_CASE_PATH}}
+- Submission: {{OF_CLOCK_START}} on {{OF_DATE_START}}
+- Report time: {{OF_CLOCK_NOW}} on {{OF_DATE_NOW}}
+
+---
+
+## Run information
+
+| Property       | Value              |
+|----------------|--------------------|
+| Host           | {{OF_HOST}}        |
+| Processors     | {{OF_NPROCS}}      |
+| Time steps     | {{OF_TIME_INDEX}}  |
+| Initial deltaT | {{initial_deltaT}} |
+| Current deltaT | {{OF_TIME_DELTAT}} |
+| Execution time | {{executionTime}}  |
+
+---
+
+## OpenFOAM information
+
+| Property       | Value              |
+|----------------|--------------------|
+| Version        | {{OF_VERSION}}     |
+| API            | {{OF_API}}         |
+| Patch          | {{OF_PATCH}}       |
+| Build          | {{OF_BUILD}}       |
+| Architecture   | {{OF_BUILD_ARCH}}  |
+
+---
+
+## Mesh statistics
+
+| Property          | Value                |
+|-------------------|----------------------|
+| Bounds            | {{OF_MESH_BOUNDS_MIN}}{{OF_MESH_BOUNDS_MAX}} |
+| Number of cells   | {{OF_MESH_NCELLS}}   |
+| Number of faces   | {{OF_MESH_NFACES}}   |
+| Number of points  | {{OF_MESH_NPOINTS}}  |
+| Number of patches | {{OF_MESH_NPATCHES}} |
+
+---
+
+## Linear solvers
+
+| Property | Value          | tolerance(rel)   | Tolerance(abs)      |
+|----------|----------------|------------------|---------------------|
+| p        | `{{solver_p}}` | {{solver_p_tol}} | {{solver_p_reltol}} |
+| U        | `{{solver_U}}` | {{solver_u_tol}} | {{solver_u_reltol}} |
+
+---
+
+## Numerical scehemes
+
+The chosen divergence schemes comprised:
+
+~~~
+{{divSchemes}}
+~~~
+
+---
+
+## Graphs
+
+Residuals
+
+![]({{OF_CASE_PATH}}/postProcessing/residualGraph1/{{OF_TIME}}/residualGraph1.svg)
+
+---
+
+## Results
+
+Forces
+
+![]({{OF_CASE_PATH}}/postProcessing/forceCoeffsGraph1/{{OF_TIME}}/forceCoeffsGraph1.svg)
+
+---
+
+Made using Open&nabla;FOAM v2412 from https://openfoam.com
diff --git a/tutorials/incompressible/simpleFoam/motorBike/system/solverInfo b/tutorials/incompressible/simpleFoam/motorBike/system/solverInfo
new file mode 100644
index 0000000000000000000000000000000000000000..5c764aa47136cebc257d965e36ba6a6d0a5792f5
--- /dev/null
+++ b/tutorials/incompressible/simpleFoam/motorBike/system/solverInfo
@@ -0,0 +1,21 @@
+/*--------------------------------*- C++ -*----------------------------------*\
+| =========                 |                                                 |
+| \\      /  F ield         | OpenFOAM: The Open Source CFD Toolbox           |
+|  \\    /   O peration     | Version:  v2406                                 |
+|   \\  /    A nd           | Website:  www.openfoam.com                      |
+|    \\/     M anipulation  |                                                 |
+\*---------------------------------------------------------------------------*/
+
+solverInfo1
+{
+    // Mandatory entries
+    type            solverInfo;
+    libs            (utilityFunctionObjects);
+    fields          (U p);
+
+    // Optional entries
+    writeResidualFields no;
+}
+
+
+// ************************************************************************* //