diff --git a/src/DynareBison.yy b/src/DynareBison.yy
index d7b058465461a4e7e35386160b90b6e7055023ba..b6fc9a3bab1de4b8c4893ca20f89dd964cc14eb7 100644
--- a/src/DynareBison.yy
+++ b/src/DynareBison.yy
@@ -219,7 +219,7 @@ str_tolower(string s)
 %token HOMOTOPY_MAX_COMPLETION_SHARE HOMOTOPY_MIN_STEP_SIZE HOMOTOPY_INITIAL_STEP_SIZE HOMOTOPY_STEP_SIZE_INCREASE_SUCCESS_COUNT
 %token HOMOTOPY_LINEARIZATION_FALLBACK HOMOTOPY_MARGINAL_LINEARIZATION_FALLBACK HOMOTOPY_EXCLUDE_VAREXO FROM_INITVAL_TO_ENDVAL
 %token STATIC_MFS RELATIVE_TO_INITVAL MATCHED_IRFS MATCHED_IRFS_WEIGHTS WEIGHTS PERPENDICULAR
-%token HETEROGENEITY HETEROGENEITY_DIMENSION SUM
+%token HETEROGENEITY HETEROGENEITY_DIMENSION SUM PERFECT_FORESIGHT_CONTROLLED_PATHS EXOGENIZE ENDOGENIZE
 
 %token <vector<string>> SYMBOL_VEC
 
@@ -263,6 +263,8 @@ str_tolower(string s)
 %type <tuple<string, string, string>> matched_irfs_weights_elem_var_varexo
 %type <pair<tuple<string, string, string, string, string, string>, expr_t>> matched_irfs_weights_elem
 %type <map<tuple<string, string, string, string, string, string>, expr_t>> matched_irfs_weights_list
+%type <tuple<string, vector<AbstractShocksStatement::period_range_t>, vector<expr_t>, string>> perfect_foresight_controlled_paths_elem
+%type <vector<tuple<string, vector<AbstractShocksStatement::period_range_t>, vector<expr_t>, string>>> perfect_foresight_controlled_paths_list
 %%
 
 %start statement_list;
@@ -384,6 +386,7 @@ statement : parameters
           | perfect_foresight_solver
           | perfect_foresight_with_expectation_errors_setup
           | perfect_foresight_with_expectation_errors_solver
+          | perfect_foresight_controlled_paths
           | prior_function
           | posterior_function
           | method_of_moments
@@ -1614,6 +1617,31 @@ perfect_foresight_with_expectation_errors_solver_options : o_pfwee_constant_simu
                                                          | perfect_foresight_solver_options
                                                          ;
 
+perfect_foresight_controlled_paths : PERFECT_FORESIGHT_CONTROLLED_PATHS ';' perfect_foresight_controlled_paths_list END ';'
+                                     { driver.perfect_foresight_controlled_paths($3, 1); }
+| PERFECT_FORESIGHT_CONTROLLED_PATHS '(' LEARNT_IN EQUAL integer_or_date ')' ';' perfect_foresight_controlled_paths_list END ';'
+                                     { driver.perfect_foresight_controlled_paths($8, $5); }
+                                   ;
+
+perfect_foresight_controlled_paths_list : perfect_foresight_controlled_paths_list perfect_foresight_controlled_paths_elem
+                                          {
+                                            $$ = $1;
+                                            $$.push_back($2);
+                                          }
+                                        | perfect_foresight_controlled_paths_elem
+                                          { $$ = { $1 }; }
+                                        ;
+
+perfect_foresight_controlled_paths_elem : EXOGENIZE symbol ';' PERIODS period_list ';' VALUES value_list ';' ENDOGENIZE symbol ';'
+                                          {
+                                            driver.check_symbol_is_endogenous($2);
+                                            driver.check_symbol_is_exogenous($11, false);
+                                            if ($5.size() != $8.size())
+                                              driver.error("The number of periods is different from the number of values");
+                                            $$ = { $2, $5, $8, $11};
+                                          }
+                                        ;
+
 method_of_moments : METHOD_OF_MOMENTS ';'
                     { driver.method_of_moments(); }
                   | METHOD_OF_MOMENTS '(' method_of_moments_options_list ')' ';'
