diff --git a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Services/TemplatePathFormatter.cs b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Services/TemplatePathFormatter.cs
new file mode 100644
index 00000000..fb5fdfdf
--- /dev/null
+++ b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Services/TemplatePathFormatter.cs
@@ -0,0 +1,33 @@
+using ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates;
+
+namespace ZB.MOM.WW.ScadaBridge.CentralUI.Services;
+
+///
+/// Renders a template's location as "Folder / Subfolder / Name" for the Create
+/// Instance template field and the picker footer. Pure; tolerates dangling or
+/// cyclic folder ids (stops after 32 hops) so a half-migrated catalogue never
+/// breaks the create page.
+///
+public static class TemplatePathFormatter
+{
+ /// Separator placed between each folder segment and the template name.
+ public const string Separator = " / ";
+
+ ///
+ /// Builds the display path for by walking its
+ /// folder chain upward through .
+ ///
+ public static string Format(Template template, IReadOnlyDictionary foldersById)
+ {
+ var parts = new List { template.Name };
+ var next = template.FolderId;
+ var hops = 0;
+ while (next is int id && foldersById.TryGetValue(id, out var folder) && hops++ < 32)
+ {
+ parts.Add(folder.Name);
+ next = folder.ParentFolderId;
+ }
+ parts.Reverse();
+ return string.Join(Separator, parts);
+ }
+}
diff --git a/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Services/TemplatePathFormatterTests.cs b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Services/TemplatePathFormatterTests.cs
new file mode 100644
index 00000000..c3ad8e62
--- /dev/null
+++ b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Services/TemplatePathFormatterTests.cs
@@ -0,0 +1,43 @@
+using ZB.MOM.WW.ScadaBridge.CentralUI.Services;
+using ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates;
+
+namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Services;
+
+public class TemplatePathFormatterTests
+{
+ private static Dictionary Folders(params TemplateFolder[] f) => f.ToDictionary(x => x.Id);
+
+ [Fact]
+ public void RootTemplate_IsNameOnly()
+ {
+ var t = new Template("Motor Controller") { Id = 1, FolderId = null };
+ Assert.Equal("Motor Controller", TemplatePathFormatter.Format(t, Folders()));
+ }
+
+ [Fact]
+ public void NestedTemplate_JoinsFolderChainWithSlashes()
+ {
+ var controllers = new TemplateFolder("Controllers") { Id = 10, ParentFolderId = null };
+ var conveyor = new TemplateFolder("Conveyor") { Id = 11, ParentFolderId = 10 };
+ var t = new Template("MES Line Controller") { Id = 1, FolderId = 11 };
+ Assert.Equal("Controllers / Conveyor / MES Line Controller",
+ TemplatePathFormatter.Format(t, Folders(controllers, conveyor)));
+ }
+
+ [Fact]
+ public void UnknownFolderId_FallsBackToNameOnly_WithoutThrowing()
+ {
+ var t = new Template("Orphan") { Id = 1, FolderId = 999 };
+ Assert.Equal("Orphan", TemplatePathFormatter.Format(t, Folders()));
+ }
+
+ [Fact]
+ public void FolderCycle_Terminates()
+ {
+ var a = new TemplateFolder("A") { Id = 1, ParentFolderId = 2 };
+ var b = new TemplateFolder("B") { Id = 2, ParentFolderId = 1 };
+ var t = new Template("T") { Id = 1, FolderId = 1 };
+ var s = TemplatePathFormatter.Format(t, Folders(a, b));
+ Assert.EndsWith("T", s); // no hang, no exception
+ }
+}