Last Updated: October 11, 2025
diff --git a/apps/sim/app/(landing)/terms/page.tsx b/apps/sim/app/(landing)/terms/page.tsx
index 61805f461dc..e5c6099649e 100644
--- a/apps/sim/app/(landing)/terms/page.tsx
+++ b/apps/sim/app/(landing)/terms/page.tsx
@@ -1,19 +1,11 @@
-'use client'
-
-import { useEffect } from 'react'
import Link from 'next/link'
import { getEnv } from '@/lib/core/config/env'
-import { LegalLayout } from '@/app/(landing)/components'
+import { ExternalRedirect, LegalLayout } from '@/app/(landing)/components'
export default function TermsOfService() {
- useEffect(() => {
- const termsUrl = getEnv('NEXT_PUBLIC_TERMS_URL')
- if (termsUrl?.startsWith('http')) {
- window.location.href = termsUrl
- }
- }, [])
return (
+
Last Updated: October 11, 2025
diff --git a/apps/sim/app/api/help/integration-request/route.ts b/apps/sim/app/api/help/integration-request/route.ts
new file mode 100644
index 00000000000..c6773d2f686
--- /dev/null
+++ b/apps/sim/app/api/help/integration-request/route.ts
@@ -0,0 +1,110 @@
+import { createLogger } from '@sim/logger'
+import { type NextRequest, NextResponse } from 'next/server'
+import { z } from 'zod'
+import { env } from '@/lib/core/config/env'
+import type { TokenBucketConfig } from '@/lib/core/rate-limiter'
+import { RateLimiter } from '@/lib/core/rate-limiter'
+import { generateRequestId } from '@/lib/core/utils/request'
+import { getEmailDomain } from '@/lib/core/utils/urls'
+import { sendEmail } from '@/lib/messaging/email/mailer'
+import { getFromEmailAddress } from '@/lib/messaging/email/utils'
+
+const logger = createLogger('IntegrationRequestAPI')
+
+const rateLimiter = new RateLimiter()
+
+const PUBLIC_ENDPOINT_RATE_LIMIT: TokenBucketConfig = {
+ maxTokens: 10,
+ refillRate: 5,
+ refillIntervalMs: 60_000,
+}
+
+const integrationRequestSchema = z.object({
+ integrationName: z.string().min(1, 'Integration name is required').max(200),
+ email: z.string().email('A valid email is required'),
+ useCase: z.string().max(2000).optional(),
+})
+
+export async function POST(req: NextRequest) {
+ const requestId = generateRequestId()
+
+ try {
+ const ip = req.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ?? 'unknown'
+ const storageKey = `public:integration-request:${ip}`
+
+ const { allowed, remaining, resetAt } = await rateLimiter.checkRateLimitDirect(
+ storageKey,
+ PUBLIC_ENDPOINT_RATE_LIMIT
+ )
+
+ if (!allowed) {
+ logger.warn(`[${requestId}] Rate limit exceeded for IP ${ip}`, { remaining, resetAt })
+ return NextResponse.json(
+ { error: 'Too many requests. Please try again later.' },
+ {
+ status: 429,
+ headers: { 'Retry-After': String(Math.ceil((resetAt.getTime() - Date.now()) / 1000)) },
+ }
+ )
+ }
+
+ const body = await req.json()
+
+ const validationResult = integrationRequestSchema.safeParse(body)
+ if (!validationResult.success) {
+ logger.warn(`[${requestId}] Invalid integration request data`, {
+ errors: validationResult.error.format(),
+ })
+ return NextResponse.json(
+ { error: 'Invalid request data', details: validationResult.error.format() },
+ { status: 400 }
+ )
+ }
+
+ const { integrationName, email, useCase } = validationResult.data
+
+ logger.info(`[${requestId}] Processing integration request`, {
+ integrationName,
+ email: `${email.substring(0, 3)}***`,
+ })
+
+ const emailText = `Integration: ${integrationName}
+From: ${email}
+Submitted: ${new Date().toISOString()}
+
+${useCase ? `Use Case:\n${useCase}` : 'No use case provided.'}
+`
+
+ const emailResult = await sendEmail({
+ to: [`help@${env.EMAIL_DOMAIN || getEmailDomain()}`],
+ subject: `[INTEGRATION REQUEST] ${integrationName}`,
+ text: emailText,
+ from: getFromEmailAddress(),
+ replyTo: email,
+ emailType: 'transactional',
+ })
+
+ if (!emailResult.success) {
+ logger.error(`[${requestId}] Error sending integration request email`, emailResult.message)
+ return NextResponse.json({ error: 'Failed to send request' }, { status: 500 })
+ }
+
+ logger.info(`[${requestId}] Integration request email sent successfully`)
+
+ return NextResponse.json(
+ { success: true, message: 'Integration request submitted successfully' },
+ { status: 200 }
+ )
+ } catch (error) {
+ if (error instanceof Error && error.message.includes('not configured')) {
+ logger.error(`[${requestId}] Email service configuration error`, error)
+ return NextResponse.json(
+ { error: 'Email service is temporarily unavailable. Please try again later.' },
+ { status: 500 }
+ )
+ }
+
+ logger.error(`[${requestId}] Error processing integration request`, error)
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
+ }
+}
diff --git a/apps/sim/app/changelog/layout.tsx b/apps/sim/app/changelog/layout.tsx
index e7d7153dfca..5fbd2896730 100644
--- a/apps/sim/app/changelog/layout.tsx
+++ b/apps/sim/app/changelog/layout.tsx
@@ -1,14 +1,16 @@
+import { getNavBlogPosts } from '@/lib/blog/registry'
import { martianMono } from '@/app/_styles/fonts/martian-mono/martian-mono'
import Footer from '@/app/(home)/components/footer/footer'
import Navbar from '@/app/(home)/components/navbar/navbar'
-export default function ChangelogLayout({ children }: { children: React.ReactNode }) {
+export default async function ChangelogLayout({ children }: { children: React.ReactNode }) {
+ const blogPosts = await getNavBlogPosts()
return (
{children}
diff --git a/apps/sim/app/llms.txt/route.ts b/apps/sim/app/llms.txt/route.ts
index d604ff443fc..21fefd84c36 100644
--- a/apps/sim/app/llms.txt/route.ts
+++ b/apps/sim/app/llms.txt/route.ts
@@ -12,7 +12,6 @@ Sim lets teams create agents, workflows, knowledge bases, tables, and docs. Over
## Core Pages
- [Homepage](${baseUrl}): Product overview, features, and pricing
-- [Templates](${baseUrl}/templates): Pre-built workflow templates to get started quickly
- [Changelog](${baseUrl}/changelog): Product updates and release notes
- [Sim Blog](${baseUrl}/blog): Announcements, insights, and guides
diff --git a/apps/sim/app/not-found.tsx b/apps/sim/app/not-found.tsx
index 90be7c5df3f..5556ae4ccab 100644
--- a/apps/sim/app/not-found.tsx
+++ b/apps/sim/app/not-found.tsx
@@ -1,16 +1,18 @@
import Link from 'next/link'
+import { getNavBlogPosts } from '@/lib/blog/registry'
import AuthBackground from '@/app/(auth)/components/auth-background'
import Navbar from '@/app/(home)/components/navbar/navbar'
const CTA_BASE =
'inline-flex items-center h-[32px] rounded-[5px] border px-[10px] font-[430] font-season text-[14px]'
-export default function NotFound() {
+export default async function NotFound() {
+ const blogPosts = await getNavBlogPosts()
return (
diff --git a/apps/sim/app/sitemap.ts b/apps/sim/app/sitemap.ts
index 3472ff961be..a3553f13d74 100644
--- a/apps/sim/app/sitemap.ts
+++ b/apps/sim/app/sitemap.ts
@@ -20,10 +20,10 @@ export default async function sitemap(): Promise
{
url: `${baseUrl}/blog/tags`,
lastModified: now,
},
- {
- url: `${baseUrl}/templates`,
- lastModified: now,
- },
+ // {
+ // url: `${baseUrl}/templates`,
+ // lastModified: now,
+ // },
{
url: `${baseUrl}/changelog`,
lastModified: now,
diff --git a/apps/sim/app/templates/[id]/layout.tsx b/apps/sim/app/templates/[id]/layout.tsx
index ed557f2b2fd..b815db12d7e 100644
--- a/apps/sim/app/templates/[id]/layout.tsx
+++ b/apps/sim/app/templates/[id]/layout.tsx
@@ -1,44 +1,20 @@
-import { db } from '@sim/db'
-import { permissions, workspace } from '@sim/db/schema'
-import { and, desc, eq } from 'drizzle-orm'
-import { redirect } from 'next/navigation'
-import { getSession } from '@/lib/auth'
+// import { db } from '@sim/db'
+// import { permissions, workspace } from '@sim/db/schema'
+// import { and, desc, eq } from 'drizzle-orm'
+// import { redirect } from 'next/navigation'
+// import { getSession } from '@/lib/auth'
-export const dynamic = 'force-dynamic'
-export const revalidate = 0
+// export const dynamic = 'force-dynamic'
+// export const revalidate = 0
interface TemplateLayoutProps {
children: React.ReactNode
- params: Promise<{
- id: string
- }>
}
/**
- * Template detail layout (public scope).
- * - If user is authenticated, redirect to workspace-scoped template detail.
- * - Otherwise render the public template detail children.
+ * Template detail layout (public scope) — currently disabled.
+ * Previously redirected authenticated users to the workspace-scoped template detail.
*/
-export default async function TemplateDetailLayout({ children, params }: TemplateLayoutProps) {
- const { id } = await params
- const session = await getSession()
-
- if (session?.user?.id) {
- const userWorkspaces = await db
- .select({
- workspace: workspace,
- })
- .from(permissions)
- .innerJoin(workspace, eq(permissions.entityId, workspace.id))
- .where(and(eq(permissions.userId, session.user.id), eq(permissions.entityType, 'workspace')))
- .orderBy(desc(workspace.createdAt))
- .limit(1)
-
- if (userWorkspaces.length > 0) {
- const firstWorkspace = userWorkspaces[0].workspace
- redirect(`/workspace/${firstWorkspace.id}/templates/${id}`)
- }
- }
-
+export default function TemplateDetailLayout({ children }: TemplateLayoutProps) {
return children
}
diff --git a/apps/sim/app/templates/[id]/page.tsx b/apps/sim/app/templates/[id]/page.tsx
index af9348be357..0c2557e4ad7 100644
--- a/apps/sim/app/templates/[id]/page.tsx
+++ b/apps/sim/app/templates/[id]/page.tsx
@@ -1,92 +1,93 @@
-import { db } from '@sim/db'
-import { templateCreators, templates } from '@sim/db/schema'
-import { createLogger } from '@sim/logger'
-import { eq } from 'drizzle-orm'
-import type { Metadata } from 'next'
-import { getBaseUrl } from '@/lib/core/utils/urls'
-import TemplateDetails from '@/app/templates/[id]/template'
+import { notFound } from 'next/navigation'
-const logger = createLogger('TemplateMetadata')
+// import { db } from '@sim/db'
+// import { templateCreators, templates } from '@sim/db/schema'
+// import { createLogger } from '@sim/logger'
+// import { eq } from 'drizzle-orm'
+// import type { Metadata } from 'next'
+// import { getBaseUrl } from '@/lib/core/utils/urls'
+// import TemplateDetails from '@/app/templates/[id]/template'
-/**
- * Generate dynamic metadata for template pages.
- * This provides OpenGraph images for social media sharing.
- */
-export async function generateMetadata({
- params,
-}: {
- params: Promise<{ id: string }>
-}): Promise {
- const { id } = await params
-
- try {
- const result = await db
- .select({
- template: templates,
- creator: templateCreators,
- })
- .from(templates)
- .leftJoin(templateCreators, eq(templates.creatorId, templateCreators.id))
- .where(eq(templates.id, id))
- .limit(1)
-
- if (result.length === 0) {
- return {
- title: 'Template Not Found',
- description: 'The requested template could not be found.',
- }
- }
+// const logger = createLogger('TemplateMetadata')
- const { template, creator } = result[0]
- const baseUrl = getBaseUrl()
-
- const details = template.details as { tagline?: string; about?: string } | null
- const description = details?.tagline || 'AI workflow template on Sim'
-
- const hasOgImage = !!template.ogImageUrl
- const ogImageUrl = template.ogImageUrl || `${baseUrl}/logo/primary/rounded.png`
-
- return {
- title: template.name,
- description,
- openGraph: {
- title: template.name,
- description,
- type: 'website',
- url: `${baseUrl}/templates/${id}`,
- siteName: 'Sim',
- images: [
- {
- url: ogImageUrl,
- width: hasOgImage ? 1200 : 512,
- height: hasOgImage ? 630 : 512,
- alt: `${template.name} - Workflow Preview`,
- },
- ],
- },
- twitter: {
- card: hasOgImage ? 'summary_large_image' : 'summary',
- title: template.name,
- description,
- images: [ogImageUrl],
- creator: creator?.details
- ? ((creator.details as Record).xHandle as string) || undefined
- : undefined,
- },
- }
- } catch (error) {
- logger.error('Failed to generate template metadata:', error)
- return {
- title: 'Template',
- description: 'AI workflow template on Sim',
- }
- }
-}
+// /**
+// * Generate dynamic metadata for template pages.
+// * This provides OpenGraph images for social media sharing.
+// */
+// export async function generateMetadata({
+// params,
+// }: {
+// params: Promise<{ id: string }>
+// }): Promise {
+// const { id } = await params
+//
+// try {
+// const result = await db
+// .select({
+// template: templates,
+// creator: templateCreators,
+// })
+// .from(templates)
+// .leftJoin(templateCreators, eq(templates.creatorId, templateCreators.id))
+// .where(eq(templates.id, id))
+// .limit(1)
+//
+// if (result.length === 0) {
+// return {
+// title: 'Template Not Found',
+// description: 'The requested template could not be found.',
+// }
+// }
+//
+// const { template, creator } = result[0]
+// const baseUrl = getBaseUrl()
+//
+// const details = template.details as { tagline?: string; about?: string } | null
+// const description = details?.tagline || 'AI workflow template on Sim'
+//
+// const hasOgImage = !!template.ogImageUrl
+// const ogImageUrl = template.ogImageUrl || `${baseUrl}/logo/primary/rounded.png`
+//
+// return {
+// title: template.name,
+// description,
+// openGraph: {
+// title: template.name,
+// description,
+// type: 'website',
+// url: `${baseUrl}/templates/${id}`,
+// siteName: 'Sim',
+// images: [
+// {
+// url: ogImageUrl,
+// width: hasOgImage ? 1200 : 512,
+// height: hasOgImage ? 630 : 512,
+// alt: `${template.name} - Workflow Preview`,
+// },
+// ],
+// },
+// twitter: {
+// card: hasOgImage ? 'summary_large_image' : 'summary',
+// title: template.name,
+// description,
+// images: [ogImageUrl],
+// creator: creator?.details
+// ? ((creator.details as Record).xHandle as string) || undefined
+// : undefined,
+// },
+// }
+// } catch (error) {
+// logger.error('Failed to generate template metadata:', error)
+// return {
+// title: 'Template',
+// description: 'AI workflow template on Sim',
+// }
+// }
+// }
/**
- * Public template detail page for unauthenticated users.
- * Authenticated-user redirect is handled in templates/[id]/layout.tsx.
+ * Public template detail page — currently disabled, returns 404.
*/
export default function TemplatePage() {
- return
+ notFound()
}
diff --git a/apps/sim/app/templates/page.tsx b/apps/sim/app/templates/page.tsx
index c74818acd3b..d2a8c12abf4 100644
--- a/apps/sim/app/templates/page.tsx
+++ b/apps/sim/app/templates/page.tsx
@@ -1,73 +1,79 @@
-import { db } from '@sim/db'
-import { permissions, templateCreators, templates, workspace } from '@sim/db/schema'
-import { and, desc, eq } from 'drizzle-orm'
-import type { Metadata } from 'next'
-import { redirect } from 'next/navigation'
-import { getSession } from '@/lib/auth'
-import type { Template } from '@/app/templates/templates'
-import Templates from '@/app/templates/templates'
+import { notFound } from 'next/navigation'
-export const metadata: Metadata = {
- title: 'Templates',
- description:
- 'Browse pre-built workflow templates to get started quickly with AI agents, automations, and integrations.',
-}
+// import { db } from '@sim/db'
+// import { permissions, templateCreators, templates, workspace } from '@sim/db/schema'
+// import { and, desc, eq } from 'drizzle-orm'
+// import type { Metadata } from 'next'
+// import { redirect } from 'next/navigation'
+// import { getSession } from '@/lib/auth'
+// import type { Template } from '@/app/templates/templates'
+// import Templates from '@/app/templates/templates'
+
+// export const metadata: Metadata = {
+// title: 'Templates',
+// description:
+// 'Browse pre-built workflow templates to get started quickly with AI agents, automations, and integrations.',
+// }
/**
* Public templates list page.
- * Redirects authenticated users to their workspace-scoped templates page.
- * Allows unauthenticated users to view templates for SEO and discovery.
+ * Currently disabled — returns 404.
*/
-export default async function TemplatesPage() {
- const session = await getSession()
-
- // Authenticated users: redirect to workspace-scoped templates
- if (session?.user?.id) {
- const userWorkspaces = await db
- .select({
- workspace: workspace,
- })
- .from(permissions)
- .innerJoin(workspace, eq(permissions.entityId, workspace.id))
- .where(and(eq(permissions.userId, session.user.id), eq(permissions.entityType, 'workspace')))
- .orderBy(desc(workspace.createdAt))
- .limit(1)
-
- if (userWorkspaces.length > 0) {
- const firstWorkspace = userWorkspaces[0].workspace
- redirect(`/workspace/${firstWorkspace.id}/templates`)
- }
- }
-
- // Unauthenticated users: show public templates
- const templatesData = await db
- .select({
- id: templates.id,
- workflowId: templates.workflowId,
- name: templates.name,
- details: templates.details,
- creatorId: templates.creatorId,
- creator: templateCreators,
- views: templates.views,
- stars: templates.stars,
- status: templates.status,
- tags: templates.tags,
- requiredCredentials: templates.requiredCredentials,
- state: templates.state,
- createdAt: templates.createdAt,
- updatedAt: templates.updatedAt,
- })
- .from(templates)
- .leftJoin(templateCreators, eq(templates.creatorId, templateCreators.id))
- .where(eq(templates.status, 'approved'))
- .orderBy(desc(templates.views), desc(templates.createdAt))
- .then((rows) => rows.map((row) => ({ ...row, isStarred: false })))
+export default function TemplatesPage() {
+ notFound()
- return (
-
- )
+ // Redirects authenticated users to their workspace-scoped templates page.
+ // Allows unauthenticated users to view templates for SEO and discovery.
+ //
+ // const session = await getSession()
+ //
+ // // Authenticated users: redirect to workspace-scoped templates
+ // if (session?.user?.id) {
+ // const userWorkspaces = await db
+ // .select({
+ // workspace: workspace,
+ // })
+ // .from(permissions)
+ // .innerJoin(workspace, eq(permissions.entityId, workspace.id))
+ // .where(and(eq(permissions.userId, session.user.id), eq(permissions.entityType, 'workspace')))
+ // .orderBy(desc(workspace.createdAt))
+ // .limit(1)
+ //
+ // if (userWorkspaces.length > 0) {
+ // const firstWorkspace = userWorkspaces[0].workspace
+ // redirect(`/workspace/${firstWorkspace.id}/templates`)
+ // }
+ // }
+ //
+ // // Unauthenticated users: show public templates
+ // const templatesData = await db
+ // .select({
+ // id: templates.id,
+ // workflowId: templates.workflowId,
+ // name: templates.name,
+ // details: templates.details,
+ // creatorId: templates.creatorId,
+ // creator: templateCreators,
+ // views: templates.views,
+ // stars: templates.stars,
+ // status: templates.status,
+ // tags: templates.tags,
+ // requiredCredentials: templates.requiredCredentials,
+ // state: templates.state,
+ // createdAt: templates.createdAt,
+ // updatedAt: templates.updatedAt,
+ // })
+ // .from(templates)
+ // .leftJoin(templateCreators, eq(templates.creatorId, templateCreators.id))
+ // .where(eq(templates.status, 'approved'))
+ // .orderBy(desc(templates.views), desc(templates.createdAt))
+ // .then((rows) => rows.map((row) => ({ ...row, isStarred: false })))
+ //
+ // return (
+ //
+ // )
}
diff --git a/apps/sim/app/workspace/[workspaceId]/home/home.tsx b/apps/sim/app/workspace/[workspaceId]/home/home.tsx
index 90adeda0480..1ffa07466ab 100644
--- a/apps/sim/app/workspace/[workspaceId]/home/home.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/home/home.tsx
@@ -8,7 +8,6 @@ import { getDocumentIcon } from '@/components/icons/document-icons'
import { useSession } from '@/lib/auth/auth-client'
import {
LandingPromptStorage,
- LandingTemplateStorage,
type LandingWorkflowSeed,
LandingWorkflowSeedStorage,
} from '@/lib/core/utils/browser-storage'
@@ -118,12 +117,12 @@ export function Home({ chatId }: HomeProps = {}) {
return
}
- const templateId = LandingTemplateStorage.consume()
- if (templateId) {
- logger.info('Retrieved landing page template, redirecting to template detail')
- router.replace(`/workspace/${workspaceId}/templates/${templateId}?use=true`)
- return
- }
+ // const templateId = LandingTemplateStorage.consume()
+ // if (templateId) {
+ // logger.info('Retrieved landing page template, redirecting to template detail')
+ // router.replace(`/workspace/${workspaceId}/templates/${templateId}?use=true`)
+ // return
+ // }
const prompt = LandingPromptStorage.consume()
if (prompt) {
diff --git a/apps/sim/app/workspace/[workspaceId]/settings/[section]/settings.tsx b/apps/sim/app/workspace/[workspaceId]/settings/[section]/settings.tsx
index a9a007303c8..fd01097fde9 100644
--- a/apps/sim/app/workspace/[workspaceId]/settings/[section]/settings.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/settings/[section]/settings.tsx
@@ -57,13 +57,13 @@ const Credentials = dynamic(
),
{ loading: () => }
)
-const TemplateProfile = dynamic(
- () =>
- import(
- '@/app/workspace/[workspaceId]/settings/components/template-profile/template-profile'
- ).then((m) => m.TemplateProfile),
- { loading: () => }
-)
+// const TemplateProfile = dynamic(
+// () =>
+// import(
+// '@/app/workspace/[workspaceId]/settings/components/template-profile/template-profile'
+// ).then((m) => m.TemplateProfile),
+// { loading: () => }
+// )
const CredentialSets = dynamic(
() =>
import(
@@ -177,7 +177,7 @@ export function SettingsPage({ section }: SettingsPageProps) {
{effectiveSection === 'general' && }
{effectiveSection === 'integrations' && }
{effectiveSection === 'secrets' && }
- {effectiveSection === 'template-profile' && }
+ {/* {effectiveSection === 'template-profile' && } */}
{effectiveSection === 'credential-sets' && }
{effectiveSection === 'access-control' && }
{effectiveSection === 'apikeys' && }
diff --git a/apps/sim/app/workspace/[workspaceId]/settings/navigation.ts b/apps/sim/app/workspace/[workspaceId]/settings/navigation.ts
index e333e52808a..9ec049a209b 100644
--- a/apps/sim/app/workspace/[workspaceId]/settings/navigation.ts
+++ b/apps/sim/app/workspace/[workspaceId]/settings/navigation.ts
@@ -14,7 +14,6 @@ import {
ShieldCheck,
TerminalWindow,
TrashOutline,
- User,
Users,
Wrench,
} from '@/components/emcn'
@@ -84,7 +83,7 @@ export const sectionConfig: { key: NavigationSection; title: string }[] = [
export const allNavigationItems: NavigationItem[] = [
{ id: 'general', label: 'General', icon: Settings, section: 'account' },
- { id: 'template-profile', label: 'Template Profile', icon: User, section: 'account' },
+ // { id: 'template-profile', label: 'Template Profile', icon: User, section: 'account' },
{
id: 'access-control',
label: 'Access Control',
diff --git a/apps/sim/app/workspace/[workspaceId]/templates/[id]/page.tsx b/apps/sim/app/workspace/[workspaceId]/templates/[id]/page.tsx
index 261e178f2ba..068fb9b0541 100644
--- a/apps/sim/app/workspace/[workspaceId]/templates/[id]/page.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/templates/[id]/page.tsx
@@ -1,115 +1,103 @@
-import { db } from '@sim/db'
-import { templateCreators, templates } from '@sim/db/schema'
-import { createLogger } from '@sim/logger'
-import { eq } from 'drizzle-orm'
-import type { Metadata } from 'next'
-import { redirect } from 'next/navigation'
-import { getSession } from '@/lib/auth'
-import { getBaseUrl } from '@/lib/core/utils/urls'
-import { verifyWorkspaceMembership } from '@/app/api/workflows/utils'
-import TemplateDetails from '@/app/templates/[id]/template'
+import { notFound } from 'next/navigation'
-const logger = createLogger('WorkspaceTemplateMetadata')
+// import { db } from '@sim/db'
+// import { templateCreators, templates } from '@sim/db/schema'
+// import { createLogger } from '@sim/logger'
+// import { eq } from 'drizzle-orm'
+// import type { Metadata } from 'next'
+// import { redirect } from 'next/navigation'
+// import { getSession } from '@/lib/auth'
+// import { getBaseUrl } from '@/lib/core/utils/urls'
+// import { verifyWorkspaceMembership } from '@/app/api/workflows/utils'
+// import TemplateDetails from '@/app/templates/[id]/template'
-interface TemplatePageProps {
- params: Promise<{
- workspaceId: string
- id: string
- }>
-}
-
-/**
- * Generate dynamic metadata for workspace template pages.
- * This provides OpenGraph images for social media sharing.
- */
-export async function generateMetadata({
- params,
-}: {
- params: Promise<{ workspaceId: string; id: string }>
-}): Promise {
- const { workspaceId, id } = await params
-
- try {
- const result = await db
- .select({
- template: templates,
- creator: templateCreators,
- })
- .from(templates)
- .leftJoin(templateCreators, eq(templates.creatorId, templateCreators.id))
- .where(eq(templates.id, id))
- .limit(1)
-
- if (result.length === 0) {
- return {
- title: 'Template Not Found',
- description: 'The requested template could not be found.',
- }
- }
+// const logger = createLogger('WorkspaceTemplateMetadata')
- const { template, creator } = result[0]
- const baseUrl = getBaseUrl()
+// interface TemplatePageProps {
+// params: Promise<{
+// workspaceId: string
+// id: string
+// }>
+// }
- const details = template.details as { tagline?: string; about?: string } | null
- const description = details?.tagline || 'AI workflow template on Sim'
-
- const hasOgImage = !!template.ogImageUrl
- const ogImageUrl = template.ogImageUrl || `${baseUrl}/logo/primary/rounded.png`
-
- return {
- title: template.name,
- description,
- openGraph: {
- title: template.name,
- description,
- type: 'website',
- url: `${baseUrl}/workspace/${workspaceId}/templates/${id}`,
- siteName: 'Sim',
- images: [
- {
- url: ogImageUrl,
- width: hasOgImage ? 1200 : 512,
- height: hasOgImage ? 630 : 512,
- alt: `${template.name} - Workflow Preview`,
- },
- ],
- },
- twitter: {
- card: hasOgImage ? 'summary_large_image' : 'summary',
- title: template.name,
- description,
- images: [ogImageUrl],
- creator: creator?.details
- ? ((creator.details as Record).xHandle as string) || undefined
- : undefined,
- },
- }
- } catch (error) {
- logger.error('Failed to generate workspace template metadata:', error)
- return {
- title: 'Template',
- description: 'AI workflow template on Sim',
- }
- }
-}
+// /**
+// * Generate dynamic metadata for workspace template pages.
+// * This provides OpenGraph images for social media sharing.
+// */
+// export async function generateMetadata({
+// params,
+// }: {
+// params: Promise<{ workspaceId: string; id: string }>
+// }): Promise {
+// const { workspaceId, id } = await params
+//
+// try {
+// const result = await db
+// .select({
+// template: templates,
+// creator: templateCreators,
+// })
+// .from(templates)
+// .leftJoin(templateCreators, eq(templates.creatorId, templateCreators.id))
+// .where(eq(templates.id, id))
+// .limit(1)
+//
+// if (result.length === 0) {
+// return {
+// title: 'Template Not Found',
+// description: 'The requested template could not be found.',
+// }
+// }
+//
+// const { template, creator } = result[0]
+// const baseUrl = getBaseUrl()
+//
+// const details = template.details as { tagline?: string; about?: string } | null
+// const description = details?.tagline || 'AI workflow template on Sim'
+//
+// const hasOgImage = !!template.ogImageUrl
+// const ogImageUrl = template.ogImageUrl || `${baseUrl}/logo/primary/rounded.png`
+//
+// return {
+// title: template.name,
+// description,
+// openGraph: {
+// title: template.name,
+// description,
+// type: 'website',
+// url: `${baseUrl}/workspace/${workspaceId}/templates/${id}`,
+// siteName: 'Sim',
+// images: [
+// {
+// url: ogImageUrl,
+// width: hasOgImage ? 1200 : 512,
+// height: hasOgImage ? 630 : 512,
+// alt: `${template.name} - Workflow Preview`,
+// },
+// ],
+// },
+// twitter: {
+// card: hasOgImage ? 'summary_large_image' : 'summary',
+// title: template.name,
+// description,
+// images: [ogImageUrl],
+// creator: creator?.details
+// ? ((creator.details as Record).xHandle as string) || undefined
+// : undefined,
+// },
+// }
+// } catch (error) {
+// logger.error('Failed to generate workspace template metadata:', error)
+// return {
+// title: 'Template',
+// description: 'AI workflow template on Sim',
+// }
+// }
+// }
/**
- * Workspace-scoped template detail page.
- * Requires authentication and workspace membership to access.
- * Uses the shared TemplateDetails component with workspace context.
+ * Workspace-scoped template detail page — currently disabled, returns 404.
*/
-export default async function TemplatePage({ params }: TemplatePageProps) {
- const { workspaceId, id } = await params
- const session = await getSession()
-
- if (!session?.user?.id) {
- redirect(`/templates/${id}`)
- }
-
- const hasPermission = await verifyWorkspaceMembership(session.user.id, workspaceId)
- if (!hasPermission) {
- redirect('/')
- }
-
- return
+export default function TemplatePage() {
+ notFound()
}
diff --git a/apps/sim/app/workspace/[workspaceId]/templates/page.tsx b/apps/sim/app/workspace/[workspaceId]/templates/page.tsx
index 38a3cffeb2c..d14d5478a47 100644
--- a/apps/sim/app/workspace/[workspaceId]/templates/page.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/templates/page.tsx
@@ -1,205 +1,29 @@
-import { db } from '@sim/db'
-import { settings, templateCreators, templateStars, templates, user } from '@sim/db/schema'
-import { and, desc, eq, sql } from 'drizzle-orm'
-import type { Metadata } from 'next'
-import { redirect } from 'next/navigation'
-import { getSession } from '@/lib/auth'
-import { verifyWorkspaceMembership } from '@/app/api/workflows/utils'
-import type { Template as WorkspaceTemplate } from '@/app/workspace/[workspaceId]/templates/templates'
-import Templates from '@/app/workspace/[workspaceId]/templates/templates'
-import { getUserPermissionConfig } from '@/ee/access-control/utils/permission-check'
-
-export const metadata: Metadata = {
- title: 'Templates',
-}
-
-interface TemplatesPageProps {
- params: Promise<{
- workspaceId: string
- }>
-}
+import { notFound } from 'next/navigation'
+
+// import { db } from '@sim/db'
+// import { settings, templateCreators, templateStars, templates, user } from '@sim/db/schema'
+// import { and, desc, eq, sql } from 'drizzle-orm'
+// import type { Metadata } from 'next'
+// import { redirect } from 'next/navigation'
+// import { getSession } from '@/lib/auth'
+// import { verifyWorkspaceMembership } from '@/app/api/workflows/utils'
+// import type { Template as WorkspaceTemplate } from '@/app/workspace/[workspaceId]/templates/templates'
+// import Templates from '@/app/workspace/[workspaceId]/templates/templates'
+// import { getUserPermissionConfig } from '@/ee/access-control/utils/permission-check'
+
+// export const metadata: Metadata = {
+// title: 'Templates',
+// }
+
+// interface TemplatesPageProps {
+// params: Promise<{
+// workspaceId: string
+// }>
+// }
/**
- * Workspace-scoped Templates page.
- * Requires authentication and workspace membership to access.
+ * Workspace-scoped Templates page — currently disabled, returns 404.
*/
-export default async function TemplatesPage({ params }: TemplatesPageProps) {
- const { workspaceId } = await params
- const session = await getSession()
-
- // Redirect unauthenticated users to public templates page
- if (!session?.user?.id) {
- redirect('/templates')
- }
-
- // Verify workspace membership
- const hasPermission = await verifyWorkspaceMembership(session.user.id, workspaceId)
- if (!hasPermission) {
- redirect('/')
- }
-
- // Check permission group restrictions
- const permissionConfig = await getUserPermissionConfig(session.user.id)
- if (permissionConfig?.hideTemplates) {
- redirect(`/workspace/${workspaceId}`)
- }
-
- // Determine effective super user (admin role AND UI mode enabled)
- const currentUser = await db
- .select({ role: user.role })
- .from(user)
- .where(eq(user.id, session.user.id))
- .limit(1)
- const userSettings = await db
- .select({ superUserModeEnabled: settings.superUserModeEnabled })
- .from(settings)
- .where(eq(settings.userId, session.user.id))
- .limit(1)
-
- const isSuperUser = currentUser[0]?.role === 'admin'
- const superUserModeEnabled = userSettings[0]?.superUserModeEnabled ?? false
- const effectiveSuperUser = isSuperUser && superUserModeEnabled
-
- // Load templates from database
- let rows:
- | Array<{
- id: string
- workflowId: string | null
- name: string
- details?: unknown
- creatorId: string | null
- creator: {
- id: string
- referenceType: 'user' | 'organization'
- referenceId: string
- name: string
- profileImageUrl?: string | null
- details?: unknown
- verified: boolean
- } | null
- views: number
- stars: number
- status: 'pending' | 'approved' | 'rejected'
- tags: string[]
- requiredCredentials: unknown
- state: unknown
- createdAt: Date | string
- updatedAt: Date | string
- isStarred?: boolean
- }>
- | undefined
-
- if (session?.user?.id) {
- const whereCondition = effectiveSuperUser ? undefined : eq(templates.status, 'approved')
- rows = await db
- .select({
- id: templates.id,
- workflowId: templates.workflowId,
- name: templates.name,
- details: templates.details,
- creatorId: templates.creatorId,
- creator: templateCreators,
- views: templates.views,
- stars: templates.stars,
- status: templates.status,
- tags: templates.tags,
- requiredCredentials: templates.requiredCredentials,
- state: templates.state,
- createdAt: templates.createdAt,
- updatedAt: templates.updatedAt,
- isStarred: sql`CASE WHEN ${templateStars.id} IS NOT NULL THEN true ELSE false END`,
- })
- .from(templates)
- .leftJoin(
- templateStars,
- and(eq(templateStars.templateId, templates.id), eq(templateStars.userId, session.user.id))
- )
- .leftJoin(templateCreators, eq(templates.creatorId, templateCreators.id))
- .where(whereCondition)
- .orderBy(desc(templates.views), desc(templates.createdAt))
- } else {
- rows = await db
- .select({
- id: templates.id,
- workflowId: templates.workflowId,
- name: templates.name,
- details: templates.details,
- creatorId: templates.creatorId,
- creator: templateCreators,
- views: templates.views,
- stars: templates.stars,
- status: templates.status,
- tags: templates.tags,
- requiredCredentials: templates.requiredCredentials,
- state: templates.state,
- createdAt: templates.createdAt,
- updatedAt: templates.updatedAt,
- })
- .from(templates)
- .leftJoin(templateCreators, eq(templates.creatorId, templateCreators.id))
- .where(eq(templates.status, 'approved'))
- .orderBy(desc(templates.views), desc(templates.createdAt))
- .then((r) => r.map((row) => ({ ...row, isStarred: false })))
- }
-
- const initialTemplates: WorkspaceTemplate[] =
- rows?.map((row) => {
- const authorType = (row.creator?.referenceType as 'user' | 'organization') ?? 'user'
- const organizationId =
- row.creator?.referenceType === 'organization' ? row.creator.referenceId : null
- const userId =
- row.creator?.referenceType === 'user' ? row.creator.referenceId : '' /* no owner context */
-
- return {
- // New structure fields
- id: row.id,
- workflowId: row.workflowId,
- name: row.name,
- details: row.details as { tagline?: string; about?: string } | null,
- creatorId: row.creatorId,
- creator: row.creator
- ? {
- id: row.creator.id,
- name: row.creator.name,
- profileImageUrl: row.creator.profileImageUrl,
- details: row.creator.details as {
- about?: string
- xUrl?: string
- linkedinUrl?: string
- websiteUrl?: string
- contactEmail?: string
- } | null,
- referenceType: row.creator.referenceType,
- referenceId: row.creator.referenceId,
- verified: row.creator.verified,
- }
- : null,
- views: row.views,
- stars: row.stars,
- status: row.status,
- tags: row.tags,
- requiredCredentials: row.requiredCredentials,
- state: row.state as WorkspaceTemplate['state'],
- createdAt: row.createdAt,
- updatedAt: row.updatedAt,
- isStarred: row.isStarred ?? false,
- isSuperUser: effectiveSuperUser,
- // Legacy fields for backward compatibility
- userId,
- description: (row.details as any)?.tagline ?? null,
- author: row.creator?.name ?? 'Unknown',
- authorType,
- organizationId,
- color: '#3972F6', // default color for workspace cards
- icon: 'Workflow', // default icon for workspace cards
- }
- }) ?? []
-
- return (
-
- )
+export default function TemplatesPage() {
+ notFound()
}
diff --git a/apps/sim/app/workspace/[workspaceId]/utils/commands-utils.ts b/apps/sim/app/workspace/[workspaceId]/utils/commands-utils.ts
index f4110084a31..fd9e9678287 100644
--- a/apps/sim/app/workspace/[workspaceId]/utils/commands-utils.ts
+++ b/apps/sim/app/workspace/[workspaceId]/utils/commands-utils.ts
@@ -9,7 +9,7 @@ import type { GlobalCommand } from '@/app/workspace/[workspaceId]/providers/glob
export type CommandId =
| 'accept-diff-changes'
| 'add-agent'
- | 'goto-templates'
+ // | 'goto-templates'
| 'goto-logs'
| 'open-search'
| 'run-workflow'
@@ -52,11 +52,11 @@ export const COMMAND_DEFINITIONS: Record = {
shortcut: 'Mod+Shift+A',
allowInEditable: true,
},
- 'goto-templates': {
- id: 'goto-templates',
- shortcut: 'Mod+Y',
- allowInEditable: true,
- },
+ // 'goto-templates': {
+ // id: 'goto-templates',
+ // shortcut: 'Mod+Y',
+ // allowInEditable: true,
+ // },
'goto-logs': {
id: 'goto-logs',
shortcut: 'Mod+L',
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/command-list/command-list.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/command-list/command-list.tsx
index 3b7cb2bb4be..f115a655f3e 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/command-list/command-list.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/command-list/command-list.tsx
@@ -2,7 +2,7 @@
import { useCallback } from 'react'
import { createLogger } from '@sim/logger'
-import { Layout, Search } from 'lucide-react'
+import { Search } from 'lucide-react'
import Image from 'next/image'
import { useParams, useRouter } from 'next/navigation'
import { Button, Library } from '@/components/emcn'
@@ -29,11 +29,11 @@ interface CommandItem {
* Available commands list
*/
const commands: CommandItem[] = [
- {
- label: 'Templates',
- icon: Layout,
- shortcut: 'Y',
- },
+ // {
+ // label: 'Templates',
+ // icon: Layout,
+ // shortcut: 'Y',
+ // },
{
label: 'New Agent',
icon: AgentIcon,
@@ -78,14 +78,14 @@ export function CommandList() {
(label: string) => {
try {
switch (label) {
- case 'Templates': {
- if (!workspaceId) {
- logger.warn('No workspace ID found, cannot navigate to templates from command list')
- return
- }
- router.push(`/workspace/${workspaceId}/templates`)
- return
- }
+ // case 'Templates': {
+ // if (!workspaceId) {
+ // logger.warn('No workspace ID found, cannot navigate to templates from command list')
+ // return
+ // }
+ // router.push(`/workspace/${workspaceId}/templates`)
+ // return
+ // }
case 'New Agent': {
const event = new CustomEvent('add-block-from-toolbar', {
detail: { type: 'agent', enableTriggerMode: false },
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/deploy-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/deploy-modal.tsx
index 712fe07d50e..ed55ce78817 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/deploy-modal.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/deploy-modal.tsx
@@ -33,7 +33,7 @@ import {
useDeployWorkflow,
useUndeployWorkflow,
} from '@/hooks/queries/deployments'
-import { useTemplateByWorkflow } from '@/hooks/queries/templates'
+// import { useTemplateByWorkflow } from '@/hooks/queries/templates'
import { useWorkflowMcpServers } from '@/hooks/queries/workflow-mcp-servers'
import { useWorkspaceSettings } from '@/hooks/queries/workspace'
import { usePermissionConfig } from '@/hooks/use-permission-config'
@@ -73,7 +73,7 @@ interface WorkflowDeploymentInfoUI {
isPublicApi: boolean
}
-type TabView = 'general' | 'api' | 'chat' | 'template' | 'mcp' | 'form' | 'a2a'
+type TabView = 'general' | 'api' | 'chat' | /* 'template' | */ 'mcp' | 'form' | 'a2a'
export function DeployModal({
open,
@@ -102,8 +102,8 @@ export function DeployModal({
const [selectedStreamingOutputs, setSelectedStreamingOutputs] = useState([])
const [showUndeployConfirm, setShowUndeployConfirm] = useState(false)
- const [templateFormValid, setTemplateFormValid] = useState(false)
- const [templateSubmitting, setTemplateSubmitting] = useState(false)
+ // const [templateFormValid, setTemplateFormValid] = useState(false)
+ // const [templateSubmitting, setTemplateSubmitting] = useState(false)
const [mcpToolSubmitting, setMcpToolSubmitting] = useState(false)
const [mcpToolCanSave, setMcpToolCanSave] = useState(false)
const [a2aSubmitting, setA2aSubmitting] = useState(false)
@@ -159,17 +159,17 @@ export function DeployModal({
const hasA2aAgent = !!existingA2aAgent
const isA2aPublished = existingA2aAgent?.isPublished ?? false
- const { data: existingTemplate } = useTemplateByWorkflow(workflowId || '', {
- enabled: !!workflowId,
- })
- const hasExistingTemplate = !!existingTemplate
- const templateStatus = existingTemplate
- ? {
- status: existingTemplate.status as 'pending' | 'approved' | 'rejected' | null,
- views: existingTemplate.views,
- stars: existingTemplate.stars,
- }
- : null
+ // const { data: existingTemplate } = useTemplateByWorkflow(workflowId || '', {
+ // enabled: !!workflowId,
+ // })
+ // const hasExistingTemplate = !!existingTemplate
+ // const templateStatus = existingTemplate
+ // ? {
+ // status: existingTemplate.status as 'pending' | 'approved' | 'rejected' | null,
+ // views: existingTemplate.views,
+ // stars: existingTemplate.stars,
+ // }
+ // : null
const deployMutation = useDeployWorkflow()
const undeployMutation = useUndeployWorkflow()
@@ -406,10 +406,10 @@ export function DeployModal({
}
}, [])
- const handleTemplateFormSubmit = useCallback(() => {
- const form = document.getElementById('template-deploy-form') as HTMLFormElement
- form?.requestSubmit()
- }, [])
+ // const handleTemplateFormSubmit = useCallback(() => {
+ // const form = document.getElementById('template-deploy-form') as HTMLFormElement
+ // form?.requestSubmit()
+ // }, [])
const handleMcpToolFormSubmit = useCallback(() => {
const form = document.getElementById('mcp-deploy-form') as HTMLFormElement
@@ -453,11 +453,11 @@ export function DeployModal({
setShowA2aDeleteConfirm(false)
}, [])
- const handleTemplateDelete = useCallback(() => {
- const form = document.getElementById('template-deploy-form')
- const deleteTrigger = form?.querySelector('[data-template-delete-trigger]') as HTMLButtonElement
- deleteTrigger?.click()
- }, [])
+ // const handleTemplateDelete = useCallback(() => {
+ // const form = document.getElementById('template-deploy-form')
+ // const deleteTrigger = form?.querySelector('[data-template-delete-trigger]') as HTMLButtonElement
+ // deleteTrigger?.click()
+ // }, [])
const isSubmitting = deployMutation.isPending
const isUndeploying = undeployMutation.isPending
@@ -938,28 +938,28 @@ function StatusBadge({ isWarning }: StatusBadgeProps) {
)
}
-interface TemplateStatusBadgeProps {
- status: 'pending' | 'approved' | 'rejected' | null
- views?: number
- stars?: number
-}
-
-function TemplateStatusBadge({ status, views, stars }: TemplateStatusBadgeProps) {
- const isPending = status === 'pending'
- const label = isPending ? 'Under review' : 'Live'
-
- const statsText =
- status === 'approved' && views !== undefined && views > 0
- ? `${views} views${stars !== undefined && stars > 0 ? ` • ${stars} stars` : ''}`
- : null
-
- return (
-
- {label}
- {statsText && • {statsText} }
-
- )
-}
+// interface TemplateStatusBadgeProps {
+// status: 'pending' | 'approved' | 'rejected' | null
+// views?: number
+// stars?: number
+// }
+
+// function TemplateStatusBadge({ status, views, stars }: TemplateStatusBadgeProps) {
+// const isPending = status === 'pending'
+// const label = isPending ? 'Under review' : 'Live'
+
+// const statsText =
+// status === 'approved' && views !== undefined && views > 0
+// ? `${views} views${stars !== undefined && stars > 0 ? ` • ${stars} stars` : ''}`
+// : null
+
+// return (
+//
+// {label}
+// {statsText && • {statsText} }
+//
+// )
+// }
interface GeneralFooterProps {
isDeployed?: boolean
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/preview/preview.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/preview/preview.tsx
index 169d643a398..890a5a7d4dc 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/components/preview/preview.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/preview/preview.tsx
@@ -197,7 +197,10 @@ export function Preview({
const childTraceSpans = extractChildTraceSpans(blockExecution)
const childBlockExecutions = buildBlockExecutions(childTraceSpans)
- const workflowName = childWorkflowState.metadata?.name || 'Nested Workflow'
+ const workflowName =
+ childWorkflowState.metadata?.name ||
+ (blockExecution?.output as { childWorkflowName?: string } | undefined)?.childWorkflowName ||
+ 'Nested Workflow'
setWorkflowStack((prev) => [
...prev,
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-workspace-management.ts b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-workspace-management.ts
index d5a8b8fef68..7004584a5e9 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-workspace-management.ts
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-workspace-management.ts
@@ -1,7 +1,7 @@
import { useCallback, useEffect, useMemo, useRef } from 'react'
import { createLogger } from '@sim/logger'
import { useQueryClient } from '@tanstack/react-query'
-import { usePathname, useRouter } from 'next/navigation'
+import { useRouter } from 'next/navigation'
import { useLeaveWorkspace } from '@/hooks/queries/invitations'
import {
useCreateWorkspace,
@@ -33,7 +33,6 @@ export function useWorkspaceManagement({
sessionUserId,
}: UseWorkspaceManagementProps) {
const router = useRouter()
- const pathname = usePathname()
const queryClient = useQueryClient()
const switchToWorkspace = useWorkflowRegistry((state) => state.switchToWorkspace)
@@ -50,12 +49,10 @@ export function useWorkspaceManagement({
const workspaceIdRef = useRef(workspaceId)
const routerRef = useRef>(router)
- const pathnameRef = useRef(pathname || null)
const hasValidatedRef = useRef(false)
workspaceIdRef.current = workspaceId
routerRef.current = router
- pathnameRef.current = pathname || null
const activeWorkspace = useMemo(() => {
if (!workspaces.length) return null
@@ -114,16 +111,7 @@ export function useWorkspaceManagement({
try {
await switchToWorkspace(workspace.id)
- const currentPath = pathnameRef.current || ''
- const templateDetailMatch = currentPath.match(/^\/workspace\/[^/]+\/templates\/([^/]+)$/)
- if (templateDetailMatch) {
- const templateId = templateDetailMatch[1]
- routerRef.current?.push(`/workspace/${workspace.id}/templates/${templateId}`)
- } else if (/^\/workspace\/[^/]+\/templates$/.test(currentPath)) {
- routerRef.current?.push(`/workspace/${workspace.id}/templates`)
- } else {
- routerRef.current?.push(`/workspace/${workspace.id}/home`)
- }
+ routerRef.current?.push(`/workspace/${workspace.id}/home`)
logger.info(`Switched to workspace: ${workspace.name} (${workspace.id})`)
} catch (error) {
logger.error('Error switching workspace:', error)
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx
index ae32f81e323..211773f2c80 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx
@@ -919,22 +919,22 @@ export const Sidebar = memo(function Sidebar() {
}
},
},
- {
- id: 'goto-templates',
- handler: () => {
- try {
- const pathWorkspaceId = resolveWorkspaceIdFromPath()
- if (pathWorkspaceId) {
- navigateToPage(`/workspace/${pathWorkspaceId}/templates`)
- logger.info('Navigated to templates', { workspaceId: pathWorkspaceId })
- } else {
- logger.warn('No workspace ID found, cannot navigate to templates')
- }
- } catch (err) {
- logger.error('Failed to navigate to templates', { err })
- }
- },
- },
+ // {
+ // id: 'goto-templates',
+ // handler: () => {
+ // try {
+ // const pathWorkspaceId = resolveWorkspaceIdFromPath()
+ // if (pathWorkspaceId) {
+ // navigateToPage(`/workspace/${pathWorkspaceId}/templates`)
+ // logger.info('Navigated to templates', { workspaceId: pathWorkspaceId })
+ // } else {
+ // logger.warn('No workspace ID found, cannot navigate to templates')
+ // }
+ // } catch (err) {
+ // logger.error('Failed to navigate to templates', { err })
+ // }
+ // },
+ // },
{
id: 'goto-logs',
handler: () => {
diff --git a/apps/sim/components/emcn/components/toast/toast.tsx b/apps/sim/components/emcn/components/toast/toast.tsx
index 965f6ede819..163f05858b5 100644
--- a/apps/sim/components/emcn/components/toast/toast.tsx
+++ b/apps/sim/components/emcn/components/toast/toast.tsx
@@ -18,6 +18,9 @@ const AUTO_DISMISS_MS = 0
const EXIT_ANIMATION_MS = 200
const MAX_VISIBLE = 20
+const RING_RADIUS = 5.5
+const RING_CIRCUMFERENCE = 2 * Math.PI * RING_RADIUS
+
type ToastVariant = 'default' | 'success' | 'error'
interface ToastAction {
@@ -28,7 +31,6 @@ interface ToastAction {
interface ToastData {
id: string
message: string
- description?: string
variant: ToastVariant
action?: ToastAction
duration: number
@@ -36,7 +38,6 @@ interface ToastData {
type ToastInput = {
message: string
- description?: string
variant?: ToastVariant
action?: ToastAction
duration?: number
@@ -90,12 +91,31 @@ export function useToast() {
return ctx
}
-const VARIANT_STYLES: Record = {
- default: 'border-[var(--border)] bg-[var(--bg)] text-[var(--text-primary)]',
- success:
- 'border-emerald-200 bg-emerald-50 text-emerald-900 dark:border-emerald-800/40 dark:bg-emerald-950/30 dark:text-emerald-200',
- error:
- 'border-red-200 bg-red-50 text-red-900 dark:border-red-800/40 dark:bg-red-950/30 dark:text-red-200',
+function CountdownRing({ duration }: { duration: number }) {
+ return (
+
+
+
+
+ )
}
function ToastItem({ toast: t, onDismiss }: { toast: ToastData; onDismiss: (id: string) => void }) {
@@ -117,38 +137,48 @@ function ToastItem({ toast: t, onDismiss }: { toast: ToastData; onDismiss: (id:
return (
-
-
{t.message}
- {t.description && (
-
{t.description}
+
+
+
+ {t.variant === 'error' && (
+
+ )}
+ {t.variant === 'success' && (
+
+ )}
+ {t.message}
+
+
+ {t.duration > 0 && }
+
+
+
+
+
+ {t.action && (
+
{
+ t.action!.onClick()
+ dismiss()
+ }}
+ className='w-full rounded-[5px] bg-[var(--surface-active)] px-[8px] py-[4px] font-medium text-[12px] hover:bg-[var(--surface-hover)]'
+ >
+ {t.action.label}
+
)}
- {t.action && (
-
{
- t.action!.onClick()
- dismiss()
- }}
- className='shrink-0 font-medium text-[13px] underline underline-offset-2 opacity-90 hover:opacity-100'
- >
- {t.action.label}
-
- )}
-
-
-
)
}
@@ -175,7 +205,6 @@ export function ToastProvider({ children }: { children?: ReactNode }) {
const data: ToastData = {
id,
message: input.message,
- description: input.description,
variant: input.variant ?? 'default',
action: input.action,
duration: input.duration ?? AUTO_DISMISS_MS,
diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx
index 440333adeab..19f650044c3 100644
--- a/apps/sim/components/icons.tsx
+++ b/apps/sim/components/icons.tsx
@@ -4140,7 +4140,7 @@ export function IncidentioIcon(props: SVGProps
) {
export function InfisicalIcon(props: SVGProps) {
return (
-
+
) {
fill='currentColor'
width='800px'
height='800px'
- viewBox='0 0 32 32'
+ viewBox='-1 9.5 34 13'
version='1.1'
xmlns='http://www.w3.org/2000/svg'
>
diff --git a/apps/sim/executor/handlers/workflow/workflow-handler.ts b/apps/sim/executor/handlers/workflow/workflow-handler.ts
index 4821d86029e..8aa478b6e84 100644
--- a/apps/sim/executor/handlers/workflow/workflow-handler.ts
+++ b/apps/sim/executor/handlers/workflow/workflow-handler.ts
@@ -363,6 +363,10 @@ export class WorkflowBlockHandler implements BlockHandler {
const workflowStateWithVariables = {
...workflowState,
variables: workflowVariables,
+ metadata: {
+ ...(workflowState.metadata || {}),
+ name: workflowData.name || DEFAULTS.WORKFLOW_NAME,
+ },
}
if (Object.keys(workflowVariables).length > 0) {
@@ -444,13 +448,18 @@ export class WorkflowBlockHandler implements BlockHandler {
)
const workflowVariables = (wfData?.variables as Record) || {}
+ const childName = wfData?.name || DEFAULTS.WORKFLOW_NAME
const workflowStateWithVariables = {
...deployedState,
variables: workflowVariables,
+ metadata: {
+ ...(deployedState.metadata || {}),
+ name: childName,
+ },
}
return {
- name: wfData?.name || DEFAULTS.WORKFLOW_NAME,
+ name: childName,
serializedState: serializedWorkflow,
variables: workflowVariables,
workflowState: workflowStateWithVariables,
diff --git a/apps/sim/lib/blog/registry.ts b/apps/sim/lib/blog/registry.ts
index fa6c9f1ee55..fe1b65a39b2 100644
--- a/apps/sim/lib/blog/registry.ts
+++ b/apps/sim/lib/blog/registry.ts
@@ -1,5 +1,6 @@
import fs from 'fs/promises'
import path from 'path'
+import { cache } from 'react'
import matter from 'gray-matter'
import { compileMDX } from 'next-mdx-remote/rsc'
import rehypeAutolinkHeadings from 'rehype-autolink-headings'
@@ -90,6 +91,20 @@ export async function getAllPostMeta(): Promise {
return (await scanFrontmatters()).filter((p) => !p.draft)
}
+export const getNavBlogPosts = cache(
+ async (): Promise[]> => {
+ const allPosts = await getAllPostMeta()
+ const featuredPost = allPosts.find((p) => p.featured) ?? allPosts[0]
+ if (!featuredPost) return []
+ const recentPosts = allPosts.filter((p) => p.slug !== featuredPost.slug).slice(0, 4)
+ return [featuredPost, ...recentPosts].map((p) => ({
+ slug: p.slug,
+ title: p.title,
+ ogImage: p.ogImage,
+ }))
+ }
+)
+
export async function getAllTags(): Promise {
const posts = await getAllPostMeta()
const counts: Record = {}
diff --git a/apps/sim/lib/copilot/tools/server/table/user-table.ts b/apps/sim/lib/copilot/tools/server/table/user-table.ts
index ec64397ea0c..733e10ca41f 100644
--- a/apps/sim/lib/copilot/tools/server/table/user-table.ts
+++ b/apps/sim/lib/copilot/tools/server/table/user-table.ts
@@ -18,6 +18,7 @@ import {
insertRow,
queryRows,
renameColumn,
+ renameTable,
updateColumnConstraints,
updateColumnType,
updateRow,
@@ -878,6 +879,36 @@ export const userTableServerTool: BaseServerTool
}
}
+ case 'rename': {
+ if (!args.tableId) {
+ return { success: false, message: 'Table ID is required' }
+ }
+ const newName = (args as Record).newName as string | undefined
+ if (!newName) {
+ return { success: false, message: 'newName is required for renaming a table' }
+ }
+ if (!workspaceId) {
+ return { success: false, message: 'Workspace ID is required' }
+ }
+
+ const table = await getTableById(args.tableId)
+ if (!table) {
+ return { success: false, message: `Table not found: ${args.tableId}` }
+ }
+ if (table.workspaceId !== workspaceId) {
+ return { success: false, message: 'Table not found' }
+ }
+
+ const requestId = crypto.randomUUID().slice(0, 8)
+ const renamed = await renameTable(args.tableId, newName, requestId)
+
+ return {
+ success: true,
+ message: `Renamed table to "${renamed.name}"`,
+ data: { table: { id: renamed.id, name: renamed.name } },
+ }
+ }
+
default:
return { success: false, message: `Unknown operation: ${operation}` }
}
diff --git a/apps/sim/lib/copilot/tools/shared/schemas.ts b/apps/sim/lib/copilot/tools/shared/schemas.ts
index addf280c05c..26a2f391134 100644
--- a/apps/sim/lib/copilot/tools/shared/schemas.ts
+++ b/apps/sim/lib/copilot/tools/shared/schemas.ts
@@ -127,6 +127,7 @@ export const UserTableArgsSchema = z.object({
'rename_column',
'delete_column',
'update_column',
+ 'rename',
]),
args: z
.object({
diff --git a/apps/sim/lib/core/rate-limiter/rate-limiter.ts b/apps/sim/lib/core/rate-limiter/rate-limiter.ts
index a48c33a0ab6..60d786d9ad2 100644
--- a/apps/sim/lib/core/rate-limiter/rate-limiter.ts
+++ b/apps/sim/lib/core/rate-limiter/rate-limiter.ts
@@ -164,6 +164,34 @@ export class RateLimiter {
}
}
+ async checkRateLimitDirect(
+ storageKey: string,
+ config: { maxTokens: number; refillRate: number; refillIntervalMs: number }
+ ): Promise {
+ try {
+ const result = await this.storage.consumeTokens(storageKey, 1, config)
+ if (!result.allowed) {
+ logger.info('Rate limit exceeded', { storageKey, tokensRemaining: result.tokensRemaining })
+ }
+ return {
+ allowed: result.allowed,
+ remaining: result.tokensRemaining,
+ resetAt: result.resetAt,
+ retryAfterMs: result.retryAfterMs,
+ }
+ } catch (error) {
+ logger.error('Rate limit storage error - failing open (allowing request)', {
+ error: error instanceof Error ? error.message : String(error),
+ storageKey,
+ })
+ return {
+ allowed: true,
+ remaining: 1,
+ resetAt: new Date(Date.now() + RATE_LIMIT_WINDOW_MS),
+ }
+ }
+ }
+
async resetRateLimit(rateLimitKey: string): Promise {
try {
await Promise.all([
diff --git a/apps/sim/public/brandbook/logo/large.png b/apps/sim/public/brandbook/logo/large.png
new file mode 100644
index 00000000000..b3ccf90b5a2
Binary files /dev/null and b/apps/sim/public/brandbook/logo/large.png differ
diff --git a/apps/sim/public/brandbook/logo/md.png b/apps/sim/public/brandbook/logo/md.png
new file mode 100644
index 00000000000..f8f7b18125c
Binary files /dev/null and b/apps/sim/public/brandbook/logo/md.png differ
diff --git a/apps/sim/public/brandbook/logo/small.png b/apps/sim/public/brandbook/logo/small.png
new file mode 100644
index 00000000000..9dbb76b5e80
Binary files /dev/null and b/apps/sim/public/brandbook/logo/small.png differ
diff --git a/biome.json b/biome.json
index 61224c957c4..e60d5b95dab 100644
--- a/biome.json
+++ b/biome.json
@@ -24,10 +24,10 @@
"!**/public/workbox-*.js",
"!**/public/worker-*.js",
"!**/public/fallback-*.js",
- "!**/apps/docs/.source/**",
- "!**/venv/**",
- "!**/.venv/**",
- "!**/uploads/**"
+ "!**/apps/docs/.source",
+ "!**/venv",
+ "!**/.venv",
+ "!**/uploads"
]
},
"formatter": {
diff --git a/scripts/generate-docs.ts b/scripts/generate-docs.ts
index c9df563a5e4..2619b2c9a63 100755
--- a/scripts/generate-docs.ts
+++ b/scripts/generate-docs.ts
@@ -317,26 +317,48 @@ function extractOperationsFromContent(blockContent: string): { label: string; id
* Scan all tool files under apps/sim/tools/ and build a map from tool ID to description.
* Used to enrich operation entries with descriptions.
*/
-async function buildToolDescriptionMap(): Promise> {
+interface ToolMaps {
+ desc: Map
+ name: Map
+}
+
+async function buildToolDescriptionMap(): Promise {
const toolsDir = path.join(rootDir, 'apps/sim/tools')
- const map = new Map()
+ const desc = new Map()
+ const name = new Map()
try {
const toolFiles = await glob(`${toolsDir}/**/*.ts`)
for (const file of toolFiles) {
if (file.endsWith('index.ts') || file.endsWith('types.ts')) continue
const content = fs.readFileSync(file, 'utf-8')
- // Match top-level id + description fields in ToolConfig objects
- const idMatches = [...content.matchAll(/\bid\s*:\s*['"]([^'"]+)['"]/g)]
- const descMatches = [...content.matchAll(/\bdescription\s*:\s*['"]([^'"]{5,})['"]/g)]
- if (idMatches.length > 0 && descMatches.length > 0) {
- // The first id match and first description match are the tool's own fields
- map.set(idMatches[0][1], descMatches[0][1])
+
+ // Find every `id: 'tool_id'` occurrence in the file. For each, search
+ // the next ~600 characters for `name:` and `description:` fields, cutting
+ // off at the first `params:` block within that window. This handles both
+ // the simple inline pattern (id → description → params in one object) and
+ // the two-step pattern (base object holds params, ToolConfig export holds
+ // id + description after the base object).
+ const idRegex = /\bid\s*:\s*['"]([^'"]+)['"]/g
+ let idMatch: RegExpExecArray | null
+ while ((idMatch = idRegex.exec(content)) !== null) {
+ const toolId = idMatch[1]
+ if (desc.has(toolId)) continue
+ const windowStart = idMatch.index
+ const windowEnd = Math.min(windowStart + 600, content.length)
+ const window = content.substring(windowStart, windowEnd)
+ // Stop before any params block so we don't pick up param-level values
+ const paramsOffset = window.search(/\bparams\s*:\s*\{/)
+ const searchWindow = paramsOffset > 0 ? window.substring(0, paramsOffset) : window
+ const descMatch = searchWindow.match(/\bdescription\s*:\s*['"]([^'"]{5,})['"]/)
+ const nameMatch = searchWindow.match(/\bname\s*:\s*['"]([^'"]+)['"]/)
+ if (descMatch) desc.set(toolId, descMatch[1])
+ if (nameMatch) name.set(toolId, nameMatch[1])
}
}
} catch {
// Non-fatal: descriptions will be empty strings
}
- return map
+ return { desc, name }
}
/**
@@ -481,7 +503,7 @@ async function writeIntegrationsJson(iconMapping: Record): Promi
}
const triggerRegistry = await buildTriggerRegistry()
- const toolDescMap = await buildToolDescriptionMap()
+ const { desc: toolDescMap, name: toolNameMap } = await buildToolDescriptionMap()
const integrations: IntegrationEntry[] = []
const seenBaseTypes = new Set()
const blockFiles = (await glob(`${BLOCKS_PATH}/*.ts`)).sort()
@@ -517,11 +539,27 @@ async function writeIntegrationsJson(iconMapping: Record): Promi
const iconName = (config as any).iconName || iconMapping[blockType] || ''
const rawOps: { label: string; id: string }[] = (config as any).operations || []
- // Enrich each operation with a description from the tool registry
+ // Enrich each operation with a description from the tool registry.
+ // Primary lookup: derive toolId as `{baseType}_{operationId}` and check
+ // the map directly. Fallback: some blocks use short op IDs that don't
+ // match tool IDs (e.g. Slack uses "send" while the tool ID is
+ // "slack_message"). In that case, find the tool in tools.access whose
+ // name exactly matches the operation label.
+ const toolsAccess: string[] = (config as any).tools?.access || []
const operations: OperationInfo[] = rawOps.map(({ label, id }) => {
const toolId = `${baseType}_${id}`
- const desc = toolDescMap.get(toolId) || toolDescMap.get(id) || ''
- return { name: label, description: desc }
+ let opDesc = toolDescMap.get(toolId) || toolDescMap.get(id) || ''
+
+ if (!opDesc && toolsAccess.length > 0) {
+ for (const tId of toolsAccess) {
+ if (toolNameMap.get(tId)?.toLowerCase() === label.toLowerCase()) {
+ opDesc = toolDescMap.get(tId) || ''
+ if (opDesc) break
+ }
+ }
+ }
+
+ return { name: label, description: opDesc }
})
const triggerIds: string[] = (config as any).triggerIds || []