diff --git a/src/DynareFlex.ll b/src/DynareFlex.ll
index 9f8cfc07fff45a6c62297e477fa71e2049a2057d..7a4aa1eb3525a2cf04b798087b181c29ae74e94f 100644
--- a/src/DynareFlex.ll
+++ b/src/DynareFlex.ll
@@ -236,6 +236,7 @@ DATE -?[0-9]+([ya]|m([1-9]|1[0-2])|q[1-4]|[sh][12])
 <INITIAL>pac_target_info {BEGIN DYNARE_BLOCK; return token::PAC_TARGET_INFO;}
 <INITIAL>matched_irfs {BEGIN DYNARE_BLOCK; return token::MATCHED_IRFS;}
 <INITIAL>matched_irfs_weights {BEGIN DYNARE_BLOCK; return token::MATCHED_IRFS_WEIGHTS;}
+<INITIAL>perfect_foresight_controlled_paths {BEGIN DYNARE_BLOCK; return token::PERFECT_FORESIGHT_CONTROLLED_PATHS;}
 
  /* For the semicolon after an "end" keyword */
 <INITIAL>; {return Dynare::parser::token_type (yytext[0]);}
@@ -847,6 +848,8 @@ DATE -?[0-9]+([ya]|m([1-9]|1[0-2])|q[1-4]|[sh][12])
   return token::DD;
 }
 <DYNARE_BLOCK>weights {return token::WEIGHTS;}
+<DYNARE_BLOCK>exogenize {return token::EXOGENIZE;}
+<DYNARE_BLOCK>endogenize {return token::ENDOGENIZE;}
 
  /* Inside Dynare statement */
 <DYNARE_STATEMENT>solve_algo {return token::SOLVE_ALGO;}
diff --git a/src/ModFile.cc b/src/ModFile.cc
index 5a0f38c064303bd443954d5588db13bb595eb68a..928ec611b51b1c724c52a1c7c22c13fc15d49dc1 100644
--- a/src/ModFile.cc
+++ b/src/ModFile.cc
@@ -1,5 +1,5 @@
 /*
- * Copyright © 2006-2024 Dynare Team
+ * Copyright © 2006-2025 Dynare Team
  *
  * This file is part of Dynare.
  *
@@ -1137,7 +1137,8 @@ ModFile::writeMOutput(const string& basename, bool clear_all, bool clear_global,
               << "M_.heteroskedastic_shocks.Qvalue_orig = [];" << endl
               << "M_.heteroskedastic_shocks.Qscale_orig = [];" << endl
               << "M_.matched_irfs = {};" << endl
-              << "M_.matched_irfs_weights = {};" << endl;
+              << "M_.matched_irfs_weights = {};" << endl
+              << "M_.perfect_foresight_controlled_paths = [];" << endl;
 
   // NB: options_.{ramsey,discretionary}_policy should rather be fields of M_
   mOutputFile << boolalpha << "options_.linear = " << linear << ";" << endl
diff --git a/src/ParsingDriver.cc b/src/ParsingDriver.cc
index abbbf4680a9ba0aea3af6a0781cffa18f04e6629..545b6ab5d833b34c08cb899b93a638eb2702e34d 100644
--- a/src/ParsingDriver.cc
+++ b/src/ParsingDriver.cc
@@ -1,5 +1,5 @@
 /*
- * Copyright © 2003-2024 Dynare Team
+ * Copyright © 2003-2025 Dynare Team
  *
  * This file is part of Dynare.
  *
@@ -3747,6 +3747,29 @@ ParsingDriver::perfect_foresight_with_expectation_errors_solver()
   options_list.clear();
 }
 
+void
+ParsingDriver::perfect_foresight_controlled_paths(
+    const vector<tuple<string, vector<AbstractShocksStatement::period_range_t>, vector<expr_t>,
+                       string>>& paths,
+    variant<int, string> learnt_in_period)
+{
+  PerfectForesightControlledPathsStatement::paths_t paths_transformed;
+  for (const auto& [exogenize, periods, values, endogenize] : paths)
+    {
+      int exogenize_id = mod_file->symbol_table.getID(exogenize);
+      int endogenize_id = mod_file->symbol_table.getID(endogenize);
+
+      vector<pair<AbstractShocksStatement::period_range_t, expr_t>> v;
+
+      for (size_t i = 0; i < periods.size(); i++)
+        v.emplace_back(periods[i], values[i]);
+
+      paths_transformed.emplace_back(exogenize_id, move(v), endogenize_id);
+    }
+  mod_file->addStatement(make_unique<PerfectForesightControlledPathsStatement>(
+      move(paths_transformed), move(learnt_in_period), mod_file->symbol_table));
+}
+
 void
 ParsingDriver::method_of_moments()
 {
diff --git a/src/ParsingDriver.hh b/src/ParsingDriver.hh
index df251e792b9d40b4e63040a5fb0bafb7544c6262..ea387be7e3ac53a5f174567385d64d3a4896769f 100644
--- a/src/ParsingDriver.hh
+++ b/src/ParsingDriver.hh
@@ -1,5 +1,5 @@
 /*
- * Copyright © 2003-2024 Dynare Team
+ * Copyright © 2003-2025 Dynare Team
  *
  * This file is part of Dynare.
  *
@@ -921,6 +921,10 @@ public:
   void perfect_foresight_solver();
   void perfect_foresight_with_expectation_errors_setup();
   void perfect_foresight_with_expectation_errors_solver();
+  void perfect_foresight_controlled_paths(
+      const vector<tuple<string, vector<AbstractShocksStatement::period_range_t>, vector<expr_t>,
+                         string>>& paths,
+      variant<int, string> learnt_in_period);
   void prior_posterior_function(bool prior_func);
   //! Method of Moments estimation statement
   void method_of_moments();
diff --git a/src/Shocks.cc b/src/Shocks.cc
index 3c33baf2ec06e9af90d475e0cbc7881502142f9e..49cfdfa812152dfb1cb0b269e084ce4dd1e2585c 100644
--- a/src/Shocks.cc
+++ b/src/Shocks.cc
@@ -45,6 +45,17 @@ static auto print_json_period_range = []<class T>(ostream& output, const T& arg)
     static_assert(always_false_v<T>, "Non-exhaustive visitor!");
 };
 
+static auto print_matlab_learnt_in = [](ostream& output, const auto& p) { output << p; };
+
+static auto print_json_learnt_in = []<class T>(ostream& output, const T& p) {
+  if constexpr (is_same_v<T, int>)
+    output << p;
+  else if constexpr (is_same_v<T, string>)
+    output << '"' << p << '"';
+  else
+    static_assert(always_false_v<T>, "Non-exhaustive visitor!");
+};
+
 AbstractShocksStatement::AbstractShocksStatement(bool overwrite_arg, ShockType type_arg,
                                                  det_shocks_t det_shocks_arg,
                                                  const SymbolTable& symbol_table_arg) :
@@ -564,7 +575,6 @@ void
 ShocksLearntInStatement::writeOutput(ostream& output, [[maybe_unused]] const string& basename,
                                      [[maybe_unused]] bool minimal_workspace) const
 {
-  auto print_matlab_learnt_in = [&](const auto& p) { output << p; };
   if (overwrite)
     {
       output << "if ~isempty(M_.learnt_shocks)" << endl
@@ -576,7 +586,7 @@ ShocksLearntInStatement::writeOutput(ostream& output, [[maybe_unused]] const str
       output << "') || x ~= ";
       /* NB: date expression not parenthesized since it can only contain a + operator, which has
          higher precedence than ~= and || */
