diff --git a/common/CMakeLists.txt b/common/CMakeLists.txt
index ef1d8896c7..194511902c 100644
--- a/common/CMakeLists.txt
+++ b/common/CMakeLists.txt
@@ -190,6 +190,7 @@ set( KICOMMON_SRCS
     systemdirsappend.cpp
     thread_pool.cpp
     ui_events.cpp
+    title_block.cpp
     trace_helpers.cpp
     wildcards_and_files_ext.cpp
     wx_filename.cpp
@@ -619,7 +620,6 @@ set( COMMON_SRCS
     stroke_params.cpp
     template_fieldnames.cpp
     textentry_tricks.cpp
-    title_block.cpp
     undo_redo_container.cpp
     validators.cpp
     drawing_sheet/ds_painter.cpp
diff --git a/common/jobs/job.cpp b/common/jobs/job.cpp
index 35cdb19bf9..b7b10eea6a 100644
--- a/common/jobs/job.cpp
+++ b/common/jobs/job.cpp
@@ -97,7 +97,14 @@ void PrependDirectoryToPath( wxFileName& aFileName, const wxString aDirPath )
 
 wxString JOB::GetFullOutputPath( PROJECT* aProject ) const
 {
-    wxString outPath = ExpandTextVars( m_outputPath, aProject );
+    std::function<bool( wxString* )> textResolver =
+            [&]( wxString* token ) -> bool
+            {
+                return m_titleBlock.TextVarResolver( token, aProject );
+            };
+
+    wxString outPath = ExpandTextVars( m_outputPath, &textResolver );
+
     if( !m_tempOutputDirectory.IsEmpty() )
     {
         if( m_outputPathIsDirectory )
diff --git a/common/jobs/job.h b/common/jobs/job.h
index d3ca28c232..c617f8cb93 100644
--- a/common/jobs/job.h
+++ b/common/jobs/job.h
@@ -26,6 +26,7 @@
 #include <settings/json_settings.h>
 #include <lseq.h>
 #include <lset.h>
+#include <title_block.h>
 
 class PROJECT;
 
@@ -193,6 +194,8 @@ public:
         m_varOverrides = aVarOverrides;
     }
 
+    void SetTitleBlock( const TITLE_BLOCK& aTitleBlock ) { m_titleBlock = aTitleBlock; }
+
     virtual void FromJson( const nlohmann::json& j );
     virtual void ToJson( nlohmann::json& j ) const;
 
@@ -216,8 +219,9 @@ public:
     bool GetOutpathIsDirectory() const { return m_outputPathIsDirectory; }
 
 protected:
-    std::string m_type;
+    std::string                  m_type;
     std::map<wxString, wxString> m_varOverrides;
+    TITLE_BLOCK                  m_titleBlock;
 
     wxString m_tempOutputDirectory;
 
diff --git a/common/project.cpp b/common/project.cpp
index 8ef69d8608..3cbe0ef677 100644
--- a/common/project.cpp
+++ b/common/project.cpp
@@ -71,15 +71,6 @@ PROJECT::~PROJECT()
 
 bool PROJECT::TextVarResolver( wxString* aToken ) const
 {
-    // Special case because the resolution of PROJECTNAME is usually tied to title blocks
-    // but we want this for jobs
-    // Future todo is to rework vars entirely
-    if( aToken->IsSameAs( wxT( "PROJECTNAME" ) )  )
-    {
-        *aToken = GetProjectName();
-        return true;
-    }
-
     if( GetTextVars().count( *aToken ) > 0 )
     {
         *aToken = GetTextVars().at( *aToken );
diff --git a/eeschema/eeschema_jobs_handler.cpp b/eeschema/eeschema_jobs_handler.cpp
index 423533de52..6d1aa5f1a9 100644
--- a/eeschema/eeschema_jobs_handler.cpp
+++ b/eeschema/eeschema_jobs_handler.cpp
@@ -157,6 +157,7 @@ EESCHEMA_JOBS_HANDLER::EESCHEMA_JOBS_HANDLER( KIWAY* aKiway ) :
               } );
 }
 
+
 SCHEMATIC* EESCHEMA_JOBS_HANDLER::getSchematic( const wxString& aPath )
 {
     SCHEMATIC* sch = nullptr;
@@ -272,6 +273,7 @@ int EESCHEMA_JOBS_HANDLER::JobExportPlot( JOB* aJob )
     if( !sch )
         return CLI::EXIT_CODES::ERR_INVALID_INPUT_FILE;
 
+    aJob->SetTitleBlock( sch->RootScreen()->GetTitleBlock() );
     sch->Prj().ApplyTextVars( aJob->GetVarOverrides() );
 
     std::unique_ptr<SCH_RENDER_SETTINGS> renderSettings = std::make_unique<SCH_RENDER_SETTINGS>();
@@ -384,6 +386,9 @@ int EESCHEMA_JOBS_HANDLER::JobExportNetlist( JOB* aJob )
     if( !sch )
         return CLI::EXIT_CODES::ERR_INVALID_INPUT_FILE;
 
+    aJob->SetTitleBlock( sch->RootScreen()->GetTitleBlock() );
+    sch->Prj().ApplyTextVars( aJob->GetVarOverrides() );
+
     // Annotation warning check
     SCH_REFERENCE_LIST referenceList;
     sch->Hierarchy().GetSymbols( referenceList );
@@ -501,6 +506,7 @@ int EESCHEMA_JOBS_HANDLER::JobExportBom( JOB* aJob )
     if( !sch )
         return CLI::EXIT_CODES::ERR_INVALID_INPUT_FILE;
 
+    aJob->SetTitleBlock( sch->RootScreen()->GetTitleBlock() );
     sch->Prj().ApplyTextVars( aJob->GetVarOverrides() );
 
     // Annotation warning check
@@ -763,6 +769,9 @@ int EESCHEMA_JOBS_HANDLER::JobExportPythonBom( JOB* aJob )
     if( !sch )
         return CLI::EXIT_CODES::ERR_INVALID_INPUT_FILE;
 
+    aJob->SetTitleBlock( sch->RootScreen()->GetTitleBlock() );
+    sch->Prj().ApplyTextVars( aJob->GetVarOverrides() );
+
     // Annotation warning check
     SCH_REFERENCE_LIST referenceList;
     sch->Hierarchy().GetSymbols( referenceList );
@@ -1104,6 +1113,7 @@ int EESCHEMA_JOBS_HANDLER::JobSchErc( JOB* aJob )
     if( !sch )
         return CLI::EXIT_CODES::ERR_INVALID_INPUT_FILE;
 
+    aJob->SetTitleBlock( sch->RootScreen()->GetTitleBlock() );
     sch->Prj().ApplyTextVars( aJob->GetVarOverrides() );
 
     if( ercJob->GetOutputPath().IsEmpty() )
diff --git a/include/title_block.h b/include/title_block.h
index 3c16151066..4c57bd6d6c 100644
--- a/include/title_block.h
+++ b/include/title_block.h
@@ -37,7 +37,7 @@ class PROJECT;
  * Hold the information shown in the lower right corner of a plot, printout, or
  * editing view.
  */
-class TITLE_BLOCK
+class KICOMMON_API TITLE_BLOCK
 {
     // Texts are stored in wxArraystring.
     // TEXTS_IDX gives the index of known texts in this array
diff --git a/pcbnew/pcbnew_jobs_handler.cpp b/pcbnew/pcbnew_jobs_handler.cpp
index 73efa6a139..ea3f5ae557 100644
--- a/pcbnew/pcbnew_jobs_handler.cpp
+++ b/pcbnew/pcbnew_jobs_handler.cpp
@@ -273,8 +273,7 @@ BOARD* PCBNEW_JOBS_HANDLER::getBoard( const wxString& aPath )
 {
     BOARD* brd = nullptr;
 
-    if( !Pgm().IsGUI() &&
-        Pgm().GetSettingsManager().IsProjectOpen() )
+    if( !Pgm().IsGUI() && Pgm().GetSettingsManager().IsProjectOpen() )
     {
         wxString pcbPath = aPath;
 
@@ -309,7 +308,7 @@ BOARD* PCBNEW_JOBS_HANDLER::getBoard( const wxString& aPath )
         brd = LoadBoard( aPath, true );
     }
 
-    if ( !brd )
+    if( !brd )
     {
         m_reporter->Report( _( "Failed to load board\n" ), RPT_SEVERITY_ERROR );
     }
@@ -330,6 +329,7 @@ int PCBNEW_JOBS_HANDLER::JobExportStep( JOB* aJob )
     if( !brd )
         return CLI::EXIT_CODES::ERR_INVALID_INPUT_FILE;
 
+    aJob->SetTitleBlock( brd->GetTitleBlock() );
     brd->GetProject()->ApplyTextVars( aJob->GetVarOverrides() );
     brd->SynchronizeProperties();
 
@@ -472,6 +472,7 @@ int PCBNEW_JOBS_HANDLER::JobExportRender( JOB* aJob )
     if( !brd )
         return CLI::EXIT_CODES::ERR_INVALID_INPUT_FILE;
 
+    aJob->SetTitleBlock( brd->GetTitleBlock() );
     brd->GetProject()->ApplyTextVars( aJob->GetVarOverrides() );
     brd->SynchronizeProperties();
 
@@ -714,6 +715,8 @@ int PCBNEW_JOBS_HANDLER::JobExportSvg( JOB* aJob )
     if( !brd )
         return CLI::EXIT_CODES::ERR_INVALID_INPUT_FILE;
 
+    aJob->SetTitleBlock( brd->GetTitleBlock() );
+
     if( aSvgJob->m_genMode == JOB_EXPORT_PCB_SVG::GEN_MODE::SINGLE )
     {
         if( aSvgJob->GetOutputPath().IsEmpty() )
@@ -785,6 +788,7 @@ int PCBNEW_JOBS_HANDLER::JobExportDxf( JOB* aJob )
     if( !brd )
         return CLI::EXIT_CODES::ERR_INVALID_INPUT_FILE;
 
+    aJob->SetTitleBlock( brd->GetTitleBlock() );
     loadOverrideDrawingSheet( brd, aDxfJob->m_drawingSheet );
     brd->GetProject()->ApplyTextVars( aJob->GetVarOverrides() );
     brd->SynchronizeProperties();
@@ -855,6 +859,7 @@ int PCBNEW_JOBS_HANDLER::JobExportPdf( JOB* aJob )
     if( !brd )
         return CLI::EXIT_CODES::ERR_INVALID_INPUT_FILE;
 
+    aJob->SetTitleBlock( brd->GetTitleBlock() );
     loadOverrideDrawingSheet( brd, aPdfJob->m_drawingSheet );
     brd->GetProject()->ApplyTextVars( aJob->GetVarOverrides() );
     brd->SynchronizeProperties();
@@ -935,6 +940,7 @@ int PCBNEW_JOBS_HANDLER::JobExportGerbers( JOB* aJob )
     if( !brd )
         return CLI::EXIT_CODES::ERR_INVALID_INPUT_FILE;
 
+    aJob->SetTitleBlock( brd->GetTitleBlock() );
     loadOverrideDrawingSheet( brd, aGerberJob->m_drawingSheet );
     brd->GetProject()->ApplyTextVars( aJob->GetVarOverrides() );
     brd->SynchronizeProperties();
@@ -1149,6 +1155,7 @@ int PCBNEW_JOBS_HANDLER::JobExportGerber( JOB* aJob )
     if( !brd )
         return CLI::EXIT_CODES::ERR_INVALID_INPUT_FILE;
 
+    aJob->SetTitleBlock( brd->GetTitleBlock() );
     brd->GetProject()->ApplyTextVars( aJob->GetVarOverrides() );
     brd->SynchronizeProperties();
 
@@ -1228,6 +1235,8 @@ int PCBNEW_JOBS_HANDLER::JobExportDrill( JOB* aJob )
     if( !brd )
         return CLI::EXIT_CODES::ERR_INVALID_INPUT_FILE;
 
+    aJob->SetTitleBlock( brd->GetTitleBlock() );
+
     wxString outPath = aDrillJob->GetFullOutputPath( brd->GetProject() );
 
     if( !PATHS::EnsurePathExists( outPath ) )
@@ -1344,9 +1353,9 @@ int PCBNEW_JOBS_HANDLER::JobExportPos( JOB* aJob )
     BOARD* brd = getBoard( aPosJob->m_filename );
 
     if( !brd )
-    {
         return CLI::EXIT_CODES::ERR_INVALID_INPUT_FILE;
-    }
+
+    aJob->SetTitleBlock( brd->GetTitleBlock() );
 
     if( aPosJob->GetOutputPath().IsEmpty() )
     {
@@ -1667,6 +1676,7 @@ int PCBNEW_JOBS_HANDLER::JobExportDrc( JOB* aJob )
     if( !brd )
         return CLI::EXIT_CODES::ERR_INVALID_INPUT_FILE;
 
+    aJob->SetTitleBlock( brd->GetTitleBlock() );
     brd->GetProject()->ApplyTextVars( aJob->GetVarOverrides() );
     brd->SynchronizeProperties();
 
@@ -1843,6 +1853,8 @@ int PCBNEW_JOBS_HANDLER::JobExportIpc2581( JOB* aJob )
     if( !brd )
         return CLI::EXIT_CODES::ERR_INVALID_INPUT_FILE;
 
+    aJob->SetTitleBlock( brd->GetTitleBlock() );
+
     if( job->OutputPathFullSpecified() )
     {
         wxFileName fn = brd->GetFileName();
@@ -1936,6 +1948,8 @@ int PCBNEW_JOBS_HANDLER::JobExportOdb( JOB* aJob )
     if( !brd )
         return CLI::EXIT_CODES::ERR_INVALID_INPUT_FILE;
 
+    aJob->SetTitleBlock( brd->GetTitleBlock() );
+
     wxFileName fn( brd->GetFileName() );
     wxString   path = job->GetOutputPath();