diff --git a/migrations_sqlx/postgres/20250101000000_initial.sql b/migrations_sqlx/postgres/20250101000000_initial.sql
index 1ecf6bb4..09ed39f0 100644
--- a/migrations_sqlx/postgres/20250101000000_initial.sql
+++ b/migrations_sqlx/postgres/20250101000000_initial.sql
@@ -1123,21 +1123,21 @@ END $$;
 -- without cross-database joins. See VectorStore trait for chunk operations.
 
 -- ======================================================================
--- Prompts
+-- Templates
 -- ======================================================================
 
 -- Reusable system prompt templates.
 
 DO $$ BEGIN
-    CREATE TYPE prompt_owner_type AS ENUM ('organization', 'team', 'project', 'user');
+    CREATE TYPE template_owner_type AS ENUM ('organization', 'team', 'project', 'user');
 EXCEPTION
     WHEN duplicate_object THEN null;
 END $$;
 
-CREATE TABLE IF NOT EXISTS prompts (
+CREATE TABLE IF NOT EXISTS templates (
     id UUID PRIMARY KEY NOT NULL,
-    -- Ownership (who can access this prompt)
-    owner_type prompt_owner_type NOT NULL,
+    -- Ownership (who can access this template)
+    owner_type template_owner_type NOT NULL,
     owner_id UUID NOT NULL,
     name VARCHAR(255) NOT NULL,
     description TEXT,
@@ -1152,13 +1152,13 @@ CREATE TABLE IF NOT EXISTS prompts (
     UNIQUE(owner_type, owner_id, name)
 );
 
-CREATE INDEX IF NOT EXISTS idx_prompts_owner ON prompts(owner_type, owner_id);
--- Partial index for non-deleted prompts (most queries filter by deleted_at IS NULL)
-CREATE INDEX IF NOT EXISTS idx_prompts_owner_active ON prompts(owner_type, owner_id) WHERE deleted_at IS NULL;
-CREATE INDEX IF NOT EXISTS idx_prompts_name ON prompts(name);
+CREATE INDEX IF NOT EXISTS idx_templates_owner ON templates(owner_type, owner_id);
+-- Partial index for non-deleted templates (most queries filter by deleted_at IS NULL)
+CREATE INDEX IF NOT EXISTS idx_templates_owner_active ON templates(owner_type, owner_id) WHERE deleted_at IS NULL;
+CREATE INDEX IF NOT EXISTS idx_templates_name ON templates(name);
 
 DO $$ BEGIN
-    CREATE TRIGGER update_prompts_updated_at BEFORE UPDATE ON prompts FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
+    CREATE TRIGGER update_templates_updated_at BEFORE UPDATE ON templates FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
 EXCEPTION WHEN duplicate_object THEN null;
 END $$;
 
diff --git a/migrations_sqlx/sqlite/20250101000000_initial.sql b/migrations_sqlx/sqlite/20250101000000_initial.sql
index 0a602c28..c31b1de8 100644
--- a/migrations_sqlx/sqlite/20250101000000_initial.sql
+++ b/migrations_sqlx/sqlite/20250101000000_initial.sql
@@ -916,14 +916,14 @@ CREATE INDEX IF NOT EXISTS idx_vector_store_files_deleted_at ON vector_store_fil
 -- without cross-database joins. See VectorStore trait for chunk operations.
 
 -- ======================================================================
--- Prompts
+-- Templates
 -- ======================================================================
 
 -- Reusable system prompt templates.
 -- owner_type: 'organization', 'team', 'project', 'user'
-CREATE TABLE IF NOT EXISTS prompts (
+CREATE TABLE IF NOT EXISTS templates (
     id TEXT PRIMARY KEY NOT NULL,
-    -- Ownership (who can access this prompt)
+    -- Ownership (who can access this template)
     owner_type TEXT NOT NULL CHECK (owner_type IN ('organization', 'team', 'project', 'user')),
     owner_id TEXT NOT NULL,
     name TEXT NOT NULL,
@@ -939,10 +939,10 @@ CREATE TABLE IF NOT EXISTS prompts (
     UNIQUE(owner_type, owner_id, name)
 );
 
-CREATE INDEX IF NOT EXISTS idx_prompts_owner ON prompts(owner_type, owner_id);
--- Partial index for non-deleted prompts (most queries filter by deleted_at IS NULL)
-CREATE INDEX IF NOT EXISTS idx_prompts_owner_active ON prompts(owner_type, owner_id) WHERE deleted_at IS NULL;
-CREATE INDEX IF NOT EXISTS idx_prompts_name ON prompts(name);
+CREATE INDEX IF NOT EXISTS idx_templates_owner ON templates(owner_type, owner_id);
+-- Partial index for non-deleted templates (most queries filter by deleted_at IS NULL)
+CREATE INDEX IF NOT EXISTS idx_templates_owner_active ON templates(owner_type, owner_id) WHERE deleted_at IS NULL;
+CREATE INDEX IF NOT EXISTS idx_templates_name ON templates(name);
 
 -- ======================================================================
 -- Service Accounts
diff --git a/src/config/limits.rs b/src/config/limits.rs
index 5868a0b6..ba2cfe08 100644
--- a/src/config/limits.rs
+++ b/src/config/limits.rs
@@ -95,9 +95,9 @@ pub struct ResourceLimits {
     #[serde(default = "default_max_conversations_per_owner")]
     pub max_conversations_per_owner: u32,
 
-    /// Maximum prompts per owner (org/team/project/user). Default: 5,000.
-    #[serde(default = "default_max_prompts_per_owner")]
-    pub max_prompts_per_owner: u32,
+    /// Maximum templates per owner (org/team/project/user). Default: 5,000.
+    #[serde(default = "default_max_templates_per_owner")]
+    pub max_templates_per_owner: u32,
 
     /// Maximum domain verifications per SSO configuration. Default: 50.
     #[serde(default = "default_max_domains_per_sso_config")]
@@ -146,7 +146,7 @@ impl Default for ResourceLimits {
             max_vector_stores_per_owner: default_max_vector_stores_per_owner(),
             max_files_per_vector_store: default_max_files_per_vector_store(),
             max_conversations_per_owner: default_max_conversations_per_owner(),
-            max_prompts_per_owner: default_max_prompts_per_owner(),
+            max_templates_per_owner: default_max_templates_per_owner(),
             max_domains_per_sso_config: default_max_domains_per_sso_config(),
             max_sso_group_mappings_per_org: default_max_sso_group_mappings_per_org(),
             max_members_per_org: default_max_members_per_org(),
@@ -218,7 +218,7 @@ fn default_max_conversations_per_owner() -> u32 {
     10_000
 }
 
-fn default_max_prompts_per_owner() -> u32 {
+fn default_max_templates_per_owner() -> u32 {
     5_000
 }
 
diff --git a/src/db/mod.rs b/src/db/mod.rs
index 1e3c84a2..4ad8c29f 100644
--- a/src/db/mod.rs
+++ b/src/db/mod.rs
@@ -53,7 +53,7 @@ struct CachedRepos {
     vector_stores: Arc<dyn VectorStoresRepo>,
     files: Arc<dyn FilesRepo>,
     teams: Arc<dyn TeamRepo>,
-    prompts: Arc<dyn PromptRepo>,
+    templates: Arc<dyn TemplateRepo>,
     #[cfg(feature = "sso")]
     sso_group_mappings: Arc<dyn SsoGroupMappingRepo>,
     #[cfg(feature = "sso")]
@@ -131,7 +131,7 @@ impl DbPool {
             vector_stores: Arc::new(sqlite::SqliteVectorStoresRepo::new(pool.clone())),
             files: Arc::new(sqlite::SqliteFilesRepo::new(pool.clone())),
             teams: Arc::new(sqlite::SqliteTeamRepo::new(pool.clone())),
-            prompts: Arc::new(sqlite::SqlitePromptRepo::new(pool.clone())),
+            templates: Arc::new(sqlite::SqliteTemplateRepo::new(pool.clone())),
             #[cfg(feature = "sso")]
             sso_group_mappings: Arc::new(sqlite::SqliteSsoGroupMappingRepo::new(pool.clone())),
             #[cfg(feature = "sso")]
@@ -169,7 +169,7 @@ impl DbPool {
             vector_stores: Arc::new(sqlite::SqliteVectorStoresRepo::new(pool.clone())),
             files: Arc::new(sqlite::SqliteFilesRepo::new(pool.clone())),
             teams: Arc::new(sqlite::SqliteTeamRepo::new(pool.clone())),
-            prompts: Arc::new(sqlite::SqlitePromptRepo::new(pool.clone())),
+            templates: Arc::new(sqlite::SqliteTemplateRepo::new(pool.clone())),
             #[cfg(feature = "sso")]
             sso_group_mappings: unreachable!("SSO not supported in WASM builds"),
             #[cfg(feature = "sso")]
@@ -244,7 +244,7 @@ impl DbPool {
                 write_pool.clone(),
                 read_pool.clone(),
             )),
-            prompts: Arc::new(postgres::PostgresPromptRepo::new(
+            templates: Arc::new(postgres::PostgresTemplateRepo::new(
                 write_pool.clone(),
                 read_pool.clone(),
             )),
@@ -330,7 +330,7 @@ impl DbPool {
                     vector_stores: Arc::new(sqlite::SqliteVectorStoresRepo::new(pool.clone())),
                     files: Arc::new(sqlite::SqliteFilesRepo::new(pool.clone())),
                     teams: Arc::new(sqlite::SqliteTeamRepo::new(pool.clone())),
-                    prompts: Arc::new(sqlite::SqlitePromptRepo::new(pool.clone())),
+                    templates: Arc::new(sqlite::SqliteTemplateRepo::new(pool.clone())),
                     #[cfg(feature = "sso")]
                     sso_group_mappings: Arc::new(sqlite::SqliteSsoGroupMappingRepo::new(
                         pool.clone(),
@@ -430,7 +430,7 @@ impl DbPool {
                         write_pool.clone(),
                         read_pool.clone(),
                     )),
-                    prompts: Arc::new(postgres::PostgresPromptRepo::new(
+                    templates: Arc::new(postgres::PostgresTemplateRepo::new(
                         write_pool.clone(),
                         read_pool.clone(),
                     )),
@@ -581,9 +581,9 @@ impl DbPool {
         Arc::clone(&self.repos.teams)
     }
 
-    /// Get prompt repository
-    pub fn prompts(&self) -> Arc<dyn PromptRepo> {
-        Arc::clone(&self.repos.prompts)
+    /// Get template repository
+    pub fn templates(&self) -> Arc<dyn TemplateRepo> {
+        Arc::clone(&self.repos.templates)
     }
 
     /// Get SSO group mapping repository
diff --git a/src/db/postgres/mod.rs b/src/db/postgres/mod.rs
index 11cfd330..4c6fc59d 100644
--- a/src/db/postgres/mod.rs
+++ b/src/db/postgres/mod.rs
@@ -10,7 +10,6 @@ mod org_rbac_policies;
 mod org_sso_configs;
 mod organizations;
 mod projects;
-mod prompts;
 mod providers;
 #[cfg(feature = "sso")]
 mod scim_configs;
@@ -22,6 +21,7 @@ mod service_accounts;
 #[cfg(feature = "sso")]
 mod sso_group_mappings;
 mod teams;
+mod templates;
 mod usage;
 mod users;
 mod vector_stores;
@@ -38,7 +38,6 @@ pub use org_rbac_policies::PostgresOrgRbacPolicyRepo;
 pub use org_sso_configs::PostgresOrgSsoConfigRepo;
 pub use organizations::PostgresOrganizationRepo;
 pub use projects::PostgresProjectRepo;
-pub use prompts::PostgresPromptRepo;
 pub use providers::PostgresDynamicProviderRepo;
 #[cfg(feature = "sso")]
 pub use scim_configs::PostgresOrgScimConfigRepo;
@@ -50,6 +49,7 @@ pub use service_accounts::PostgresServiceAccountRepo;
 #[cfg(feature = "sso")]
 pub use sso_group_mappings::PostgresSsoGroupMappingRepo;
 pub use teams::PostgresTeamRepo;
+pub use templates::PostgresTemplateRepo;
 pub use usage::PostgresUsageRepo;
 pub use users::PostgresUserRepo;
 pub use vector_stores::PostgresVectorStoresRepo;
diff --git a/src/db/postgres/prompts.rs b/src/db/postgres/templates.rs
similarity index 66%
rename from src/db/postgres/prompts.rs
rename to src/db/postgres/templates.rs
index 4e407c44..81a2dc37 100644
--- a/src/db/postgres/prompts.rs
+++ b/src/db/postgres/templates.rs
@@ -8,19 +8,19 @@ use crate::{
     db::{
         error::{DbError, DbResult},
         repos::{
-            Cursor, CursorDirection, ListParams, ListResult, PageCursors, PromptRepo,
+            Cursor, CursorDirection, ListParams, ListResult, PageCursors, TemplateRepo,
             cursor_from_row,
         },
     },
-    models::{CreatePrompt, Prompt, PromptOwnerType, UpdatePrompt},
+    models::{CreateTemplate, Template, TemplateOwnerType, UpdateTemplate},
 };
 
-pub struct PostgresPromptRepo {
+pub struct PostgresTemplateRepo {
     write_pool: PgPool,
     read_pool: PgPool,
 }
 
-impl PostgresPromptRepo {
+impl PostgresTemplateRepo {
     pub fn new(write_pool: PgPool, read_pool: Option<PgPool>) -> Self {
         let read_pool = read_pool.unwrap_or_else(|| write_pool.clone());
         Self {
@@ -29,10 +29,10 @@ impl PostgresPromptRepo {
         }
     }
 
-    /// Parse a Prompt from a database row.
-    fn parse_prompt(row: &sqlx::postgres::PgRow) -> DbResult<Prompt> {
+    /// Parse a Template from a database row.
+    fn parse_template(row: &sqlx::postgres::PgRow) -> DbResult<Template> {
         let owner_type_str: String = row.get("owner_type");
-        let owner_type: PromptOwnerType = owner_type_str
+        let owner_type: TemplateOwnerType = owner_type_str
             .parse()
             .map_err(|e: String| DbError::Internal(e))?;
 
@@ -42,7 +42,7 @@ impl PostgresPromptRepo {
             .transpose()
             .map_err(|e| DbError::Internal(format!("Failed to parse metadata: {}", e)))?;
 
-        Ok(Prompt {
+        Ok(Template {
             id: row.get("id"),
             owner_type,
             owner_id: row.get("owner_id"),
@@ -58,13 +58,13 @@ impl PostgresPromptRepo {
     /// Helper method for cursor-based pagination.
     async fn list_with_cursor(
         &self,
-        owner_type: PromptOwnerType,
+        owner_type: TemplateOwnerType,
         owner_id: Uuid,
         params: &ListParams,
         cursor: &Cursor,
         fetch_limit: i64,
         limit: i64,
-    ) -> DbResult<ListResult<Prompt>> {
+    ) -> DbResult<ListResult<Template>> {
         let (comparison, order, should_reverse) =
             params.sort_order.cursor_query_params(params.direction);
 
@@ -77,7 +77,7 @@ impl PostgresPromptRepo {
         let query = format!(
             r#"
             SELECT id, owner_type::TEXT, owner_id, name, description, content, metadata, created_at, updated_at
-            FROM prompts
+            FROM templates
             WHERE owner_type = $1 AND owner_id = $2 AND ROW(created_at, id) {} ROW($3, $4)
             {}
             ORDER BY created_at {}, id {}
@@ -96,10 +96,10 @@ impl PostgresPromptRepo {
             .await?;
 
         let has_more = rows.len() as i64 > limit;
-        let mut items: Vec<Prompt> = rows
+        let mut items: Vec<Template> = rows
             .iter()
             .take(limit as usize)
-            .map(Self::parse_prompt)
+            .map(Self::parse_template)
             .collect::<DbResult<Vec<_>>>()?;
 
         if should_reverse {
@@ -117,8 +117,8 @@ impl PostgresPromptRepo {
 
 #[cfg_attr(not(target_arch = "wasm32"), async_trait)]
 #[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
-impl PromptRepo for PostgresPromptRepo {
-    async fn create(&self, input: CreatePrompt) -> DbResult<Prompt> {
+impl TemplateRepo for PostgresTemplateRepo {
+    async fn create(&self, input: CreateTemplate) -> DbResult<Template> {
         let id = Uuid::new_v4();
         let owner_type = input.owner.owner_type();
         let owner_id = input.owner.owner_id();
@@ -132,7 +132,7 @@ impl PromptRepo for PostgresPromptRepo {
 
         let row = sqlx::query(
             r#"
-            INSERT INTO prompts (id, owner_type, owner_id, name, description, content, metadata)
+            INSERT INTO templates (id, owner_type, owner_id, name, description, content, metadata)
             VALUES ($1, $2, $3, $4, $5, $6, $7)
             RETURNING id, owner_type::TEXT, owner_id, name, description, content, metadata, created_at, updated_at
             "#,
@@ -149,21 +149,21 @@ impl PromptRepo for PostgresPromptRepo {
         .map_err(|e| match e {
             sqlx::Error::Database(db_err) if db_err.is_unique_violation() => {
                 DbError::Conflict(format!(
-                    "Prompt with name '{}' already exists for this owner",
+                    "Template with name '{}' already exists for this owner",
                     input.name
                 ))
             }
             _ => DbError::from(e),
         })?;
 
-        Self::parse_prompt(&row)
+        Self::parse_template(&row)
     }
 
-    async fn get_by_id(&self, id: Uuid) -> DbResult<Option<Prompt>> {
+    async fn get_by_id(&self, id: Uuid) -> DbResult<Option<Template>> {
         let result = sqlx::query(
             r#"
             SELECT id, owner_type::TEXT, owner_id, name, description, content, metadata, created_at, updated_at
-            FROM prompts
+            FROM templates
             WHERE id = $1 AND deleted_at IS NULL
             "#,
         )
@@ -172,16 +172,16 @@ impl PromptRepo for PostgresPromptRepo {
         .await?;
 
         match result {
-            Some(row) => Ok(Some(Self::parse_prompt(&row)?)),
+            Some(row) => Ok(Some(Self::parse_template(&row)?)),
             None => Ok(None),
         }
     }
 
-    async fn get_by_id_and_org(&self, id: Uuid, org_id: Uuid) -> DbResult<Option<Prompt>> {
+    async fn get_by_id_and_org(&self, id: Uuid, org_id: Uuid) -> DbResult<Option<Template>> {
         let result = sqlx::query(
             r#"
             SELECT p.id, p.owner_type::TEXT, p.owner_id, p.name, p.description, p.content, p.metadata, p.created_at, p.updated_at
-            FROM prompts p
+            FROM templates p
             WHERE p.id = $1 AND p.deleted_at IS NULL
             AND (
                 (p.owner_type = 'organization' AND p.owner_id = $2)
@@ -209,17 +209,17 @@ impl PromptRepo for PostgresPromptRepo {
         .await?;
 
         match result {
-            Some(row) => Ok(Some(Self::parse_prompt(&row)?)),
+            Some(row) => Ok(Some(Self::parse_template(&row)?)),
             None => Ok(None),
         }
     }
 
     async fn list_by_owner(
         &self,
-        owner_type: PromptOwnerType,
+        owner_type: TemplateOwnerType,
         owner_id: Uuid,
         params: ListParams,
-    ) -> DbResult<ListResult<Prompt>> {
+    ) -> DbResult<ListResult<Template>> {
         let limit = params.limit.unwrap_or(100);
         let fetch_limit = limit + 1;
 
@@ -232,7 +232,7 @@ impl PromptRepo for PostgresPromptRepo {
         let query = if params.include_deleted {
             r#"
             SELECT id, owner_type::TEXT, owner_id, name, description, content, metadata, created_at, updated_at
-            FROM prompts
+            FROM templates
             WHERE owner_type = $1 AND owner_id = $2
             ORDER BY created_at DESC, id DESC
             LIMIT $3
@@ -240,7 +240,7 @@ impl PromptRepo for PostgresPromptRepo {
         } else {
             r#"
             SELECT id, owner_type::TEXT, owner_id, name, description, content, metadata, created_at, updated_at
-            FROM prompts
+            FROM templates
             WHERE owner_type = $1 AND owner_id = $2 AND deleted_at IS NULL
             ORDER BY created_at DESC, id DESC
             LIMIT $3
@@ -255,10 +255,110 @@ impl PromptRepo for PostgresPromptRepo {
             .await?;
 
         let has_more = rows.len() as i64 > limit;
-        let items: Vec<Prompt> = rows
+        let items: Vec<Template> = rows
             .iter()
             .take(limit as usize)
-            .map(Self::parse_prompt)
+            .map(Self::parse_template)
+            .collect::<DbResult<Vec<_>>>()?;
+
+        let cursors =
+            PageCursors::from_items(&items, has_more, CursorDirection::Forward, None, |p| {
+                cursor_from_row(p.created_at, p.id)
+            });
+
+        Ok(ListResult::new(items, has_more, cursors))
+    }
+
+    async fn list_by_org(
+        &self,
+        org_id: Uuid,
+        params: ListParams,
+    ) -> DbResult<ListResult<Template>> {
+        let limit = params.limit.unwrap_or(100);
+        let fetch_limit = limit + 1;
+
+        // Org-scoped filter shared by both cursor and non-cursor paths
+        let org_filter = r#"
+            AND (
+                (p.owner_type = 'organization' AND p.owner_id = $1)
+                OR (p.owner_type = 'team' AND EXISTS (
+                    SELECT 1 FROM teams t WHERE t.id = p.owner_id AND t.org_id = $1
+                ))
+                OR (p.owner_type = 'project' AND EXISTS (
+                    SELECT 1 FROM projects pr WHERE pr.id = p.owner_id AND pr.org_id = $1
+                ))
+                OR (p.owner_type = 'user' AND EXISTS (
+                    SELECT 1 FROM org_memberships om WHERE om.user_id = p.owner_id AND om.org_id = $1
+                ))
+            )
+        "#;
+
+        if let Some(ref cursor) = params.cursor {
+            let (comparison, order, should_reverse) =
+                params.sort_order.cursor_query_params(params.direction);
+
+            let sql = format!(
+                r#"
+                SELECT p.id, p.owner_type::TEXT, p.owner_id, p.name, p.description, p.content, p.metadata, p.created_at, p.updated_at
+                FROM templates p
+                WHERE p.deleted_at IS NULL AND ROW(p.created_at, p.id) {} ROW($2, $3)
+                {}
+                ORDER BY p.created_at {}, p.id {}
+                LIMIT $4
+                "#,
+                comparison, org_filter, order, order
+            );
+
+            let rows = sqlx::query(&sql)
+                .bind(org_id)
+                .bind(cursor.created_at)
+                .bind(cursor.id)
+                .bind(fetch_limit)
+                .fetch_all(&self.read_pool)
+                .await?;
+
+            let has_more = rows.len() as i64 > limit;
+            let mut items: Vec<Template> = rows
+                .iter()
+                .take(limit as usize)
+                .map(Self::parse_template)
+                .collect::<DbResult<Vec<_>>>()?;
+
+            if should_reverse {
+                items.reverse();
+            }
+
+            let cursors =
+                PageCursors::from_items(&items, has_more, params.direction, Some(cursor), |p| {
+                    cursor_from_row(p.created_at, p.id)
+                });
+
+            return Ok(ListResult::new(items, has_more, cursors));
+        }
+
+        let sql = format!(
+            r#"
+            SELECT p.id, p.owner_type::TEXT, p.owner_id, p.name, p.description, p.content, p.metadata, p.created_at, p.updated_at
+            FROM templates p
+            WHERE p.deleted_at IS NULL
+            {}
+            ORDER BY p.created_at DESC, p.id DESC
+            LIMIT $2
+            "#,
+            org_filter
+        );
+
+        let rows = sqlx::query(&sql)
+            .bind(org_id)
+            .bind(fetch_limit)
+            .fetch_all(&self.read_pool)
+            .await?;
+
+        let has_more = rows.len() as i64 > limit;
+        let items: Vec<Template> = rows
+            .iter()
+            .take(limit as usize)
+            .map(Self::parse_template)
             .collect::<DbResult<Vec<_>>>()?;
 
         let cursors =
@@ -271,14 +371,14 @@ impl PromptRepo for PostgresPromptRepo {
 
     async fn count_by_owner(
         &self,
-        owner_type: PromptOwnerType,
+        owner_type: TemplateOwnerType,
         owner_id: Uuid,
         include_deleted: bool,
     ) -> DbResult<i64> {
         let query = if include_deleted {
-            "SELECT COUNT(*) as count FROM prompts WHERE owner_type = $1 AND owner_id = $2"
+            "SELECT COUNT(*) as count FROM templates WHERE owner_type = $1 AND owner_id = $2"
         } else {
-            "SELECT COUNT(*) as count FROM prompts WHERE owner_type = $1 AND owner_id = $2 AND deleted_at IS NULL"
+            "SELECT COUNT(*) as count FROM templates WHERE owner_type = $1 AND owner_id = $2 AND deleted_at IS NULL"
         };
 
         let row = sqlx::query(query)
@@ -290,7 +390,7 @@ impl PromptRepo for PostgresPromptRepo {
         Ok(row.get::<i64, _>("count"))
     }
 
-    async fn update(&self, id: Uuid, input: UpdatePrompt) -> DbResult<Prompt> {
+    async fn update(&self, id: Uuid, input: UpdateTemplate) -> DbResult<Template> {
         let has_name = input.name.is_some();
         let has_description = input.description.is_some();
         let has_content = input.content.is_some();
@@ -322,7 +422,7 @@ impl PromptRepo for PostgresPromptRepo {
 
         let query = format!(
             r#"
-            UPDATE prompts
+            UPDATE templates
             SET {}
             WHERE id = ${} AND deleted_at IS NULL
             RETURNING id, owner_type::TEXT, owner_id, name, description, content, metadata, created_at, updated_at
@@ -353,20 +453,20 @@ impl PromptRepo for PostgresPromptRepo {
             .fetch_optional(&self.write_pool)
             .await
             .map_err(|e| match e {
-                sqlx::Error::Database(db_err) if db_err.is_unique_violation() => {
-                    DbError::Conflict("Prompt with this name already exists for this owner".into())
-                }
+                sqlx::Error::Database(db_err) if db_err.is_unique_violation() => DbError::Conflict(
+                    "Template with this name already exists for this owner".into(),
+                ),
                 _ => DbError::from(e),
             })?
             .ok_or(DbError::NotFound)?;
 
-        Self::parse_prompt(&row)
+        Self::parse_template(&row)
     }
 
     async fn delete(&self, id: Uuid) -> DbResult<()> {
         let result = sqlx::query(
             r#"
-            UPDATE prompts
+            UPDATE templates
             SET deleted_at = NOW()
             WHERE id = $1 AND deleted_at IS NULL
             "#,
diff --git a/src/db/repos/mod.rs b/src/db/repos/mod.rs
index 20a4eb63..d906bd3f 100644
--- a/src/db/repos/mod.rs
+++ b/src/db/repos/mod.rs
@@ -11,7 +11,6 @@ mod org_rbac_policies;
 mod org_sso_configs;
 mod organizations;
 mod projects;
-mod prompts;
 mod providers;
 #[cfg(feature = "sso")]
 mod scim_configs;
@@ -23,6 +22,7 @@ mod service_accounts;
 #[cfg(feature = "sso")]
 mod sso_group_mappings;
 mod teams;
+mod templates;
 mod usage;
 mod users;
 mod vector_stores;
@@ -41,7 +41,6 @@ pub use org_rbac_policies::*;
 pub use org_sso_configs::*;
 pub use organizations::*;
 pub use projects::*;
-pub use prompts::*;
 pub use providers::*;
 #[cfg(feature = "sso")]
 pub use scim_configs::*;
@@ -53,6 +52,7 @@ pub use service_accounts::*;
 #[cfg(feature = "sso")]
 pub use sso_group_mappings::*;
 pub use teams::*;
+pub use templates::*;
 pub use usage::*;
 pub use users::*;
 pub use vector_stores::*;
diff --git a/src/db/repos/prompts.rs b/src/db/repos/prompts.rs
deleted file mode 100644
index a94368c8..00000000
--- a/src/db/repos/prompts.rs
+++ /dev/null
@@ -1,49 +0,0 @@
-use async_trait::async_trait;
-use uuid::Uuid;
-
-use super::{ListParams, ListResult};
-use crate::{
-    db::error::DbResult,
-    models::{CreatePrompt, Prompt, PromptOwnerType, UpdatePrompt},
-};
-
-#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
-#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
-pub trait PromptRepo: Send + Sync {
-    /// Create a new prompt.
-    async fn create(&self, input: CreatePrompt) -> DbResult<Prompt>;
-
-    /// Get a prompt by its ID.
-    async fn get_by_id(&self, id: Uuid) -> DbResult<Option<Prompt>>;
-
-    /// Get a prompt by ID, scoped to a specific organization.
-    ///
-    /// Verifies the prompt belongs to the given org by checking the owner relationship:
-    /// - Organization-owned: `owner_id` matches directly
-    /// - Team-owned: joins through `teams.org_id`
-    /// - Project-owned: joins through `projects.org_id`
-    /// - User-owned: joins through `org_memberships`
-    async fn get_by_id_and_org(&self, id: Uuid, org_id: Uuid) -> DbResult<Option<Prompt>>;
-
-    /// List prompts by owner (organization, team, project, or user).
-    async fn list_by_owner(
-        &self,
-        owner_type: PromptOwnerType,
-        owner_id: Uuid,
-        params: ListParams,
-    ) -> DbResult<ListResult<Prompt>>;
-
-    /// Count prompts by owner.
-    async fn count_by_owner(
-        &self,
-        owner_type: PromptOwnerType,
-        owner_id: Uuid,
-        include_deleted: bool,
-    ) -> DbResult<i64>;
-
-    /// Update a prompt.
-    async fn update(&self, id: Uuid, input: UpdatePrompt) -> DbResult<Prompt>;
-
-    /// Soft-delete a prompt.
-    async fn delete(&self, id: Uuid) -> DbResult<()>;
-}
diff --git a/src/db/repos/templates.rs b/src/db/repos/templates.rs
new file mode 100644
index 00000000..5db5c157
--- /dev/null
+++ b/src/db/repos/templates.rs
@@ -0,0 +1,59 @@
+use async_trait::async_trait;
+use uuid::Uuid;
+
+use super::{ListParams, ListResult};
+use crate::{
+    db::error::DbResult,
+    models::{CreateTemplate, Template, TemplateOwnerType, UpdateTemplate},
+};
+
+#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
+#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
+pub trait TemplateRepo: Send + Sync {
+    /// Create a new template.
+    async fn create(&self, input: CreateTemplate) -> DbResult<Template>;
+
+    /// Get a template by its ID.
+    async fn get_by_id(&self, id: Uuid) -> DbResult<Option<Template>>;
+
+    /// Get a template by ID, scoped to a specific organization.
+    ///
+    /// Verifies the template belongs to the given org by checking the owner relationship:
+    /// - Organization-owned: `owner_id` matches directly
+    /// - Team-owned: joins through `teams.org_id`
+    /// - Project-owned: joins through `projects.org_id`
+    /// - User-owned: joins through `org_memberships`
+    async fn get_by_id_and_org(&self, id: Uuid, org_id: Uuid) -> DbResult<Option<Template>>;
+
+    /// List templates by owner (organization, team, project, or user).
+    async fn list_by_owner(
+        &self,
+        owner_type: TemplateOwnerType,
+        owner_id: Uuid,
+        params: ListParams,
+    ) -> DbResult<ListResult<Template>>;
+
+    /// List all templates accessible within an organization.
+    ///
+    /// Returns templates from all scopes within the org:
+    /// - Organization-owned (owner_id = org_id)
+    /// - Team-owned (team belongs to org)
+    /// - Project-owned (project belongs to org)
+    /// - User-owned (user is a member of org)
+    async fn list_by_org(&self, org_id: Uuid, params: ListParams)
+    -> DbResult<ListResult<Template>>;
+
+    /// Count templates by owner.
+    async fn count_by_owner(
+        &self,
+        owner_type: TemplateOwnerType,
+        owner_id: Uuid,
+        include_deleted: bool,
+    ) -> DbResult<i64>;
+
+    /// Update a template.
+    async fn update(&self, id: Uuid, input: UpdateTemplate) -> DbResult<Template>;
+
+    /// Soft-delete a template.
+    async fn delete(&self, id: Uuid) -> DbResult<()>;
+}
diff --git a/src/db/sqlite/mod.rs b/src/db/sqlite/mod.rs
index 4a152772..45824c03 100644
--- a/src/db/sqlite/mod.rs
+++ b/src/db/sqlite/mod.rs
@@ -12,7 +12,6 @@ mod org_rbac_policies;
 mod org_sso_configs;
 mod organizations;
 mod projects;
-mod prompts;
 mod providers;
 #[cfg(feature = "sso")]
 mod scim_configs;
@@ -24,6 +23,7 @@ mod service_accounts;
 #[cfg(feature = "sso")]
 mod sso_group_mappings;
 mod teams;
+mod templates;
 mod usage;
 mod users;
 mod vector_stores;
@@ -40,7 +40,6 @@ pub use org_rbac_policies::SqliteOrgRbacPolicyRepo;
 pub use org_sso_configs::SqliteOrgSsoConfigRepo;
 pub use organizations::SqliteOrganizationRepo;
 pub use projects::SqliteProjectRepo;
-pub use prompts::SqlitePromptRepo;
 pub use providers::SqliteDynamicProviderRepo;
 #[cfg(feature = "sso")]
 pub use scim_configs::SqliteOrgScimConfigRepo;
@@ -52,6 +51,7 @@ pub use service_accounts::SqliteServiceAccountRepo;
 #[cfg(feature = "sso")]
 pub use sso_group_mappings::SqliteSsoGroupMappingRepo;
 pub use teams::SqliteTeamRepo;
+pub use templates::SqliteTemplateRepo;
 pub use usage::SqliteUsageRepo;
 pub use users::SqliteUserRepo;
 pub use vector_stores::SqliteVectorStoresRepo;
diff --git a/src/db/sqlite/prompts.rs b/src/db/sqlite/templates.rs
similarity index 59%
rename from src/db/sqlite/prompts.rs
rename to src/db/sqlite/templates.rs
index 99caa0b9..b65af966 100644
--- a/src/db/sqlite/prompts.rs
+++ b/src/db/sqlite/templates.rs
@@ -11,26 +11,26 @@ use crate::{
     db::{
         error::{DbError, DbResult},
         repos::{
-            Cursor, CursorDirection, ListParams, ListResult, PageCursors, PromptRepo,
+            Cursor, CursorDirection, ListParams, ListResult, PageCursors, TemplateRepo,
             cursor_from_row, truncate_to_millis,
         },
     },
-    models::{CreatePrompt, Prompt, PromptOwnerType, UpdatePrompt},
+    models::{CreateTemplate, Template, TemplateOwnerType, UpdateTemplate},
 };
 
-pub struct SqlitePromptRepo {
+pub struct SqliteTemplateRepo {
     pool: Pool,
 }
 
-impl SqlitePromptRepo {
+impl SqliteTemplateRepo {
     pub fn new(pool: Pool) -> Self {
         Self { pool }
     }
 
-    /// Parse a Prompt from a database row.
-    fn parse_prompt(row: &super::backend::Row) -> DbResult<Prompt> {
+    /// Parse a Template from a database row.
+    fn parse_template(row: &super::backend::Row) -> DbResult<Template> {
         let owner_type_str: String = row.col("owner_type");
-        let owner_type: PromptOwnerType = owner_type_str
+        let owner_type: TemplateOwnerType = owner_type_str
             .parse()
             .map_err(|e: String| DbError::Internal(e))?;
 
@@ -40,7 +40,7 @@ impl SqlitePromptRepo {
             .transpose()
             .map_err(|e| DbError::Internal(format!("Failed to parse metadata: {}", e)))?;
 
-        Ok(Prompt {
+        Ok(Template {
             id: parse_uuid(&row.col::<String>("id"))?,
             owner_type,
             owner_id: parse_uuid(&row.col::<String>("owner_id"))?,
@@ -53,16 +53,35 @@ impl SqlitePromptRepo {
         })
     }
 
+    /// Org-scoped WHERE clause for templates reachable within an organization.
+    const ORG_SCOPE_FILTER: &'static str = r#"
+        AND (
+            (p.owner_type = 'organization' AND p.owner_id = ?)
+            OR
+            (p.owner_type = 'team' AND EXISTS (
+                SELECT 1 FROM teams t WHERE t.id = p.owner_id AND t.org_id = ?
+            ))
+            OR
+            (p.owner_type = 'project' AND EXISTS (
+                SELECT 1 FROM projects pr WHERE pr.id = p.owner_id AND pr.org_id = ?
+            ))
+            OR
+            (p.owner_type = 'user' AND EXISTS (
+                SELECT 1 FROM org_memberships om WHERE om.user_id = p.owner_id AND om.org_id = ?
+            ))
+        )
+    "#;
+
     /// Helper method for cursor-based pagination.
     async fn list_with_cursor(
         &self,
-        owner_type: PromptOwnerType,
+        owner_type: TemplateOwnerType,
         owner_id: Uuid,
         params: &ListParams,
         cursor: &Cursor,
         fetch_limit: i64,
         limit: i64,
-    ) -> DbResult<ListResult<Prompt>> {
+    ) -> DbResult<ListResult<Template>> {
         let (comparison, order, should_reverse) =
             params.sort_order.cursor_query_params(params.direction);
 
@@ -75,7 +94,7 @@ impl SqlitePromptRepo {
         let sql = format!(
             r#"
             SELECT id, owner_type, owner_id, name, description, content, metadata, created_at, updated_at
-            FROM prompts
+            FROM templates
             WHERE owner_type = ? AND owner_id = ? AND (created_at, id) {} (?, ?)
             {}
             ORDER BY created_at {}, id {}
@@ -94,10 +113,10 @@ impl SqlitePromptRepo {
             .await?;
 
         let has_more = rows.len() as i64 > limit;
-        let mut items: Vec<Prompt> = rows
+        let mut items: Vec<Template> = rows
             .iter()
             .take(limit as usize)
-            .map(Self::parse_prompt)
+            .map(Self::parse_template)
             .collect::<DbResult<Vec<_>>>()?;
 
         if should_reverse {
@@ -115,8 +134,8 @@ impl SqlitePromptRepo {
 
 #[cfg_attr(not(target_arch = "wasm32"), async_trait)]
 #[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
-impl PromptRepo for SqlitePromptRepo {
-    async fn create(&self, input: CreatePrompt) -> DbResult<Prompt> {
+impl TemplateRepo for SqliteTemplateRepo {
+    async fn create(&self, input: CreateTemplate) -> DbResult<Template> {
         let id = Uuid::new_v4();
         let now = truncate_to_millis(chrono::Utc::now());
         let owner_type = input.owner.owner_type();
@@ -131,7 +150,7 @@ impl PromptRepo for SqlitePromptRepo {
 
         query(
             r#"
-            INSERT INTO prompts (id, owner_type, owner_id, name, description, content, metadata, created_at, updated_at)
+            INSERT INTO templates (id, owner_type, owner_id, name, description, content, metadata, created_at, updated_at)
             VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
             "#,
         )
@@ -147,11 +166,11 @@ impl PromptRepo for SqlitePromptRepo {
         .execute(&self.pool)
         .await
         .map_err(map_unique_violation(format!(
-            "Prompt with name '{}' already exists for this owner",
+            "Template with name '{}' already exists for this owner",
             input.name
         )))?;
 
-        Ok(Prompt {
+        Ok(Template {
             id,
             owner_type,
             owner_id,
@@ -164,11 +183,11 @@ impl PromptRepo for SqlitePromptRepo {
         })
     }
 
-    async fn get_by_id(&self, id: Uuid) -> DbResult<Option<Prompt>> {
+    async fn get_by_id(&self, id: Uuid) -> DbResult<Option<Template>> {
         let result = query(
             r#"
             SELECT id, owner_type, owner_id, name, description, content, metadata, created_at, updated_at
-            FROM prompts
+            FROM templates
             WHERE id = ? AND deleted_at IS NULL
             "#,
         )
@@ -177,16 +196,16 @@ impl PromptRepo for SqlitePromptRepo {
         .await?;
 
         match result {
-            Some(row) => Ok(Some(Self::parse_prompt(&row)?)),
+            Some(row) => Ok(Some(Self::parse_template(&row)?)),
             None => Ok(None),
         }
     }
 
-    async fn get_by_id_and_org(&self, id: Uuid, org_id: Uuid) -> DbResult<Option<Prompt>> {
+    async fn get_by_id_and_org(&self, id: Uuid, org_id: Uuid) -> DbResult<Option<Template>> {
         let result = query(
             r#"
             SELECT p.id, p.owner_type, p.owner_id, p.name, p.description, p.content, p.metadata, p.created_at, p.updated_at
-            FROM prompts p
+            FROM templates p
             WHERE p.id = ? AND p.deleted_at IS NULL
             AND (
                 (p.owner_type = 'organization' AND p.owner_id = ?)
@@ -214,17 +233,17 @@ impl PromptRepo for SqlitePromptRepo {
         .await?;
 
         match result {
-            Some(row) => Ok(Some(Self::parse_prompt(&row)?)),
+            Some(row) => Ok(Some(Self::parse_template(&row)?)),
             None => Ok(None),
         }
     }
 
     async fn list_by_owner(
         &self,
-        owner_type: PromptOwnerType,
+        owner_type: TemplateOwnerType,
         owner_id: Uuid,
         params: ListParams,
-    ) -> DbResult<ListResult<Prompt>> {
+    ) -> DbResult<ListResult<Template>> {
         let limit = params.limit.unwrap_or(100);
         let fetch_limit = limit + 1;
 
@@ -237,7 +256,7 @@ impl PromptRepo for SqlitePromptRepo {
         let sql = if params.include_deleted {
             r#"
             SELECT id, owner_type, owner_id, name, description, content, metadata, created_at, updated_at
-            FROM prompts
+            FROM templates
             WHERE owner_type = ? AND owner_id = ?
             ORDER BY created_at DESC, id DESC
             LIMIT ?
@@ -245,7 +264,7 @@ impl PromptRepo for SqlitePromptRepo {
         } else {
             r#"
             SELECT id, owner_type, owner_id, name, description, content, metadata, created_at, updated_at
-            FROM prompts
+            FROM templates
             WHERE owner_type = ? AND owner_id = ? AND deleted_at IS NULL
             ORDER BY created_at DESC, id DESC
             LIMIT ?
@@ -260,10 +279,104 @@ impl PromptRepo for SqlitePromptRepo {
             .await?;
 
         let has_more = rows.len() as i64 > limit;
-        let items: Vec<Prompt> = rows
+        let items: Vec<Template> = rows
+            .iter()
+            .take(limit as usize)
+            .map(Self::parse_template)
+            .collect::<DbResult<Vec<_>>>()?;
+
+        let cursors =
+            PageCursors::from_items(&items, has_more, CursorDirection::Forward, None, |p| {
+                cursor_from_row(p.created_at, p.id)
+            });
+
+        Ok(ListResult::new(items, has_more, cursors))
+    }
+
+    async fn list_by_org(
+        &self,
+        org_id: Uuid,
+        params: ListParams,
+    ) -> DbResult<ListResult<Template>> {
+        let limit = params.limit.unwrap_or(100);
+        let fetch_limit = limit + 1;
+        let org_str = org_id.to_string();
+
+        if let Some(ref cursor) = params.cursor {
+            let (comparison, order, should_reverse) =
+                params.sort_order.cursor_query_params(params.direction);
+
+            let sql = format!(
+                r#"
+                SELECT p.id, p.owner_type, p.owner_id, p.name, p.description, p.content, p.metadata, p.created_at, p.updated_at
+                FROM templates p
+                WHERE p.deleted_at IS NULL AND (p.created_at, p.id) {} (?, ?)
+                {}
+                ORDER BY p.created_at {}, p.id {}
+                LIMIT ?
+                "#,
+                comparison,
+                Self::ORG_SCOPE_FILTER,
+                order,
+                order
+            );
+
+            let rows = query(&sql)
+                .bind(cursor.created_at)
+                .bind(cursor.id.to_string())
+                .bind(&org_str)
+                .bind(&org_str)
+                .bind(&org_str)
+                .bind(&org_str)
+                .bind(fetch_limit)
+                .fetch_all(&self.pool)
+                .await?;
+
+            let has_more = rows.len() as i64 > limit;
+            let mut items: Vec<Template> = rows
+                .iter()
+                .take(limit as usize)
+                .map(Self::parse_template)
+                .collect::<DbResult<Vec<_>>>()?;
+
+            if should_reverse {
+                items.reverse();
+            }
+
+            let cursors =
+                PageCursors::from_items(&items, has_more, params.direction, Some(cursor), |p| {
+                    cursor_from_row(p.created_at, p.id)
+                });
+
+            return Ok(ListResult::new(items, has_more, cursors));
+        }
+
+        let sql = format!(
+            r#"
+            SELECT p.id, p.owner_type, p.owner_id, p.name, p.description, p.content, p.metadata, p.created_at, p.updated_at
+            FROM templates p
+            WHERE p.deleted_at IS NULL
+            {}
+            ORDER BY p.created_at DESC, p.id DESC
+            LIMIT ?
+            "#,
+            Self::ORG_SCOPE_FILTER
+        );
+
+        let rows = query(&sql)
+            .bind(&org_str)
+            .bind(&org_str)
+            .bind(&org_str)
+            .bind(&org_str)
+            .bind(fetch_limit)
+            .fetch_all(&self.pool)
+            .await?;
+
+        let has_more = rows.len() as i64 > limit;
+        let items: Vec<Template> = rows
             .iter()
             .take(limit as usize)
-            .map(Self::parse_prompt)
+            .map(Self::parse_template)
             .collect::<DbResult<Vec<_>>>()?;
 
         let cursors =
@@ -276,14 +389,14 @@ impl PromptRepo for SqlitePromptRepo {
 
     async fn count_by_owner(
         &self,
-        owner_type: PromptOwnerType,
+        owner_type: TemplateOwnerType,
         owner_id: Uuid,
         include_deleted: bool,
     ) -> DbResult<i64> {
         let sql = if include_deleted {
-            "SELECT COUNT(*) as count FROM prompts WHERE owner_type = ? AND owner_id = ?"
+            "SELECT COUNT(*) as count FROM templates WHERE owner_type = ? AND owner_id = ?"
         } else {
-            "SELECT COUNT(*) as count FROM prompts WHERE owner_type = ? AND owner_id = ? AND deleted_at IS NULL"
+            "SELECT COUNT(*) as count FROM templates WHERE owner_type = ? AND owner_id = ? AND deleted_at IS NULL"
         };
 
         let row = query(sql)
@@ -295,7 +408,7 @@ impl PromptRepo for SqlitePromptRepo {
         Ok(row.col::<i64>("count"))
     }
 
-    async fn update(&self, id: Uuid, input: UpdatePrompt) -> DbResult<Prompt> {
+    async fn update(&self, id: Uuid, input: UpdateTemplate) -> DbResult<Template> {
         let has_name = input.name.is_some();
         let has_description = input.description.is_some();
         let has_content = input.content.is_some();
@@ -322,7 +435,7 @@ impl PromptRepo for SqlitePromptRepo {
         }
 
         let sql = format!(
-            "UPDATE prompts SET {} WHERE id = ? AND deleted_at IS NULL",
+            "UPDATE templates SET {} WHERE id = ? AND deleted_at IS NULL",
             set_clauses.join(", ")
         );
 
@@ -348,7 +461,7 @@ impl PromptRepo for SqlitePromptRepo {
             .execute(&self.pool)
             .await
             .map_err(map_unique_violation(
-                "Prompt with this name already exists for this owner",
+                "Template with this name already exists for this owner",
             ))?;
 
         if result.rows_affected() == 0 {
@@ -363,7 +476,7 @@ impl PromptRepo for SqlitePromptRepo {
 
         let result = query(
             r#"
-            UPDATE prompts
+            UPDATE templates
             SET deleted_at = ?
             WHERE id = ? AND deleted_at IS NULL
             "#,
@@ -386,7 +499,7 @@ mod tests {
     use sqlx::SqlitePool;
 
     use super::*;
-    use crate::models::PromptOwner;
+    use crate::models::TemplateOwner;
 
     async fn create_test_pool() -> SqlitePool {
         let pool = sqlx::sqlite::SqlitePoolOptions::new()
@@ -397,7 +510,7 @@ mod tests {
 
         sqlx::query(
             r#"
-            CREATE TABLE prompts (
+            CREATE TABLE templates (
                 id TEXT PRIMARY KEY NOT NULL,
                 owner_type TEXT NOT NULL CHECK (owner_type IN ('organization', 'team', 'project', 'user')),
                 owner_id TEXT NOT NULL,
@@ -414,14 +527,14 @@ mod tests {
         )
         .execute(&pool)
         .await
-        .expect("Failed to create prompts table");
+        .expect("Failed to create templates table");
 
         pool
     }
 
-    fn create_prompt_input(name: &str, content: &str, user_id: Uuid) -> CreatePrompt {
-        CreatePrompt {
-            owner: PromptOwner::User { user_id },
+    fn create_template_input(name: &str, content: &str, user_id: Uuid) -> CreateTemplate {
+        CreateTemplate {
+            owner: TemplateOwner::User { user_id },
             name: name.to_string(),
             description: None,
             content: content.to_string(),
@@ -430,25 +543,25 @@ mod tests {
     }
 
     #[tokio::test]
-    async fn test_create_prompt() {
+    async fn test_create_template() {
         let pool = create_test_pool().await;
-        let repo = SqlitePromptRepo::new(pool);
+        let repo = SqliteTemplateRepo::new(pool);
         let user_id = Uuid::new_v4();
 
-        let input = create_prompt_input("test-prompt", "You are a helpful assistant.", user_id);
-        let prompt = repo.create(input).await.expect("Failed to create prompt");
+        let input = create_template_input("test-template", "You are a helpful assistant.", user_id);
+        let template = repo.create(input).await.expect("Failed to create template");
 
-        assert_eq!(prompt.name, "test-prompt");
-        assert_eq!(prompt.content, "You are a helpful assistant.");
-        assert_eq!(prompt.owner_type, PromptOwnerType::User);
-        assert_eq!(prompt.owner_id, user_id);
-        assert!(!prompt.id.is_nil());
+        assert_eq!(template.name, "test-template");
+        assert_eq!(template.content, "You are a helpful assistant.");
+        assert_eq!(template.owner_type, TemplateOwnerType::User);
+        assert_eq!(template.owner_id, user_id);
+        assert!(!template.id.is_nil());
     }
 
     #[tokio::test]
-    async fn test_create_prompt_with_metadata() {
+    async fn test_create_template_with_metadata() {
         let pool = create_test_pool().await;
-        let repo = SqlitePromptRepo::new(pool);
+        let repo = SqliteTemplateRepo::new(pool);
         let user_id = Uuid::new_v4();
 
         let mut metadata = HashMap::new();
@@ -457,31 +570,31 @@ mod tests {
             serde_json::Value::Number(serde_json::Number::from_f64(0.7).unwrap()),
         );
 
-        let input = CreatePrompt {
-            owner: PromptOwner::User { user_id },
-            name: "test-prompt".to_string(),
-            description: Some("A test prompt".to_string()),
+        let input = CreateTemplate {
+            owner: TemplateOwner::User { user_id },
+            name: "test-template".to_string(),
+            description: Some("A test template".to_string()),
             content: "You are a helpful assistant.".to_string(),
             metadata: Some(metadata),
         };
 
-        let prompt = repo.create(input).await.expect("Failed to create prompt");
+        let template = repo.create(input).await.expect("Failed to create template");
 
-        assert!(prompt.metadata.is_some());
-        assert!(prompt.description.is_some());
-        assert_eq!(prompt.description.unwrap(), "A test prompt");
+        assert!(template.metadata.is_some());
+        assert!(template.description.is_some());
+        assert_eq!(template.description.unwrap(), "A test template");
     }
 
     #[tokio::test]
     async fn test_create_duplicate_name_fails() {
         let pool = create_test_pool().await;
-        let repo = SqlitePromptRepo::new(pool);
+        let repo = SqliteTemplateRepo::new(pool);
         let user_id = Uuid::new_v4();
 
-        let input = create_prompt_input("duplicate", "First content", user_id);
-        repo.create(input).await.expect("Failed to create prompt");
+        let input = create_template_input("duplicate", "First content", user_id);
+        repo.create(input).await.expect("Failed to create template");
 
-        let input2 = create_prompt_input("duplicate", "Second content", user_id);
+        let input2 = create_template_input("duplicate", "Second content", user_id);
         let result = repo.create(input2).await;
 
         assert!(matches!(result, Err(DbError::Conflict(_))));
@@ -490,35 +603,41 @@ mod tests {
     #[tokio::test]
     async fn test_same_name_different_owners_succeeds() {
         let pool = create_test_pool().await;
-        let repo = SqlitePromptRepo::new(pool);
+        let repo = SqliteTemplateRepo::new(pool);
         let user1_id = Uuid::new_v4();
         let user2_id = Uuid::new_v4();
 
-        let input1 = create_prompt_input("same-name", "User 1 content", user1_id);
-        let prompt1 = repo.create(input1).await.expect("Failed to create prompt");
+        let input1 = create_template_input("same-name", "User 1 content", user1_id);
+        let template1 = repo
+            .create(input1)
+            .await
+            .expect("Failed to create template");
 
-        let input2 = create_prompt_input("same-name", "User 2 content", user2_id);
-        let prompt2 = repo.create(input2).await.expect("Failed to create prompt");
+        let input2 = create_template_input("same-name", "User 2 content", user2_id);
+        let template2 = repo
+            .create(input2)
+            .await
+            .expect("Failed to create template");
 
-        assert_eq!(prompt1.name, prompt2.name);
-        assert_ne!(prompt1.id, prompt2.id);
-        assert_ne!(prompt1.owner_id, prompt2.owner_id);
+        assert_eq!(template1.name, template2.name);
+        assert_ne!(template1.id, template2.id);
+        assert_ne!(template1.owner_id, template2.owner_id);
     }
 
     #[tokio::test]
     async fn test_get_by_id() {
         let pool = create_test_pool().await;
-        let repo = SqlitePromptRepo::new(pool);
+        let repo = SqliteTemplateRepo::new(pool);
         let user_id = Uuid::new_v4();
 
-        let input = create_prompt_input("get-test", "Test content", user_id);
-        let created = repo.create(input).await.expect("Failed to create prompt");
+        let input = create_template_input("get-test", "Test content", user_id);
+        let created = repo.create(input).await.expect("Failed to create template");
 
         let fetched = repo
             .get_by_id(created.id)
             .await
-            .expect("Failed to get prompt")
-            .expect("Prompt should exist");
+            .expect("Failed to get template")
+            .expect("Template should exist");
 
         assert_eq!(fetched.id, created.id);
         assert_eq!(fetched.name, "get-test");
@@ -528,7 +647,7 @@ mod tests {
     #[tokio::test]
     async fn test_get_by_id_not_found() {
         let pool = create_test_pool().await;
-        let repo = SqlitePromptRepo::new(pool);
+        let repo = SqliteTemplateRepo::new(pool);
 
         let result = repo
             .get_by_id(Uuid::new_v4())
@@ -541,33 +660,33 @@ mod tests {
     #[tokio::test]
     async fn test_list_by_owner_empty() {
         let pool = create_test_pool().await;
-        let repo = SqlitePromptRepo::new(pool);
+        let repo = SqliteTemplateRepo::new(pool);
         let user_id = Uuid::new_v4();
 
         let result = repo
-            .list_by_owner(PromptOwnerType::User, user_id, ListParams::default())
+            .list_by_owner(TemplateOwnerType::User, user_id, ListParams::default())
             .await
-            .expect("Failed to list prompts");
+            .expect("Failed to list templates");
 
         assert!(result.items.is_empty());
         assert!(!result.has_more);
     }
 
     #[tokio::test]
-    async fn test_list_by_owner_with_prompts() {
+    async fn test_list_by_owner_with_templates() {
         let pool = create_test_pool().await;
-        let repo = SqlitePromptRepo::new(pool);
+        let repo = SqliteTemplateRepo::new(pool);
         let user_id = Uuid::new_v4();
 
         for i in 0..3 {
-            let input = create_prompt_input(&format!("prompt-{}", i), "Content", user_id);
-            repo.create(input).await.expect("Failed to create prompt");
+            let input = create_template_input(&format!("template-{}", i), "Content", user_id);
+            repo.create(input).await.expect("Failed to create template");
         }
 
         let result = repo
-            .list_by_owner(PromptOwnerType::User, user_id, ListParams::default())
+            .list_by_owner(TemplateOwnerType::User, user_id, ListParams::default())
             .await
-            .expect("Failed to list prompts");
+            .expect("Failed to list templates");
 
         assert_eq!(result.items.len(), 3);
         assert!(!result.has_more);
@@ -576,45 +695,45 @@ mod tests {
     #[tokio::test]
     async fn test_list_by_owner_filters_by_owner() {
         let pool = create_test_pool().await;
-        let repo = SqlitePromptRepo::new(pool);
+        let repo = SqliteTemplateRepo::new(pool);
         let user1_id = Uuid::new_v4();
         let user2_id = Uuid::new_v4();
 
-        repo.create(create_prompt_input("user1-prompt", "Content", user1_id))
+        repo.create(create_template_input("user1-template", "Content", user1_id))
             .await
             .expect("Failed to create");
-        repo.create(create_prompt_input("user2-prompt", "Content", user2_id))
+        repo.create(create_template_input("user2-template", "Content", user2_id))
             .await
             .expect("Failed to create");
 
         let user1_result = repo
-            .list_by_owner(PromptOwnerType::User, user1_id, ListParams::default())
+            .list_by_owner(TemplateOwnerType::User, user1_id, ListParams::default())
             .await
             .expect("Failed to list");
         let user2_result = repo
-            .list_by_owner(PromptOwnerType::User, user2_id, ListParams::default())
+            .list_by_owner(TemplateOwnerType::User, user2_id, ListParams::default())
             .await
             .expect("Failed to list");
 
         assert_eq!(user1_result.items.len(), 1);
-        assert_eq!(user1_result.items[0].name, "user1-prompt");
+        assert_eq!(user1_result.items[0].name, "user1-template");
         assert_eq!(user2_result.items.len(), 1);
-        assert_eq!(user2_result.items[0].name, "user2-prompt");
+        assert_eq!(user2_result.items[0].name, "user2-template");
     }
 
     #[tokio::test]
     async fn test_count_by_owner() {
         let pool = create_test_pool().await;
-        let repo = SqlitePromptRepo::new(pool);
+        let repo = SqliteTemplateRepo::new(pool);
         let user_id = Uuid::new_v4();
 
         for i in 0..3 {
-            let input = create_prompt_input(&format!("prompt-{}", i), "Content", user_id);
+            let input = create_template_input(&format!("template-{}", i), "Content", user_id);
             repo.create(input).await.expect("Failed to create");
         }
 
         let count = repo
-            .count_by_owner(PromptOwnerType::User, user_id, false)
+            .count_by_owner(TemplateOwnerType::User, user_id, false)
             .await
             .expect("Failed to count");
 
@@ -622,18 +741,18 @@ mod tests {
     }
 
     #[tokio::test]
-    async fn test_update_prompt() {
+    async fn test_update_template() {
         let pool = create_test_pool().await;
-        let repo = SqlitePromptRepo::new(pool);
+        let repo = SqliteTemplateRepo::new(pool);
         let user_id = Uuid::new_v4();
 
-        let input = create_prompt_input("update-test", "Original content", user_id);
+        let input = create_template_input("update-test", "Original content", user_id);
         let created = repo.create(input).await.expect("Failed to create");
 
         let updated = repo
             .update(
                 created.id,
-                UpdatePrompt {
+                UpdateTemplate {
                     name: Some("updated-name".to_string()),
                     description: Some("New description".to_string()),
                     content: Some("Updated content".to_string()),
@@ -653,16 +772,16 @@ mod tests {
     #[tokio::test]
     async fn test_update_no_changes() {
         let pool = create_test_pool().await;
-        let repo = SqlitePromptRepo::new(pool);
+        let repo = SqliteTemplateRepo::new(pool);
         let user_id = Uuid::new_v4();
 
-        let input = create_prompt_input("no-change", "Original", user_id);
+        let input = create_template_input("no-change", "Original", user_id);
         let created = repo.create(input).await.expect("Failed to create");
 
         let result = repo
             .update(
                 created.id,
-                UpdatePrompt {
+                UpdateTemplate {
                     name: None,
                     description: None,
                     content: None,
@@ -678,12 +797,12 @@ mod tests {
     #[tokio::test]
     async fn test_update_not_found() {
         let pool = create_test_pool().await;
-        let repo = SqlitePromptRepo::new(pool);
+        let repo = SqliteTemplateRepo::new(pool);
 
         let result = repo
             .update(
                 Uuid::new_v4(),
-                UpdatePrompt {
+                UpdateTemplate {
                     name: Some("New Name".to_string()),
                     description: None,
                     content: None,
@@ -696,17 +815,17 @@ mod tests {
     }
 
     #[tokio::test]
-    async fn test_delete_prompt() {
+    async fn test_delete_template() {
         let pool = create_test_pool().await;
-        let repo = SqlitePromptRepo::new(pool);
+        let repo = SqliteTemplateRepo::new(pool);
         let user_id = Uuid::new_v4();
 
-        let input = create_prompt_input("delete-test", "Content", user_id);
+        let input = create_template_input("delete-test", "Content", user_id);
         let created = repo.create(input).await.expect("Failed to create");
 
         repo.delete(created.id)
             .await
-            .expect("Failed to delete prompt");
+            .expect("Failed to delete template");
 
         let result = repo
             .get_by_id(created.id)
@@ -715,7 +834,7 @@ mod tests {
         assert!(result.is_none());
 
         let list = repo
-            .list_by_owner(PromptOwnerType::User, user_id, ListParams::default())
+            .list_by_owner(TemplateOwnerType::User, user_id, ListParams::default())
             .await
             .expect("Failed to list");
         assert!(list.items.is_empty());
@@ -724,7 +843,7 @@ mod tests {
     #[tokio::test]
     async fn test_delete_not_found() {
         let pool = create_test_pool().await;
-        let repo = SqlitePromptRepo::new(pool);
+        let repo = SqliteTemplateRepo::new(pool);
 
         let result = repo.delete(Uuid::new_v4()).await;
         assert!(matches!(result, Err(DbError::NotFound)));
@@ -733,27 +852,27 @@ mod tests {
     #[tokio::test]
     async fn test_count_excludes_deleted() {
         let pool = create_test_pool().await;
-        let repo = SqlitePromptRepo::new(pool);
+        let repo = SqliteTemplateRepo::new(pool);
         let user_id = Uuid::new_v4();
 
-        let prompt1 = repo
-            .create(create_prompt_input("prompt-1", "Content", user_id))
+        let template1 = repo
+            .create(create_template_input("template-1", "Content", user_id))
             .await
             .expect("Failed to create");
-        repo.create(create_prompt_input("prompt-2", "Content", user_id))
+        repo.create(create_template_input("template-2", "Content", user_id))
             .await
             .expect("Failed to create");
 
-        repo.delete(prompt1.id).await.expect("Failed to delete");
+        repo.delete(template1.id).await.expect("Failed to delete");
 
         let count = repo
-            .count_by_owner(PromptOwnerType::User, user_id, false)
+            .count_by_owner(TemplateOwnerType::User, user_id, false)
             .await
             .expect("Failed to count");
         assert_eq!(count, 1);
 
         let count_all = repo
-            .count_by_owner(PromptOwnerType::User, user_id, true)
+            .count_by_owner(TemplateOwnerType::User, user_id, true)
             .await
             .expect("Failed to count all");
         assert_eq!(count_all, 2);
@@ -762,83 +881,91 @@ mod tests {
     #[tokio::test]
     async fn test_different_owner_types() {
         let pool = create_test_pool().await;
-        let repo = SqlitePromptRepo::new(pool);
+        let repo = SqliteTemplateRepo::new(pool);
 
         let org_id = Uuid::new_v4();
         let team_id = Uuid::new_v4();
         let project_id = Uuid::new_v4();
         let user_id = Uuid::new_v4();
 
-        // Create prompts for different owner types
-        repo.create(CreatePrompt {
-            owner: PromptOwner::Organization {
+        // Create templates for different owner types
+        repo.create(CreateTemplate {
+            owner: TemplateOwner::Organization {
                 organization_id: org_id,
             },
-            name: "org-prompt".to_string(),
+            name: "org-template".to_string(),
             description: None,
             content: "Org content".to_string(),
             metadata: None,
         })
         .await
-        .expect("Failed to create org prompt");
+        .expect("Failed to create org template");
 
-        repo.create(CreatePrompt {
-            owner: PromptOwner::Team { team_id },
-            name: "team-prompt".to_string(),
+        repo.create(CreateTemplate {
+            owner: TemplateOwner::Team { team_id },
+            name: "team-template".to_string(),
             description: None,
             content: "Team content".to_string(),
             metadata: None,
         })
         .await
-        .expect("Failed to create team prompt");
+        .expect("Failed to create team template");
 
-        repo.create(CreatePrompt {
-            owner: PromptOwner::Project { project_id },
-            name: "project-prompt".to_string(),
+        repo.create(CreateTemplate {
+            owner: TemplateOwner::Project { project_id },
+            name: "project-template".to_string(),
             description: None,
             content: "Project content".to_string(),
             metadata: None,
         })
         .await
-        .expect("Failed to create project prompt");
+        .expect("Failed to create project template");
 
-        repo.create(CreatePrompt {
-            owner: PromptOwner::User { user_id },
-            name: "user-prompt".to_string(),
+        repo.create(CreateTemplate {
+            owner: TemplateOwner::User { user_id },
+            name: "user-template".to_string(),
             description: None,
             content: "User content".to_string(),
             metadata: None,
         })
         .await
-        .expect("Failed to create user prompt");
-
-        // Verify each owner type only sees their prompts
-        let org_prompts = repo
-            .list_by_owner(PromptOwnerType::Organization, org_id, ListParams::default())
+        .expect("Failed to create user template");
+
+        // Verify each owner type only sees their templates
+        let org_templates = repo
+            .list_by_owner(
+                TemplateOwnerType::Organization,
+                org_id,
+                ListParams::default(),
+            )
             .await
             .expect("Failed to list");
-        assert_eq!(org_prompts.items.len(), 1);
-        assert_eq!(org_prompts.items[0].name, "org-prompt");
+        assert_eq!(org_templates.items.len(), 1);
+        assert_eq!(org_templates.items[0].name, "org-template");
 
-        let team_prompts = repo
-            .list_by_owner(PromptOwnerType::Team, team_id, ListParams::default())
+        let team_templates = repo
+            .list_by_owner(TemplateOwnerType::Team, team_id, ListParams::default())
             .await
             .expect("Failed to list");
-        assert_eq!(team_prompts.items.len(), 1);
-        assert_eq!(team_prompts.items[0].name, "team-prompt");
-
-        let project_prompts = repo
-            .list_by_owner(PromptOwnerType::Project, project_id, ListParams::default())
+        assert_eq!(team_templates.items.len(), 1);
+        assert_eq!(team_templates.items[0].name, "team-template");
+
+        let project_templates = repo
+            .list_by_owner(
+                TemplateOwnerType::Project,
+                project_id,
+                ListParams::default(),
+            )
             .await
             .expect("Failed to list");
-        assert_eq!(project_prompts.items.len(), 1);
-        assert_eq!(project_prompts.items[0].name, "project-prompt");
+        assert_eq!(project_templates.items.len(), 1);
+        assert_eq!(project_templates.items[0].name, "project-template");
 
-        let user_prompts = repo
-            .list_by_owner(PromptOwnerType::User, user_id, ListParams::default())
+        let user_templates = repo
+            .list_by_owner(TemplateOwnerType::User, user_id, ListParams::default())
             .await
             .expect("Failed to list");
-        assert_eq!(user_prompts.items.len(), 1);
-        assert_eq!(user_prompts.items[0].name, "user-prompt");
+        assert_eq!(user_templates.items.len(), 1);
+        assert_eq!(user_templates.items[0].name, "user-template");
     }
 }
diff --git a/src/models/mod.rs b/src/models/mod.rs
index 1e2dc6eb..77013437 100644
--- a/src/models/mod.rs
+++ b/src/models/mod.rs
@@ -14,7 +14,6 @@ mod org_sso_config;
 mod organization;
 mod prefixed_id;
 mod project;
-mod prompt;
 mod ranking_options;
 #[cfg(feature = "sso")]
 mod scim;
@@ -22,6 +21,7 @@ mod service_account;
 #[cfg(feature = "sso")]
 mod sso_group_mapping;
 mod team;
+mod template;
 mod usage;
 mod user;
 mod validators;
@@ -43,7 +43,6 @@ pub use org_sso_config::*;
 pub use organization::*;
 pub use prefixed_id::*;
 pub use project::*;
-pub use prompt::*;
 pub use ranking_options::*;
 #[cfg(feature = "sso")]
 pub use scim::*;
@@ -51,6 +50,7 @@ pub use service_account::*;
 #[cfg(feature = "sso")]
 pub use sso_group_mapping::*;
 pub use team::*;
+pub use template::*;
 pub use usage::*;
 pub use user::*;
 pub use vector_store::*;
diff --git a/src/models/prompt.rs b/src/models/template.rs
similarity index 56%
rename from src/models/prompt.rs
rename to src/models/template.rs
index 45fb1d09..8dada895 100644
--- a/src/models/prompt.rs
+++ b/src/models/template.rs
@@ -5,55 +5,55 @@ use serde::{Deserialize, Serialize};
 use uuid::Uuid;
 use validator::Validate;
 
-/// Owner type for prompts (organization, team, project, or user)
+/// Owner type for templates (organization, team, project, or user)
 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
 #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
 #[serde(rename_all = "snake_case")]
-pub enum PromptOwnerType {
+pub enum TemplateOwnerType {
     Organization,
     Team,
     Project,
     User,
 }
 
-impl PromptOwnerType {
+impl TemplateOwnerType {
     pub fn as_str(&self) -> &'static str {
         match self {
-            PromptOwnerType::Organization => "organization",
-            PromptOwnerType::Team => "team",
-            PromptOwnerType::Project => "project",
-            PromptOwnerType::User => "user",
+            TemplateOwnerType::Organization => "organization",
+            TemplateOwnerType::Team => "team",
+            TemplateOwnerType::Project => "project",
+            TemplateOwnerType::User => "user",
         }
     }
 }
 
-impl std::str::FromStr for PromptOwnerType {
+impl std::str::FromStr for TemplateOwnerType {
     type Err = String;
 
     fn from_str(s: &str) -> Result<Self, Self::Err> {
         match s {
-            "organization" => Ok(PromptOwnerType::Organization),
-            "team" => Ok(PromptOwnerType::Team),
-            "project" => Ok(PromptOwnerType::Project),
-            "user" => Ok(PromptOwnerType::User),
-            _ => Err(format!("Invalid prompt owner type: {}", s)),
+            "organization" => Ok(TemplateOwnerType::Organization),
+            "team" => Ok(TemplateOwnerType::Team),
+            "project" => Ok(TemplateOwnerType::Project),
+            "user" => Ok(TemplateOwnerType::User),
+            _ => Err(format!("Invalid template owner type: {}", s)),
         }
     }
 }
 
-/// A reusable system prompt template
+/// A reusable system template
 #[derive(Debug, Clone, Serialize, Deserialize)]
 #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
-pub struct Prompt {
+pub struct Template {
     pub id: Uuid,
-    pub owner_type: PromptOwnerType,
+    pub owner_type: TemplateOwnerType,
     pub owner_id: Uuid,
-    /// Name of the prompt template
+    /// Name of the template
     pub name: String,
-    /// Optional description of what this prompt does
+    /// Optional description of what this template does
     #[serde(skip_serializing_if = "Option::is_none")]
     pub description: Option<String>,
-    /// The actual prompt content (system message template)
+    /// The actual template content (system message template)
     pub content: String,
     /// Optional metadata (e.g., recommended temperature, max_tokens, tags)
     #[serde(skip_serializing_if = "Option::is_none")]
@@ -62,60 +62,60 @@ pub struct Prompt {
     pub updated_at: DateTime<Utc>,
 }
 
-/// Owner specification for creating a prompt
+/// Owner specification for creating a template
 #[derive(Debug, Clone, Deserialize)]
 #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
 #[serde(tag = "type", rename_all = "snake_case")]
-pub enum PromptOwner {
+pub enum TemplateOwner {
     Organization { organization_id: Uuid },
     Team { team_id: Uuid },
     Project { project_id: Uuid },
     User { user_id: Uuid },
 }
 
-impl PromptOwner {
-    pub fn owner_type(&self) -> PromptOwnerType {
+impl TemplateOwner {
+    pub fn owner_type(&self) -> TemplateOwnerType {
         match self {
-            PromptOwner::Organization { .. } => PromptOwnerType::Organization,
-            PromptOwner::Team { .. } => PromptOwnerType::Team,
-            PromptOwner::Project { .. } => PromptOwnerType::Project,
-            PromptOwner::User { .. } => PromptOwnerType::User,
+            TemplateOwner::Organization { .. } => TemplateOwnerType::Organization,
+            TemplateOwner::Team { .. } => TemplateOwnerType::Team,
+            TemplateOwner::Project { .. } => TemplateOwnerType::Project,
+            TemplateOwner::User { .. } => TemplateOwnerType::User,
         }
     }
 
     pub fn owner_id(&self) -> Uuid {
         match self {
-            PromptOwner::Organization { organization_id } => *organization_id,
-            PromptOwner::Team { team_id } => *team_id,
-            PromptOwner::Project { project_id } => *project_id,
-            PromptOwner::User { user_id } => *user_id,
+            TemplateOwner::Organization { organization_id } => *organization_id,
+            TemplateOwner::Team { team_id } => *team_id,
+            TemplateOwner::Project { project_id } => *project_id,
+            TemplateOwner::User { user_id } => *user_id,
         }
     }
 }
 
-/// Request to create a new prompt
+/// Request to create a new template
 #[derive(Debug, Clone, Deserialize, Validate)]
 #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
-pub struct CreatePrompt {
-    /// Owner of the prompt
-    pub owner: PromptOwner,
-    /// Name of the prompt template (unique per owner)
+pub struct CreateTemplate {
+    /// Owner of the template
+    pub owner: TemplateOwner,
+    /// Name of the template (unique per owner)
     #[validate(length(min = 1, max = 255))]
     pub name: String,
     /// Optional description
     #[validate(length(max = 1000))]
     pub description: Option<String>,
-    /// The actual prompt content
+    /// The actual template content
     #[validate(length(min = 1))]
     pub content: String,
     /// Optional metadata
     pub metadata: Option<HashMap<String, serde_json::Value>>,
 }
 
-/// Request to update a prompt
+/// Request to update a template
 #[derive(Debug, Clone, Deserialize, Validate)]
 #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
-pub struct UpdatePrompt {
+pub struct UpdateTemplate {
     /// New name (unique per owner)
     #[validate(length(min = 1, max = 255))]
     pub name: Option<String>,
diff --git a/src/openapi.rs b/src/openapi.rs
index 88a6e99b..360bc5b3 100644
--- a/src/openapi.rs
+++ b/src/openapi.rs
@@ -362,7 +362,7 @@ requests_per_minute = 120
         (name = "usage", description = "Query usage statistics for API keys including token counts, costs, and breakdowns by date, model, or referer."),
         (name = "model-pricing", description = "Configure per-model pricing for cost tracking. Pricing can be set globally, per-provider, per-organization, per-project, or per-user."),
         (name = "conversations", description = "Store and manage chat conversation history. Conversations can be owned by users or projects and support multiple models."),
-        (name = "prompts", description = "Manage reusable system prompt templates. Prompts can be owned by organizations, teams, projects, or users and include metadata for configuration."),
+        (name = "templates", description = "Manage reusable prompt templates. Templates can be owned by organizations, teams, projects, or users and include metadata for configuration."),
         (name = "audit-logs", description = "Query audit logs for admin operations. All sensitive operations like API key creation, user permission changes, and resource modifications are logged."),
         (name = "teams", description = "Teams group users within an organization for easier permission management. Users can belong to multiple teams, and projects can be assigned to a team."),
         (name = "service_accounts", description = "Service accounts are machine identities that can own API keys and carry roles for RBAC evaluation. They enable unified authorization across human users and automated systems."),
@@ -574,15 +574,15 @@ requests_per_minute = 120
         admin::conversations::list_by_project,
         admin::conversations::list_by_user,
         admin::conversations::list_accessible_for_user,
-        // Admin routes - Prompts
-        admin::prompts::create,
-        admin::prompts::get,
-        admin::prompts::update,
-        admin::prompts::delete,
-        admin::prompts::list_by_org,
-        admin::prompts::list_by_team,
-        admin::prompts::list_by_project,
-        admin::prompts::list_by_user,
+        // Admin routes - Templates
+        admin::templates::create,
+        admin::templates::get,
+        admin::templates::update,
+        admin::templates::delete,
+        admin::templates::list_by_org,
+        admin::templates::list_by_team,
+        admin::templates::list_by_project,
+        admin::templates::list_by_user,
         // Admin routes - Provider Management
         admin::providers::list_circuit_breakers,
         admin::providers::get_circuit_breaker,
@@ -879,13 +879,13 @@ requests_per_minute = 120
         admin::conversations::ConversationListResponse,
         admin::conversations::ConversationWithProjectListResponse,
         admin::conversations::ListAccessibleQuery,
-        // Admin models - Prompt
-        models::Prompt,
-        models::CreatePrompt,
-        models::UpdatePrompt,
-        models::PromptOwner,
-        models::PromptOwnerType,
-        admin::prompts::PromptListResponse,
+        // Admin models - Template
+        models::Template,
+        models::CreateTemplate,
+        models::UpdateTemplate,
+        models::TemplateOwner,
+        models::TemplateOwnerType,
+        admin::templates::TemplateListResponse,
         // Admin routes - DLQ
         admin::dlq::DlqListQuery,
         admin::dlq::DlqEntryResponse,
diff --git a/src/routes/admin/mod.rs b/src/routes/admin/mod.rs
index 02631883..234bcce5 100644
--- a/src/routes/admin/mod.rs
+++ b/src/routes/admin/mod.rs
@@ -19,7 +19,6 @@ pub mod org_rbac_policies;
 pub mod org_sso_configs;
 pub mod organizations;
 pub mod projects;
-pub mod prompts;
 pub mod providers;
 #[cfg(feature = "sso")]
 pub mod scim_configs;
@@ -32,6 +31,7 @@ pub mod sso_connections;
 #[cfg(feature = "sso")]
 pub mod sso_group_mappings;
 pub mod teams;
+pub mod templates;
 pub mod ui_config;
 pub mod usage;
 pub mod users;
@@ -587,27 +587,27 @@ pub(crate) fn admin_v1_routes() -> Router<AppState> {
             "/users/{user_id}/conversations/accessible",
             get(conversations::list_accessible_for_user),
         )
-        // Prompts
-        .route("/prompts", post(prompts::create))
+        // Templates
+        .route("/templates", post(templates::create))
         .route(
-            "/prompts/{id}",
-            get(prompts::get)
-                .merge(patch(prompts::update))
-                .merge(delete(prompts::delete)),
+            "/templates/{id}",
+            get(templates::get)
+                .merge(patch(templates::update))
+                .merge(delete(templates::delete)),
         )
         .route(
-            "/organizations/{org_slug}/prompts",
-            get(prompts::list_by_org),
+            "/organizations/{org_slug}/templates",
+            get(templates::list_by_org),
         )
         .route(
-            "/organizations/{org_slug}/teams/{team_slug}/prompts",
-            get(prompts::list_by_team),
+            "/organizations/{org_slug}/teams/{team_slug}/templates",
+            get(templates::list_by_team),
         )
         .route(
-            "/organizations/{org_slug}/projects/{project_slug}/prompts",
-            get(prompts::list_by_project),
+            "/organizations/{org_slug}/projects/{project_slug}/templates",
+            get(templates::list_by_project),
         )
-        .route("/users/{user_id}/prompts", get(prompts::list_by_user))
+        .route("/users/{user_id}/templates", get(templates::list_by_user))
         // Provider management
         .route(
             "/providers/circuit-breakers",
@@ -4687,7 +4687,7 @@ ttl_secs = 86400
     }
 
     // ============================================================================
-    // Prompt Tests
+    // Template Tests
     // ============================================================================
 
     async fn create_project(app: &axum::Router, org_slug: &str, project_slug: &str) -> String {
@@ -4702,9 +4702,9 @@ ttl_secs = 86400
     }
 
     #[tokio::test]
-    async fn test_create_prompt_for_organization() {
+    async fn test_create_template_for_organization() {
         let app = test_app().await;
-        let org_slug = create_org(&app, "prompt-org").await;
+        let org_slug = create_org(&app, "template-org").await;
 
         // Get org ID
         let (_, org) = get_json(&app, &format!("/admin/v1/organizations/{}", org_slug)).await;
@@ -4712,12 +4712,12 @@ ttl_secs = 86400
 
         let (status, body) = post_json(
             &app,
-            "/admin/v1/prompts",
+            "/admin/v1/templates",
             json!({
                 "owner": {"type": "organization", "organization_id": org_id},
-                "name": "test-prompt",
+                "name": "test-template",
                 "content": "You are a helpful assistant.",
-                "description": "A test prompt"
+                "description": "A test template"
             }),
         )
         .await;
@@ -4729,69 +4729,69 @@ ttl_secs = 86400
             );
         }
         assert_eq!(status, StatusCode::CREATED);
-        assert_eq!(body["name"], "test-prompt");
+        assert_eq!(body["name"], "test-template");
         assert_eq!(body["content"], "You are a helpful assistant.");
-        assert_eq!(body["description"], "A test prompt");
+        assert_eq!(body["description"], "A test template");
         assert_eq!(body["owner_type"], "organization");
         assert_eq!(body["owner_id"], org_id);
         assert!(body["id"].is_string());
     }
 
     #[tokio::test]
-    async fn test_create_prompt_for_project() {
+    async fn test_create_template_for_project() {
         let app = test_app().await;
-        let org_slug = create_org(&app, "prompt-proj-org").await;
-        let project_id = create_project(&app, &org_slug, "prompt-project").await;
+        let org_slug = create_org(&app, "template-proj-org").await;
+        let project_id = create_project(&app, &org_slug, "template-project").await;
 
         let (status, body) = post_json(
             &app,
-            "/admin/v1/prompts",
+            "/admin/v1/templates",
             json!({
                 "owner": {"type": "project", "project_id": project_id},
-                "name": "project-prompt",
-                "content": "Project system prompt"
+                "name": "project-template",
+                "content": "Project system template"
             }),
         )
         .await;
 
         assert_eq!(status, StatusCode::CREATED);
-        assert_eq!(body["name"], "project-prompt");
+        assert_eq!(body["name"], "project-template");
         assert_eq!(body["owner_type"], "project");
         assert_eq!(body["owner_id"], project_id);
     }
 
     #[tokio::test]
-    async fn test_create_prompt_for_team() {
+    async fn test_create_template_for_team() {
         let app = test_app().await;
-        let org_slug = create_org(&app, "prompt-team-org").await;
-        let team_id = create_team(&app, &org_slug, "prompt-team").await;
+        let org_slug = create_org(&app, "template-team-org").await;
+        let team_id = create_team(&app, &org_slug, "template-team").await;
 
         let (status, body) = post_json(
             &app,
-            "/admin/v1/prompts",
+            "/admin/v1/templates",
             json!({
                 "owner": {"type": "team", "team_id": team_id},
-                "name": "team-prompt",
-                "content": "Team system prompt"
+                "name": "team-template",
+                "content": "Team system template"
             }),
         )
         .await;
 
         assert_eq!(status, StatusCode::CREATED);
-        assert_eq!(body["name"], "team-prompt");
+        assert_eq!(body["name"], "team-template");
         assert_eq!(body["owner_type"], "team");
         assert_eq!(body["owner_id"], team_id);
     }
 
     #[tokio::test]
-    async fn test_create_prompt_for_user() {
+    async fn test_create_template_for_user() {
         let app = test_app().await;
 
         // Create a user
         let (status, user) = post_json(
             &app,
             "/admin/v1/users",
-            json!({"external_id": "prompt-user", "email": "prompt@example.com", "name": "Prompt User"}),
+            json!({"external_id": "template-user", "email": "template@example.com", "name": "Template User"}),
         )
         .await;
         assert_eq!(status, StatusCode::CREATED);
@@ -4799,35 +4799,35 @@ ttl_secs = 86400
 
         let (status, body) = post_json(
             &app,
-            "/admin/v1/prompts",
+            "/admin/v1/templates",
             json!({
                 "owner": {"type": "user", "user_id": user_id},
-                "name": "user-prompt",
-                "content": "User system prompt"
+                "name": "user-template",
+                "content": "User system template"
             }),
         )
         .await;
 
         assert_eq!(status, StatusCode::CREATED);
-        assert_eq!(body["name"], "user-prompt");
+        assert_eq!(body["name"], "user-template");
         assert_eq!(body["owner_type"], "user");
         assert_eq!(body["owner_id"], user_id);
     }
 
     #[tokio::test]
-    async fn test_create_prompt_with_metadata() {
+    async fn test_create_template_with_metadata() {
         let app = test_app().await;
-        let org_slug = create_org(&app, "prompt-meta-org").await;
+        let org_slug = create_org(&app, "template-meta-org").await;
         let (_, org) = get_json(&app, &format!("/admin/v1/organizations/{}", org_slug)).await;
         let org_id = org["id"].as_str().unwrap();
 
         let (status, body) = post_json(
             &app,
-            "/admin/v1/prompts",
+            "/admin/v1/templates",
             json!({
                 "owner": {"type": "organization", "organization_id": org_id},
-                "name": "meta-prompt",
-                "content": "System prompt with metadata",
+                "name": "meta-template",
+                "content": "System template with metadata",
                 "metadata": {
                     "temperature": 0.7,
                     "max_tokens": 1000,
@@ -4844,19 +4844,19 @@ ttl_secs = 86400
     }
 
     #[tokio::test]
-    async fn test_create_prompt_duplicate_name_same_owner() {
+    async fn test_create_template_duplicate_name_same_owner() {
         let app = test_app().await;
-        let org_slug = create_org(&app, "dup-prompt-org").await;
+        let org_slug = create_org(&app, "dup-template-org").await;
         let (_, org) = get_json(&app, &format!("/admin/v1/organizations/{}", org_slug)).await;
         let org_id = org["id"].as_str().unwrap();
 
         let (status, _) = post_json(
             &app,
-            "/admin/v1/prompts",
+            "/admin/v1/templates",
             json!({
                 "owner": {"type": "organization", "organization_id": org_id},
-                "name": "duplicate-prompt",
-                "content": "First prompt"
+                "name": "duplicate-template",
+                "content": "First template"
             }),
         )
         .await;
@@ -4864,11 +4864,11 @@ ttl_secs = 86400
 
         let (status, body) = post_json(
             &app,
-            "/admin/v1/prompts",
+            "/admin/v1/templates",
             json!({
                 "owner": {"type": "organization", "organization_id": org_id},
-                "name": "duplicate-prompt",
-                "content": "Second prompt"
+                "name": "duplicate-template",
+                "content": "Second template"
             }),
         )
         .await;
@@ -4878,20 +4878,20 @@ ttl_secs = 86400
     }
 
     #[tokio::test]
-    async fn test_create_prompt_same_name_different_owners() {
+    async fn test_create_template_same_name_different_owners() {
         let app = test_app().await;
-        let org1_slug = create_org(&app, "prompt-org1").await;
-        let org2_slug = create_org(&app, "prompt-org2").await;
+        let org1_slug = create_org(&app, "template-org1").await;
+        let org2_slug = create_org(&app, "template-org2").await;
         let (_, org1) = get_json(&app, &format!("/admin/v1/organizations/{}", org1_slug)).await;
         let (_, org2) = get_json(&app, &format!("/admin/v1/organizations/{}", org2_slug)).await;
 
         let (status, _) = post_json(
             &app,
-            "/admin/v1/prompts",
+            "/admin/v1/templates",
             json!({
                 "owner": {"type": "organization", "organization_id": org1["id"]},
                 "name": "shared-name",
-                "content": "Org 1 prompt"
+                "content": "Org 1 template"
             }),
         )
         .await;
@@ -4899,11 +4899,11 @@ ttl_secs = 86400
 
         let (status, _) = post_json(
             &app,
-            "/admin/v1/prompts",
+            "/admin/v1/templates",
             json!({
                 "owner": {"type": "organization", "organization_id": org2["id"]},
                 "name": "shared-name",
-                "content": "Org 2 prompt"
+                "content": "Org 2 template"
             }),
         )
         .await;
@@ -4911,54 +4911,54 @@ ttl_secs = 86400
     }
 
     #[tokio::test]
-    async fn test_get_prompt() {
+    async fn test_get_template() {
         let app = test_app().await;
-        let org_slug = create_org(&app, "get-prompt-org").await;
+        let org_slug = create_org(&app, "get-template-org").await;
         let (_, org) = get_json(&app, &format!("/admin/v1/organizations/{}", org_slug)).await;
         let org_id = org["id"].as_str().unwrap();
 
         let (status, created) = post_json(
             &app,
-            "/admin/v1/prompts",
+            "/admin/v1/templates",
             json!({
                 "owner": {"type": "organization", "organization_id": org_id},
-                "name": "get-test-prompt",
+                "name": "get-test-template",
                 "content": "Test content"
             }),
         )
         .await;
         assert_eq!(status, StatusCode::CREATED);
-        let prompt_id = created["id"].as_str().unwrap();
+        let template_id = created["id"].as_str().unwrap();
 
-        let (status, body) = get_json(&app, &format!("/admin/v1/prompts/{}", prompt_id)).await;
+        let (status, body) = get_json(&app, &format!("/admin/v1/templates/{}", template_id)).await;
 
         assert_eq!(status, StatusCode::OK);
-        assert_eq!(body["id"], prompt_id);
-        assert_eq!(body["name"], "get-test-prompt");
+        assert_eq!(body["id"], template_id);
+        assert_eq!(body["name"], "get-test-template");
         assert_eq!(body["content"], "Test content");
     }
 
     #[tokio::test]
-    async fn test_get_prompt_not_found() {
+    async fn test_get_template_not_found() {
         let app = test_app().await;
         let fake_id = uuid::Uuid::new_v4();
 
-        let (status, body) = get_json(&app, &format!("/admin/v1/prompts/{}", fake_id)).await;
+        let (status, body) = get_json(&app, &format!("/admin/v1/templates/{}", fake_id)).await;
 
         assert_eq!(status, StatusCode::NOT_FOUND);
         assert!(body["error"]["code"].is_string());
     }
 
     #[tokio::test]
-    async fn test_update_prompt() {
+    async fn test_update_template() {
         let app = test_app().await;
-        let org_slug = create_org(&app, "update-prompt-org").await;
+        let org_slug = create_org(&app, "update-template-org").await;
         let (_, org) = get_json(&app, &format!("/admin/v1/organizations/{}", org_slug)).await;
         let org_id = org["id"].as_str().unwrap();
 
         let (status, created) = post_json(
             &app,
-            "/admin/v1/prompts",
+            "/admin/v1/templates",
             json!({
                 "owner": {"type": "organization", "organization_id": org_id},
                 "name": "update-test",
@@ -4967,11 +4967,11 @@ ttl_secs = 86400
         )
         .await;
         assert_eq!(status, StatusCode::CREATED);
-        let prompt_id = created["id"].as_str().unwrap();
+        let template_id = created["id"].as_str().unwrap();
 
         let (status, body) = patch_json(
             &app,
-            &format!("/admin/v1/prompts/{}", prompt_id),
+            &format!("/admin/v1/templates/{}", template_id),
             json!({
                 "name": "updated-name",
                 "content": "Updated content",
@@ -4987,7 +4987,7 @@ ttl_secs = 86400
     }
 
     #[tokio::test]
-    async fn test_update_prompt_partial() {
+    async fn test_update_template_partial() {
         let app = test_app().await;
         let org_slug = create_org(&app, "partial-update-org").await;
         let (_, org) = get_json(&app, &format!("/admin/v1/organizations/{}", org_slug)).await;
@@ -4995,7 +4995,7 @@ ttl_secs = 86400
 
         let (status, created) = post_json(
             &app,
-            "/admin/v1/prompts",
+            "/admin/v1/templates",
             json!({
                 "owner": {"type": "organization", "organization_id": org_id},
                 "name": "partial-update",
@@ -5004,12 +5004,12 @@ ttl_secs = 86400
         )
         .await;
         assert_eq!(status, StatusCode::CREATED);
-        let prompt_id = created["id"].as_str().unwrap();
+        let template_id = created["id"].as_str().unwrap();
 
         // Only update content
         let (status, body) = patch_json(
             &app,
-            &format!("/admin/v1/prompts/{}", prompt_id),
+            &format!("/admin/v1/templates/{}", template_id),
             json!({"content": "Only content changed"}),
         )
         .await;
@@ -5020,13 +5020,13 @@ ttl_secs = 86400
     }
 
     #[tokio::test]
-    async fn test_update_prompt_not_found() {
+    async fn test_update_template_not_found() {
         let app = test_app().await;
         let fake_id = uuid::Uuid::new_v4();
 
         let (status, body) = patch_json(
             &app,
-            &format!("/admin/v1/prompts/{}", fake_id),
+            &format!("/admin/v1/templates/{}", fake_id),
             json!({"name": "new-name"}),
         )
         .await;
@@ -5036,15 +5036,15 @@ ttl_secs = 86400
     }
 
     #[tokio::test]
-    async fn test_delete_prompt() {
+    async fn test_delete_template() {
         let app = test_app().await;
-        let org_slug = create_org(&app, "delete-prompt-org").await;
+        let org_slug = create_org(&app, "delete-template-org").await;
         let (_, org) = get_json(&app, &format!("/admin/v1/organizations/{}", org_slug)).await;
         let org_id = org["id"].as_str().unwrap();
 
         let (status, created) = post_json(
             &app,
-            "/admin/v1/prompts",
+            "/admin/v1/templates",
             json!({
                 "owner": {"type": "organization", "organization_id": org_id},
                 "name": "delete-test",
@@ -5053,42 +5053,42 @@ ttl_secs = 86400
         )
         .await;
         assert_eq!(status, StatusCode::CREATED);
-        let prompt_id = created["id"].as_str().unwrap();
+        let template_id = created["id"].as_str().unwrap();
 
-        let (status, _) = delete_json(&app, &format!("/admin/v1/prompts/{}", prompt_id)).await;
+        let (status, _) = delete_json(&app, &format!("/admin/v1/templates/{}", template_id)).await;
         assert_eq!(status, StatusCode::OK);
 
         // Verify it's deleted
-        let (status, _) = get_json(&app, &format!("/admin/v1/prompts/{}", prompt_id)).await;
+        let (status, _) = get_json(&app, &format!("/admin/v1/templates/{}", template_id)).await;
         assert_eq!(status, StatusCode::NOT_FOUND);
     }
 
     #[tokio::test]
-    async fn test_delete_prompt_not_found() {
+    async fn test_delete_template_not_found() {
         let app = test_app().await;
         let fake_id = uuid::Uuid::new_v4();
 
-        let (status, body) = delete_json(&app, &format!("/admin/v1/prompts/{}", fake_id)).await;
+        let (status, body) = delete_json(&app, &format!("/admin/v1/templates/{}", fake_id)).await;
 
         assert_eq!(status, StatusCode::NOT_FOUND);
         assert!(body["error"]["code"].is_string());
     }
 
     #[tokio::test]
-    async fn test_list_prompts_by_organization() {
+    async fn test_list_templates_by_organization() {
         let app = test_app().await;
-        let org_slug = create_org(&app, "list-prompt-org").await;
+        let org_slug = create_org(&app, "list-template-org").await;
         let (_, org) = get_json(&app, &format!("/admin/v1/organizations/{}", org_slug)).await;
         let org_id = org["id"].as_str().unwrap();
 
-        // Create 3 prompts for the org
+        // Create 3 templates for the org
         for i in 0..3 {
             let (status, _) = post_json(
                 &app,
-                "/admin/v1/prompts",
+                "/admin/v1/templates",
                 json!({
                     "owner": {"type": "organization", "organization_id": org_id},
-                    "name": format!("org-prompt-{}", i),
+                    "name": format!("org-template-{}", i),
                     "content": format!("Content {}", i)
                 }),
             )
@@ -5098,7 +5098,7 @@ ttl_secs = 86400
 
         let (status, body) = get_json(
             &app,
-            &format!("/admin/v1/organizations/{}/prompts", org_slug),
+            &format!("/admin/v1/organizations/{}/templates", org_slug),
         )
         .await;
 
@@ -5109,19 +5109,19 @@ ttl_secs = 86400
     }
 
     #[tokio::test]
-    async fn test_list_prompts_by_team() {
+    async fn test_list_templates_by_team() {
         let app = test_app().await;
-        let org_slug = create_org(&app, "list-team-prompt-org").await;
-        let team_id = create_team(&app, &org_slug, "prompt-list-team").await;
+        let org_slug = create_org(&app, "list-team-template-org").await;
+        let team_id = create_team(&app, &org_slug, "template-list-team").await;
 
-        // Create 2 prompts for the team
+        // Create 2 templates for the team
         for i in 0..2 {
             let (status, _) = post_json(
                 &app,
-                "/admin/v1/prompts",
+                "/admin/v1/templates",
                 json!({
                     "owner": {"type": "team", "team_id": team_id},
-                    "name": format!("team-prompt-{}", i),
+                    "name": format!("team-template-{}", i),
                     "content": format!("Content {}", i)
                 }),
             )
@@ -5132,7 +5132,7 @@ ttl_secs = 86400
         let (status, body) = get_json(
             &app,
             &format!(
-                "/admin/v1/organizations/{}/teams/prompt-list-team/prompts",
+                "/admin/v1/organizations/{}/teams/template-list-team/templates",
                 org_slug
             ),
         )
@@ -5143,19 +5143,19 @@ ttl_secs = 86400
     }
 
     #[tokio::test]
-    async fn test_list_prompts_by_project() {
+    async fn test_list_templates_by_project() {
         let app = test_app().await;
-        let org_slug = create_org(&app, "list-proj-prompt-org").await;
-        let project_id = create_project(&app, &org_slug, "prompt-list-project").await;
+        let org_slug = create_org(&app, "list-proj-template-org").await;
+        let project_id = create_project(&app, &org_slug, "template-list-project").await;
 
-        // Create 2 prompts for the project
+        // Create 2 templates for the project
         for i in 0..2 {
             let (status, _) = post_json(
                 &app,
-                "/admin/v1/prompts",
+                "/admin/v1/templates",
                 json!({
                     "owner": {"type": "project", "project_id": project_id},
-                    "name": format!("project-prompt-{}", i),
+                    "name": format!("project-template-{}", i),
                     "content": format!("Content {}", i)
                 }),
             )
@@ -5166,7 +5166,7 @@ ttl_secs = 86400
         let (status, body) = get_json(
             &app,
             &format!(
-                "/admin/v1/organizations/{}/projects/prompt-list-project/prompts",
+                "/admin/v1/organizations/{}/projects/template-list-project/templates",
                 org_slug
             ),
         )
@@ -5177,27 +5177,27 @@ ttl_secs = 86400
     }
 
     #[tokio::test]
-    async fn test_list_prompts_by_user() {
+    async fn test_list_templates_by_user() {
         let app = test_app().await;
 
         // Create a user
         let (status, user) = post_json(
             &app,
             "/admin/v1/users",
-            json!({"external_id": "list-prompt-user", "email": "listprompt@example.com", "name": "List User"}),
+            json!({"external_id": "list-template-user", "email": "listtemplate@example.com", "name": "List User"}),
         )
         .await;
         assert_eq!(status, StatusCode::CREATED);
         let user_id = user["id"].as_str().unwrap();
 
-        // Create 2 prompts for the user
+        // Create 2 templates for the user
         for i in 0..2 {
             let (status, _) = post_json(
                 &app,
-                "/admin/v1/prompts",
+                "/admin/v1/templates",
                 json!({
                     "owner": {"type": "user", "user_id": user_id},
-                    "name": format!("user-prompt-{}", i),
+                    "name": format!("user-template-{}", i),
                     "content": format!("Content {}", i)
                 }),
             )
@@ -5205,20 +5205,21 @@ ttl_secs = 86400
             assert_eq!(status, StatusCode::CREATED);
         }
 
-        let (status, body) = get_json(&app, &format!("/admin/v1/users/{}/prompts", user_id)).await;
+        let (status, body) =
+            get_json(&app, &format!("/admin/v1/users/{}/templates", user_id)).await;
 
         assert_eq!(status, StatusCode::OK);
         assert_eq!(body["data"].as_array().unwrap().len(), 2);
     }
 
     #[tokio::test]
-    async fn test_list_prompts_empty() {
+    async fn test_list_templates_empty() {
         let app = test_app().await;
-        let org_slug = create_org(&app, "empty-prompt-org").await;
+        let org_slug = create_org(&app, "empty-template-org").await;
 
         let (status, body) = get_json(
             &app,
-            &format!("/admin/v1/organizations/{}/prompts", org_slug),
+            &format!("/admin/v1/organizations/{}/templates", org_slug),
         )
         .await;
 
@@ -5227,25 +5228,25 @@ ttl_secs = 86400
     }
 
     #[tokio::test]
-    async fn test_list_prompts_org_not_found() {
+    async fn test_list_templates_org_not_found() {
         let app = test_app().await;
 
         let (status, body) =
-            get_json(&app, "/admin/v1/organizations/nonexistent-org/prompts").await;
+            get_json(&app, "/admin/v1/organizations/nonexistent-org/templates").await;
 
         assert_eq!(status, StatusCode::NOT_FOUND);
         assert!(body["error"]["code"].is_string());
     }
 
     #[tokio::test]
-    async fn test_list_prompts_team_not_found() {
+    async fn test_list_templates_team_not_found() {
         let app = test_app().await;
-        let org_slug = create_org(&app, "team-404-prompt-org").await;
+        let org_slug = create_org(&app, "team-404-template-org").await;
 
         let (status, body) = get_json(
             &app,
             &format!(
-                "/admin/v1/organizations/{}/teams/nonexistent-team/prompts",
+                "/admin/v1/organizations/{}/teams/nonexistent-team/templates",
                 org_slug
             ),
         )
@@ -5256,14 +5257,14 @@ ttl_secs = 86400
     }
 
     #[tokio::test]
-    async fn test_list_prompts_project_not_found() {
+    async fn test_list_templates_project_not_found() {
         let app = test_app().await;
-        let org_slug = create_org(&app, "proj-404-prompt-org").await;
+        let org_slug = create_org(&app, "proj-404-template-org").await;
 
         let (status, body) = get_json(
             &app,
             &format!(
-                "/admin/v1/organizations/{}/projects/nonexistent-project/prompts",
+                "/admin/v1/organizations/{}/projects/nonexistent-project/templates",
                 org_slug
             ),
         )
@@ -5274,20 +5275,20 @@ ttl_secs = 86400
     }
 
     #[tokio::test]
-    async fn test_list_prompts_pagination() {
+    async fn test_list_templates_pagination() {
         let app = test_app().await;
-        let org_slug = create_org(&app, "paginate-prompt-org").await;
+        let org_slug = create_org(&app, "paginate-template-org").await;
         let (_, org) = get_json(&app, &format!("/admin/v1/organizations/{}", org_slug)).await;
         let org_id = org["id"].as_str().unwrap();
 
-        // Create 5 prompts
+        // Create 5 templates
         for i in 0..5 {
             let (status, _) = post_json(
                 &app,
-                "/admin/v1/prompts",
+                "/admin/v1/templates",
                 json!({
                     "owner": {"type": "organization", "organization_id": org_id},
-                    "name": format!("paginate-prompt-{}", i),
+                    "name": format!("paginate-template-{}", i),
                     "content": format!("Content {}", i)
                 }),
             )
@@ -5298,7 +5299,7 @@ ttl_secs = 86400
         // Request with limit=2
         let (status, body) = get_json(
             &app,
-            &format!("/admin/v1/organizations/{}/prompts?limit=2", org_slug),
+            &format!("/admin/v1/organizations/{}/templates?limit=2", org_slug),
         )
         .await;
 
diff --git a/src/routes/admin/prompts.rs b/src/routes/admin/templates.rs
similarity index 59%
rename from src/routes/admin/prompts.rs
rename to src/routes/admin/templates.rs
index b5a27f61..c91d6444 100644
--- a/src/routes/admin/prompts.rs
+++ b/src/routes/admin/templates.rs
@@ -12,17 +12,17 @@ use super::{AuditActor, error::AdminError, organizations::ListQuery};
 use crate::{
     AppState,
     middleware::{AdminAuth, AuthzContext, ClientInfo},
-    models::{CreateAuditLog, CreatePrompt, Prompt, PromptOwnerType, UpdatePrompt},
+    models::{CreateAuditLog, CreateTemplate, Template, TemplateOwnerType, UpdateTemplate},
     openapi::PaginationMeta,
     services::Services,
 };
 
-/// Paginated list of prompts
+/// Paginated list of templates
 #[derive(Debug, Serialize, Deserialize)]
 #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
-pub struct PromptListResponse {
-    /// List of prompts
-    pub data: Vec<Prompt>,
+pub struct TemplateListResponse {
+    /// List of templates
+    pub data: Vec<Template>,
     /// Pagination metadata
     pub pagination: PaginationMeta,
 }
@@ -31,53 +31,53 @@ fn get_services(state: &AppState) -> Result<&Services, AdminError> {
     state.services.as_ref().ok_or(AdminError::ServicesRequired)
 }
 
-/// Create a prompt template
+/// Create a template
 #[cfg_attr(feature = "utoipa", utoipa::path(
     post,
-    path = "/admin/v1/prompts",
-    tag = "prompts",
-    operation_id = "prompt_create",
-    request_body = CreatePrompt,
+    path = "/admin/v1/templates",
+    tag = "templates",
+    operation_id = "template_create",
+    request_body = CreateTemplate,
     responses(
-        (status = 201, description = "Prompt created", body = Prompt),
+        (status = 201, description = "Template created", body = Template),
         (status = 404, description = "Owner not found", body = crate::openapi::ErrorResponse),
-        (status = 409, description = "Prompt with this name already exists", body = crate::openapi::ErrorResponse),
+        (status = 409, description = "Template with this name already exists", body = crate::openapi::ErrorResponse),
     )
 ))]
-#[tracing::instrument(name = "admin.prompts.create", skip(state, admin_auth, authz, input))]
+#[tracing::instrument(name = "admin.templates.create", skip(state, admin_auth, authz, input))]
 pub async fn create(
     State(state): State<AppState>,
     Extension(admin_auth): Extension<AdminAuth>,
     Extension(authz): Extension<AuthzContext>,
     Extension(client_info): Extension<ClientInfo>,
-    Valid(Json(input)): Valid<Json<CreatePrompt>>,
-) -> Result<(StatusCode, Json<Prompt>), AdminError> {
+    Valid(Json(input)): Valid<Json<CreateTemplate>>,
+) -> Result<(StatusCode, Json<Template>), AdminError> {
     let services = get_services(&state)?;
     let actor = AuditActor::from(&admin_auth);
 
-    authz.require("prompt", "create", None, None, None, None)?;
+    authz.require("template", "create", None, None, None, None)?;
 
-    // Check prompt limit
-    let max = state.config.limits.resource_limits.max_prompts_per_owner;
+    // Check template limit
+    let max = state.config.limits.resource_limits.max_templates_per_owner;
     if max > 0 {
         let count = services
-            .prompts
+            .templates
             .count_by_owner(input.owner.owner_type(), input.owner.owner_id(), false)
             .await?;
         if count >= max as i64 {
             return Err(AdminError::Conflict(format!(
-                "Owner has reached the maximum number of prompts ({max})"
+                "Owner has reached the maximum number of templates ({max})"
             )));
         }
     }
 
-    let prompt = services.prompts.create(input).await?;
+    let template = services.templates.create(input).await?;
 
     // Extract org_id and project_id from owner for audit log
-    let (org_id, project_id) = match prompt.owner_type {
-        PromptOwnerType::Organization => (Some(prompt.owner_id), None),
-        PromptOwnerType::Project => (None, Some(prompt.owner_id)),
-        PromptOwnerType::Team | PromptOwnerType::User => (None, None),
+    let (org_id, project_id) = match template.owner_type {
+        TemplateOwnerType::Organization => (Some(template.owner_id), None),
+        TemplateOwnerType::Project => (None, Some(template.owner_id)),
+        TemplateOwnerType::Team | TemplateOwnerType::User => (None, None),
     };
 
     // Log audit event (fire-and-forget)
@@ -86,82 +86,82 @@ pub async fn create(
         .create(CreateAuditLog {
             actor_type: actor.actor_type,
             actor_id: actor.actor_id,
-            action: "prompt.create".to_string(),
-            resource_type: "prompt".to_string(),
-            resource_id: prompt.id,
+            action: "template.create".to_string(),
+            resource_type: "template".to_string(),
+            resource_id: template.id,
             org_id,
             project_id,
             details: json!({
-                "name": prompt.name,
-                "owner_type": prompt.owner_type,
-                "owner_id": prompt.owner_id,
+                "name": template.name,
+                "owner_type": template.owner_type,
+                "owner_id": template.owner_id,
             }),
             ip_address: client_info.ip_address,
             user_agent: client_info.user_agent,
         })
         .await;
 
-    Ok((StatusCode::CREATED, Json(prompt)))
+    Ok((StatusCode::CREATED, Json(template)))
 }
 
-/// Get a prompt by ID
+/// Get a template by ID
 #[cfg_attr(feature = "utoipa", utoipa::path(
     get,
-    path = "/admin/v1/prompts/{id}",
-    tag = "prompts",
-    operation_id = "prompt_get",
-    params(("id" = Uuid, Path, description = "Prompt ID")),
+    path = "/admin/v1/templates/{id}",
+    tag = "templates",
+    operation_id = "template_get",
+    params(("id" = Uuid, Path, description = "Template ID")),
     responses(
-        (status = 200, description = "Prompt found", body = Prompt),
-        (status = 404, description = "Prompt not found", body = crate::openapi::ErrorResponse),
+        (status = 200, description = "Template found", body = Template),
+        (status = 404, description = "Template not found", body = crate::openapi::ErrorResponse),
     )
 ))]
-#[tracing::instrument(name = "admin.prompts.get", skip(state, authz), fields(%id))]
+#[tracing::instrument(name = "admin.templates.get", skip(state, authz), fields(%id))]
 pub async fn get(
     State(state): State<AppState>,
     Extension(authz): Extension<AuthzContext>,
     Path(id): Path<Uuid>,
-) -> Result<Json<Prompt>, AdminError> {
+) -> Result<Json<Template>, AdminError> {
     let services = get_services(&state)?;
 
-    authz.require("prompt", "read", None, None, None, None)?;
+    authz.require("template", "read", None, None, None, None)?;
 
-    let prompt = services
-        .prompts
+    let template = services
+        .templates
         .get_by_id(id)
         .await?
-        .ok_or_else(|| AdminError::NotFound("Prompt not found".to_string()))?;
+        .ok_or_else(|| AdminError::NotFound("Template not found".to_string()))?;
 
-    Ok(Json(prompt))
+    Ok(Json(template))
 }
 
-/// Update a prompt
+/// Update a template
 #[cfg_attr(feature = "utoipa", utoipa::path(
     patch,
-    path = "/admin/v1/prompts/{id}",
-    tag = "prompts",
-    operation_id = "prompt_update",
-    params(("id" = Uuid, Path, description = "Prompt ID")),
-    request_body = UpdatePrompt,
+    path = "/admin/v1/templates/{id}",
+    tag = "templates",
+    operation_id = "template_update",
+    params(("id" = Uuid, Path, description = "Template ID")),
+    request_body = UpdateTemplate,
     responses(
-        (status = 200, description = "Prompt updated", body = Prompt),
-        (status = 404, description = "Prompt not found", body = crate::openapi::ErrorResponse),
-        (status = 409, description = "Prompt with this name already exists", body = crate::openapi::ErrorResponse),
+        (status = 200, description = "Template updated", body = Template),
+        (status = 404, description = "Template not found", body = crate::openapi::ErrorResponse),
+        (status = 409, description = "Template with this name already exists", body = crate::openapi::ErrorResponse),
     )
 ))]
-#[tracing::instrument(name = "admin.prompts.update", skip(state, admin_auth, authz, input), fields(%id))]
+#[tracing::instrument(name = "admin.templates.update", skip(state, admin_auth, authz, input), fields(%id))]
 pub async fn update(
     State(state): State<AppState>,
     Extension(admin_auth): Extension<AdminAuth>,
     Extension(authz): Extension<AuthzContext>,
     Extension(client_info): Extension<ClientInfo>,
     Path(id): Path<Uuid>,
-    Valid(Json(input)): Valid<Json<UpdatePrompt>>,
-) -> Result<Json<Prompt>, AdminError> {
+    Valid(Json(input)): Valid<Json<UpdateTemplate>>,
+) -> Result<Json<Template>, AdminError> {
     let services = get_services(&state)?;
     let actor = AuditActor::from(&admin_auth);
 
-    authz.require("prompt", "update", None, None, None, None)?;
+    authz.require("template", "update", None, None, None, None)?;
 
     // Capture changes for audit log
     let changes = json!({
@@ -171,13 +171,13 @@ pub async fn update(
         "metadata": input.metadata,
     });
 
-    let prompt = services.prompts.update(id, input).await?;
+    let template = services.templates.update(id, input).await?;
 
     // Extract org_id and project_id from owner for audit log
-    let (org_id, project_id) = match prompt.owner_type {
-        PromptOwnerType::Organization => (Some(prompt.owner_id), None),
-        PromptOwnerType::Project => (None, Some(prompt.owner_id)),
-        PromptOwnerType::Team | PromptOwnerType::User => (None, None),
+    let (org_id, project_id) = match template.owner_type {
+        TemplateOwnerType::Organization => (Some(template.owner_id), None),
+        TemplateOwnerType::Project => (None, Some(template.owner_id)),
+        TemplateOwnerType::Team | TemplateOwnerType::User => (None, None),
     };
 
     // Log audit event (fire-and-forget)
@@ -186,13 +186,13 @@ pub async fn update(
         .create(CreateAuditLog {
             actor_type: actor.actor_type,
             actor_id: actor.actor_id,
-            action: "prompt.update".to_string(),
-            resource_type: "prompt".to_string(),
-            resource_id: prompt.id,
+            action: "template.update".to_string(),
+            resource_type: "template".to_string(),
+            resource_id: template.id,
             org_id,
             project_id,
             details: json!({
-                "name": prompt.name,
+                "name": template.name,
                 "changes": changes,
             }),
             ip_address: client_info.ip_address,
@@ -200,22 +200,22 @@ pub async fn update(
         })
         .await;
 
-    Ok(Json(prompt))
+    Ok(Json(template))
 }
 
-/// Delete a prompt
+/// Delete a template
 #[cfg_attr(feature = "utoipa", utoipa::path(
     delete,
-    path = "/admin/v1/prompts/{id}",
-    tag = "prompts",
-    operation_id = "prompt_delete",
-    params(("id" = Uuid, Path, description = "Prompt ID")),
+    path = "/admin/v1/templates/{id}",
+    tag = "templates",
+    operation_id = "template_delete",
+    params(("id" = Uuid, Path, description = "Template ID")),
     responses(
-        (status = 200, description = "Prompt deleted"),
-        (status = 404, description = "Prompt not found", body = crate::openapi::ErrorResponse),
+        (status = 200, description = "Template deleted"),
+        (status = 404, description = "Template not found", body = crate::openapi::ErrorResponse),
     )
 ))]
-#[tracing::instrument(name = "admin.prompts.delete", skip(state, admin_auth, authz), fields(%id))]
+#[tracing::instrument(name = "admin.templates.delete", skip(state, admin_auth, authz), fields(%id))]
 pub async fn delete(
     State(state): State<AppState>,
     Extension(admin_auth): Extension<AdminAuth>,
@@ -226,28 +226,28 @@ pub async fn delete(
     let services = get_services(&state)?;
     let actor = AuditActor::from(&admin_auth);
 
-    authz.require("prompt", "delete", None, None, None, None)?;
+    authz.require("template", "delete", None, None, None, None)?;
 
-    // Get prompt details before deletion for audit log
-    let prompt = services
-        .prompts
+    // Get template details before deletion for audit log
+    let template = services
+        .templates
         .get_by_id(id)
         .await?
-        .ok_or_else(|| AdminError::NotFound("Prompt not found".to_string()))?;
+        .ok_or_else(|| AdminError::NotFound("Template not found".to_string()))?;
 
     // Extract org_id and project_id from owner for audit log
-    let (org_id, project_id) = match prompt.owner_type {
-        PromptOwnerType::Organization => (Some(prompt.owner_id), None),
-        PromptOwnerType::Project => (None, Some(prompt.owner_id)),
-        PromptOwnerType::Team | PromptOwnerType::User => (None, None),
+    let (org_id, project_id) = match template.owner_type {
+        TemplateOwnerType::Organization => (Some(template.owner_id), None),
+        TemplateOwnerType::Project => (None, Some(template.owner_id)),
+        TemplateOwnerType::Team | TemplateOwnerType::User => (None, None),
     };
 
     // Capture details for audit log before deletion
-    let prompt_name = prompt.name.clone();
-    let prompt_owner_type = prompt.owner_type;
-    let prompt_owner_id = prompt.owner_id;
+    let template_name = template.name.clone();
+    let template_owner_type = template.owner_type;
+    let template_owner_id = template.owner_id;
 
-    services.prompts.delete(id).await?;
+    services.templates.delete(id).await?;
 
     // Log audit event (fire-and-forget)
     let _ = services
@@ -255,15 +255,15 @@ pub async fn delete(
         .create(CreateAuditLog {
             actor_type: actor.actor_type,
             actor_id: actor.actor_id,
-            action: "prompt.delete".to_string(),
-            resource_type: "prompt".to_string(),
+            action: "template.delete".to_string(),
+            resource_type: "template".to_string(),
             resource_id: id,
             org_id,
             project_id,
             details: json!({
-                "name": prompt_name,
-                "owner_type": prompt_owner_type,
-                "owner_id": prompt_owner_id,
+                "name": template_name,
+                "owner_type": template_owner_type,
+                "owner_id": template_owner_id,
             }),
             ip_address: client_info.ip_address,
             user_agent: client_info.user_agent,
@@ -273,29 +273,29 @@ pub async fn delete(
     Ok(Json(()))
 }
 
-/// List prompts by organization
+/// List templates by organization
 #[cfg_attr(feature = "utoipa", utoipa::path(
     get,
-    path = "/admin/v1/organizations/{org_slug}/prompts",
-    tag = "prompts",
-    operation_id = "prompt_list_by_org",
+    path = "/admin/v1/organizations/{org_slug}/templates",
+    tag = "templates",
+    operation_id = "template_list_by_org",
     params(
         ("org_slug" = String, Path, description = "Organization slug"),
         ListQuery,
     ),
     responses(
-        (status = 200, description = "List of prompts", body = PromptListResponse),
+        (status = 200, description = "List of templates", body = TemplateListResponse),
         (status = 400, description = "Invalid cursor or direction", body = crate::openapi::ErrorResponse),
         (status = 404, description = "Organization not found", body = crate::openapi::ErrorResponse),
     )
 ))]
-#[tracing::instrument(name = "admin.prompts.list_by_org", skip(state, authz, query), fields(%org_slug))]
+#[tracing::instrument(name = "admin.templates.list_by_org", skip(state, authz, query), fields(%org_slug))]
 pub async fn list_by_org(
     State(state): State<AppState>,
     Extension(authz): Extension<AuthzContext>,
     Path(org_slug): Path<String>,
     Query(query): Query<ListQuery>,
-) -> Result<Json<PromptListResponse>, AdminError> {
+) -> Result<Json<TemplateListResponse>, AdminError> {
     let services = get_services(&state)?;
 
     // Get org by slug
@@ -306,7 +306,7 @@ pub async fn list_by_org(
         .ok_or_else(|| AdminError::NotFound(format!("Organization '{}' not found", org_slug)))?;
 
     authz.require(
-        "prompt",
+        "template",
         "list",
         None,
         Some(&org.id.to_string()),
@@ -317,10 +317,7 @@ pub async fn list_by_org(
     let limit = query.limit.unwrap_or(100);
     let params = query.try_into_with_cursor()?;
 
-    let result = services
-        .prompts
-        .list_by_owner(PromptOwnerType::Organization, org.id, params)
-        .await?;
+    let result = services.templates.list_by_org(org.id, params).await?;
 
     let pagination = PaginationMeta::with_cursors(
         limit,
@@ -329,36 +326,36 @@ pub async fn list_by_org(
         result.cursors.prev.map(|c| c.encode()),
     );
 
-    Ok(Json(PromptListResponse {
+    Ok(Json(TemplateListResponse {
         data: result.items,
         pagination,
     }))
 }
 
-/// List prompts by team
+/// List templates by team
 #[cfg_attr(feature = "utoipa", utoipa::path(
     get,
-    path = "/admin/v1/organizations/{org_slug}/teams/{team_slug}/prompts",
-    tag = "prompts",
-    operation_id = "prompt_list_by_team",
+    path = "/admin/v1/organizations/{org_slug}/teams/{team_slug}/templates",
+    tag = "templates",
+    operation_id = "template_list_by_team",
     params(
         ("org_slug" = String, Path, description = "Organization slug"),
         ("team_slug" = String, Path, description = "Team slug"),
         ListQuery,
     ),
     responses(
-        (status = 200, description = "List of prompts", body = PromptListResponse),
+        (status = 200, description = "List of templates", body = TemplateListResponse),
         (status = 400, description = "Invalid cursor or direction", body = crate::openapi::ErrorResponse),
         (status = 404, description = "Organization or team not found", body = crate::openapi::ErrorResponse),
     )
 ))]
-#[tracing::instrument(name = "admin.prompts.list_by_team", skip(state, authz, query), fields(%org_slug, %team_slug))]
+#[tracing::instrument(name = "admin.templates.list_by_team", skip(state, authz, query), fields(%org_slug, %team_slug))]
 pub async fn list_by_team(
     State(state): State<AppState>,
     Extension(authz): Extension<AuthzContext>,
     Path((org_slug, team_slug)): Path<(String, String)>,
     Query(query): Query<ListQuery>,
-) -> Result<Json<PromptListResponse>, AdminError> {
+) -> Result<Json<TemplateListResponse>, AdminError> {
     let services = get_services(&state)?;
 
     // Get org by slug
@@ -381,7 +378,7 @@ pub async fn list_by_team(
         })?;
 
     authz.require(
-        "prompt",
+        "template",
         "list",
         None,
         Some(&org.id.to_string()),
@@ -393,8 +390,8 @@ pub async fn list_by_team(
     let params = query.try_into_with_cursor()?;
 
     let result = services
-        .prompts
-        .list_by_owner(PromptOwnerType::Team, team.id, params)
+        .templates
+        .list_by_owner(TemplateOwnerType::Team, team.id, params)
         .await?;
 
     let pagination = PaginationMeta::with_cursors(
@@ -404,36 +401,36 @@ pub async fn list_by_team(
         result.cursors.prev.map(|c| c.encode()),
     );
 
-    Ok(Json(PromptListResponse {
+    Ok(Json(TemplateListResponse {
         data: result.items,
         pagination,
     }))
 }
 
-/// List prompts by project
+/// List templates by project
 #[cfg_attr(feature = "utoipa", utoipa::path(
     get,
-    path = "/admin/v1/organizations/{org_slug}/projects/{project_slug}/prompts",
-    tag = "prompts",
-    operation_id = "prompt_list_by_project",
+    path = "/admin/v1/organizations/{org_slug}/projects/{project_slug}/templates",
+    tag = "templates",
+    operation_id = "template_list_by_project",
     params(
         ("org_slug" = String, Path, description = "Organization slug"),
         ("project_slug" = String, Path, description = "Project slug"),
         ListQuery,
     ),
     responses(
-        (status = 200, description = "List of prompts", body = PromptListResponse),
+        (status = 200, description = "List of templates", body = TemplateListResponse),
         (status = 400, description = "Invalid cursor or direction", body = crate::openapi::ErrorResponse),
         (status = 404, description = "Organization or project not found", body = crate::openapi::ErrorResponse),
     )
 ))]
-#[tracing::instrument(name = "admin.prompts.list_by_project", skip(state, authz, query), fields(%org_slug, %project_slug))]
+#[tracing::instrument(name = "admin.templates.list_by_project", skip(state, authz, query), fields(%org_slug, %project_slug))]
 pub async fn list_by_project(
     State(state): State<AppState>,
     Extension(authz): Extension<AuthzContext>,
     Path((org_slug, project_slug)): Path<(String, String)>,
     Query(query): Query<ListQuery>,
-) -> Result<Json<PromptListResponse>, AdminError> {
+) -> Result<Json<TemplateListResponse>, AdminError> {
     let services = get_services(&state)?;
 
     // Get org by slug
@@ -456,7 +453,7 @@ pub async fn list_by_project(
         })?;
 
     authz.require(
-        "prompt",
+        "template",
         "list",
         None,
         Some(&org.id.to_string()),
@@ -468,8 +465,8 @@ pub async fn list_by_project(
     let params = query.try_into_with_cursor()?;
 
     let result = services
-        .prompts
-        .list_by_owner(PromptOwnerType::Project, project.id, params)
+        .templates
+        .list_by_owner(TemplateOwnerType::Project, project.id, params)
         .await?;
 
     let pagination = PaginationMeta::with_cursors(
@@ -479,44 +476,44 @@ pub async fn list_by_project(
         result.cursors.prev.map(|c| c.encode()),
     );
 
-    Ok(Json(PromptListResponse {
+    Ok(Json(TemplateListResponse {
         data: result.items,
         pagination,
     }))
 }
 
-/// List prompts by user
+/// List templates by user
 #[cfg_attr(feature = "utoipa", utoipa::path(
     get,
-    path = "/admin/v1/users/{user_id}/prompts",
-    tag = "prompts",
-    operation_id = "prompt_list_by_user",
+    path = "/admin/v1/users/{user_id}/templates",
+    tag = "templates",
+    operation_id = "template_list_by_user",
     params(
         ("user_id" = Uuid, Path, description = "User ID"),
         ListQuery,
     ),
     responses(
-        (status = 200, description = "List of prompts", body = PromptListResponse),
+        (status = 200, description = "List of templates", body = TemplateListResponse),
         (status = 400, description = "Invalid cursor or direction", body = crate::openapi::ErrorResponse),
     )
 ))]
-#[tracing::instrument(name = "admin.prompts.list_by_user", skip(state, authz, query), fields(%user_id))]
+#[tracing::instrument(name = "admin.templates.list_by_user", skip(state, authz, query), fields(%user_id))]
 pub async fn list_by_user(
     State(state): State<AppState>,
     Extension(authz): Extension<AuthzContext>,
     Path(user_id): Path<Uuid>,
     Query(query): Query<ListQuery>,
-) -> Result<Json<PromptListResponse>, AdminError> {
+) -> Result<Json<TemplateListResponse>, AdminError> {
     let services = get_services(&state)?;
 
-    authz.require("prompt", "list", None, None, None, None)?;
+    authz.require("template", "list", None, None, None, None)?;
 
     let limit = query.limit.unwrap_or(100);
     let params = query.try_into_with_cursor()?;
 
     let result = services
-        .prompts
-        .list_by_owner(PromptOwnerType::User, user_id, params)
+        .templates
+        .list_by_owner(TemplateOwnerType::User, user_id, params)
         .await?;
 
     let pagination = PaginationMeta::with_cursors(
@@ -526,7 +523,7 @@ pub async fn list_by_user(
         result.cursors.prev.map(|c| c.encode()),
     );
 
-    Ok(Json(PromptListResponse {
+    Ok(Json(TemplateListResponse {
         data: result.items,
         pagination,
     }))
diff --git a/src/services/mod.rs b/src/services/mod.rs
index 68631cf0..1bdc5872 100644
--- a/src/services/mod.rs
+++ b/src/services/mod.rs
@@ -25,7 +25,6 @@ mod projects;
 pub mod prometheus_client;
 #[cfg(feature = "prometheus")]
 pub mod prometheus_parser;
-mod prompts;
 pub mod provider_metrics;
 mod providers;
 mod reranker;
@@ -37,6 +36,7 @@ mod service_accounts;
 #[cfg(feature = "sso")]
 mod sso_group_mappings;
 mod teams;
+mod templates;
 mod usage;
 mod users;
 mod vector_stores;
@@ -82,7 +82,6 @@ pub use org_rbac_policies::{OrgRbacPolicyError, OrgRbacPolicyService};
 pub use org_sso_configs::{OrgSsoConfigError, OrgSsoConfigService, OrgSsoConfigWithClientSecret};
 pub use organizations::OrganizationService;
 pub use projects::ProjectService;
-pub use prompts::PromptService;
 pub use provider_metrics::{
     ProviderMetricsError, ProviderMetricsService, ProviderStats, ProviderStatsHistorical,
     StatsGranularity, TimeBucketStats,
@@ -103,6 +102,7 @@ pub use service_accounts::ServiceAccountService;
 #[cfg(feature = "sso")]
 pub use sso_group_mappings::SsoGroupMappingService;
 pub use teams::TeamService;
+pub use templates::TemplateService;
 pub use usage::UsageService;
 pub use users::UserService;
 pub use vector_stores::VectorStoresService;
@@ -128,7 +128,7 @@ pub struct Services {
     pub usage: UsageService,
     pub model_pricing: ModelPricingService,
     pub conversations: ConversationService,
-    pub prompts: PromptService,
+    pub templates: TemplateService,
     pub audit_logs: AuditLogService,
     pub access_reviews: AccessReviewService,
     pub vector_stores: VectorStoresService,
@@ -163,7 +163,7 @@ impl Services {
             usage: UsageService::new(db.clone()),
             model_pricing: ModelPricingService::new(db.clone()),
             conversations: ConversationService::new(db.clone()),
-            prompts: PromptService::new(db.clone()),
+            templates: TemplateService::new(db.clone()),
             audit_logs: AuditLogService::new(db.clone()),
             access_reviews: AccessReviewService::new(db.clone()),
             vector_stores: VectorStoresService::new(db.clone()),
@@ -200,7 +200,7 @@ impl Services {
             usage: UsageService::new(db.clone()),
             model_pricing: ModelPricingService::new(db.clone()),
             conversations: ConversationService::new(db.clone()),
-            prompts: PromptService::new(db.clone()),
+            templates: TemplateService::new(db.clone()),
             audit_logs: AuditLogService::with_event_bus(db.clone(), event_bus),
             access_reviews: AccessReviewService::new(db.clone()),
             vector_stores: VectorStoresService::new(db.clone()),
diff --git a/src/services/prompts.rs b/src/services/prompts.rs
deleted file mode 100644
index 5a17bb67..00000000
--- a/src/services/prompts.rs
+++ /dev/null
@@ -1,73 +0,0 @@
-use std::sync::Arc;
-
-use uuid::Uuid;
-
-use crate::{
-    db::{DbPool, DbResult, ListParams, repos::ListResult},
-    models::{CreatePrompt, Prompt, PromptOwnerType, UpdatePrompt},
-};
-
-/// Service layer for prompt template operations
-#[derive(Clone)]
-pub struct PromptService {
-    db: Arc<DbPool>,
-}
-
-impl PromptService {
-    pub fn new(db: Arc<DbPool>) -> Self {
-        Self { db }
-    }
-
-    /// Create a new prompt template
-    pub async fn create(&self, input: CreatePrompt) -> DbResult<Prompt> {
-        self.db.prompts().create(input).await
-    }
-
-    /// Get a prompt by ID
-    pub async fn get_by_id(&self, id: Uuid) -> DbResult<Option<Prompt>> {
-        self.db.prompts().get_by_id(id).await
-    }
-
-    /// Get a prompt by ID, scoped to a specific organization.
-    ///
-    /// Use this variant when org context is available to prevent cross-org access.
-    pub async fn get_by_id_and_org(&self, id: Uuid, org_id: Uuid) -> DbResult<Option<Prompt>> {
-        self.db.prompts().get_by_id_and_org(id, org_id).await
-    }
-
-    /// List prompts by owner with pagination
-    pub async fn list_by_owner(
-        &self,
-        owner_type: PromptOwnerType,
-        owner_id: Uuid,
-        params: ListParams,
-    ) -> DbResult<ListResult<Prompt>> {
-        self.db
-            .prompts()
-            .list_by_owner(owner_type, owner_id, params)
-            .await
-    }
-
-    /// Count prompts by owner
-    pub async fn count_by_owner(
-        &self,
-        owner_type: PromptOwnerType,
-        owner_id: Uuid,
-        include_deleted: bool,
-    ) -> DbResult<i64> {
-        self.db
-            .prompts()
-            .count_by_owner(owner_type, owner_id, include_deleted)
-            .await
-    }
-
-    /// Update a prompt by ID
-    pub async fn update(&self, id: Uuid, input: UpdatePrompt) -> DbResult<Prompt> {
-        self.db.prompts().update(id, input).await
-    }
-
-    /// Delete (soft-delete) a prompt by ID
-    pub async fn delete(&self, id: Uuid) -> DbResult<()> {
-        self.db.prompts().delete(id).await
-    }
-}
diff --git a/src/services/templates.rs b/src/services/templates.rs
new file mode 100644
index 00000000..a29713a2
--- /dev/null
+++ b/src/services/templates.rs
@@ -0,0 +1,82 @@
+use std::sync::Arc;
+
+use uuid::Uuid;
+
+use crate::{
+    db::{DbPool, DbResult, ListParams, repos::ListResult},
+    models::{CreateTemplate, Template, TemplateOwnerType, UpdateTemplate},
+};
+
+/// Service layer for template operations
+#[derive(Clone)]
+pub struct TemplateService {
+    db: Arc<DbPool>,
+}
+
+impl TemplateService {
+    pub fn new(db: Arc<DbPool>) -> Self {
+        Self { db }
+    }
+
+    /// Create a new template
+    pub async fn create(&self, input: CreateTemplate) -> DbResult<Template> {
+        self.db.templates().create(input).await
+    }
+
+    /// Get a template by ID
+    pub async fn get_by_id(&self, id: Uuid) -> DbResult<Option<Template>> {
+        self.db.templates().get_by_id(id).await
+    }
+
+    /// Get a template by ID, scoped to a specific organization.
+    ///
+    /// Use this variant when org context is available to prevent cross-org access.
+    pub async fn get_by_id_and_org(&self, id: Uuid, org_id: Uuid) -> DbResult<Option<Template>> {
+        self.db.templates().get_by_id_and_org(id, org_id).await
+    }
+
+    /// List all templates accessible within an organization
+    pub async fn list_by_org(
+        &self,
+        org_id: Uuid,
+        params: ListParams,
+    ) -> DbResult<ListResult<Template>> {
+        self.db.templates().list_by_org(org_id, params).await
+    }
+
+    /// List templates by owner with pagination
+    pub async fn list_by_owner(
+        &self,
+        owner_type: TemplateOwnerType,
+        owner_id: Uuid,
+        params: ListParams,
+    ) -> DbResult<ListResult<Template>> {
+        self.db
+            .templates()
+            .list_by_owner(owner_type, owner_id, params)
+            .await
+    }
+
+    /// Count templates by owner
+    pub async fn count_by_owner(
+        &self,
+        owner_type: TemplateOwnerType,
+        owner_id: Uuid,
+        include_deleted: bool,
+    ) -> DbResult<i64> {
+        self.db
+            .templates()
+            .count_by_owner(owner_type, owner_id, include_deleted)
+            .await
+    }
+
+    /// Update a template by ID
+    pub async fn update(&self, id: Uuid, input: UpdateTemplate) -> DbResult<Template> {
+        self.db.templates().update(id, input).await
+    }
+
+    /// Delete (soft-delete) a template by ID
+    pub async fn delete(&self, id: Uuid) -> DbResult<()> {
+        self.db.templates().delete(id).await
+    }
+}
diff --git a/ui/src/api/openapi.json b/ui/src/api/openapi.json
index 516aac23..666a0377 100644
--- a/ui/src/api/openapi.json
+++ b/ui/src/api/openapi.json
@@ -5926,13 +5926,13 @@
         }
       }
     },
-    "/admin/v1/organizations/{org_slug}/projects/{project_slug}/prompts": {
+    "/admin/v1/organizations/{org_slug}/projects/{project_slug}/templates": {
       "get": {
         "tags": [
-          "prompts"
+          "templates"
         ],
-        "summary": "List prompts by project",
-        "operationId": "prompt_list_by_project",
+        "summary": "List templates by project",
+        "operationId": "template_list_by_project",
         "parameters": [
           {
             "name": "org_slug",
@@ -6004,11 +6004,11 @@
         ],
         "responses": {
           "200": {
-            "description": "List of prompts",
+            "description": "List of templates",
             "content": {
               "application/json": {
                 "schema": {
-                  "$ref": "#/components/schemas/PromptListResponse"
+                  "$ref": "#/components/schemas/TemplateListResponse"
                 }
               }
             }
@@ -6851,107 +6851,6 @@
         }
       }
     },
-    "/admin/v1/organizations/{org_slug}/prompts": {
-      "get": {
-        "tags": [
-          "prompts"
-        ],
-        "summary": "List prompts by organization",
-        "operationId": "prompt_list_by_org",
-        "parameters": [
-          {
-            "name": "org_slug",
-            "in": "path",
-            "description": "Organization slug",
-            "required": true,
-            "schema": {
-              "type": "string"
-            }
-          },
-          {
-            "name": "limit",
-            "in": "query",
-            "description": "Maximum number of results to return",
-            "required": false,
-            "schema": {
-              "type": [
-                "integer",
-                "null"
-              ],
-              "format": "int64"
-            }
-          },
-          {
-            "name": "cursor",
-            "in": "query",
-            "description": "Cursor for keyset pagination. Encoded as base64 string.",
-            "required": false,
-            "schema": {
-              "type": [
-                "string",
-                "null"
-              ]
-            }
-          },
-          {
-            "name": "direction",
-            "in": "query",
-            "description": "Pagination direction: \"forward\" (default) or \"backward\".",
-            "required": false,
-            "schema": {
-              "type": [
-                "string",
-                "null"
-              ]
-            }
-          },
-          {
-            "name": "include_deleted",
-            "in": "query",
-            "description": "Include soft-deleted records in results",
-            "required": false,
-            "schema": {
-              "type": [
-                "boolean",
-                "null"
-              ]
-            }
-          }
-        ],
-        "responses": {
-          "200": {
-            "description": "List of prompts",
-            "content": {
-              "application/json": {
-                "schema": {
-                  "$ref": "#/components/schemas/PromptListResponse"
-                }
-              }
-            }
-          },
-          "400": {
-            "description": "Invalid cursor or direction",
-            "content": {
-              "application/json": {
-                "schema": {
-                  "$ref": "#/components/schemas/ErrorResponse"
-                }
-              }
-            }
-          },
-          "404": {
-            "description": "Organization not found",
-            "content": {
-              "application/json": {
-                "schema": {
-                  "$ref": "#/components/schemas/ErrorResponse"
-                }
-              }
-            }
-          }
-        }
-      }
-    },
     "/admin/v1/organizations/{org_slug}/rbac-policies": {
       "get": {
         "tags": [
@@ -10364,13 +10263,13 @@
         }
       }
     },
-    "/admin/v1/organizations/{org_slug}/teams/{team_slug}/prompts": {
+    "/admin/v1/organizations/{org_slug}/teams/{team_slug}/templates": {
       "get": {
         "tags": [
-          "prompts"
+          "templates"
         ],
-        "summary": "List prompts by team",
-        "operationId": "prompt_list_by_team",
+        "summary": "List templates by team",
+        "operationId": "template_list_by_team",
         "parameters": [
           {
             "name": "org_slug",
@@ -10442,11 +10341,11 @@
         ],
         "responses": {
           "200": {
-            "description": "List of prompts",
+            "description": "List of templates",
             "content": {
               "application/json": {
                 "schema": {
-                  "$ref": "#/components/schemas/PromptListResponse"
+                  "$ref": "#/components/schemas/TemplateListResponse"
                 }
               }
             }
@@ -11444,6 +11343,107 @@
         }
       }
     },
+    "/admin/v1/organizations/{org_slug}/templates": {
+      "get": {
+        "tags": [
+          "templates"
+        ],
+        "summary": "List templates by organization",
+        "operationId": "template_list_by_org",
+        "parameters": [
+          {
+            "name": "org_slug",
+            "in": "path",
+            "description": "Organization slug",
+            "required": true,
+            "schema": {
+              "type": "string"
+            }
+          },
+          {
+            "name": "limit",
+            "in": "query",
+            "description": "Maximum number of results to return",
+            "required": false,
+            "schema": {
+              "type": [
+                "integer",
+                "null"
+              ],
+              "format": "int64"
+            }
+          },
+          {
+            "name": "cursor",
+            "in": "query",
+            "description": "Cursor for keyset pagination. Encoded as base64 string.",
+            "required": false,
+            "schema": {
+              "type": [
+                "string",
+                "null"
+              ]
+            }
+          },
+          {
+            "name": "direction",
+            "in": "query",
+            "description": "Pagination direction: \"forward\" (default) or \"backward\".",
+            "required": false,
+            "schema": {
+              "type": [
+                "string",
+                "null"
+              ]
+            }
+          },
+          {
+            "name": "include_deleted",
+            "in": "query",
+            "description": "Include soft-deleted records in results",
+            "required": false,
+            "schema": {
+              "type": [
+                "boolean",
+                "null"
+              ]
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "List of templates",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/TemplateListResponse"
+                }
+              }
+            }
+          },
+          "400": {
+            "description": "Invalid cursor or direction",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/ErrorResponse"
+                }
+              }
+            }
+          },
+          "404": {
+            "description": "Organization not found",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/ErrorResponse"
+                }
+              }
+            }
+          }
+        }
+      }
+    },
     "/admin/v1/organizations/{slug}": {
       "get": {
         "tags": [
@@ -12591,268 +12591,79 @@
         }
       }
     },
-    "/admin/v1/prompts": {
-      "post": {
+    "/admin/v1/providers/circuit-breakers": {
+      "get": {
         "tags": [
-          "prompts"
+          "providers"
         ],
-        "summary": "Create a prompt template",
-        "operationId": "prompt_create",
-        "requestBody": {
-          "content": {
-            "application/json": {
-              "schema": {
-                "$ref": "#/components/schemas/CreatePrompt"
-              }
-            }
-          },
-          "required": true
-        },
+        "summary": "Get circuit breaker status for all providers.",
+        "description": "Returns the current state and failure count for all configured\ncircuit breakers. Useful for monitoring provider health.",
+        "operationId": "list_circuit_breakers",
         "responses": {
-          "201": {
-            "description": "Prompt created",
+          "200": {
+            "description": "Circuit breaker status for all providers",
             "content": {
               "application/json": {
                 "schema": {
-                  "$ref": "#/components/schemas/Prompt"
+                  "$ref": "#/components/schemas/CircuitBreakersResponse"
                 }
               }
             }
-          },
-          "404": {
-            "description": "Owner not found",
+          }
+        }
+      }
+    },
+    "/admin/v1/providers/health": {
+      "get": {
+        "tags": [
+          "providers"
+        ],
+        "summary": "Get health status for all providers.",
+        "description": "Returns the current health status for all providers that have health checks\nenabled. Includes status, latency, last check time, and consecutive\nsuccess/failure counts.\n\nNote: Only providers with `health_check.enabled = true` in their config\nwill appear in this list. Providers without health checks rely solely\non circuit breaker status.",
+        "operationId": "list_provider_health",
+        "responses": {
+          "200": {
+            "description": "Health status for all providers",
             "content": {
               "application/json": {
                 "schema": {
-                  "$ref": "#/components/schemas/ErrorResponse"
+                  "$ref": "#/components/schemas/ProviderHealthResponse"
                 }
               }
             }
-          },
-          "409": {
-            "description": "Prompt with this name already exists",
+          }
+        }
+      }
+    },
+    "/admin/v1/providers/stats": {
+      "get": {
+        "tags": [
+          "providers"
+        ],
+        "summary": "Get aggregated statistics for all providers.",
+        "description": "Returns current metrics including latency percentiles, error counts,\ntoken usage, and costs for all providers.\n\nIn single-node deployments, stats are derived from the local /metrics endpoint.\nIn multi-node deployments with Prometheus configured, stats are aggregated\nfrom Prometheus queries.",
+        "operationId": "list_provider_stats",
+        "responses": {
+          "200": {
+            "description": "Stats for all providers",
             "content": {
               "application/json": {
                 "schema": {
-                  "$ref": "#/components/schemas/ErrorResponse"
+                  "$ref": "#/components/schemas/ProviderStatsResponse"
                 }
               }
             }
+          },
+          "500": {
+            "description": "Failed to fetch stats"
           }
         }
       }
     },
-    "/admin/v1/prompts/{id}": {
+    "/admin/v1/providers/{provider_name}/circuit-breaker": {
       "get": {
         "tags": [
-          "prompts"
-        ],
-        "summary": "Get a prompt by ID",
-        "operationId": "prompt_get",
-        "parameters": [
-          {
-            "name": "id",
-            "in": "path",
-            "description": "Prompt ID",
-            "required": true,
-            "schema": {
-              "type": "string",
-              "format": "uuid"
-            }
-          }
-        ],
-        "responses": {
-          "200": {
-            "description": "Prompt found",
-            "content": {
-              "application/json": {
-                "schema": {
-                  "$ref": "#/components/schemas/Prompt"
-                }
-              }
-            }
-          },
-          "404": {
-            "description": "Prompt not found",
-            "content": {
-              "application/json": {
-                "schema": {
-                  "$ref": "#/components/schemas/ErrorResponse"
-                }
-              }
-            }
-          }
-        }
-      },
-      "delete": {
-        "tags": [
-          "prompts"
-        ],
-        "summary": "Delete a prompt",
-        "operationId": "prompt_delete",
-        "parameters": [
-          {
-            "name": "id",
-            "in": "path",
-            "description": "Prompt ID",
-            "required": true,
-            "schema": {
-              "type": "string",
-              "format": "uuid"
-            }
-          }
-        ],
-        "responses": {
-          "200": {
-            "description": "Prompt deleted"
-          },
-          "404": {
-            "description": "Prompt not found",
-            "content": {
-              "application/json": {
-                "schema": {
-                  "$ref": "#/components/schemas/ErrorResponse"
-                }
-              }
-            }
-          }
-        }
-      },
-      "patch": {
-        "tags": [
-          "prompts"
-        ],
-        "summary": "Update a prompt",
-        "operationId": "prompt_update",
-        "parameters": [
-          {
-            "name": "id",
-            "in": "path",
-            "description": "Prompt ID",
-            "required": true,
-            "schema": {
-              "type": "string",
-              "format": "uuid"
-            }
-          }
-        ],
-        "requestBody": {
-          "content": {
-            "application/json": {
-              "schema": {
-                "$ref": "#/components/schemas/UpdatePrompt"
-              }
-            }
-          },
-          "required": true
-        },
-        "responses": {
-          "200": {
-            "description": "Prompt updated",
-            "content": {
-              "application/json": {
-                "schema": {
-                  "$ref": "#/components/schemas/Prompt"
-                }
-              }
-            }
-          },
-          "404": {
-            "description": "Prompt not found",
-            "content": {
-              "application/json": {
-                "schema": {
-                  "$ref": "#/components/schemas/ErrorResponse"
-                }
-              }
-            }
-          },
-          "409": {
-            "description": "Prompt with this name already exists",
-            "content": {
-              "application/json": {
-                "schema": {
-                  "$ref": "#/components/schemas/ErrorResponse"
-                }
-              }
-            }
-          }
-        }
-      }
-    },
-    "/admin/v1/providers/circuit-breakers": {
-      "get": {
-        "tags": [
-          "providers"
-        ],
-        "summary": "Get circuit breaker status for all providers.",
-        "description": "Returns the current state and failure count for all configured\ncircuit breakers. Useful for monitoring provider health.",
-        "operationId": "list_circuit_breakers",
-        "responses": {
-          "200": {
-            "description": "Circuit breaker status for all providers",
-            "content": {
-              "application/json": {
-                "schema": {
-                  "$ref": "#/components/schemas/CircuitBreakersResponse"
-                }
-              }
-            }
-          }
-        }
-      }
-    },
-    "/admin/v1/providers/health": {
-      "get": {
-        "tags": [
-          "providers"
-        ],
-        "summary": "Get health status for all providers.",
-        "description": "Returns the current health status for all providers that have health checks\nenabled. Includes status, latency, last check time, and consecutive\nsuccess/failure counts.\n\nNote: Only providers with `health_check.enabled = true` in their config\nwill appear in this list. Providers without health checks rely solely\non circuit breaker status.",
-        "operationId": "list_provider_health",
-        "responses": {
-          "200": {
-            "description": "Health status for all providers",
-            "content": {
-              "application/json": {
-                "schema": {
-                  "$ref": "#/components/schemas/ProviderHealthResponse"
-                }
-              }
-            }
-          }
-        }
-      }
-    },
-    "/admin/v1/providers/stats": {
-      "get": {
-        "tags": [
-          "providers"
-        ],
-        "summary": "Get aggregated statistics for all providers.",
-        "description": "Returns current metrics including latency percentiles, error counts,\ntoken usage, and costs for all providers.\n\nIn single-node deployments, stats are derived from the local /metrics endpoint.\nIn multi-node deployments with Prometheus configured, stats are aggregated\nfrom Prometheus queries.",
-        "operationId": "list_provider_stats",
-        "responses": {
-          "200": {
-            "description": "Stats for all providers",
-            "content": {
-              "application/json": {
-                "schema": {
-                  "$ref": "#/components/schemas/ProviderStatsResponse"
-                }
-              }
-            }
-          },
-          "500": {
-            "description": "Failed to fetch stats"
-          }
-        }
-      }
-    },
-    "/admin/v1/providers/{provider_name}/circuit-breaker": {
-      "get": {
-        "tags": [
-          "providers"
+          "providers"
         ],
         "summary": "Get circuit breaker status for a specific provider.",
         "description": "Returns the current state and failure count for the specified provider's\ncircuit breaker.",
@@ -13352,13 +13163,193 @@
             "content": {
               "application/json": {
                 "schema": {
-                  "$ref": "#/components/schemas/SsoConnectionsResponse"
+                  "$ref": "#/components/schemas/SsoConnectionsResponse"
+                }
+              }
+            }
+          },
+          "403": {
+            "description": "Access denied",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/ErrorResponse"
+                }
+              }
+            }
+          }
+        }
+      }
+    },
+    "/admin/v1/sso-connections/{name}": {
+      "get": {
+        "tags": [
+          "sso"
+        ],
+        "summary": "Get a specific SSO connection by name",
+        "operationId": "sso_connection_get",
+        "parameters": [
+          {
+            "name": "name",
+            "in": "path",
+            "description": "SSO connection name",
+            "required": true,
+            "schema": {
+              "type": "string"
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "SSO connection found",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/SsoConnection"
+                }
+              }
+            }
+          },
+          "403": {
+            "description": "Access denied",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/ErrorResponse"
+                }
+              }
+            }
+          },
+          "404": {
+            "description": "SSO connection not found",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/ErrorResponse"
+                }
+              }
+            }
+          }
+        }
+      }
+    },
+    "/admin/v1/templates": {
+      "post": {
+        "tags": [
+          "templates"
+        ],
+        "summary": "Create a template",
+        "operationId": "template_create",
+        "requestBody": {
+          "content": {
+            "application/json": {
+              "schema": {
+                "$ref": "#/components/schemas/CreateTemplate"
+              }
+            }
+          },
+          "required": true
+        },
+        "responses": {
+          "201": {
+            "description": "Template created",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/Template"
+                }
+              }
+            }
+          },
+          "404": {
+            "description": "Owner not found",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/ErrorResponse"
+                }
+              }
+            }
+          },
+          "409": {
+            "description": "Template with this name already exists",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/ErrorResponse"
+                }
+              }
+            }
+          }
+        }
+      }
+    },
+    "/admin/v1/templates/{id}": {
+      "get": {
+        "tags": [
+          "templates"
+        ],
+        "summary": "Get a template by ID",
+        "operationId": "template_get",
+        "parameters": [
+          {
+            "name": "id",
+            "in": "path",
+            "description": "Template ID",
+            "required": true,
+            "schema": {
+              "type": "string",
+              "format": "uuid"
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "Template found",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/Template"
+                }
+              }
+            }
+          },
+          "404": {
+            "description": "Template not found",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/ErrorResponse"
                 }
               }
             }
+          }
+        }
+      },
+      "delete": {
+        "tags": [
+          "templates"
+        ],
+        "summary": "Delete a template",
+        "operationId": "template_delete",
+        "parameters": [
+          {
+            "name": "id",
+            "in": "path",
+            "description": "Template ID",
+            "required": true,
+            "schema": {
+              "type": "string",
+              "format": "uuid"
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "Template deleted"
           },
-          "403": {
-            "description": "Access denied",
+          "404": {
+            "description": "Template not found",
             "content": {
               "application/json": {
                 "schema": {
@@ -13368,39 +13359,48 @@
             }
           }
         }
-      }
-    },
-    "/admin/v1/sso-connections/{name}": {
-      "get": {
+      },
+      "patch": {
         "tags": [
-          "sso"
+          "templates"
         ],
-        "summary": "Get a specific SSO connection by name",
-        "operationId": "sso_connection_get",
+        "summary": "Update a template",
+        "operationId": "template_update",
         "parameters": [
           {
-            "name": "name",
+            "name": "id",
             "in": "path",
-            "description": "SSO connection name",
+            "description": "Template ID",
             "required": true,
             "schema": {
-              "type": "string"
+              "type": "string",
+              "format": "uuid"
             }
           }
         ],
+        "requestBody": {
+          "content": {
+            "application/json": {
+              "schema": {
+                "$ref": "#/components/schemas/UpdateTemplate"
+              }
+            }
+          },
+          "required": true
+        },
         "responses": {
           "200": {
-            "description": "SSO connection found",
+            "description": "Template updated",
             "content": {
               "application/json": {
                 "schema": {
-                  "$ref": "#/components/schemas/SsoConnection"
+                  "$ref": "#/components/schemas/Template"
                 }
               }
             }
           },
-          "403": {
-            "description": "Access denied",
+          "404": {
+            "description": "Template not found",
             "content": {
               "application/json": {
                 "schema": {
@@ -13409,8 +13409,8 @@
               }
             }
           },
-          "404": {
-            "description": "SSO connection not found",
+          "409": {
+            "description": "Template with this name already exists",
             "content": {
               "application/json": {
                 "schema": {
@@ -15427,98 +15427,6 @@
         }
       }
     },
-    "/admin/v1/users/{user_id}/prompts": {
-      "get": {
-        "tags": [
-          "prompts"
-        ],
-        "summary": "List prompts by user",
-        "operationId": "prompt_list_by_user",
-        "parameters": [
-          {
-            "name": "user_id",
-            "in": "path",
-            "description": "User ID",
-            "required": true,
-            "schema": {
-              "type": "string",
-              "format": "uuid"
-            }
-          },
-          {
-            "name": "limit",
-            "in": "query",
-            "description": "Maximum number of results to return",
-            "required": false,
-            "schema": {
-              "type": [
-                "integer",
-                "null"
-              ],
-              "format": "int64"
-            }
-          },
-          {
-            "name": "cursor",
-            "in": "query",
-            "description": "Cursor for keyset pagination. Encoded as base64 string.",
-            "required": false,
-            "schema": {
-              "type": [
-                "string",
-                "null"
-              ]
-            }
-          },
-          {
-            "name": "direction",
-            "in": "query",
-            "description": "Pagination direction: \"forward\" (default) or \"backward\".",
-            "required": false,
-            "schema": {
-              "type": [
-                "string",
-                "null"
-              ]
-            }
-          },
-          {
-            "name": "include_deleted",
-            "in": "query",
-            "description": "Include soft-deleted records in results",
-            "required": false,
-            "schema": {
-              "type": [
-                "boolean",
-                "null"
-              ]
-            }
-          }
-        ],
-        "responses": {
-          "200": {
-            "description": "List of prompts",
-            "content": {
-              "application/json": {
-                "schema": {
-                  "$ref": "#/components/schemas/PromptListResponse"
-                }
-              }
-            }
-          },
-          "400": {
-            "description": "Invalid cursor or direction",
-            "content": {
-              "application/json": {
-                "schema": {
-                  "$ref": "#/components/schemas/ErrorResponse"
-                }
-              }
-            }
-          }
-        }
-      }
-    },
     "/admin/v1/users/{user_id}/sessions": {
       "get": {
         "tags": [
@@ -15719,6 +15627,98 @@
         }
       }
     },
+    "/admin/v1/users/{user_id}/templates": {
+      "get": {
+        "tags": [
+          "templates"
+        ],
+        "summary": "List templates by user",
+        "operationId": "template_list_by_user",
+        "parameters": [
+          {
+            "name": "user_id",
+            "in": "path",
+            "description": "User ID",
+            "required": true,
+            "schema": {
+              "type": "string",
+              "format": "uuid"
+            }
+          },
+          {
+            "name": "limit",
+            "in": "query",
+            "description": "Maximum number of results to return",
+            "required": false,
+            "schema": {
+              "type": [
+                "integer",
+                "null"
+              ],
+              "format": "int64"
+            }
+          },
+          {
+            "name": "cursor",
+            "in": "query",
+            "description": "Cursor for keyset pagination. Encoded as base64 string.",
+            "required": false,
+            "schema": {
+              "type": [
+                "string",
+                "null"
+              ]
+            }
+          },
+          {
+            "name": "direction",
+            "in": "query",
+            "description": "Pagination direction: \"forward\" (default) or \"backward\".",
+            "required": false,
+            "schema": {
+              "type": [
+                "string",
+                "null"
+              ]
+            }
+          },
+          {
+            "name": "include_deleted",
+            "in": "query",
+            "description": "Include soft-deleted records in results",
+            "required": false,
+            "schema": {
+              "type": [
+                "boolean",
+                "null"
+              ]
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "List of templates",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/TemplateListResponse"
+                }
+              }
+            }
+          },
+          "400": {
+            "description": "Invalid cursor or direction",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/ErrorResponse"
+                }
+              }
+            }
+          }
+        }
+      }
+    },
     "/admin/v1/users/{user_id}/usage": {
       "get": {
         "tags": [
@@ -20720,6 +20720,17 @@
             "type": "object",
             "description": "**Hadrian Extension:** Provider routing configuration"
           },
+          "sovereignty_requirements": {
+            "oneOf": [
+              {
+                "type": "null"
+              },
+              {
+                "$ref": "#/components/schemas/SovereigntyRequirements",
+                "description": "**Hadrian Extension:** Per-request sovereignty requirements.\nMerged with API key requirements (most restrictive wins)."
+              }
+            ]
+          },
           "user": {
             "type": [
               "string",
@@ -20968,19 +20979,30 @@
                 "type": "null"
               },
               {
-                "$ref": "#/components/schemas/ImageResponseFormat",
-                "description": "Format for returning generated images (url or b64_json)."
+                "$ref": "#/components/schemas/ImageResponseFormat",
+                "description": "Format for returning generated images (url or b64_json)."
+              }
+            ]
+          },
+          "size": {
+            "oneOf": [
+              {
+                "type": "null"
+              },
+              {
+                "$ref": "#/components/schemas/ImageSize",
+                "description": "Size of the generated images."
               }
             ]
           },
-          "size": {
+          "sovereignty_requirements": {
             "oneOf": [
               {
                 "type": "null"
               },
               {
-                "$ref": "#/components/schemas/ImageSize",
-                "description": "Size of the generated images."
+                "$ref": "#/components/schemas/SovereigntyRequirements",
+                "description": "**Hadrian Extension:** Per-request sovereignty requirements.\nMerged with API key requirements (most restrictive wins)."
               }
             ]
           },
@@ -21510,47 +21532,6 @@
           }
         }
       },
-      "CreatePrompt": {
-        "type": "object",
-        "description": "Request to create a new prompt",
-        "required": [
-          "owner",
-          "name",
-          "content"
-        ],
-        "properties": {
-          "content": {
-            "type": "string",
-            "description": "The actual prompt content"
-          },
-          "description": {
-            "type": [
-              "string",
-              "null"
-            ],
-            "description": "Optional description"
-          },
-          "metadata": {
-            "type": [
-              "object",
-              "null"
-            ],
-            "description": "Optional metadata",
-            "additionalProperties": {},
-            "propertyNames": {
-              "type": "string"
-            }
-          },
-          "name": {
-            "type": "string",
-            "description": "Name of the prompt template (unique per owner)"
-          },
-          "owner": {
-            "$ref": "#/components/schemas/PromptOwner",
-            "description": "Owner of the prompt"
-          }
-        }
-      },
       "CreateResponsesPayload": {
         "type": "object",
         "description": "Create responses request (OpenAI Responses API)",
@@ -21947,6 +21928,17 @@
               }
             ]
           },
+          "sovereignty_requirements": {
+            "oneOf": [
+              {
+                "type": "null"
+              },
+              {
+                "$ref": "#/components/schemas/SovereigntyRequirements",
+                "description": "**Hadrian Extension:** Per-request sovereignty requirements.\nMerged with API key requirements (most restrictive wins)."
+              }
+            ]
+          },
           "speed": {
             "type": [
               "number",
@@ -22026,6 +22018,47 @@
           }
         }
       },
+      "CreateTemplate": {
+        "type": "object",
+        "description": "Request to create a new template",
+        "required": [
+          "owner",
+          "name",
+          "content"
+        ],
+        "properties": {
+          "content": {
+            "type": "string",
+            "description": "The actual template content"
+          },
+          "description": {
+            "type": [
+              "string",
+              "null"
+            ],
+            "description": "Optional description"
+          },
+          "metadata": {
+            "type": [
+              "object",
+              "null"
+            ],
+            "description": "Optional metadata",
+            "additionalProperties": {},
+            "propertyNames": {
+              "type": "string"
+            }
+          },
+          "name": {
+            "type": "string",
+            "description": "Name of the template (unique per owner)"
+          },
+          "owner": {
+            "$ref": "#/components/schemas/TemplateOwner",
+            "description": "Owner of the template"
+          }
+        }
+      },
       "CreateTranscriptionRequest": {
         "type": "object",
         "description": "Create transcription request (POST /v1/audio/transcriptions)\n\nNote: This endpoint accepts multipart/form-data. The `file` field\nis a file upload, not a JSON field.",
@@ -26820,195 +26853,23 @@
           },
           "project_name": {
             "type": [
-              "string",
-              "null"
-            ]
-          },
-          "request_count": {
-            "type": "integer",
-            "format": "int64"
-          },
-          "total_cost": {
-            "type": "number",
-            "format": "double"
-          },
-          "total_tokens": {
-            "type": "integer",
-            "format": "int64"
-          }
-        }
-      },
-      "Prompt": {
-        "type": "object",
-        "description": "A reusable system prompt template",
-        "required": [
-          "id",
-          "owner_type",
-          "owner_id",
-          "name",
-          "content",
-          "created_at",
-          "updated_at"
-        ],
-        "properties": {
-          "content": {
-            "type": "string",
-            "description": "The actual prompt content (system message template)"
-          },
-          "created_at": {
-            "type": "string",
-            "format": "date-time"
-          },
-          "description": {
-            "type": [
-              "string",
-              "null"
-            ],
-            "description": "Optional description of what this prompt does"
-          },
-          "id": {
-            "type": "string",
-            "format": "uuid"
-          },
-          "metadata": {
-            "type": [
-              "object",
-              "null"
-            ],
-            "description": "Optional metadata (e.g., recommended temperature, max_tokens, tags)",
-            "additionalProperties": {},
-            "propertyNames": {
-              "type": "string"
-            }
-          },
-          "name": {
-            "type": "string",
-            "description": "Name of the prompt template"
-          },
-          "owner_id": {
-            "type": "string",
-            "format": "uuid"
-          },
-          "owner_type": {
-            "$ref": "#/components/schemas/PromptOwnerType"
-          },
-          "updated_at": {
-            "type": "string",
-            "format": "date-time"
-          }
-        }
-      },
-      "PromptListResponse": {
-        "type": "object",
-        "description": "Paginated list of prompts",
-        "required": [
-          "data",
-          "pagination"
-        ],
-        "properties": {
-          "data": {
-            "type": "array",
-            "items": {
-              "$ref": "#/components/schemas/Prompt"
-            },
-            "description": "List of prompts"
-          },
-          "pagination": {
-            "$ref": "#/components/schemas/PaginationMeta",
-            "description": "Pagination metadata"
-          }
-        }
-      },
-      "PromptOwner": {
-        "oneOf": [
-          {
-            "type": "object",
-            "required": [
-              "organization_id",
-              "type"
-            ],
-            "properties": {
-              "organization_id": {
-                "type": "string",
-                "format": "uuid"
-              },
-              "type": {
-                "type": "string",
-                "enum": [
-                  "organization"
-                ]
-              }
-            }
-          },
-          {
-            "type": "object",
-            "required": [
-              "team_id",
-              "type"
-            ],
-            "properties": {
-              "team_id": {
-                "type": "string",
-                "format": "uuid"
-              },
-              "type": {
-                "type": "string",
-                "enum": [
-                  "team"
-                ]
-              }
-            }
-          },
-          {
-            "type": "object",
-            "required": [
-              "project_id",
-              "type"
-            ],
-            "properties": {
-              "project_id": {
-                "type": "string",
-                "format": "uuid"
-              },
-              "type": {
-                "type": "string",
-                "enum": [
-                  "project"
-                ]
-              }
-            }
-          },
-          {
-            "type": "object",
-            "required": [
-              "user_id",
-              "type"
-            ],
-            "properties": {
-              "type": {
-                "type": "string",
-                "enum": [
-                  "user"
-                ]
-              },
-              "user_id": {
-                "type": "string",
-                "format": "uuid"
-              }
-            }
+              "string",
+              "null"
+            ]
+          },
+          "request_count": {
+            "type": "integer",
+            "format": "int64"
+          },
+          "total_cost": {
+            "type": "number",
+            "format": "double"
+          },
+          "total_tokens": {
+            "type": "integer",
+            "format": "int64"
           }
-        ],
-        "description": "Owner specification for creating a prompt"
-      },
-      "PromptOwnerType": {
-        "type": "string",
-        "description": "Owner type for prompts (organization, team, project, or user)",
-        "enum": [
-          "organization",
-          "team",
-          "project",
-          "user"
-        ]
+        }
       },
       "ProviderCircuitBreakerResponse": {
         "type": "object",
@@ -29329,6 +29190,178 @@
           }
         }
       },
+      "Template": {
+        "type": "object",
+        "description": "A reusable system template",
+        "required": [
+          "id",
+          "owner_type",
+          "owner_id",
+          "name",
+          "content",
+          "created_at",
+          "updated_at"
+        ],
+        "properties": {
+          "content": {
+            "type": "string",
+            "description": "The actual template content (system message template)"
+          },
+          "created_at": {
+            "type": "string",
+            "format": "date-time"
+          },
+          "description": {
+            "type": [
+              "string",
+              "null"
+            ],
+            "description": "Optional description of what this template does"
+          },
+          "id": {
+            "type": "string",
+            "format": "uuid"
+          },
+          "metadata": {
+            "type": [
+              "object",
+              "null"
+            ],
+            "description": "Optional metadata (e.g., recommended temperature, max_tokens, tags)",
+            "additionalProperties": {},
+            "propertyNames": {
+              "type": "string"
+            }
+          },
+          "name": {
+            "type": "string",
+            "description": "Name of the template"
+          },
+          "owner_id": {
+            "type": "string",
+            "format": "uuid"
+          },
+          "owner_type": {
+            "$ref": "#/components/schemas/TemplateOwnerType"
+          },
+          "updated_at": {
+            "type": "string",
+            "format": "date-time"
+          }
+        }
+      },
+      "TemplateListResponse": {
+        "type": "object",
+        "description": "Paginated list of templates",
+        "required": [
+          "data",
+          "pagination"
+        ],
+        "properties": {
+          "data": {
+            "type": "array",
+            "items": {
+              "$ref": "#/components/schemas/Template"
+            },
+            "description": "List of templates"
+          },
+          "pagination": {
+            "$ref": "#/components/schemas/PaginationMeta",
+            "description": "Pagination metadata"
+          }
+        }
+      },
+      "TemplateOwner": {
+        "oneOf": [
+          {
+            "type": "object",
+            "required": [
+              "organization_id",
+              "type"
+            ],
+            "properties": {
+              "organization_id": {
+                "type": "string",
+                "format": "uuid"
+              },
+              "type": {
+                "type": "string",
+                "enum": [
+                  "organization"
+                ]
+              }
+            }
+          },
+          {
+            "type": "object",
+            "required": [
+              "team_id",
+              "type"
+            ],
+            "properties": {
+              "team_id": {
+                "type": "string",
+                "format": "uuid"
+              },
+              "type": {
+                "type": "string",
+                "enum": [
+                  "team"
+                ]
+              }
+            }
+          },
+          {
+            "type": "object",
+            "required": [
+              "project_id",
+              "type"
+            ],
+            "properties": {
+              "project_id": {
+                "type": "string",
+                "format": "uuid"
+              },
+              "type": {
+                "type": "string",
+                "enum": [
+                  "project"
+                ]
+              }
+            }
+          },
+          {
+            "type": "object",
+            "required": [
+              "user_id",
+              "type"
+            ],
+            "properties": {
+              "type": {
+                "type": "string",
+                "enum": [
+                  "user"
+                ]
+              },
+              "user_id": {
+                "type": "string",
+                "format": "uuid"
+              }
+            }
+          }
+        ],
+        "description": "Owner specification for creating a template"
+      },
+      "TemplateOwnerType": {
+        "type": "string",
+        "description": "Owner type for templates (organization, team, project, or user)",
+        "enum": [
+          "organization",
+          "team",
+          "project",
+          "user"
+        ]
+      },
       "TestMappingRequest": {
         "type": "object",
         "description": "Request to test SSO group mapping resolution",
@@ -30228,7 +30261,7 @@
               },
               {
                 "$ref": "#/components/schemas/SovereigntyMetadata",
-                "description": "Sovereignty and compliance metadata"
+                "description": "Sovereignty and compliance metadata (set to null to clear)"
               }
             ]
           }
@@ -30764,44 +30797,6 @@
           }
         }
       },
-      "UpdatePrompt": {
-        "type": "object",
-        "description": "Request to update a prompt",
-        "properties": {
-          "content": {
-            "type": [
-              "string",
-              "null"
-            ],
-            "description": "New content"
-          },
-          "description": {
-            "type": [
-              "string",
-              "null"
-            ],
-            "description": "New description"
-          },
-          "metadata": {
-            "type": [
-              "object",
-              "null"
-            ],
-            "description": "New metadata (replaces existing)",
-            "additionalProperties": {},
-            "propertyNames": {
-              "type": "string"
-            }
-          },
-          "name": {
-            "type": [
-              "string",
-              "null"
-            ],
-            "description": "New name (unique per owner)"
-          }
-        }
-      },
       "UpdateServiceAccount": {
         "type": "object",
         "description": "Request to update an existing service account",
@@ -30893,6 +30888,44 @@
           }
         }
       },
+      "UpdateTemplate": {
+        "type": "object",
+        "description": "Request to update a template",
+        "properties": {
+          "content": {
+            "type": [
+              "string",
+              "null"
+            ],
+            "description": "New content"
+          },
+          "description": {
+            "type": [
+              "string",
+              "null"
+            ],
+            "description": "New description"
+          },
+          "metadata": {
+            "type": [
+              "object",
+              "null"
+            ],
+            "description": "New metadata (replaces existing)",
+            "additionalProperties": {},
+            "propertyNames": {
+              "type": "string"
+            }
+          },
+          "name": {
+            "type": [
+              "string",
+              "null"
+            ],
+            "description": "New name (unique per owner)"
+          }
+        }
+      },
       "UpdateUser": {
         "type": "object",
         "properties": {
@@ -32715,8 +32748,8 @@
       "description": "Store and manage chat conversation history. Conversations can be owned by users or projects and support multiple models."
     },
     {
-      "name": "prompts",
-      "description": "Manage reusable system prompt templates. Prompts can be owned by organizations, teams, projects, or users and include metadata for configuration."
+      "name": "templates",
+      "description": "Manage reusable prompt templates. Templates can be owned by organizations, teams, projects, or users and include metadata for configuration."
     },
     {
       "name": "audit-logs",
diff --git a/ui/src/components/Admin/PromptFormModal/PromptFormModal.tsx b/ui/src/components/Admin/PromptFormModal/PromptFormModal.tsx
new file mode 100644
index 00000000..9b10417d
--- /dev/null
+++ b/ui/src/components/Admin/PromptFormModal/PromptFormModal.tsx
@@ -0,0 +1,234 @@
+import { useEffect, useState } from "react";
+import { useForm } from "react-hook-form";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { z } from "zod";
+import { useMutation, useQueryClient } from "@tanstack/react-query";
+import { ClipboardPenLine } from "lucide-react";
+
+import type { CreateTemplate, TemplateOwner, Template } from "@/api/generated/types.gen";
+import { templateCreate, templateUpdate } from "@/api/generated/sdk.gen";
+import { Button } from "@/components/Button/Button";
+import { FormField } from "@/components/FormField/FormField";
+import { Input } from "@/components/Input/Input";
+import {
+  Modal,
+  ModalClose,
+  ModalHeader,
+  ModalTitle,
+  ModalContent,
+  ModalFooter,
+} from "@/components/Modal/Modal";
+import { TemplateVariableEditor } from "@/components/TemplateVariableEditor/TemplateVariableEditor";
+import { type TemplateVariable, parseVariables } from "@/lib/templateVariables";
+
+const promptFormSchema = z.object({
+  name: z.string().min(1, "Name is required").max(100, "Name must be 100 characters or less"),
+  description: z.string().max(500, "Description must be 500 characters or less").optional(),
+  content: z.string().min(1, "Prompt content is required"),
+});
+
+type PromptFormValues = z.infer<typeof promptFormSchema>;
+
+export interface AdminPromptFormModalProps {
+  open: boolean;
+  onClose: () => void;
+  editingPrompt?: Template | null;
+  ownerOverride: TemplateOwner;
+  onSaved?: (template: Template) => void;
+}
+
+export function AdminPromptFormModal({
+  open,
+  onClose,
+  editingPrompt,
+  ownerOverride,
+  onSaved,
+}: AdminPromptFormModalProps) {
+  const queryClient = useQueryClient();
+  const isEditing = !!editingPrompt;
+  const [variables, setVariables] = useState<TemplateVariable[]>([]);
+
+  const form = useForm<PromptFormValues>({
+    resolver: zodResolver(promptFormSchema),
+    defaultValues: { name: "", description: "", content: "" },
+  });
+
+  useEffect(() => {
+    if (open) {
+      if (editingPrompt) {
+        form.reset({
+          name: editingPrompt.name,
+          description: editingPrompt.description || "",
+          content: editingPrompt.content,
+        });
+        setVariables(parseVariables(editingPrompt.metadata));
+      } else {
+        form.reset({ name: "", description: "", content: "" });
+        setVariables([]);
+      }
+    }
+  }, [open, editingPrompt, form]);
+
+  const createMutation = useMutation({
+    mutationFn: async (data: CreateTemplate) => {
+      const response = await templateCreate({ body: data });
+      if (response.error) {
+        throw new Error(
+          typeof response.error === "object" && "message" in response.error
+            ? String(response.error.message)
+            : "Failed to create prompt"
+        );
+      }
+      return response.data as Template;
+    },
+    onSuccess: (prompt) => {
+      queryClient.invalidateQueries({ queryKey: [{ _id: "templateListByOrg" }] });
+      queryClient.invalidateQueries({ queryKey: [{ _id: "templateListByTeam" }] });
+      queryClient.invalidateQueries({ queryKey: [{ _id: "templateListByProject" }] });
+      queryClient.invalidateQueries({ queryKey: [{ _id: "templateListByUser" }] });
+      onSaved?.(prompt);
+      onClose();
+    },
+  });
+
+  const updateMutation = useMutation({
+    mutationFn: async ({ id, data }: { id: string; data: Partial<CreateTemplate> }) => {
+      const response = await templateUpdate({
+        path: { id },
+        body: {
+          name: data.name,
+          description: data.description,
+          content: data.content,
+          metadata: data.metadata,
+        },
+      });
+      if (response.error) {
+        throw new Error(
+          typeof response.error === "object" && "message" in response.error
+            ? String(response.error.message)
+            : "Failed to update prompt"
+        );
+      }
+      return response.data as Template;
+    },
+    onSuccess: (prompt) => {
+      queryClient.invalidateQueries({ queryKey: [{ _id: "templateListByOrg" }] });
+      queryClient.invalidateQueries({ queryKey: [{ _id: "templateListByTeam" }] });
+      queryClient.invalidateQueries({ queryKey: [{ _id: "templateListByProject" }] });
+      queryClient.invalidateQueries({ queryKey: [{ _id: "templateListByUser" }] });
+      onSaved?.(prompt);
+      onClose();
+    },
+  });
+
+  const isLoading = createMutation.isPending || updateMutation.isPending;
+  const error = createMutation.error || updateMutation.error;
+
+  const handleSubmit = form.handleSubmit((data) => {
+    const metadata = variables.length > 0 ? { variables } : undefined;
+
+    if (isEditing && editingPrompt) {
+      updateMutation.mutate({
+        id: editingPrompt.id,
+        data: {
+          name: data.name,
+          description: data.description || undefined,
+          content: data.content,
+          metadata,
+        },
+      });
+    } else {
+      createMutation.mutate({
+        name: data.name,
+        description: data.description || undefined,
+        content: data.content,
+        owner: ownerOverride,
+        metadata,
+      });
+    }
+  });
+
+  const handleClose = () => {
+    if (!isLoading) {
+      form.reset();
+      createMutation.reset();
+      updateMutation.reset();
+      onClose();
+    }
+  };
+
+  return (
+    <Modal open={open} onClose={handleClose} className="max-w-lg">
+      <ModalClose onClose={handleClose} />
+      <form onSubmit={handleSubmit}>
+        <ModalHeader>
+          <ModalTitle className="flex items-center gap-2">
+            <ClipboardPenLine className="h-5 w-5" />
+            {isEditing ? "Edit Template" : "New Template"}
+          </ModalTitle>
+        </ModalHeader>
+
+        <ModalContent>
+          <div className="space-y-4">
+            {error && (
+              <div className="rounded-md bg-destructive/10 px-3 py-2 text-sm text-destructive">
+                {error.message}
+              </div>
+            )}
+
+            <FormField
+              label="Name"
+              htmlFor="admin-prompt-name"
+              required
+              error={form.formState.errors.name?.message}
+            >
+              <Input
+                id="admin-prompt-name"
+                {...form.register("name")}
+                placeholder="e.g., Code Review Assistant"
+              />
+            </FormField>
+
+            <FormField
+              label="Description"
+              htmlFor="admin-prompt-description"
+              helpText="Optional description to help identify this template"
+              error={form.formState.errors.description?.message}
+            >
+              <Input
+                id="admin-prompt-description"
+                {...form.register("description")}
+                placeholder="e.g., Reviews code for best practices and potential issues"
+              />
+            </FormField>
+
+            <FormField
+              label="Content"
+              htmlFor="admin-prompt-content"
+              required
+              error={form.formState.errors.content?.message}
+            >
+              <textarea
+                id="admin-prompt-content"
+                {...form.register("content")}
+                placeholder="Enter the system prompt content..."
+                className="w-full min-h-[150px] rounded-md border bg-background px-3 py-2 text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 resize-y"
+              />
+            </FormField>
+
+            <TemplateVariableEditor variables={variables} onChange={setVariables} />
+          </div>
+        </ModalContent>
+
+        <ModalFooter>
+          <Button type="button" variant="ghost" onClick={handleClose} disabled={isLoading}>
+            Cancel
+          </Button>
+          <Button type="submit" isLoading={isLoading}>
+            {isEditing ? "Save Changes" : "Create Template"}
+          </Button>
+        </ModalFooter>
+      </form>
+    </Modal>
+  );
+}
diff --git a/ui/src/components/Admin/PromptFormModal/index.ts b/ui/src/components/Admin/PromptFormModal/index.ts
new file mode 100644
index 00000000..2408aad1
--- /dev/null
+++ b/ui/src/components/Admin/PromptFormModal/index.ts
@@ -0,0 +1 @@
+export { AdminPromptFormModal, type AdminPromptFormModalProps } from "./PromptFormModal";
diff --git a/ui/src/components/Admin/index.ts b/ui/src/components/Admin/index.ts
index 71d080be..de173ecf 100644
--- a/ui/src/components/Admin/index.ts
+++ b/ui/src/components/Admin/index.ts
@@ -82,3 +82,4 @@ export {
   type TimeRangeSelectorProps,
 } from "./TimeRangeSelector";
 export { ProviderHistoryCharts, type ProviderHistoryChartsProps } from "./ProviderHistoryCharts";
+export { AdminPromptFormModal, type AdminPromptFormModalProps } from "./PromptFormModal";
diff --git a/ui/src/components/ChatInput/ChatInput.tsx b/ui/src/components/ChatInput/ChatInput.tsx
index 27c104f4..3e13c6e0 100644
--- a/ui/src/components/ChatInput/ChatInput.tsx
+++ b/ui/src/components/ChatInput/ChatInput.tsx
@@ -32,7 +32,7 @@ import type { VectorStoreOwnerType } from "@/api/generated/types.gen";
 import { Button } from "@/components/Button/Button";
 import { Textarea } from "@/components/Textarea/Textarea";
 import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/Tooltip/Tooltip";
-import { PromptsButton } from "@/components/PromptsButton";
+import { TemplatesButton } from "@/components/PromptsButton";
 import { ToolsBar } from "@/components/ToolsBar";
 import type { ModelInfo } from "@/components/ModelPicker/ModelPicker";
 import { useConfig } from "@/config/ConfigProvider";
@@ -454,13 +454,9 @@ export function ChatInput({
               </Tooltip>
             )}
 
-            {/* Prompt templates */}
+            {/* Templates */}
             {onApplyPrompt && (
-              <PromptsButton
-                onApplyPrompt={onApplyPrompt}
-                hasActivePrompt={hasSystemPrompt}
-                disabled={disabled || isStreaming}
-              />
+              <TemplatesButton onApplyTemplate={onApplyPrompt} disabled={disabled || isStreaming} />
             )}
 
             {/* History mode toggle - only show when multiple models */}
diff --git a/ui/src/components/ChatView/ChatView.stories.tsx b/ui/src/components/ChatView/ChatView.stories.tsx
index b0bc9269..7052de16 100644
--- a/ui/src/components/ChatView/ChatView.stories.tsx
+++ b/ui/src/components/ChatView/ChatView.stories.tsx
@@ -11,6 +11,7 @@ import type {
   Artifact,
 } from "@/components/chat-types";
 import type { ModelInfo } from "@/components/ModelSelector/ModelSelector";
+import { AuthProvider } from "@/auth";
 import { ConfigProvider } from "@/config/ConfigProvider";
 import { PreferencesProvider } from "@/preferences/PreferencesProvider";
 import { ToastProvider } from "@/components/Toast/Toast";
@@ -102,15 +103,17 @@ const meta: Meta<typeof ChatView> = {
     (Story) => (
       <QueryClientProvider client={queryClient}>
         <ConfigProvider>
-          <PreferencesProvider>
-            <ToastProvider>
-              <TooltipProvider>
-                <div className="h-screen">
-                  <Story />
-                </div>
-              </TooltipProvider>
-            </ToastProvider>
-          </PreferencesProvider>
+          <AuthProvider>
+            <PreferencesProvider>
+              <ToastProvider>
+                <TooltipProvider>
+                  <div className="h-screen">
+                    <Story />
+                  </div>
+                </TooltipProvider>
+              </ToastProvider>
+            </PreferencesProvider>
+          </AuthProvider>
         </ConfigProvider>
       </QueryClientProvider>
     ),
diff --git a/ui/src/components/ChatView/ChatView.tsx b/ui/src/components/ChatView/ChatView.tsx
index d2a4dbf5..ad6dcef9 100644
--- a/ui/src/components/ChatView/ChatView.tsx
+++ b/ui/src/components/ChatView/ChatView.tsx
@@ -129,6 +129,7 @@ export function ChatView({
     setTTSVoice,
     setTTSSpeed,
     setSubAgentModel,
+    setPendingPrompt,
   } = useChatUIStore();
 
   // Stable callback for instance parameter changes
@@ -234,7 +235,7 @@ export function ChatView({
             subAgentModel={subAgentModel}
             onSubAgentModelChange={setSubAgentModel}
             onOpenMCPConfig={() => setMCPConfigModalOpen(true)}
-            onApplyPrompt={setSystemPrompt}
+            onApplyPrompt={setPendingPrompt}
           />
         </div>
       </footer>
diff --git a/ui/src/components/ChatView/ChatView.video.stories.tsx b/ui/src/components/ChatView/ChatView.video.stories.tsx
index 10f661db..fd3bd8de 100644
--- a/ui/src/components/ChatView/ChatView.video.stories.tsx
+++ b/ui/src/components/ChatView/ChatView.video.stories.tsx
@@ -10,6 +10,7 @@ import { fn } from "storybook/test";
 import { useEffect, useRef, useCallback } from "react";
 
 import type { ModelInfo } from "@/components/ModelSelector/ModelSelector";
+import { AuthProvider } from "@/auth";
 import { ConfigProvider } from "@/config/ConfigProvider";
 import { PreferencesProvider } from "@/preferences/PreferencesProvider";
 import { ToastProvider } from "@/components/Toast/Toast";
@@ -232,15 +233,17 @@ const meta: Meta<typeof ChatView> = {
     (Story) => (
       <QueryClientProvider client={queryClient}>
         <ConfigProvider>
-          <PreferencesProvider>
-            <ToastProvider>
-              <TooltipProvider>
-                <div className="h-screen">
-                  <Story />
-                </div>
-              </TooltipProvider>
-            </ToastProvider>
-          </PreferencesProvider>
+          <AuthProvider>
+            <PreferencesProvider>
+              <ToastProvider>
+                <TooltipProvider>
+                  <div className="h-screen">
+                    <Story />
+                  </div>
+                </TooltipProvider>
+              </ToastProvider>
+            </PreferencesProvider>
+          </AuthProvider>
         </ConfigProvider>
       </QueryClientProvider>
     ),
diff --git a/ui/src/components/ConversationSettingsModal/ConversationSettingsModal.stories.tsx b/ui/src/components/ConversationSettingsModal/ConversationSettingsModal.stories.tsx
index bcef6b60..a27fc657 100644
--- a/ui/src/components/ConversationSettingsModal/ConversationSettingsModal.stories.tsx
+++ b/ui/src/components/ConversationSettingsModal/ConversationSettingsModal.stories.tsx
@@ -2,9 +2,11 @@ import type { Meta, StoryObj } from "@storybook/react";
 import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
 import { http, HttpResponse, delay } from "msw";
 import { useState } from "react";
+import { AuthProvider } from "@/auth";
+import { ConfigProvider } from "@/config/ConfigProvider";
 import { ConversationSettingsModal } from "./ConversationSettingsModal";
 import { Button } from "../Button/Button";
-import type { VectorStore, Organization, Prompt } from "@/api/generated/types.gen";
+import type { VectorStore, Organization, Template } from "@/api/generated/types.gen";
 
 // Mock organizations
 const mockOrganizations: Organization[] = [
@@ -20,7 +22,7 @@ const mockOrganizations: Organization[] = [
 ];
 
 // Mock prompts
-const mockPrompts: Prompt[] = [
+const mockPrompts: Template[] = [
   {
     id: "prompt_001",
     name: "Code Review Assistant",
@@ -141,7 +143,11 @@ const meta: Meta<typeof ConversationSettingsModal> = {
   decorators: [
     (Story) => (
       <QueryClientProvider client={queryClient}>
-        <Story />
+        <ConfigProvider>
+          <AuthProvider>
+            <Story />
+          </AuthProvider>
+        </ConfigProvider>
       </QueryClientProvider>
     ),
   ],
@@ -169,7 +175,7 @@ const meta: Meta<typeof ConversationSettingsModal> = {
           });
         }),
         // Mock prompts list
-        http.get("/api/admin/v1/organizations/:org_slug/prompts", async () => {
+        http.get("/api/admin/v1/organizations/:org_slug/templates", async () => {
           await delay(200);
           return HttpResponse.json({
             data: mockPrompts,
@@ -177,10 +183,10 @@ const meta: Meta<typeof ConversationSettingsModal> = {
           });
         }),
         // Mock create prompt
-        http.post("/api/admin/v1/prompts", async ({ request }) => {
+        http.post("/api/admin/v1/templates", async ({ request }) => {
           await delay(500);
           const body = (await request.json()) as Record<string, unknown>;
-          const newPrompt: Prompt = {
+          const newPrompt: Template = {
             id: "prompt_new",
             name: body.name as string,
             description: (body.description as string) || null,
diff --git a/ui/src/components/ConversationSettingsModal/ConversationSettingsModal.tsx b/ui/src/components/ConversationSettingsModal/ConversationSettingsModal.tsx
index 34f4fc7d..448f2ccb 100644
--- a/ui/src/components/ConversationSettingsModal/ConversationSettingsModal.tsx
+++ b/ui/src/components/ConversationSettingsModal/ConversationSettingsModal.tsx
@@ -1,7 +1,7 @@
 import { useState } from "react";
-import { Settings2, RotateCcw, Database, Sparkles, Save, ChevronDown, Volume2 } from "lucide-react";
+import { Settings2, RotateCcw, Database, Save, Volume2 } from "lucide-react";
 
-import type { VectorStoreOwnerType, Prompt, Voice } from "@/api/generated/types.gen";
+import type { VectorStoreOwnerType, Voice } from "@/api/generated/types.gen";
 import { TTS_VOICES, DEFAULT_TTS_VOICE, DEFAULT_TTS_SPEED } from "@/hooks/useAudioPlayback";
 import { Button } from "@/components/Button/Button";
 import type { ResponseActionConfig } from "@/components/chat-types";
@@ -14,18 +14,11 @@ import {
   ModalHeader,
   ModalTitle,
 } from "@/components/Modal/Modal";
-import {
-  Dropdown,
-  DropdownContent,
-  DropdownItem,
-  DropdownTrigger,
-} from "@/components/Dropdown/Dropdown";
 import { PromptFormModal } from "@/components/PromptFormModal/PromptFormModal";
 import { Select, type SelectOption } from "@/components/Select/Select";
 import { Slider } from "@/components/Slider/Slider";
 import { Switch } from "@/components/Switch/Switch";
 import { VectorStoreSelector } from "@/components/VectorStores/VectorStoreSelector";
-import { useUserPrompts, type PromptWithOrg } from "@/hooks/useUserPrompts";
 
 interface ConversationSettingsModalProps {
   open: boolean;
@@ -88,22 +81,11 @@ export function ConversationSettingsModal({
   onTTSSpeedChange,
 }: ConversationSettingsModalProps) {
   const [promptFormOpen, setPromptFormOpen] = useState(false);
-  const [editingPrompt, setEditingPrompt] = useState<Prompt | null>(null);
-  const { prompts, isLoading: promptsLoading } = useUserPrompts();
-
-  const handleApplyPrompt = (prompt: PromptWithOrg) => {
-    onSystemPromptChange(prompt.content);
-  };
 
   const handleSaveAsTemplate = () => {
-    setEditingPrompt(null);
     setPromptFormOpen(true);
   };
 
-  const handlePromptSaved = (_prompt: Prompt) => {
-    // Prompt saved successfully, modal will close automatically
-  };
-
   const updateActionConfig = <K extends keyof ResponseActionConfig>(
     key: K,
     value: ResponseActionConfig[K]
@@ -150,58 +132,17 @@ export function ConversationSettingsModal({
                 settings icon on each model chip.
               </p>
             </div>
-            <div className="flex items-center gap-2 shrink-0">
-              {/* Template Selector */}
-              <Dropdown>
-                <DropdownTrigger asChild>
-                  <Button
-                    variant="outline"
-                    size="sm"
-                    className="h-8 px-2 gap-1"
-                    disabled={promptsLoading || prompts.length === 0}
-                  >
-                    <Sparkles className="h-3.5 w-3.5" />
-                    <span className="hidden sm:inline">Templates</span>
-                    <ChevronDown className="h-3 w-3" />
-                  </Button>
-                </DropdownTrigger>
-                <DropdownContent align="end" className="w-56 max-h-64 overflow-y-auto">
-                  {prompts.length === 0 ? (
-                    <div className="px-2 py-3 text-sm text-muted-foreground text-center">
-                      No templates available
-                    </div>
-                  ) : (
-                    prompts.map((prompt) => (
-                      <DropdownItem
-                        key={prompt.id}
-                        onClick={() => handleApplyPrompt(prompt)}
-                        className="flex flex-col items-start gap-0.5"
-                      >
-                        <span className="font-medium">{prompt.name}</span>
-                        {prompt.description && (
-                          <span className="text-xs text-muted-foreground line-clamp-1">
-                            {prompt.description}
-                          </span>
-                        )}
-                      </DropdownItem>
-                    ))
-                  )}
-                </DropdownContent>
-              </Dropdown>
-
-              {/* Save as Template Button */}
-              <Button
-                variant="outline"
-                size="sm"
-                className="h-8 px-2 gap-1"
-                onClick={handleSaveAsTemplate}
-                disabled={!systemPrompt.trim()}
-                title={systemPrompt.trim() ? "Save as template" : "Enter a prompt to save"}
-              >
-                <Save className="h-3.5 w-3.5" />
-                <span className="hidden sm:inline">Save</span>
-              </Button>
-            </div>
+            <Button
+              variant="outline"
+              size="sm"
+              className="h-8 px-2 gap-1 shrink-0"
+              onClick={handleSaveAsTemplate}
+              disabled={!systemPrompt.trim()}
+              title={systemPrompt.trim() ? "Save as template" : "Enter a prompt to save"}
+            >
+              <Save className="h-3.5 w-3.5" />
+              <span className="hidden sm:inline">Save as Template</span>
+            </Button>
           </div>
           <textarea
             value={systemPrompt}
@@ -399,13 +340,11 @@ export function ConversationSettingsModal({
         </Button>
       </ModalFooter>
 
-      {/* Prompt Form Modal */}
+      {/* Save as Template Modal */}
       <PromptFormModal
         open={promptFormOpen}
         onClose={() => setPromptFormOpen(false)}
         initialContent={systemPrompt}
-        editingPrompt={editingPrompt}
-        onSaved={handlePromptSaved}
       />
     </Modal>
   );
diff --git a/ui/src/components/PromptFormModal/PromptFormModal.stories.tsx b/ui/src/components/PromptFormModal/PromptFormModal.stories.tsx
index 7c6cabad..ebea307d 100644
--- a/ui/src/components/PromptFormModal/PromptFormModal.stories.tsx
+++ b/ui/src/components/PromptFormModal/PromptFormModal.stories.tsx
@@ -2,9 +2,11 @@ import type { Meta, StoryObj } from "@storybook/react";
 import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
 import { http, HttpResponse, delay } from "msw";
 import { useState } from "react";
+import { AuthProvider } from "@/auth";
+import { ConfigProvider } from "@/config/ConfigProvider";
 import { PromptFormModal } from "./PromptFormModal";
 import { Button } from "../Button/Button";
-import type { Organization, Prompt } from "@/api/generated/types.gen";
+import type { Organization, Template } from "@/api/generated/types.gen";
 
 // Mock organizations
 const mockOrganizations: Organization[] = [
@@ -29,7 +31,7 @@ const mockOrganizations: Organization[] = [
 ];
 
 // Mock existing prompt for editing
-const mockPrompt: Prompt = {
+const mockTemplate: Template = {
   id: "prompt_001",
   name: "Code Review Assistant",
   description: "Reviews code for best practices and potential issues",
@@ -57,7 +59,11 @@ const meta: Meta<typeof PromptFormModal> = {
   decorators: [
     (Story) => (
       <QueryClientProvider client={queryClient}>
-        <Story />
+        <ConfigProvider>
+          <AuthProvider>
+            <Story />
+          </AuthProvider>
+        </ConfigProvider>
       </QueryClientProvider>
     ),
   ],
@@ -74,7 +80,7 @@ const meta: Meta<typeof PromptFormModal> = {
           });
         }),
         // Mock prompts list (empty initially)
-        http.get("/api/admin/v1/organizations/:org_slug/prompts", async () => {
+        http.get("/api/admin/v1/organizations/:org_slug/templates", async () => {
           await delay(200);
           return HttpResponse.json({
             data: [],
@@ -82,10 +88,10 @@ const meta: Meta<typeof PromptFormModal> = {
           });
         }),
         // Mock create prompt
-        http.post("/api/admin/v1/prompts", async ({ request }) => {
+        http.post("/api/admin/v1/templates", async ({ request }) => {
           await delay(500);
           const body = (await request.json()) as Record<string, unknown>;
-          const newPrompt: Prompt = {
+          const newTemplate: Template = {
             id: "prompt_new",
             name: body.name as string,
             description: (body.description as string) || null,
@@ -96,20 +102,20 @@ const meta: Meta<typeof PromptFormModal> = {
             created_at: new Date().toISOString(),
             updated_at: new Date().toISOString(),
           };
-          return HttpResponse.json(newPrompt, { status: 201 });
+          return HttpResponse.json(newTemplate, { status: 201 });
         }),
         // Mock update prompt
-        http.patch("/api/admin/v1/prompts/:id", async ({ request }) => {
+        http.patch("/api/admin/v1/templates/:id", async ({ request }) => {
           await delay(500);
           const body = (await request.json()) as Record<string, unknown>;
-          const updatedPrompt: Prompt = {
-            ...mockPrompt,
-            name: (body.name as string) || mockPrompt.name,
-            description: (body.description as string) || mockPrompt.description,
-            content: (body.content as string) || mockPrompt.content,
+          const updatedTemplate: Template = {
+            ...mockTemplate,
+            name: (body.name as string) || mockTemplate.name,
+            description: (body.description as string) || mockTemplate.description,
+            content: (body.content as string) || mockTemplate.content,
             updated_at: new Date().toISOString(),
           };
-          return HttpResponse.json(updatedPrompt);
+          return HttpResponse.json(updatedTemplate);
         }),
       ],
     },
@@ -145,7 +151,7 @@ function WithInitialContentStory() {
 
   return (
     <>
-      <Button onClick={() => setOpen(true)}>Save Current Prompt</Button>
+      <Button onClick={() => setOpen(true)}>Save Current Template</Button>
       <div className="mt-4 p-4 bg-muted rounded-md max-w-md">
         <p className="text-xs text-muted-foreground mb-2">Current system prompt:</p>
         <p className="text-sm">{initialContent}</p>
@@ -169,16 +175,16 @@ function EditExistingStory() {
 
   return (
     <>
-      <Button onClick={() => setOpen(true)}>Edit Prompt</Button>
+      <Button onClick={() => setOpen(true)}>Edit Template</Button>
       <div className="mt-4 p-4 bg-muted rounded-md max-w-md">
         <p className="text-xs text-muted-foreground mb-1">Editing:</p>
-        <p className="text-sm font-medium">{mockPrompt.name}</p>
-        <p className="text-xs text-muted-foreground mt-1">{mockPrompt.description}</p>
+        <p className="text-sm font-medium">{mockTemplate.name}</p>
+        <p className="text-xs text-muted-foreground mt-1">{mockTemplate.description}</p>
       </div>
       <PromptFormModal
         open={open}
         onClose={() => setOpen(false)}
-        editingPrompt={mockPrompt}
+        editingPrompt={mockTemplate}
         onSaved={(prompt) => console.log("Updated:", prompt)}
       />
     </>
@@ -219,7 +225,7 @@ function LongContentStory() {
 
   return (
     <>
-      <Button onClick={() => setOpen(true)}>Save Long Prompt</Button>
+      <Button onClick={() => setOpen(true)}>Save Long Template</Button>
       <PromptFormModal
         open={open}
         onClose={() => setOpen(false)}
diff --git a/ui/src/components/PromptFormModal/PromptFormModal.tsx b/ui/src/components/PromptFormModal/PromptFormModal.tsx
index bb861151..91bc665f 100644
--- a/ui/src/components/PromptFormModal/PromptFormModal.tsx
+++ b/ui/src/components/PromptFormModal/PromptFormModal.tsx
@@ -1,12 +1,12 @@
-import { useEffect } from "react";
+import { useEffect, useState } from "react";
 import { useForm, Controller } from "react-hook-form";
 import { zodResolver } from "@hookform/resolvers/zod";
 import { z } from "zod";
 import { useMutation, useQueryClient } from "@tanstack/react-query";
-import { Sparkles } from "lucide-react";
+import { ClipboardPenLine } from "lucide-react";
 
-import type { CreatePrompt, PromptOwner, Prompt } from "@/api/generated/types.gen";
-import { promptCreate, promptUpdate } from "@/api/generated/sdk.gen";
+import type { CreateTemplate, TemplateOwner, Template } from "@/api/generated/types.gen";
+import { templateCreate, templateUpdate } from "@/api/generated/sdk.gen";
 import { Button } from "@/components/Button/Button";
 import { FormField } from "@/components/FormField/FormField";
 import { Input } from "@/components/Input/Input";
@@ -18,7 +18,9 @@ import {
   ModalContent,
   ModalFooter,
 } from "@/components/Modal/Modal";
+import { TemplateVariableEditor } from "@/components/TemplateVariableEditor/TemplateVariableEditor";
 import { useUserPrompts } from "@/hooks/useUserPrompts";
+import { type TemplateVariable, parseVariables } from "@/lib/templateVariables";
 
 const promptFormSchema = z.object({
   name: z.string().min(1, "Name is required").max(100, "Name must be 100 characters or less"),
@@ -37,9 +39,9 @@ export interface PromptFormModalProps {
   /** Initial content to pre-fill (e.g., from current system prompt) */
   initialContent?: string;
   /** Existing prompt to edit (if editing) */
-  editingPrompt?: Prompt | null;
+  editingPrompt?: Template | null;
   /** Callback after successful save */
-  onSaved?: (prompt: Prompt) => void;
+  onSaved?: (template: Template) => void;
 }
 
 export function PromptFormModal({
@@ -53,6 +55,7 @@ export function PromptFormModal({
   const { organizations } = useUserPrompts();
 
   const isEditing = !!editingPrompt;
+  const [variables, setVariables] = useState<TemplateVariable[]>([]);
 
   const form = useForm<PromptFormValues>({
     resolver: zodResolver(promptFormSchema),
@@ -68,32 +71,31 @@ export function PromptFormModal({
   useEffect(() => {
     if (open) {
       if (editingPrompt) {
-        // Editing mode: populate with existing prompt data
         form.reset({
           name: editingPrompt.name,
           description: editingPrompt.description || "",
           content: editingPrompt.content,
-          // For editing, we need to find the org ID from the prompt's owner
           organization_id:
             editingPrompt.owner_type === "organization"
               ? editingPrompt.owner_id
               : organizations[0]?.id || "",
         });
+        setVariables(parseVariables(editingPrompt.metadata));
       } else {
-        // Create mode: use initial content if provided
         form.reset({
           name: "",
           description: "",
           content: initialContent,
           organization_id: organizations[0]?.id || "",
         });
+        setVariables([]);
       }
     }
   }, [open, editingPrompt, initialContent, form, organizations]);
 
   const createMutation = useMutation({
-    mutationFn: async (data: CreatePrompt) => {
-      const response = await promptCreate({ body: data });
+    mutationFn: async (data: CreateTemplate) => {
+      const response = await templateCreate({ body: data });
       if (response.error) {
         throw new Error(
           typeof response.error === "object" && "message" in response.error
@@ -101,23 +103,24 @@ export function PromptFormModal({
             : "Failed to create prompt"
         );
       }
-      return response.data as Prompt;
+      return response.data as Template;
     },
     onSuccess: (prompt) => {
-      queryClient.invalidateQueries({ queryKey: [{ _id: "promptListByOrg" }] });
+      queryClient.invalidateQueries({ queryKey: [{ _id: "templateListByOrg" }] });
       onSaved?.(prompt);
       onClose();
     },
   });
 
   const updateMutation = useMutation({
-    mutationFn: async ({ id, data }: { id: string; data: Partial<CreatePrompt> }) => {
-      const response = await promptUpdate({
+    mutationFn: async ({ id, data }: { id: string; data: Partial<CreateTemplate> }) => {
+      const response = await templateUpdate({
         path: { id },
         body: {
           name: data.name,
           description: data.description,
           content: data.content,
+          metadata: data.metadata,
         },
       });
       if (response.error) {
@@ -127,10 +130,10 @@ export function PromptFormModal({
             : "Failed to update prompt"
         );
       }
-      return response.data as Prompt;
+      return response.data as Template;
     },
     onSuccess: (prompt) => {
-      queryClient.invalidateQueries({ queryKey: [{ _id: "promptListByOrg" }] });
+      queryClient.invalidateQueries({ queryKey: [{ _id: "templateListByOrg" }] });
       onSaved?.(prompt);
       onClose();
     },
@@ -140,10 +143,11 @@ export function PromptFormModal({
   const error = createMutation.error || updateMutation.error;
 
   const handleSubmit = form.handleSubmit((data) => {
-    const owner: PromptOwner = {
+    const owner: TemplateOwner = {
       type: "organization",
       organization_id: data.organization_id,
     };
+    const metadata = variables.length > 0 ? { variables } : undefined;
 
     if (isEditing && editingPrompt) {
       updateMutation.mutate({
@@ -152,14 +156,16 @@ export function PromptFormModal({
           name: data.name,
           description: data.description || undefined,
           content: data.content,
+          metadata,
         },
       });
     } else {
-      const body: CreatePrompt = {
+      const body: CreateTemplate = {
         name: data.name,
         description: data.description || undefined,
         content: data.content,
         owner,
+        metadata,
       };
       createMutation.mutate(body);
     }
@@ -180,7 +186,7 @@ export function PromptFormModal({
       <form onSubmit={handleSubmit}>
         <ModalHeader>
           <ModalTitle className="flex items-center gap-2">
-            <Sparkles className="h-5 w-5" />
+            <ClipboardPenLine className="h-5 w-5" />
             {isEditing ? "Edit Prompt Template" : "Save as Template"}
           </ModalTitle>
         </ModalHeader>
@@ -233,7 +239,9 @@ export function PromptFormModal({
               />
             </FormField>
 
-            {!isEditing && (
+            <TemplateVariableEditor variables={variables} onChange={setVariables} />
+
+            {!isEditing && organizations.length > 1 && (
               <FormField
                 label="Organization"
                 htmlFor="prompt-org"
diff --git a/ui/src/components/PromptsButton/PromptsButton.stories.tsx b/ui/src/components/PromptsButton/PromptsButton.stories.tsx
index 430193fa..ef59251e 100644
--- a/ui/src/components/PromptsButton/PromptsButton.stories.tsx
+++ b/ui/src/components/PromptsButton/PromptsButton.stories.tsx
@@ -1,7 +1,7 @@
 import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
 import type { Meta, StoryObj } from "@storybook/react";
 import { MemoryRouter } from "react-router-dom";
-import { PromptsButton } from "./PromptsButton";
+import { TemplatesButton } from "./PromptsButton";
 import { AuthProvider } from "@/auth";
 import { ConfigProvider } from "@/config/ConfigProvider";
 import { ToastProvider } from "../Toast/Toast";
@@ -14,9 +14,9 @@ const queryClient = new QueryClient({
   },
 });
 
-const meta: Meta<typeof PromptsButton> = {
-  title: "Chat/PromptsButton",
-  component: PromptsButton,
+const meta: Meta<typeof TemplatesButton> = {
+  title: "Chat/TemplatesButton",
+  component: TemplatesButton,
   parameters: {
     layout: "centered",
   },
@@ -38,31 +38,14 @@ const meta: Meta<typeof PromptsButton> = {
     ),
   ],
   args: {
-    onApplyPrompt: (content: string) => console.log("Apply prompt:", content),
+    onApplyTemplate: (content: string) => console.log("Apply template:", content),
   },
 };
 
 export default meta;
 type Story = StoryObj<typeof meta>;
 
-export const Default: Story = {
-  args: {
-    hasActivePrompt: false,
-  },
-};
-
-export const Active: Story = {
-  args: {
-    hasActivePrompt: true,
-  },
-  parameters: {
-    docs: {
-      description: {
-        story: "Button highlighted when a system prompt is active.",
-      },
-    },
-  },
-};
+export const Default: Story = {};
 
 export const Disabled: Story = {
   args: {
diff --git a/ui/src/components/PromptsButton/PromptsButton.tsx b/ui/src/components/PromptsButton/PromptsButton.tsx
index 6ac13afe..c81ef062 100644
--- a/ui/src/components/PromptsButton/PromptsButton.tsx
+++ b/ui/src/components/PromptsButton/PromptsButton.tsx
@@ -1,36 +1,94 @@
 import { useState } from "react";
-import { Loader2, Plus, Sparkles } from "lucide-react";
+import { ClipboardPenLine, Loader2, Plus, Trash2 } from "lucide-react";
+import { useMutation, useQueryClient } from "@tanstack/react-query";
 
+import type { Template } from "@/api/generated/types.gen";
+import { templateDelete } from "@/api/generated/sdk.gen";
 import { Button } from "@/components/Button/Button";
+import {
+  Modal,
+  ModalClose,
+  ModalContent,
+  ModalFooter,
+  ModalHeader,
+  ModalTitle,
+} from "@/components/Modal/Modal";
 import { Popover, PopoverContent, PopoverTrigger } from "@/components/Popover/Popover";
 import { PromptFormModal } from "@/components/PromptFormModal/PromptFormModal";
+import { TemplateVariableForm } from "@/components/TemplateVariableForm/TemplateVariableForm";
 import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/Tooltip/Tooltip";
 import { useToast } from "@/components/Toast/Toast";
-import { useUserPrompts } from "@/hooks/useUserPrompts";
-import { cn } from "@/utils/cn";
+import { useUserTemplates } from "@/hooks/useUserPrompts";
+import {
+  parseVariables,
+  substituteVariables,
+  validateVariableValues,
+} from "@/lib/templateVariables";
 
-interface PromptsButtonProps {
-  /** Called when a prompt is selected */
-  onApplyPrompt: (content: string) => void;
-  /** Whether a system prompt is currently active (highlights the button) */
-  hasActivePrompt?: boolean;
+interface TemplatesButtonProps {
+  /** Called when a template is selected */
+  onApplyTemplate: (content: string) => void;
   /** Whether the button is disabled */
   disabled?: boolean;
 }
 
-export function PromptsButton({
-  onApplyPrompt,
-  hasActivePrompt = false,
-  disabled = false,
-}: PromptsButtonProps) {
+export function TemplatesButton({ onApplyTemplate, disabled = false }: TemplatesButtonProps) {
   const [open, setOpen] = useState(false);
   const [createModalOpen, setCreateModalOpen] = useState(false);
-  const { prompts, isLoading } = useUserPrompts();
+  const [variableTemplate, setVariableTemplate] = useState<Template | null>(null);
+  const [variableValues, setVariableValues] = useState<Record<string, string>>({});
+  const [variableErrors, setVariableErrors] = useState<Record<string, string>>({});
+  const { templates, isLoading, hasMore } = useUserTemplates();
   const { toast } = useToast();
+  const queryClient = useQueryClient();
 
-  const handleSelect = (content: string) => {
-    onApplyPrompt(content);
+  const deleteMutation = useMutation({
+    mutationFn: async (id: string) => {
+      const response = await templateDelete({ path: { id } });
+      if (response.error) throw new Error("Failed to delete template");
+    },
+    onSuccess: () => {
+      queryClient.invalidateQueries({ queryKey: [{ _id: "templateListByOrg" }] });
+      queryClient.invalidateQueries({ queryKey: [{ _id: "templateListByUser" }] });
+      toast({ title: "Template deleted", type: "success" });
+    },
+    onError: () => {
+      toast({ title: "Failed to delete template", type: "error" });
+    },
+  });
+
+  const handleSelect = (template: Template) => {
     setOpen(false);
+    const vars = parseVariables(template.metadata);
+    if (vars.length > 0) {
+      const defaults: Record<string, string> = {};
+      for (const v of vars) {
+        if (v.default) defaults[v.name] = v.default;
+      }
+      setVariableValues(defaults);
+      setVariableErrors({});
+      setVariableTemplate(template);
+    } else {
+      onApplyTemplate(template.content);
+    }
+  };
+
+  const handleVariableSubmit = () => {
+    if (!variableTemplate) return;
+    const vars = parseVariables(variableTemplate.metadata);
+    const errors = validateVariableValues(vars, variableValues);
+    if (Object.keys(errors).length > 0) {
+      setVariableErrors(errors);
+      return;
+    }
+    const content = substituteVariables(variableTemplate.content, variableValues);
+    setVariableTemplate(null);
+    onApplyTemplate(content);
+  };
+
+  const handleDelete = (e: React.MouseEvent, template: Template) => {
+    e.stopPropagation();
+    deleteMutation.mutate(template.id);
   };
 
   return (
@@ -43,21 +101,18 @@ export function PromptsButton({
                 type="button"
                 size="icon"
                 variant="ghost"
-                className={cn(
-                  "h-8 w-8 shrink-0 rounded-lg",
-                  hasActivePrompt ? "text-primary" : "text-muted-foreground hover:text-foreground"
-                )}
+                className="h-8 w-8 shrink-0 rounded-lg text-muted-foreground hover:text-foreground"
                 disabled={disabled}
-                aria-label="Prompt templates"
+                aria-label="Templates"
               >
-                <Sparkles className="h-4 w-4" />
+                <ClipboardPenLine className="h-4 w-4" />
               </Button>
             </PopoverTrigger>
           </TooltipTrigger>
           <TooltipContent side="top">Templates</TooltipContent>
         </Tooltip>
 
-        <PopoverContent align="start" className="w-64 p-0">
+        <PopoverContent align="start" className="w-72 p-0">
           {/* Header */}
           <div className="flex items-center justify-between border-b px-3 py-2">
             <span className="text-sm font-medium">Templates</span>
@@ -81,9 +136,9 @@ export function PromptsButton({
               <div className="flex items-center justify-center py-6">
                 <Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
               </div>
-            ) : prompts.length === 0 ? (
+            ) : templates.length === 0 ? (
               <div className="px-3 py-6 text-center">
-                <Sparkles className="mx-auto mb-2 h-5 w-5 text-muted-foreground" />
+                <ClipboardPenLine className="mx-auto mb-2 h-5 w-5 text-muted-foreground" />
                 <p className="text-xs text-muted-foreground">No templates yet</p>
                 <button
                   type="button"
@@ -97,25 +152,43 @@ export function PromptsButton({
                 </button>
               </div>
             ) : (
-              <ul className="space-y-0.5">
-                {prompts.map((prompt) => (
-                  <li key={prompt.id}>
-                    <button
-                      type="button"
-                      className={cn(
-                        "flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-sm",
-                        "hover:bg-accent/50 transition-colors",
-                        "text-foreground/80"
-                      )}
-                      onClick={() => handleSelect(prompt.content)}
-                      title={prompt.description || "Apply this template"}
-                    >
-                      <Sparkles className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
-                      <span className="min-w-0 flex-1 truncate">{prompt.name}</span>
-                    </button>
-                  </li>
-                ))}
-              </ul>
+              <>
+                <ul className="space-y-0.5">
+                  {templates.map((template) => (
+                    <li key={template.id} className="group relative">
+                      <button
+                        type="button"
+                        className="flex w-full items-start gap-2 rounded-md px-2 py-1.5 pr-8 text-left text-sm hover:bg-accent/50 transition-colors text-foreground/80"
+                        onClick={() => handleSelect(template)}
+                        title={template.description || "Apply this template"}
+                      >
+                        <ClipboardPenLine className="mt-0.5 h-3.5 w-3.5 shrink-0 text-muted-foreground" />
+                        <div className="min-w-0 flex-1">
+                          <span className="block truncate">{template.name}</span>
+                          {template.description && (
+                            <span className="text-xs text-muted-foreground line-clamp-1">
+                              {template.description}
+                            </span>
+                          )}
+                        </div>
+                      </button>
+                      <button
+                        type="button"
+                        className="absolute right-1.5 top-1.5 hidden rounded p-0.5 text-muted-foreground hover:bg-destructive/10 hover:text-destructive group-hover:block"
+                        onClick={(e) => handleDelete(e, template)}
+                        aria-label={`Delete template: ${template.name}`}
+                      >
+                        <Trash2 className="h-3 w-3" />
+                      </button>
+                    </li>
+                  ))}
+                </ul>
+                {hasMore && (
+                  <p className="px-2 py-1.5 text-center text-xs text-muted-foreground">
+                    Showing first 50 templates
+                  </p>
+                )}
+              </>
             )}
           </div>
         </PopoverContent>
@@ -128,6 +201,49 @@ export function PromptsButton({
           toast({ title: "Template created", type: "success" });
         }}
       />
+
+      {/* Variable fill modal */}
+      <Modal
+        open={!!variableTemplate}
+        onClose={() => setVariableTemplate(null)}
+        className="max-w-md"
+      >
+        <ModalClose onClose={() => setVariableTemplate(null)} />
+        <ModalHeader>
+          <ModalTitle className="flex items-center gap-2">
+            <ClipboardPenLine className="h-5 w-5" />
+            {variableTemplate?.name}
+          </ModalTitle>
+        </ModalHeader>
+        <ModalContent>
+          {variableTemplate && (
+            <>
+              {variableTemplate.description && (
+                <p className="mb-3 text-sm text-muted-foreground">{variableTemplate.description}</p>
+              )}
+              <TemplateVariableForm
+                variables={parseVariables(variableTemplate.metadata)}
+                values={variableValues}
+                onChange={setVariableValues}
+                errors={variableErrors}
+              />
+              {/* Preview */}
+              <div className="mt-4">
+                <p className="mb-1 text-xs font-medium text-muted-foreground">Preview</p>
+                <pre className="max-h-32 overflow-y-auto rounded-md border bg-muted/50 p-2 text-xs whitespace-pre-wrap">
+                  {substituteVariables(variableTemplate.content, variableValues)}
+                </pre>
+              </div>
+            </>
+          )}
+        </ModalContent>
+        <ModalFooter>
+          <Button variant="ghost" onClick={() => setVariableTemplate(null)}>
+            Cancel
+          </Button>
+          <Button onClick={handleVariableSubmit}>Apply Template</Button>
+        </ModalFooter>
+      </Modal>
     </>
   );
 }
diff --git a/ui/src/components/PromptsButton/index.ts b/ui/src/components/PromptsButton/index.ts
index c9fab631..12eb3abc 100644
--- a/ui/src/components/PromptsButton/index.ts
+++ b/ui/src/components/PromptsButton/index.ts
@@ -1 +1 @@
-export { PromptsButton } from "./PromptsButton";
+export { TemplatesButton } from "./PromptsButton";
diff --git a/ui/src/components/TemplateVariableEditor/TemplateVariableEditor.tsx b/ui/src/components/TemplateVariableEditor/TemplateVariableEditor.tsx
new file mode 100644
index 00000000..7735c4ca
--- /dev/null
+++ b/ui/src/components/TemplateVariableEditor/TemplateVariableEditor.tsx
@@ -0,0 +1,171 @@
+import { ChevronDown, ChevronRight, Plus, Trash2 } from "lucide-react";
+import { useRef, useState } from "react";
+
+import { Button } from "@/components/Button/Button";
+import { Input } from "@/components/Input/Input";
+import { Switch } from "@/components/Switch/Switch";
+import type { TemplateVariable } from "@/lib/templateVariables";
+
+export interface TemplateVariableEditorProps {
+  variables: TemplateVariable[];
+  onChange: (variables: TemplateVariable[]) => void;
+}
+
+const EMPTY_VARIABLE: TemplateVariable = {
+  name: "",
+  label: "",
+  type: "text",
+};
+
+let globalKeyCounter = 0;
+
+export function TemplateVariableEditor({ variables, onChange }: TemplateVariableEditorProps) {
+  const [expanded, setExpanded] = useState(variables.length > 0);
+  const keysRef = useRef<number[]>([]);
+
+  // Sync keys when variables array changes externally (e.g. form reset)
+  if (keysRef.current.length !== variables.length) {
+    keysRef.current = variables.map((_, i) => keysRef.current[i] ?? globalKeyCounter++);
+  }
+
+  const addVariable = () => {
+    keysRef.current = [...keysRef.current, globalKeyCounter++];
+    onChange([...variables, { ...EMPTY_VARIABLE }]);
+    setExpanded(true);
+  };
+
+  const removeVariable = (index: number) => {
+    keysRef.current = keysRef.current.filter((_, i) => i !== index);
+    onChange(variables.filter((_, i) => i !== index));
+  };
+
+  const updateVariable = (index: number, updates: Partial<TemplateVariable>) => {
+    onChange(variables.map((v, i) => (i === index ? { ...v, ...updates } : v)));
+  };
+
+  return (
+    <div className="space-y-2">
+      <button
+        type="button"
+        className="flex items-center gap-1 text-sm font-medium text-foreground hover:text-foreground/80"
+        onClick={() => setExpanded(!expanded)}
+      >
+        {expanded ? <ChevronDown className="h-4 w-4" /> : <ChevronRight className="h-4 w-4" />}
+        Variables
+        {variables.length > 0 && (
+          <span className="ml-1 text-xs text-muted-foreground">({variables.length})</span>
+        )}
+      </button>
+
+      {expanded && (
+        <div className="space-y-3 rounded-md border p-3">
+          <p className="text-xs text-muted-foreground">
+            Define variables that users fill in when applying this template. Reference them in
+            content as {"{{variable_name}}"}.
+          </p>
+
+          {variables.map((variable, index) => (
+            <div
+              key={keysRef.current[index]}
+              className="space-y-2 rounded-md border bg-muted/30 p-3"
+            >
+              <div className="flex items-center justify-between">
+                <span className="text-xs font-medium text-muted-foreground">
+                  Variable {index + 1}
+                </span>
+                <Button
+                  type="button"
+                  variant="ghost"
+                  size="icon"
+                  className="h-6 w-6 text-muted-foreground hover:text-destructive"
+                  onClick={() => removeVariable(index)}
+                  aria-label={`Remove variable ${index + 1}`}
+                >
+                  <Trash2 className="h-3.5 w-3.5" />
+                </Button>
+              </div>
+
+              <div className="grid grid-cols-2 gap-2">
+                <Input
+                  placeholder="name (e.g. language)"
+                  value={variable.name}
+                  onChange={(e) =>
+                    updateVariable(index, {
+                      name: e.target.value.replace(/[^a-zA-Z0-9_]/g, ""),
+                    })
+                  }
+                />
+                <Input
+                  placeholder="Label (e.g. Language)"
+                  value={variable.label}
+                  onChange={(e) => updateVariable(index, { label: e.target.value })}
+                />
+              </div>
+
+              <div className="grid grid-cols-2 gap-2">
+                <select
+                  value={variable.type}
+                  onChange={(e) =>
+                    updateVariable(index, {
+                      type: e.target.value as TemplateVariable["type"],
+                    })
+                  }
+                  className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
+                  aria-label="Variable type"
+                >
+                  <option value="text">Text</option>
+                  <option value="textarea">Textarea</option>
+                  <option value="select">Select</option>
+                </select>
+                <Input
+                  placeholder="Default value"
+                  value={variable.default ?? ""}
+                  onChange={(e) => updateVariable(index, { default: e.target.value || undefined })}
+                />
+              </div>
+
+              <div className="flex items-center gap-4">
+                <Input
+                  placeholder="Placeholder text"
+                  value={variable.placeholder ?? ""}
+                  onChange={(e) =>
+                    updateVariable(index, { placeholder: e.target.value || undefined })
+                  }
+                  className="flex-1"
+                />
+                <span className="flex items-center gap-2 text-sm whitespace-nowrap">
+                  <Switch
+                    checked={variable.required ?? false}
+                    onChange={(e) => updateVariable(index, { required: e.target.checked })}
+                    aria-label="Required"
+                  />
+                  Required
+                </span>
+              </div>
+
+              {variable.type === "select" && (
+                <Input
+                  placeholder="Options (comma-separated)"
+                  value={variable.options?.join(", ") ?? ""}
+                  onChange={(e) =>
+                    updateVariable(index, {
+                      options: e.target.value
+                        .split(",")
+                        .map((s) => s.trim())
+                        .filter(Boolean),
+                    })
+                  }
+                />
+              )}
+            </div>
+          ))}
+
+          <Button type="button" variant="outline" size="sm" onClick={addVariable}>
+            <Plus className="mr-1 h-3.5 w-3.5" />
+            Add Variable
+          </Button>
+        </div>
+      )}
+    </div>
+  );
+}
diff --git a/ui/src/components/TemplateVariableForm/TemplateVariableForm.tsx b/ui/src/components/TemplateVariableForm/TemplateVariableForm.tsx
new file mode 100644
index 00000000..4b9b87f2
--- /dev/null
+++ b/ui/src/components/TemplateVariableForm/TemplateVariableForm.tsx
@@ -0,0 +1,66 @@
+import { FormField } from "@/components/FormField/FormField";
+import { Input } from "@/components/Input/Input";
+import type { TemplateVariable } from "@/lib/templateVariables";
+
+export interface TemplateVariableFormProps {
+  variables: TemplateVariable[];
+  values: Record<string, string>;
+  onChange: (values: Record<string, string>) => void;
+  errors?: Record<string, string>;
+}
+
+export function TemplateVariableForm({
+  variables,
+  values,
+  onChange,
+  errors,
+}: TemplateVariableFormProps) {
+  const handleChange = (name: string, value: string) => {
+    onChange({ ...values, [name]: value });
+  };
+
+  return (
+    <div className="space-y-3">
+      {variables.map((v) => (
+        <FormField
+          key={v.name}
+          label={v.label}
+          htmlFor={`tplvar-${v.name}`}
+          required={v.required}
+          error={errors?.[v.name]}
+        >
+          {v.type === "select" ? (
+            <select
+              id={`tplvar-${v.name}`}
+              value={values[v.name] ?? v.default ?? ""}
+              onChange={(e) => handleChange(v.name, e.target.value)}
+              className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
+            >
+              <option value="">{v.placeholder ?? "Select..."}</option>
+              {v.options?.map((opt) => (
+                <option key={opt} value={opt}>
+                  {opt}
+                </option>
+              ))}
+            </select>
+          ) : v.type === "textarea" ? (
+            <textarea
+              id={`tplvar-${v.name}`}
+              value={values[v.name] ?? v.default ?? ""}
+              onChange={(e) => handleChange(v.name, e.target.value)}
+              placeholder={v.placeholder}
+              className="w-full min-h-[80px] rounded-md border bg-background px-3 py-2 text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 resize-y"
+            />
+          ) : (
+            <Input
+              id={`tplvar-${v.name}`}
+              value={values[v.name] ?? v.default ?? ""}
+              onChange={(e) => handleChange(v.name, e.target.value)}
+              placeholder={v.placeholder}
+            />
+          )}
+        </FormField>
+      ))}
+    </div>
+  );
+}
diff --git a/ui/src/components/UserMenu/UserMenu.tsx b/ui/src/components/UserMenu/UserMenu.tsx
index 3c2a3dc5..69e9784c 100644
--- a/ui/src/components/UserMenu/UserMenu.tsx
+++ b/ui/src/components/UserMenu/UserMenu.tsx
@@ -20,7 +20,7 @@ interface UserMenuProps {
 }
 
 export function UserMenu({ className }: UserMenuProps) {
-  const { user, logout, isAuthenticated } = useAuth();
+  const { user, logout, isAuthenticated, method } = useAuth();
   const { config } = useConfig();
   const navigate = useNavigate();
   const visibleNavItems = navItems.filter((item) => {
@@ -29,6 +29,7 @@ export function UserMenu({ className }: UserMenuProps) {
   });
   const showAdmin = config?.admin.enabled && hasAdminAccess(user);
   const allNavItems = showAdmin ? [...visibleNavItems, adminNavItem] : visibleNavItems;
+  const isAnonymous = method === "none";
 
   if (!isAuthenticated) {
     return null;
@@ -53,7 +54,7 @@ export function UserMenu({ className }: UserMenuProps) {
         )}
         aria-label="User menu"
       >
-        {initials || <User className="h-4 w-4" />}
+        {isAnonymous ? <User className="h-4 w-4" /> : initials || <User className="h-4 w-4" />}
       </DropdownTrigger>
       <DropdownContent align="end" className="w-56">
         <DropdownLabel>
diff --git a/ui/src/hooks/useUserPrompts.ts b/ui/src/hooks/useUserPrompts.ts
index 3bcc67a5..d9bb0a91 100644
--- a/ui/src/hooks/useUserPrompts.ts
+++ b/ui/src/hooks/useUserPrompts.ts
@@ -3,83 +3,131 @@ import { useQuery, useQueries } from "@tanstack/react-query";
 
 import {
   organizationListOptions,
-  promptListByOrgOptions,
+  templateListByOrgOptions,
+  templateListByUserOptions,
 } from "@/api/generated/@tanstack/react-query.gen";
-import type { Organization, Prompt } from "@/api/generated/types.gen";
+import type { Organization, Template } from "@/api/generated/types.gen";
+import { useAuth } from "@/auth";
 
-export interface PromptWithOrg extends Prompt {
-  org_id: string;
-  org_slug: string;
-  org_name: string;
+export interface TemplateWithContext extends Template {
+  /** Which org this template is accessible through (for org/team/project-owned templates) */
+  org_id?: string;
+  org_slug?: string;
+  org_name?: string;
 }
 
-export interface UseUserPromptsResult {
-  prompts: PromptWithOrg[];
+export interface UseUserTemplatesResult {
+  templates: TemplateWithContext[];
   organizations: Organization[];
   isLoading: boolean;
   error: Error | null;
+  hasMore: boolean;
 }
 
 /**
- * Hook to fetch all prompts accessible to the current user.
+ * Hook to fetch all templates accessible to the current user.
  *
- * This fetches all organizations the user can access, then fetches
- * prompts for each organization. The authorization layer on the
- * backend filters what the user can see based on their permissions.
+ * Fetches:
+ * - Templates owned by the user directly
+ * - Templates from each organization the user can access (includes org, team, and project-scoped templates
+ *   that the backend authorization layer makes visible)
  */
-export function useUserPrompts(): UseUserPromptsResult {
-  // First, fetch all organizations the user can access
+export function useUserTemplates(): UseUserTemplatesResult {
+  const { user } = useAuth();
+
+  // Fetch user's own templates
+  const {
+    data: userTemplatesData,
+    isLoading: userTemplatesLoading,
+    error: userTemplatesError,
+  } = useQuery({
+    ...templateListByUserOptions({
+      path: { user_id: user?.id ?? "" },
+      query: { limit: 50 },
+    }),
+    staleTime: 5 * 60 * 1000,
+    enabled: !!user?.id,
+  });
+
+  // Fetch all organizations the user can access
   const {
     data: orgsData,
     isLoading: orgsLoading,
     error: orgsError,
   } = useQuery({
     ...organizationListOptions(),
-    staleTime: 5 * 60 * 1000, // Cache for 5 minutes
+    staleTime: 5 * 60 * 1000,
   });
 
   const organizations = useMemo(() => orgsData?.data ?? [], [orgsData?.data]);
 
-  // Then fetch prompts for each organization
-  const promptQueries = useQueries({
+  // Fetch templates for each organization (backend returns org + team + project templates the user can see)
+  const orgQueries = useQueries({
     queries: organizations.map((org) => ({
-      ...promptListByOrgOptions({
+      ...templateListByOrgOptions({
         path: { org_slug: org.slug },
-        query: { limit: 50 }, // Get up to 50 prompts per org
+        query: { limit: 50 },
       }),
       staleTime: 5 * 60 * 1000,
       enabled: organizations.length > 0,
     })),
   });
 
-  // Combine all prompts with their org info
-  const prompts = useMemo(() => {
-    const result: PromptWithOrg[] = [];
+  // Combine all templates, deduplicating by id
+  const templates = useMemo(() => {
+    const seen = new Set<string>();
+    const result: TemplateWithContext[] = [];
+
+    // User-owned templates first
+    for (const t of userTemplatesData?.data ?? []) {
+      if (!seen.has(t.id)) {
+        seen.add(t.id);
+        result.push(t);
+      }
+    }
+
+    // Then org-scoped templates
     for (let i = 0; i < organizations.length; i++) {
       const org = organizations[i];
-      const promptsData = promptQueries[i]?.data?.data ?? [];
-      for (const prompt of promptsData) {
-        result.push({
-          ...prompt,
-          org_id: org.id,
-          org_slug: org.slug,
-          org_name: org.name,
-        });
+      for (const t of orgQueries[i]?.data?.data ?? []) {
+        if (!seen.has(t.id)) {
+          seen.add(t.id);
+          result.push({
+            ...t,
+            org_id: org.id,
+            org_slug: org.slug,
+            org_name: org.name,
+          });
+        }
       }
     }
-    // Sort prompts by name
+
     result.sort((a, b) => a.name.localeCompare(b.name));
     return result;
-  }, [organizations, promptQueries]);
+  }, [userTemplatesData?.data, organizations, orgQueries]);
 
-  const isLoading = orgsLoading || promptQueries.some((q) => q.isLoading);
-  const promptError = promptQueries.find((q) => q.error)?.error;
-  const error = orgsError ?? promptError ?? null;
+  const isLoading = userTemplatesLoading || orgsLoading || orgQueries.some((q) => q.isLoading);
+  const queryError = orgQueries.find((q) => q.error)?.error;
+  const error = userTemplatesError ?? orgsError ?? queryError ?? null;
+  const hasMore =
+    (userTemplatesData?.pagination?.has_more ?? false) ||
+    orgQueries.some((q) => q.data?.pagination?.has_more === true);
 
   return {
-    prompts,
+    templates,
     organizations,
     isLoading,
     error: error as Error | null,
+    hasMore,
   };
 }
+
+// Backwards compat aliases
+export type PromptWithOrg = TemplateWithContext;
+export interface UseUserPromptsResult extends UseUserTemplatesResult {
+  prompts: TemplateWithContext[];
+}
+export const useUserPrompts = (): UseUserPromptsResult => {
+  const result = useUserTemplates();
+  return { ...result, prompts: result.templates };
+};
diff --git a/ui/src/lib/templateVariables.ts b/ui/src/lib/templateVariables.ts
new file mode 100644
index 00000000..80a1c290
--- /dev/null
+++ b/ui/src/lib/templateVariables.ts
@@ -0,0 +1,52 @@
+export interface TemplateVariable {
+  /** {{name}} reference in content */
+  name: string;
+  /** Display label */
+  label: string;
+  /** Field type */
+  type: "text" | "textarea" | "select";
+  /** Whether the variable is required */
+  required?: boolean;
+  /** Placeholder text */
+  placeholder?: string;
+  /** Default value */
+  default?: string;
+  /** Options for select type */
+  options?: string[];
+}
+
+/** Parse variables from prompt metadata */
+export function parseVariables(metadata?: Record<string, unknown> | null): TemplateVariable[] {
+  if (!metadata?.variables || !Array.isArray(metadata.variables)) return [];
+  return metadata.variables.filter(
+    (v: unknown): v is TemplateVariable =>
+      typeof v === "object" && v !== null && "name" in v && "label" in v && "type" in v
+  );
+}
+
+/** Extract {{var}} references from content string */
+export function extractVariableRefs(content: string): string[] {
+  const matches = content.match(/\{\{(\w+)\}\}/g);
+  if (!matches) return [];
+  return [...new Set(matches.map((m) => m.slice(2, -2)))];
+}
+
+/** Replace {{var}} placeholders with provided values */
+export function substituteVariables(content: string, values: Record<string, string>): string {
+  return content.replace(/\{\{(\w+)\}\}/g, (match, name: string) => values[name] ?? match);
+}
+
+/** Validate variable values, return map of field name -> error message */
+export function validateVariableValues(
+  variables: TemplateVariable[],
+  values: Record<string, string>
+): Record<string, string> {
+  const errors: Record<string, string> = {};
+  for (const v of variables) {
+    const val = values[v.name]?.trim() ?? "";
+    if (v.required && !val) {
+      errors[v.name] = `${v.label} is required`;
+    }
+  }
+  return errors;
+}
diff --git a/ui/src/pages/admin/OrganizationDetailPage.tsx b/ui/src/pages/admin/OrganizationDetailPage.tsx
index 0a75c1a2..598f6275 100644
--- a/ui/src/pages/admin/OrganizationDetailPage.tsx
+++ b/ui/src/pages/admin/OrganizationDetailPage.tsx
@@ -13,6 +13,7 @@ import {
   Shield,
   BarChart3,
   Building2,
+  ClipboardPenLine,
 } from "lucide-react";
 import { useState } from "react";
 import { useForm } from "react-hook-form";
@@ -26,6 +27,8 @@ import {
   apiKeyListByOrgOptions,
   dynamicProviderListByOrgOptions,
   modelPricingListByOrgOptions,
+  templateListByOrgOptions,
+  templateDeleteMutation,
   teamListOptions,
   orgMemberAddMutation,
   orgMemberRemoveMutation,
@@ -38,6 +41,7 @@ import type {
   ApiKey,
   DynamicProvider,
   DbModelPricing,
+  Template,
 } from "@/api/generated/types.gen";
 import { Button } from "@/components/Button/Button";
 import { Card, CardContent, CardHeader, CardTitle } from "@/components/Card/Card";
@@ -53,6 +57,7 @@ import {
   DetailPageHeader,
   TabNavigation,
   AddMemberModal,
+  AdminPromptFormModal,
   ApiKeyStatusBadge,
   EnabledStatusBadge,
   type Tab,
@@ -60,6 +65,7 @@ import {
 import { Badge } from "@/components/Badge/Badge";
 import { formatDateTime, formatCurrency } from "@/utils/formatters";
 import UsageDashboard from "@/components/UsageDashboard/UsageDashboard";
+import { createTemplateColumns } from "./promptColumns";
 
 type TabId =
   | "projects"
@@ -68,6 +74,7 @@ type TabId =
   | "api-keys"
   | "providers"
   | "pricing"
+  | "templates"
   | "sso"
   | "usage";
 
@@ -78,6 +85,7 @@ const tabs: Tab<TabId>[] = [
   { id: "api-keys", label: "API Keys", icon: <Key className="h-4 w-4" /> },
   { id: "providers", label: "Providers", icon: <Server className="h-4 w-4" /> },
   { id: "pricing", label: "Pricing", icon: <DollarSign className="h-4 w-4" /> },
+  { id: "templates", label: "Templates", icon: <ClipboardPenLine className="h-4 w-4" /> },
   { id: "usage", label: "Usage", icon: <BarChart3 className="h-4 w-4" /> },
   { id: "sso", label: "SSO", icon: <Shield className="h-4 w-4" /> },
 ];
@@ -105,6 +113,8 @@ export default function OrganizationDetailPage() {
   const [activeTab, setActiveTab] = useState<TabId>("projects");
   const [isEditModalOpen, setIsEditModalOpen] = useState(false);
   const [isAddMemberModalOpen, setIsAddMemberModalOpen] = useState(false);
+  const [isPromptModalOpen, setIsPromptModalOpen] = useState(false);
+  const [editingPrompt, setEditingPrompt] = useState<Template | null>(null);
 
   const editForm = useForm<EditOrgForm>({
     resolver: zodResolver(editOrgSchema),
@@ -154,6 +164,12 @@ export default function OrganizationDetailPage() {
     enabled: activeTab === "pricing",
   });
 
+  // Fetch templates
+  const { data: promptsData, isLoading: promptsLoading } = useQuery({
+    ...templateListByOrgOptions({ path: { org_slug: slug! } }),
+    enabled: activeTab === "templates",
+  });
+
   // Fetch all users for member selection
   const { data: allUsers } = useQuery({
     ...userListOptions(),
@@ -198,6 +214,37 @@ export default function OrganizationDetailPage() {
     },
   });
 
+  // Delete prompt mutation
+  const deletePromptMutation = useMutation({
+    ...templateDeleteMutation(),
+    onSuccess: () => {
+      queryClient.invalidateQueries({ queryKey: [{ _id: "templateListByOrg" }] });
+      toast({ title: "Template deleted", type: "success" });
+    },
+    onError: (error) => {
+      toast({ title: "Failed to delete template", description: String(error), type: "error" });
+    },
+  });
+
+  const handlePromptEdit = (prompt: Template) => {
+    setEditingPrompt(prompt);
+    setIsPromptModalOpen(true);
+  };
+
+  const handlePromptDelete = async (prompt: Template) => {
+    const confirmed = await confirm({
+      title: "Delete Template",
+      message: `Are you sure you want to delete "${prompt.name}"? This action cannot be undone.`,
+      confirmLabel: "Delete",
+      variant: "destructive",
+    });
+    if (confirmed) {
+      deletePromptMutation.mutate({ path: { id: prompt.id } });
+    }
+  };
+
+  const promptColumns = createTemplateColumns(handlePromptEdit, handlePromptDelete);
+
   const onEditSubmit = (data: EditOrgForm) => {
     updateMutation.mutate({
       path: { slug: slug! },
@@ -426,6 +473,18 @@ export default function OrganizationDetailPage() {
               New Project
             </Button>
           )}
+          {activeTab === "templates" && (
+            <Button
+              size="sm"
+              onClick={() => {
+                setEditingPrompt(null);
+                setIsPromptModalOpen(true);
+              }}
+            >
+              <Plus className="mr-2 h-4 w-4" />
+              New Template
+            </Button>
+          )}
         </CardHeader>
         <CardContent>
           {activeTab === "projects" && (
@@ -488,6 +547,16 @@ export default function OrganizationDetailPage() {
               searchPlaceholder="Search models..."
             />
           )}
+          {activeTab === "templates" && (
+            <DataTable
+              columns={promptColumns as ColumnDef<Template>[]}
+              data={promptsData?.data || []}
+              isLoading={promptsLoading}
+              emptyMessage="No templates in this organization."
+              searchColumn="name"
+              searchPlaceholder="Search templates..."
+            />
+          )}
           {activeTab === "usage" && slug && org?.id && (
             <UsageDashboard scope={{ type: "organization", slug, orgId: org.id }} />
           )}
@@ -563,6 +632,25 @@ export default function OrganizationDetailPage() {
         isLoading={addMemberMutation.isPending}
         emptyMessage="All users are already members of this organization."
       />
+
+      {/* Prompt Template Modal */}
+      {org && (
+        <AdminPromptFormModal
+          open={isPromptModalOpen}
+          onClose={() => {
+            setIsPromptModalOpen(false);
+            setEditingPrompt(null);
+          }}
+          editingPrompt={editingPrompt}
+          ownerOverride={{ type: "organization", organization_id: org.id }}
+          onSaved={() => {
+            toast({
+              title: editingPrompt ? "Template updated" : "Template created",
+              type: "success",
+            });
+          }}
+        />
+      )}
     </div>
   );
 }
diff --git a/ui/src/pages/admin/ProjectDetailPage.tsx b/ui/src/pages/admin/ProjectDetailPage.tsx
index 5cd4546d..0bd46116 100644
--- a/ui/src/pages/admin/ProjectDetailPage.tsx
+++ b/ui/src/pages/admin/ProjectDetailPage.tsx
@@ -18,6 +18,7 @@ import {
   Power,
   PowerOff,
   Calendar,
+  ClipboardPenLine,
 } from "lucide-react";
 import { useState } from "react";
 import { useForm } from "react-hook-form";
@@ -30,6 +31,8 @@ import {
   apiKeyListByProjectOptions,
   dynamicProviderListByProjectOptions,
   modelPricingListByProjectOptions,
+  templateListByProjectOptions,
+  templateDeleteMutation,
   projectMemberAddMutation,
   projectMemberRemoveMutation,
   dynamicProviderCreateMutation,
@@ -44,6 +47,7 @@ import type {
   ApiKey,
   DynamicProvider,
   DbModelPricing,
+  Template,
   CreateDynamicProvider,
   UpdateDynamicProvider,
   ConnectivityTestResponse,
@@ -69,6 +73,7 @@ import {
   DetailPageHeader,
   TabNavigation,
   AddMemberModal,
+  AdminPromptFormModal,
   ApiKeyStatusBadge,
   EnabledStatusBadge,
   ProviderFormModal,
@@ -78,14 +83,16 @@ import {
 import { getProviderTypeLabel, TestResultDisplay } from "@/pages/providers/shared";
 import { formatDateTime, formatCurrency } from "@/utils/formatters";
 import UsageDashboard from "@/components/UsageDashboard/UsageDashboard";
+import { createTemplateColumns } from "./promptColumns";
 
-type TabId = "members" | "api-keys" | "providers" | "pricing" | "usage";
+type TabId = "members" | "api-keys" | "providers" | "pricing" | "templates" | "usage";
 
 const tabs: Tab<TabId>[] = [
   { id: "members", label: "Members", icon: <Users className="h-4 w-4" /> },
   { id: "api-keys", label: "API Keys", icon: <Key className="h-4 w-4" /> },
   { id: "providers", label: "Providers", icon: <Server className="h-4 w-4" /> },
   { id: "pricing", label: "Pricing", icon: <DollarSign className="h-4 w-4" /> },
+  { id: "templates", label: "Templates", icon: <ClipboardPenLine className="h-4 w-4" /> },
   { id: "usage", label: "Usage", icon: <BarChart3 className="h-4 w-4" /> },
 ];
 
@@ -332,6 +339,8 @@ export default function ProjectDetailPage() {
   const [isAddMemberModalOpen, setIsAddMemberModalOpen] = useState(false);
   const [isProviderModalOpen, setIsProviderModalOpen] = useState(false);
   const [editingProvider, setEditingProvider] = useState<DynamicProvider | null>(null);
+  const [isPromptModalOpen, setIsPromptModalOpen] = useState(false);
+  const [editingPrompt, setEditingPrompt] = useState<Template | null>(null);
   const [providerSearch, setProviderSearch] = useState("");
   const [testResults, setTestResults] = useState<Record<string, ConnectivityTestResponse>>({});
   const [testingIds, setTestingIds] = useState<Set<string>>(new Set());
@@ -380,6 +389,14 @@ export default function ProjectDetailPage() {
     enabled: activeTab === "pricing",
   });
 
+  // Fetch templates
+  const { data: promptsData, isLoading: promptsLoading } = useQuery({
+    ...templateListByProjectOptions({
+      path: { org_slug: orgSlug!, project_slug: projectSlug! },
+    }),
+    enabled: activeTab === "templates",
+  });
+
   // Fetch all users for member selection
   const { data: allUsers } = useQuery({
     ...userListOptions(),
@@ -492,6 +509,37 @@ export default function ProjectDetailPage() {
     },
   });
 
+  // Delete prompt mutation
+  const deletePromptMutation = useMutation({
+    ...templateDeleteMutation(),
+    onSuccess: () => {
+      queryClient.invalidateQueries({ queryKey: [{ _id: "templateListByProject" }] });
+      toast({ title: "Template deleted", type: "success" });
+    },
+    onError: (error) => {
+      toast({ title: "Failed to delete template", description: String(error), type: "error" });
+    },
+  });
+
+  const handlePromptEdit = (prompt: Template) => {
+    setEditingPrompt(prompt);
+    setIsPromptModalOpen(true);
+  };
+
+  const handlePromptDelete = async (prompt: Template) => {
+    const confirmed = await confirm({
+      title: "Delete Template",
+      message: `Are you sure you want to delete "${prompt.name}"? This action cannot be undone.`,
+      confirmLabel: "Delete",
+      variant: "destructive",
+    });
+    if (confirmed) {
+      deletePromptMutation.mutate({ path: { id: prompt.id } });
+    }
+  };
+
+  const promptColumns = createTemplateColumns(handlePromptEdit, handlePromptDelete);
+
   const handleProviderCreate = () => {
     setEditingProvider(null);
     setIsProviderModalOpen(true);
@@ -711,6 +759,18 @@ export default function ProjectDetailPage() {
               New Provider
             </Button>
           )}
+          {activeTab === "templates" && (
+            <Button
+              size="sm"
+              onClick={() => {
+                setEditingPrompt(null);
+                setIsPromptModalOpen(true);
+              }}
+            >
+              <Plus className="mr-2 h-4 w-4" />
+              New Template
+            </Button>
+          )}
         </CardHeader>
         <CardContent>
           {activeTab === "members" && (
@@ -758,6 +818,16 @@ export default function ProjectDetailPage() {
               searchPlaceholder="Search models..."
             />
           )}
+          {activeTab === "templates" && (
+            <DataTable
+              columns={promptColumns as ColumnDef<Template>[]}
+              data={promptsData?.data || []}
+              isLoading={promptsLoading}
+              emptyMessage="No templates in this project."
+              searchColumn="name"
+              searchPlaceholder="Search templates..."
+            />
+          )}
           {activeTab === "usage" && orgSlug && projectSlug && project?.id && (
             <UsageDashboard
               scope={{ type: "project", orgSlug, projectSlug, projectId: project.id }}
@@ -830,6 +900,25 @@ export default function ProjectDetailPage() {
         editingProvider={editingProvider}
         ownerOverride={{ type: "project", project_id: project.id }}
       />
+
+      {/* Prompt Template Modal */}
+      {project && (
+        <AdminPromptFormModal
+          open={isPromptModalOpen}
+          onClose={() => {
+            setIsPromptModalOpen(false);
+            setEditingPrompt(null);
+          }}
+          editingPrompt={editingPrompt}
+          ownerOverride={{ type: "project", project_id: project.id }}
+          onSaved={() => {
+            toast({
+              title: editingPrompt ? "Template updated" : "Template created",
+              type: "success",
+            });
+          }}
+        />
+      )}
     </div>
   );
 }
diff --git a/ui/src/pages/admin/TeamDetailPage.tsx b/ui/src/pages/admin/TeamDetailPage.tsx
index 1f35b349..65114bba 100644
--- a/ui/src/pages/admin/TeamDetailPage.tsx
+++ b/ui/src/pages/admin/TeamDetailPage.tsx
@@ -2,7 +2,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
 import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
 import { createColumnHelper, type ColumnDef } from "@tanstack/react-table";
 import { useParams, useNavigate } from "react-router-dom";
-import { ArrowLeft, Users, Plus, Trash2, BarChart3 } from "lucide-react";
+import { ArrowLeft, Users, Plus, Trash2, BarChart3, ClipboardPenLine } from "lucide-react";
 import { useState } from "react";
 import { useForm } from "react-hook-form";
 import { z } from "zod";
@@ -13,9 +13,11 @@ import {
   teamMemberListOptions,
   teamMemberAddMutation,
   teamMemberRemoveMutation,
+  templateListByTeamOptions,
+  templateDeleteMutation,
   userListOptions,
 } from "@/api/generated/@tanstack/react-query.gen";
-import type { TeamMember } from "@/api/generated/types.gen";
+import type { TeamMember, Template } from "@/api/generated/types.gen";
 import { Button } from "@/components/Button/Button";
 import { Card, CardContent, CardHeader, CardTitle } from "@/components/Card/Card";
 import { CodeBadge } from "@/components/CodeBadge/CodeBadge";
@@ -27,14 +29,22 @@ import { Skeleton } from "@/components/Skeleton/Skeleton";
 import { Badge } from "@/components/Badge/Badge";
 import { useToast } from "@/components/Toast/Toast";
 import { useConfirm } from "@/components/ConfirmDialog/ConfirmDialog";
-import { DetailPageHeader, TabNavigation, AddMemberModal, type Tab } from "@/components/Admin";
+import {
+  DetailPageHeader,
+  TabNavigation,
+  AddMemberModal,
+  AdminPromptFormModal,
+  type Tab,
+} from "@/components/Admin";
 import { formatDateTime } from "@/utils/formatters";
 import UsageDashboard from "@/components/UsageDashboard/UsageDashboard";
+import { createTemplateColumns } from "./promptColumns";
 
-type TabId = "members" | "usage";
+type TabId = "members" | "templates" | "usage";
 
 const tabs: Tab<TabId>[] = [
   { id: "members", label: "Members", icon: <Users className="h-4 w-4" /> },
+  { id: "templates", label: "Templates", icon: <ClipboardPenLine className="h-4 w-4" /> },
   { id: "usage", label: "Usage", icon: <BarChart3 className="h-4 w-4" /> },
 ];
 
@@ -56,6 +66,8 @@ export default function TeamDetailPage() {
   const [activeTab, setActiveTab] = useState<TabId>("members");
   const [isEditModalOpen, setIsEditModalOpen] = useState(false);
   const [isAddMemberModalOpen, setIsAddMemberModalOpen] = useState(false);
+  const [isPromptModalOpen, setIsPromptModalOpen] = useState(false);
+  const [editingPrompt, setEditingPrompt] = useState<Template | null>(null);
 
   const editForm = useForm<EditTeamForm>({
     resolver: zodResolver(editTeamSchema),
@@ -77,6 +89,14 @@ export default function TeamDetailPage() {
     enabled: activeTab === "members",
   });
 
+  // Fetch templates
+  const { data: promptsData, isLoading: promptsLoading } = useQuery({
+    ...templateListByTeamOptions({
+      path: { org_slug: orgSlug!, team_slug: teamSlug! },
+    }),
+    enabled: activeTab === "templates",
+  });
+
   // Fetch all users for member selection
   const { data: allUsers } = useQuery({
     ...userListOptions(),
@@ -121,6 +141,37 @@ export default function TeamDetailPage() {
     },
   });
 
+  // Delete prompt mutation
+  const deletePromptMutation = useMutation({
+    ...templateDeleteMutation(),
+    onSuccess: () => {
+      queryClient.invalidateQueries({ queryKey: [{ _id: "templateListByTeam" }] });
+      toast({ title: "Template deleted", type: "success" });
+    },
+    onError: (error) => {
+      toast({ title: "Failed to delete template", description: String(error), type: "error" });
+    },
+  });
+
+  const handlePromptEdit = (prompt: Template) => {
+    setEditingPrompt(prompt);
+    setIsPromptModalOpen(true);
+  };
+
+  const handlePromptDelete = async (prompt: Template) => {
+    const confirmed = await confirm({
+      title: "Delete Template",
+      message: `Are you sure you want to delete "${prompt.name}"? This action cannot be undone.`,
+      confirmLabel: "Delete",
+      variant: "destructive",
+    });
+    if (confirmed) {
+      deletePromptMutation.mutate({ path: { id: prompt.id } });
+    }
+  };
+
+  const promptColumns = createTemplateColumns(handlePromptEdit, handlePromptDelete);
+
   const onEditSubmit = (data: EditTeamForm) => {
     updateMutation.mutate({
       path: { org_slug: orgSlug!, team_slug: teamSlug! },
@@ -246,6 +297,18 @@ export default function TeamDetailPage() {
               Add Member
             </Button>
           )}
+          {activeTab === "templates" && (
+            <Button
+              size="sm"
+              onClick={() => {
+                setEditingPrompt(null);
+                setIsPromptModalOpen(true);
+              }}
+            >
+              <Plus className="mr-2 h-4 w-4" />
+              New Template
+            </Button>
+          )}
         </CardHeader>
         <CardContent>
           {activeTab === "members" && (
@@ -258,6 +321,16 @@ export default function TeamDetailPage() {
               searchPlaceholder="Search members..."
             />
           )}
+          {activeTab === "templates" && (
+            <DataTable
+              columns={promptColumns as ColumnDef<Template>[]}
+              data={promptsData?.data || []}
+              isLoading={promptsLoading}
+              emptyMessage="No templates in this team."
+              searchColumn="name"
+              searchPlaceholder="Search templates..."
+            />
+          )}
           {activeTab === "usage" && orgSlug && teamSlug && team?.id && (
             <UsageDashboard scope={{ type: "team", orgSlug, teamSlug, teamId: team.id }} />
           )}
@@ -298,6 +371,25 @@ export default function TeamDetailPage() {
         isLoading={addMemberMutation.isPending}
         emptyMessage="All users are already members of this team."
       />
+
+      {/* Prompt Template Modal */}
+      {team && (
+        <AdminPromptFormModal
+          open={isPromptModalOpen}
+          onClose={() => {
+            setIsPromptModalOpen(false);
+            setEditingPrompt(null);
+          }}
+          editingPrompt={editingPrompt}
+          ownerOverride={{ type: "team", team_id: team.id }}
+          onSaved={() => {
+            toast({
+              title: editingPrompt ? "Template updated" : "Template created",
+              type: "success",
+            });
+          }}
+        />
+      )}
     </div>
   );
 }
diff --git a/ui/src/pages/admin/promptColumns.tsx b/ui/src/pages/admin/promptColumns.tsx
new file mode 100644
index 00000000..367dba84
--- /dev/null
+++ b/ui/src/pages/admin/promptColumns.tsx
@@ -0,0 +1,69 @@
+import { createColumnHelper, type ColumnDef } from "@tanstack/react-table";
+import { MoreHorizontal, Pencil, Trash2 } from "lucide-react";
+
+import type { Template } from "@/api/generated/types.gen";
+import {
+  Dropdown,
+  DropdownContent,
+  DropdownItem,
+  DropdownTrigger,
+} from "@/components/Dropdown/Dropdown";
+import { formatDateTime } from "@/utils/formatters";
+
+const columnHelper = createColumnHelper<Template>();
+
+export function createTemplateColumns(
+  onEdit: (prompt: Template) => void,
+  onDelete: (prompt: Template) => void
+): ColumnDef<Template, unknown>[] {
+  return [
+    columnHelper.accessor("name", {
+      header: "Name",
+      cell: (info) => <span className="font-medium">{info.getValue()}</span>,
+    }),
+    columnHelper.accessor("description", {
+      header: "Description",
+      cell: (info) => {
+        const val = info.getValue();
+        return val ? (
+          <span className="line-clamp-1">{val}</span>
+        ) : (
+          <span className="text-muted-foreground">-</span>
+        );
+      },
+    }),
+    columnHelper.accessor("content", {
+      header: "Content",
+      cell: (info) => (
+        <span className="text-muted-foreground line-clamp-1 max-w-[200px]">
+          {info.getValue().slice(0, 80)}
+          {info.getValue().length > 80 ? "..." : ""}
+        </span>
+      ),
+    }),
+    columnHelper.accessor("created_at", {
+      header: "Created",
+      cell: (info) => formatDateTime(info.getValue()),
+    }),
+    columnHelper.display({
+      id: "actions",
+      cell: ({ row }) => (
+        <Dropdown>
+          <DropdownTrigger aria-label="Template actions" variant="ghost" className="h-8 w-8 p-0">
+            <MoreHorizontal className="h-4 w-4" />
+          </DropdownTrigger>
+          <DropdownContent align="end">
+            <DropdownItem onClick={() => onEdit(row.original)}>
+              <Pencil className="mr-2 h-4 w-4" />
+              Edit
+            </DropdownItem>
+            <DropdownItem className="text-destructive" onClick={() => onDelete(row.original)}>
+              <Trash2 className="mr-2 h-4 w-4" />
+              Delete
+            </DropdownItem>
+          </DropdownContent>
+        </Dropdown>
+      ),
+    }),
+  ] as ColumnDef<Template, unknown>[];
+}
diff --git a/ui/src/pages/project/ProjectDetailPage.tsx b/ui/src/pages/project/ProjectDetailPage.tsx
index e7e3db41..3638ef14 100644
--- a/ui/src/pages/project/ProjectDetailPage.tsx
+++ b/ui/src/pages/project/ProjectDetailPage.tsx
@@ -1,7 +1,16 @@
 import { zodResolver } from "@hookform/resolvers/zod";
 import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
 import { useParams, Link } from "react-router-dom";
-import { ArrowLeft, Users, Key, Server, DollarSign, BarChart3, Pencil } from "lucide-react";
+import {
+  ArrowLeft,
+  Users,
+  Key,
+  Server,
+  DollarSign,
+  BarChart3,
+  Pencil,
+  ClipboardPenLine,
+} from "lucide-react";
 import { useState } from "react";
 import { useForm } from "react-hook-form";
 import { z } from "zod";
@@ -25,15 +34,17 @@ import { MembersTab } from "./MembersTab";
 import { ApiKeysTab } from "./ApiKeysTab";
 import { ProvidersTab } from "./ProvidersTab";
 import { PricingTab } from "./PricingTab";
+import { TemplatesTab } from "./TemplatesTab";
 import { UsageTab } from "./UsageTab";
 
-type TabId = "members" | "api-keys" | "providers" | "pricing" | "usage";
+type TabId = "members" | "api-keys" | "providers" | "pricing" | "templates" | "usage";
 
 const tabs: Tab<TabId>[] = [
   { id: "members", label: "Members", icon: <Users className="h-4 w-4" /> },
   { id: "api-keys", label: "API Keys", icon: <Key className="h-4 w-4" /> },
   { id: "providers", label: "Providers", icon: <Server className="h-4 w-4" /> },
   { id: "pricing", label: "Pricing", icon: <DollarSign className="h-4 w-4" /> },
+  { id: "templates", label: "Templates", icon: <ClipboardPenLine className="h-4 w-4" /> },
   { id: "usage", label: "Usage", icon: <BarChart3 className="h-4 w-4" /> },
 ];
 
@@ -166,6 +177,9 @@ export default function ProjectDetailPage() {
         <ProvidersTab orgSlug={orgSlug!} projectSlug={projectSlug!} projectId={project.id} />
       )}
       {activeTab === "pricing" && <PricingTab orgSlug={orgSlug!} projectSlug={projectSlug!} />}
+      {activeTab === "templates" && (
+        <TemplatesTab orgSlug={orgSlug!} projectSlug={projectSlug!} projectId={project.id} />
+      )}
       {activeTab === "usage" && (
         <UsageTab orgSlug={orgSlug!} projectSlug={projectSlug!} projectId={project.id} />
       )}
diff --git a/ui/src/pages/project/TemplatesTab.tsx b/ui/src/pages/project/TemplatesTab.tsx
new file mode 100644
index 00000000..e06bcefe
--- /dev/null
+++ b/ui/src/pages/project/TemplatesTab.tsx
@@ -0,0 +1,114 @@
+import { useState } from "react";
+import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
+import { type ColumnDef } from "@tanstack/react-table";
+import { Plus } from "lucide-react";
+
+import {
+  templateListByProjectOptions,
+  templateDeleteMutation,
+} from "@/api/generated/@tanstack/react-query.gen";
+import type { Template } from "@/api/generated/types.gen";
+import { Button } from "@/components/Button/Button";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/Card/Card";
+import { DataTable } from "@/components/DataTable/DataTable";
+import { AdminPromptFormModal } from "@/components/Admin";
+import { useToast } from "@/components/Toast/Toast";
+import { useConfirm } from "@/components/ConfirmDialog/ConfirmDialog";
+import { createTemplateColumns } from "@/pages/admin/promptColumns";
+
+interface TemplatesTabProps {
+  orgSlug: string;
+  projectSlug: string;
+  projectId: string;
+}
+
+export function TemplatesTab({ orgSlug, projectSlug, projectId }: TemplatesTabProps) {
+  const { toast } = useToast();
+  const confirm = useConfirm();
+  const queryClient = useQueryClient();
+  const [isModalOpen, setIsModalOpen] = useState(false);
+  const [editingTemplate, setEditingTemplate] = useState<Template | null>(null);
+
+  const { data: templatesData, isLoading } = useQuery(
+    templateListByProjectOptions({
+      path: { org_slug: orgSlug, project_slug: projectSlug },
+    })
+  );
+
+  const deleteTemplateMutation = useMutation({
+    ...templateDeleteMutation(),
+    onSuccess: () => {
+      queryClient.invalidateQueries({ queryKey: [{ _id: "templateListByProject" }] });
+      toast({ title: "Template deleted", type: "success" });
+    },
+    onError: (error) => {
+      toast({ title: "Failed to delete template", description: String(error), type: "error" });
+    },
+  });
+
+  const handleEdit = (template: Template) => {
+    setEditingTemplate(template);
+    setIsModalOpen(true);
+  };
+
+  const handleDelete = async (template: Template) => {
+    const confirmed = await confirm({
+      title: "Delete Template",
+      message: `Are you sure you want to delete "${template.name}"? This action cannot be undone.`,
+      confirmLabel: "Delete",
+      variant: "destructive",
+    });
+    if (confirmed) {
+      deleteTemplateMutation.mutate({ path: { id: template.id } });
+    }
+  };
+
+  const columns = createTemplateColumns(handleEdit, handleDelete);
+
+  return (
+    <>
+      <Card>
+        <CardHeader className="flex-row items-center justify-between">
+          <CardTitle>Templates</CardTitle>
+          <Button
+            size="sm"
+            onClick={() => {
+              setEditingTemplate(null);
+              setIsModalOpen(true);
+            }}
+          >
+            <Plus className="mr-2 h-4 w-4" />
+            New Template
+          </Button>
+        </CardHeader>
+        <CardContent>
+          <DataTable
+            columns={columns as ColumnDef<Template>[]}
+            data={templatesData?.data || []}
+            isLoading={isLoading}
+            emptyMessage="No templates in this project."
+            searchColumn="name"
+            searchPlaceholder="Search templates..."
+          />
+        </CardContent>
+      </Card>
+
+      <AdminPromptFormModal
+        open={isModalOpen}
+        onClose={() => {
+          setIsModalOpen(false);
+          setEditingTemplate(null);
+        }}
+        editingPrompt={editingTemplate}
+        ownerOverride={{ type: "project", project_id: projectId }}
+        onSaved={() => {
+          queryClient.invalidateQueries({ queryKey: [{ _id: "templateListByProject" }] });
+          toast({
+            title: editingTemplate ? "Template updated" : "Template created",
+            type: "success",
+          });
+        }}
+      />
+    </>
+  );
+}