-      visit(print_matlab_learnt_in, learnt_in_period);
+      visit(bind(print_matlab_learnt_in, ref(output), placeholders::_1), learnt_in_period);
       output << ", {M_.learnt_shocks.learnt_in}));" << endl << "end" << endl;
     }
 
@@ -585,7 +595,7 @@ ShocksLearntInStatement::writeOutput(ostream& output, [[maybe_unused]] const str
     for (const auto& [type, period_range, value] : shock_vec)
       {
         output << "struct('learnt_in',";
-        visit(print_matlab_learnt_in, learnt_in_period);
+        visit(bind(print_matlab_learnt_in, ref(output), placeholders::_1), learnt_in_period);
         output << ",'exo_id'," << symbol_table.getTypeSpecificID(id) + 1 << ",'periods',";
         visit(bind(print_matlab_period_range, ref(output), placeholders::_1), period_range);
         output << ",'type','" << typeToString(type) << "'"
@@ -601,16 +611,7 @@ ShocksLearntInStatement::writeJsonOutput(ostream& output) const
 {
   output << R"({"statementName": "shocks")"
          << R"(, "learnt_in": )";
-  visit(
-      [&]<class T>(const T& p) {
-        if constexpr (is_same_v<T, int>)
-          output << p;
-        else if constexpr (is_same_v<T, string>)
-          output << '"' << p << '"';
-        else
-          static_assert(always_false_v<T>, "Non-exhaustive visitor!");
-      },
-      learnt_in_period);
+  visit(bind(print_json_learnt_in, ref(output), placeholders::_1), learnt_in_period);
   output << R"(, "overwrite": )" << boolalpha << overwrite << R"(, "learnt_shocks": [)";
   for (bool printed_something {false}; const auto& [id, shock_vec] : learnt_shocks)
     {
@@ -915,6 +916,65 @@ ConditionalForecastPathsStatement::writeJsonOutput(ostream& output) const
   output << "]}";
 }
 
+PerfectForesightControlledPathsStatement::PerfectForesightControlledPathsStatement(
+    paths_t paths_arg, variant<int, string> learnt_in_period_arg,
+    const SymbolTable& symbol_table_arg) :
+    paths {move(paths_arg)},
+    learnt_in_period {move(learnt_in_period_arg)},
+    symbol_table {symbol_table_arg}
+{
+}
+
+void
+PerfectForesightControlledPathsStatement::writeOutput(ostream& output,
+                                                      [[maybe_unused]] const string& basename,
+                                                      [[maybe_unused]] bool minimal_workspace) const
+{
+  for (const auto& [exogenize_id, constraints, endogenize_id] : paths)
+    for (const auto& [period_range, value] : constraints)
+      {
+        output << "M_.perfect_foresight_controlled_paths = [ "
+                  "M_.perfect_foresight_controlled_paths;"
+               << endl
+               << "struct('exogenize_id'," << symbol_table.getTypeSpecificID(exogenize_id) + 1
+               << ",'periods',";
+        visit(bind(print_matlab_period_range, ref(output), placeholders::_1), period_range);
+        output << ",'value',";
+        value->writeOutput(output);
+        output << ",'endogenize_id'," << symbol_table.getTypeSpecificID(endogenize_id) + 1
+               << ",'learnt_in',";
+        visit(bind(print_matlab_learnt_in, ref(output), placeholders::_1), learnt_in_period);
+        output << ") ];" << endl;
+      }
+}
+
+void
+PerfectForesightControlledPathsStatement::writeJsonOutput(ostream& output) const
+{
+  output << R"({"statementName": "perfect_foresight_controlled_paths")"
+         << R"(, "paths": [)";
+  for (bool printed_something {false};
+       const auto& [exogenize_id, constraints, endogenize_id] : paths)
+    {
+      if (exchange(printed_something, true))
+        output << ", ";
+      output << R"({"exogenize": ")" << symbol_table.getName(exogenize_id) << R"(", )"
+             << R"("values": [)";
+      for (bool printed_something2 {false}; const auto& [period_range, value] : constraints)
+        {
+          if (exchange(printed_something2, true))
+            output << ", ";
+          output << "{";
+          visit(bind(print_json_period_range, ref(output), placeholders::_1), period_range);
+          output << R"(, "value": ")";
+          value->writeJsonOutput(output, {}, {});
+          output << R"("})";
+        }
+      output << R"(], "endogenize": ")" << symbol_table.getName(endogenize_id) << R"("})";
+    }
+  output << "]}";
+}
+
 MomentCalibration::MomentCalibration(constraints_t constraints_arg,
                                      const SymbolTable& symbol_table_arg) :
     constraints {move(constraints_arg)}, symbol_table {symbol_table_arg}
