diff --git a/src/functionObjects/utilities/Make/files b/src/functionObjects/utilities/Make/files
index bf81b19565e4315902ba61e9409822a97d181842..9da1341dfa4c24e926fd98462203b5aec976a3d3 100644
--- a/src/functionObjects/utilities/Make/files
+++ b/src/functionObjects/utilities/Make/files
@@ -9,6 +9,15 @@ 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
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