diff --git a/src/Shocks.hh b/src/Shocks.hh
index dc021e4ab7604d422ad624c2393a9d87b75f4d40..fabffe54d230adac1c4bad43fddfe137d35485c5 100644
--- a/src/Shocks.hh
+++ b/src/Shocks.hh
@@ -1,5 +1,5 @@
 /*
- * Copyright © 2003-2024 Dynare Team
+ * Copyright © 2003-2025 Dynare Team
  *
  * This file is part of Dynare.
  *
@@ -216,6 +216,26 @@ public:
   static int computePathLength(const AbstractShocksStatement::det_shocks_t& paths);
 };
 
+class PerfectForesightControlledPathsStatement : public Statement
+{
+public:
+  // (exogenize_id, vector of (period range, value), endogenize_id)
+  using paths_t
+      = vector<tuple<int, vector<pair<AbstractShocksStatement::period_range_t, expr_t>>, int>>;
+
+private:
+  const paths_t paths;
+  const variant<int, string> learnt_in_period;
+  const SymbolTable& symbol_table;
+
+public:
+  PerfectForesightControlledPathsStatement(paths_t paths_arg,
+                                           variant<int, string> learnt_in_period_arg,
+                                           const SymbolTable& symbol_table_arg);
+  void writeOutput(ostream& output, const string& basename, bool minimal_workspace) const override;
+  void writeJsonOutput(ostream& output) const override;
+};
+
 class MomentCalibration : public Statement
 {
 public: