improvement(platform): added more email validation utils, added integrations page, improved enterprise section, update docs generation script (#3667)

* improvement(platform): added more email validation utils, added integrations page, improved enterprise section, update docs generation script

* remove unused route

* restore hardcoded ff

* updated

* chore: install soap package types for workday integration

* fix(integrations): strip version suffix for template matching, add MX DNS cache

* change ff

* remove extraneous comments

* fix(email): cache timeout results in MX check to prevent repeated 5s waits
This commit is contained in:
Waleed
2026-03-19 13:02:03 -07:00
committed by GitHub
parent c3c22e4674
commit 17bdc80eb9
26 changed files with 13494 additions and 153 deletions
@@ -303,7 +303,7 @@ export default function Collaboration() {
</div>
<Link
href='/studio/multiplayer'
href='/blog/multiplayer'
target='_blank'
rel='noopener noreferrer'
className='relative mx-4 mb-6 flex cursor-none items-center gap-[14px] rounded-[5px] border border-[#2A2A2A] bg-[#1C1C1C] px-[12px] py-[10px] transition-colors hover:border-[#3d3d3d] hover:bg-[#232323] sm:mx-8 md:absolute md:bottom-10 md:left-[80px] md:z-20 md:mx-0 md:mb-0'
@@ -15,12 +15,13 @@
'use client'
import { useEffect, useRef, useState } from 'react'
import { AnimatePresence, motion } from 'framer-motion'
import { AnimatePresence, motion, useInView } from 'framer-motion'
import Image from 'next/image'
import Link from 'next/link'
import { Badge, ChevronDown } from '@/components/emcn'
import { Lock } from '@/components/emcn/icons'
import { GithubIcon } from '@/components/icons'
import { PROVIDER_DEFINITIONS } from '@/providers/models'
/** Consistent color per actor — same pattern as Collaboration section cursors. */
const ACTOR_COLORS: Record<string, string> = {
@@ -32,7 +33,7 @@ const ACTOR_COLORS: Record<string, string> = {
}
/** Left accent bar opacity by recency — newest is brightest. */
const ACCENT_OPACITIES = [0.75, 0.45, 0.28, 0.15, 0.07] as const
const ACCENT_OPACITIES = [0.75, 0.5, 0.35, 0.22, 0.12, 0.05] as const
/** Human-readable label per resource type. */
const RESOURCE_TYPE_LABEL: Record<string, string> = {
@@ -150,7 +151,7 @@ const ENTRY_TEMPLATES: Omit<LogEntry, 'id' | 'insertedAt'>[] = [
{ actor: 'Theo L.', description: 'Locked workflow "Customer Sync"', resourceType: 'workflow' },
]
const INITIAL_OFFSETS_MS = [0, 20_000, 75_000, 240_000, 540_000]
const INITIAL_OFFSETS_MS = [0, 20_000, 75_000, 180_000, 360_000, 600_000]
const MARQUEE_KEYFRAMES = `
@keyframes marquee {
@@ -191,7 +192,7 @@ function AuditRow({ entry, index }: AuditRowProps) {
const resourceLabel = RESOURCE_TYPE_LABEL[entry.resourceType]
return (
<div className='group relative overflow-hidden border-[#2A2A2A] border-b bg-[#191919] transition-colors duration-150 last:border-b-0 hover:bg-[#212121]'>
<div className='group relative overflow-hidden border-[#2A2A2A] border-b bg-[#1C1C1C] transition-colors duration-150 last:border-b-0 hover:bg-[#212121]'>
{/* Left accent bar — brightness encodes recency */}
<div
aria-hidden='true'
@@ -216,7 +217,6 @@ function AuditRow({ entry, index }: AuditRowProps) {
{timeAgo}
</span>
{/* Description — description hidden on mobile to avoid truncation */}
<span className='min-w-0 truncate font-[430] font-season text-[12px] leading-none tracking-[0.02em]'>
<span className='text-[#F6F6F6]/80'>{entry.actor}</span>
<span className='hidden sm:inline'>
@@ -224,13 +224,6 @@ function AuditRow({ entry, index }: AuditRowProps) {
<span className='text-[#F6F6F6]/55'>{entry.description}</span>
</span>
</span>
{/* Resource type label — formatted name, neutral so it doesn't compete with actor colors */}
{resourceLabel && (
<span className='ml-auto shrink-0 rounded border border-[#2A2A2A] px-[7px] py-[3px] font-[430] font-season text-[#F6F6F6]/25 text-[10px] leading-none tracking-[0.04em]'>
{resourceLabel}
</span>
)}
</div>
</div>
)
@@ -238,11 +231,11 @@ function AuditRow({ entry, index }: AuditRowProps) {
function AuditLogPreview() {
const counterRef = useRef(ENTRY_TEMPLATES.length)
const templateIndexRef = useRef(5 % ENTRY_TEMPLATES.length)
const templateIndexRef = useRef(6 % ENTRY_TEMPLATES.length)
const now = Date.now()
const [entries, setEntries] = useState<LogEntry[]>(() =>
ENTRY_TEMPLATES.slice(0, 5).map((t, i) => ({
ENTRY_TEMPLATES.slice(0, 6).map((t, i) => ({
...t,
id: i,
insertedAt: now - INITIAL_OFFSETS_MS[i],
@@ -257,7 +250,7 @@ function AuditLogPreview() {
setEntries((prev) => [
{ ...template, id: counterRef.current++, insertedAt: Date.now() },
...prev.slice(0, 4),
...prev.slice(0, 5),
])
}, 2600)
@@ -271,60 +264,217 @@ function AuditLogPreview() {
}, [])
return (
<div className='mx-6 mt-6 overflow-hidden rounded-[8px] border border-[#2A2A2A] md:mx-8 md:mt-8'>
{/* Header */}
<div className='flex items-center justify-between border-[#2A2A2A] border-b bg-[#161616] px-4 py-[10px]'>
<div className='flex items-center gap-2'>
{/* Pulsing live indicator */}
<span className='relative flex h-[8px] w-[8px]'>
<span
className='absolute inline-flex h-full w-full animate-ping rounded-full opacity-50'
style={{ backgroundColor: '#33C482' }}
/>
<span
className='relative inline-flex h-[8px] w-[8px] rounded-full'
style={{ backgroundColor: '#33C482' }}
/>
</span>
<span className='font-[430] font-season text-[#F6F6F6]/40 text-[11px] uppercase tracking-[0.08em]'>
Audit Log
</span>
</div>
<div className='flex items-center gap-2'>
<span className='rounded border border-[#2A2A2A] px-[8px] py-[3px] font-[430] font-season text-[#F6F6F6]/20 text-[11px] tracking-[0.02em]'>
Export
</span>
<span className='rounded border border-[#2A2A2A] px-[8px] py-[3px] font-[430] font-season text-[#F6F6F6]/20 text-[11px] tracking-[0.02em]'>
Filter
</span>
</div>
<div className='mt-5 overflow-hidden px-6 md:mt-6 md:px-8'>
<AnimatePresence mode='popLayout' initial={false}>
{entries.map((entry, index) => (
<motion.div
key={entry.id}
layout
initial={{ y: -48, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
exit={{ opacity: 0 }}
transition={{
layout: {
type: 'spring',
stiffness: 380,
damping: 38,
mass: 0.8,
},
y: { duration: 0.32, ease: [0.25, 0.46, 0.45, 0.94] },
opacity: { duration: 0.25 },
}}
>
<AuditRow entry={entry} index={index} />
</motion.div>
))}
</AnimatePresence>
</div>
)
}
const CHECK_PATH =
'M10.97 4.97a.75.75 0 0 1 1.07 1.05l-3.99 4.99a.75.75 0 0 1-1.08.02L4.324 8.384a.75.75 0 1 1 1.06-1.06l2.094 2.093 3.473-4.425z'
interface PermissionFeature {
name: string
key: string
defaultEnabled: boolean
providerId?: string
}
interface PermissionCategory {
label: string
color: string
features: PermissionFeature[]
}
const PERMISSION_CATEGORIES: PermissionCategory[] = [
{
label: 'Providers',
color: '#FA4EDF',
features: [
{ key: 'openai', name: 'OpenAI', defaultEnabled: true, providerId: 'openai' },
{ key: 'anthropic', name: 'Anthropic', defaultEnabled: true, providerId: 'anthropic' },
{ key: 'google', name: 'Google', defaultEnabled: false, providerId: 'google' },
{ key: 'xai', name: 'xAI', defaultEnabled: true, providerId: 'xai' },
],
},
{
label: 'Workspace',
color: '#2ABBF8',
features: [
{ key: 'knowledge-base', name: 'Knowledge Base', defaultEnabled: true },
{ key: 'tables', name: 'Tables', defaultEnabled: true },
{ key: 'copilot', name: 'Copilot', defaultEnabled: false },
{ key: 'environment', name: 'Environment', defaultEnabled: false },
],
},
{
label: 'Tools',
color: '#33C482',
features: [
{ key: 'mcp-tools', name: 'MCP Tools', defaultEnabled: true },
{ key: 'custom-tools', name: 'Custom Tools', defaultEnabled: false },
{ key: 'skills', name: 'Skills', defaultEnabled: true },
{ key: 'invitations', name: 'Invitations', defaultEnabled: true },
],
},
]
const INITIAL_ACCESS_STATE = Object.fromEntries(
PERMISSION_CATEGORIES.flatMap((category) =>
category.features.map((feature) => [feature.key, feature.defaultEnabled])
)
)
function CheckboxIcon({ checked, color }: { checked: boolean; color: string }) {
return (
<div
className='h-[6px] w-[6px] shrink-0 rounded-full transition-colors duration-200'
style={{
backgroundColor: checked ? color : 'transparent',
border: checked ? 'none' : '1.5px solid #3A3A3A',
}}
/>
)
}
function ProviderPreviewIcon({ providerId }: { providerId?: string }) {
if (!providerId) return null
const ProviderIcon = PROVIDER_DEFINITIONS[providerId]?.icon
if (!ProviderIcon) return null
return (
<div className='relative flex h-[14px] w-[14px] shrink-0 items-center justify-center opacity-50 brightness-0 invert'>
<ProviderIcon className='!h-[14px] !w-[14px]' />
</div>
)
}
function AccessControlPanel() {
const ref = useRef(null)
const isInView = useInView(ref, { once: true, margin: '-40px' })
const [accessState, setAccessState] = useState<Record<string, boolean>>(INITIAL_ACCESS_STATE)
const allFeatures = PERMISSION_CATEGORIES.flatMap((c) => c.features)
return (
<div ref={ref}>
<div className='lg:hidden'>
{PERMISSION_CATEGORIES.map((category, catIdx) => {
const offsetBefore = PERMISSION_CATEGORIES.slice(0, catIdx).reduce(
(sum, c) => sum + c.features.length,
0
)
return (
<div key={category.label} className={catIdx > 0 ? 'mt-4' : ''}>
<span className='font-[430] font-season text-[#F6F6F6]/30 text-[10px] uppercase leading-none tracking-[0.08em]'>
{category.label}
</span>
<div className='mt-[8px] grid grid-cols-2 gap-x-4 gap-y-[8px]'>
{category.features.map((feature, featIdx) => {
const enabled = accessState[feature.key]
return (
<motion.div
key={feature.key}
className='flex cursor-pointer items-center gap-[8px] rounded-[4px] py-[2px]'
initial={{ opacity: 0, x: -6 }}
animate={isInView ? { opacity: 1, x: 0 } : {}}
transition={{
delay: 0.05 + (offsetBefore + featIdx) * 0.04,
duration: 0.3,
}}
onClick={() =>
setAccessState((prev) => ({ ...prev, [feature.key]: !prev[feature.key] }))
}
whileTap={{ scale: 0.98 }}
>
<CheckboxIcon checked={enabled} color={category.color} />
<ProviderPreviewIcon providerId={feature.providerId} />
<span
className='truncate font-[430] font-season text-[13px] leading-none tracking-[0.02em]'
style={{ color: enabled ? '#F6F6F6AA' : '#F6F6F640' }}
>
{feature.name}
</span>
</motion.div>
)
})}
</div>
</div>
)
})}
</div>
{/* Log entries — new items push existing ones down */}
<div className='overflow-hidden'>
<AnimatePresence mode='popLayout' initial={false}>
{entries.map((entry, index) => (
<motion.div
key={entry.id}
layout
initial={{ y: -48, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
exit={{ opacity: 0 }}
transition={{
layout: {
type: 'spring',
stiffness: 380,
damping: 38,
mass: 0.8,
},
y: { duration: 0.32, ease: [0.25, 0.46, 0.45, 0.94] },
opacity: { duration: 0.25 },
}}
>
<AuditRow entry={entry} index={index} />
</motion.div>
))}
</AnimatePresence>
{/* Desktop — categorized grid */}
<div className='hidden lg:block'>
{PERMISSION_CATEGORIES.map((category, catIdx) => (
<div key={category.label} className={catIdx > 0 ? 'mt-4' : ''}>
<span className='font-[430] font-season text-[#F6F6F6]/30 text-[10px] uppercase leading-none tracking-[0.08em]'>
{category.label}
</span>
<div className='mt-[8px] grid grid-cols-2 gap-x-4 gap-y-[8px]'>
{category.features.map((feature, featIdx) => {
const enabled = accessState[feature.key]
const currentIndex =
PERMISSION_CATEGORIES.slice(0, catIdx).reduce(
(sum, c) => sum + c.features.length,
0
) + featIdx
return (
<motion.div
key={feature.key}
className='flex cursor-pointer items-center gap-[8px] rounded-[4px] py-[2px]'
initial={{ opacity: 0, x: -6 }}
animate={isInView ? { opacity: 1, x: 0 } : {}}
transition={{
delay: 0.1 + currentIndex * 0.04,
duration: 0.3,
ease: [0.25, 0.46, 0.45, 0.94],
}}
onClick={() =>
setAccessState((prev) => ({ ...prev, [feature.key]: !prev[feature.key] }))
}
whileTap={{ scale: 0.98 }}
>
<CheckboxIcon checked={enabled} color={category.color} />
<ProviderPreviewIcon providerId={feature.providerId} />
<span
className='truncate font-[430] font-season text-[11px] leading-none tracking-[0.02em] transition-opacity duration-200'
style={{ color: enabled ? '#F6F6F6AA' : '#F6F6F640' }}
>
{feature.name}
</span>
</motion.div>
)
})}
</div>
</div>
))}
</div>
</div>
)
@@ -420,7 +570,37 @@ export default function Enterprise() {
</div>
<div className='mt-8 overflow-hidden rounded-[12px] bg-[#1C1C1C] sm:mt-10 md:mt-12'>
<AuditLogPreview />
<div className='grid grid-cols-1 border-[#2A2A2A] border-b lg:grid-cols-[1fr_420px]'>
{/* Audit Trail */}
<div className='border-[#2A2A2A] lg:border-r'>
<div className='px-6 pt-6 md:px-8 md:pt-8'>
<h3 className='font-[430] font-season text-[16px] text-white leading-[120%] tracking-[-0.01em]'>
Audit Trail
</h3>
<p className='mt-2 max-w-[480px] font-[430] font-season text-[#F6F6F6]/50 text-[14px] leading-[150%] tracking-[0.02em]'>
Every action is captured with full actor attribution.
</p>
</div>
<AuditLogPreview />
<div className='h-6 md:h-8' />
</div>
{/* Access Control */}
<div className='border-[#2A2A2A] border-t lg:border-t-0'>
<div className='px-6 pt-6 md:px-8 md:pt-8'>
<h3 className='font-[430] font-season text-[16px] text-white leading-[120%] tracking-[-0.01em]'>
Access Control
</h3>
<p className='mt-[6px] font-[430] font-season text-[#F6F6F6]/50 text-[14px] leading-[150%] tracking-[0.02em]'>
Restrict providers, surfaces, and tools per group.
</p>
</div>
<div className='mt-5 px-6 pb-6 md:mt-6 md:px-8 md:pb-8'>
<AccessControlPanel />
</div>
</div>
</div>
<TrustStrip />
{/* Scrolling feature ticker */}
@@ -11,15 +11,19 @@ interface FooterItem {
}
const PRODUCT_LINKS: FooterItem[] = [
{ label: 'Pricing', href: '#pricing' },
{ label: 'Pricing', href: '/#pricing' },
{ label: 'Enterprise', href: 'https://form.typeform.com/to/jqCO12pF', external: true },
{ label: 'Self Hosting', href: 'https://docs.sim.ai/self-hosting', external: true },
{ label: 'MCP', href: 'https://docs.sim.ai/mcp', external: true },
{ label: 'Knowledge Base', href: 'https://docs.sim.ai/knowledgebase', external: true },
{ label: 'Tables', href: 'https://docs.sim.ai/tables', external: true },
{ label: 'API', href: 'https://docs.sim.ai/api-reference/getting-started', external: true },
{ label: 'Status', href: 'https://status.sim.ai', external: true },
]
const RESOURCES_LINKS: FooterItem[] = [
{ label: 'Blog', href: '/blog' },
{ label: 'Templates', href: '/templates' },
{ label: 'Docs', href: 'https://docs.sim.ai', external: true },
{ label: 'Careers', href: 'https://jobs.ashbyhq.com/sim', external: true },
{ label: 'Changelog', href: '/changelog' },
@@ -39,6 +43,7 @@ const BLOCK_LINKS: FooterItem[] = [
]
const INTEGRATION_LINKS: FooterItem[] = [
{ label: 'All Integrations →', href: '/integrations' },
{ label: 'Confluence', href: 'https://docs.sim.ai/tools/confluence', external: true },
{ label: 'Slack', href: 'https://docs.sim.ai/tools/slack', external: true },
{ label: 'GitHub', href: 'https://docs.sim.ai/tools/github', external: true },
@@ -1,23 +1,11 @@
import Link from 'next/link'
import { cn } from '@/lib/core/utils/cn'
const FEATURED_POST = {
title: 'Build with Sim for Enterprise',
slug: 'enterprise',
image: '/blog/thumbnails/enterprise.webp',
} as const
const POSTS = [
{ title: 'Introducing Sim v0.5', slug: 'v0-5', image: '/blog/thumbnails/v0-5.webp' },
{ title: '$7M Series A', slug: 'series-a', image: '/blog/thumbnails/series-a.webp' },
{
title: 'Realtime Collaboration',
slug: 'multiplayer',
image: '/blog/thumbnails/multiplayer.webp',
},
{ title: 'Inside the Executor', slug: 'executor', image: '/blog/thumbnails/executor.webp' },
{ title: 'Inside Sim Copilot', slug: 'copilot', image: '/blog/thumbnails/copilot.webp' },
] as const
export interface NavBlogPost {
slug: string
title: string
ogImage: string
}
function BlogCard({
slug,
@@ -63,34 +51,32 @@ function BlogCard({
)
}
export function BlogDropdown() {
interface BlogDropdownProps {
posts: NavBlogPost[]
}
export function BlogDropdown({ posts }: BlogDropdownProps) {
const [featured, ...rest] = posts
if (!featured) return null
return (
<div className='w-[560px] rounded-[5px] border border-[#2A2A2A] bg-[#1C1C1C] p-[16px] shadow-[0_16px_48px_rgba(0,0,0,0.4)]'>
<div className='grid grid-cols-3 gap-[8px]'>
<BlogCard
slug={FEATURED_POST.slug}
image={FEATURED_POST.image}
title={FEATURED_POST.title}
slug={featured.slug}
image={featured.ogImage}
title={featured.title}
imageHeight='190px'
titleSize='13px'
className='col-span-2 row-span-2'
/>
{POSTS.slice(0, 2).map((post) => (
{rest.map((post) => (
<BlogCard
key={post.slug}
slug={post.slug}
image={post.image}
title={post.title}
imageHeight='72px'
/>
))}
{POSTS.slice(2).map((post) => (
<BlogCard
key={post.slug}
slug={post.slug}
image={post.image}
image={post.ogImage}
title={post.title}
imageHeight='72px'
/>
@@ -5,7 +5,10 @@ import Image from 'next/image'
import Link from 'next/link'
import { GithubOutlineIcon } from '@/components/icons'
import { cn } from '@/lib/core/utils/cn'
import { BlogDropdown } from '@/app/(home)/components/navbar/components/blog-dropdown'
import {
BlogDropdown,
type NavBlogPost,
} from '@/app/(home)/components/navbar/components/blog-dropdown'
import { DocsDropdown } from '@/app/(home)/components/navbar/components/docs-dropdown'
import { GitHubStars } from '@/app/(home)/components/navbar/components/github-stars'
import { getBrandConfig } from '@/ee/whitelabeling'
@@ -23,7 +26,7 @@ interface NavLink {
const NAV_LINKS: NavLink[] = [
{ label: 'Docs', href: 'https://docs.sim.ai', external: true, icon: 'chevron', dropdown: 'docs' },
{ label: 'Blog', href: '/blog', icon: 'chevron', dropdown: 'blog' },
{ label: 'Pricing', href: '#pricing' },
{ label: 'Pricing', href: '/#pricing' },
{ label: 'Enterprise', href: 'https://form.typeform.com/to/jqCO12pF', external: true },
]
@@ -32,9 +35,10 @@ const LINK_CELL = 'flex items-center px-[14px]'
interface NavbarProps {
logoOnly?: boolean
blogPosts?: NavBlogPost[]
}
export default function Navbar({ logoOnly = false }: NavbarProps) {
export default function Navbar({ logoOnly = false, blogPosts = [] }: NavbarProps) {
const brand = getBrandConfig()
const [activeDropdown, setActiveDropdown] = useState<DropdownId>(null)
const [hoveredLink, setHoveredLink] = useState<string | null>(null)
@@ -161,7 +165,7 @@ export default function Navbar({ logoOnly = false }: NavbarProps) {
}}
>
{dropdown === 'docs' && <DocsDropdown />}
{dropdown === 'blog' && <BlogDropdown />}
{dropdown === 'blog' && <BlogDropdown posts={blogPosts} />}
</div>
</li>
)
+9 -1
View File
@@ -1,3 +1,4 @@
import { getAllPostMeta } from '@/lib/blog/registry'
import { martianMono } from '@/app/_styles/fonts/martian-mono/martian-mono'
import { season } from '@/app/_styles/fonts/season/season'
import {
@@ -32,11 +33,18 @@ import {
* enterprise (Enterprise) -> pricing (Pricing) -> testimonials (Testimonials).
*/
export default async function Landing() {
const allPosts = await getAllPostMeta()
const featuredPost = allPosts.find((p) => p.featured) ?? allPosts[0]
const recentPosts = allPosts.filter((p) => p !== featuredPost).slice(0, 4)
const blogPosts = [featuredPost, ...recentPosts]
.filter(Boolean)
.map((p) => ({ slug: p.slug, title: p.title, ogImage: p.ogImage }))
return (
<div className={`${season.variable} ${martianMono.variable} min-h-screen bg-[#1C1C1C]`}>
<StructuredData />
<header>
<Navbar />
<Navbar blogPosts={blogPosts} />
</header>
<main>
<Hero />
@@ -0,0 +1,54 @@
'use client'
import { useState } from 'react'
import { ChevronDown } from '@/components/emcn'
import { cn } from '@/lib/core/utils/cn'
import type { FAQItem } from '@/app/(landing)/integrations/data/types'
interface IntegrationFAQProps {
faqs: FAQItem[]
}
export function IntegrationFAQ({ faqs }: IntegrationFAQProps) {
const [openIndex, setOpenIndex] = useState<number | null>(0)
return (
<div className='divide-y divide-[#2A2A2A]'>
{faqs.map(({ question, answer }, index) => {
const isOpen = openIndex === index
return (
<div key={question}>
<button
type='button'
onClick={() => setOpenIndex(isOpen ? null : index)}
className='flex w-full items-start justify-between gap-4 py-5 text-left'
aria-expanded={isOpen}
>
<span
className={cn(
'font-[500] text-[15px] leading-snug transition-colors',
isOpen ? 'text-[#ECECEC]' : 'text-[#999] hover:text-[#ECECEC]'
)}
>
{question}
</span>
<ChevronDown
className={cn(
'mt-0.5 h-4 w-4 shrink-0 text-[#555] transition-transform duration-200',
isOpen ? 'rotate-180' : 'rotate-0'
)}
aria-hidden='true'
/>
</button>
{isOpen && (
<div className='pb-5'>
<p className='text-[#999] text-[14px] leading-[1.75]'>{answer}</p>
</div>
)}
</div>
)
})}
</div>
)
}
@@ -0,0 +1,28 @@
'use client'
import { useRouter } from 'next/navigation'
import { LandingPromptStorage } from '@/lib/core/utils/browser-storage'
interface TemplateCardButtonProps {
prompt: string
children: React.ReactNode
}
export function TemplateCardButton({ prompt, children }: TemplateCardButtonProps) {
const router = useRouter()
function handleClick() {
LandingPromptStorage.store(prompt)
router.push('/signup')
}
return (
<button
type='button'
onClick={handleClick}
className='group flex w-full flex-col items-start rounded-lg border border-[#2A2A2A] bg-[#242424] p-5 text-left transition-colors hover:border-[#3d3d3d] hover:bg-[#2A2A2A]'
>
{children}
</button>
)
}
@@ -0,0 +1,761 @@
import type { Metadata } from 'next'
import Link from 'next/link'
import { notFound } from 'next/navigation'
import { TEMPLATES } from '@/app/workspace/[workspaceId]/home/components/template-prompts/consts'
import { IntegrationIcon } from '../components/integration-icon'
import { blockTypeToIconMap } from '../data/icon-mapping'
import integrations from '../data/integrations.json'
import { POPULAR_WORKFLOWS } from '../data/popular-workflows'
import type { AuthType, FAQItem, Integration } from '../data/types'
import { IntegrationFAQ } from './components/integration-faq'
import { TemplateCardButton } from './components/template-card-button'
const allIntegrations = integrations as Integration[]
const INTEGRATION_COUNT = allIntegrations.length
/** Fast O(1) lookups — avoids repeated linear scans inside render loops. */
const byName = new Map(allIntegrations.map((i) => [i.name, i]))
const bySlug = new Map(allIntegrations.map((i) => [i.slug, i]))
const byType = new Map(allIntegrations.map((i) => [i.type, i]))
/** Returns workflow pairs that feature the given integration on either side. */
function getPairsFor(name: string) {
return POPULAR_WORKFLOWS.filter((p) => p.from === name || p.to === name)
}
/**
* Returns up to `limit` related integration slugs.
*
* Scoring:
* +100 — integration appears as a workflow pair partner (explicit editorial signal)
* +N — N operation names shared with the current integration (semantic similarity)
*
* This means genuine partners always rank first; operation-similar integrations
* (e.g. Slack → Teams → Discord for "Send Message") fill the rest organically.
*/
function getRelatedSlugs(
name: string,
slug: string,
operations: Integration['operations'],
limit = 6
): string[] {
const partners = new Set(getPairsFor(name).map((p) => (p.from === name ? p.to : p.from)))
const currentOps = new Set(operations.map((o) => o.name.toLowerCase()))
return allIntegrations
.filter((i) => i.slug !== slug)
.map((i) => ({
slug: i.slug,
score:
(partners.has(i.name) ? 100 : 0) +
i.operations.filter((o) => currentOps.has(o.name.toLowerCase())).length,
}))
.filter(({ score }) => score > 0)
.sort((a, b) => b.score - a.score)
.slice(0, limit)
.map(({ slug: s }) => s)
}
const AUTH_STEP: Record<AuthType, string> = {
oauth: 'Authenticate with one-click OAuth — no credentials to copy-paste.',
'api-key': 'Add your API key to authenticate — find it in your account settings.',
none: 'Authenticate your account to connect.',
}
/**
* Generates targeted FAQs from integration metadata.
* Questions mirror real search queries to drive FAQPage rich snippets.
*/
function buildFAQs(integration: Integration): FAQItem[] {
const { name, description, operations, triggers, authType } = integration
const topOps = operations.slice(0, 5)
const topOpNames = topOps.map((o) => o.name)
const pairs = getPairsFor(name)
const authStep = AUTH_STEP[authType]
const faqs: FAQItem[] = [
{
question: `What is Sim's ${name} integration?`,
answer: `Sim's ${name} integration lets you build AI-powered workflows that automate tasks in ${name} without writing code. ${description} You can connect ${name} to hundreds of other services in the same workflow — from CRMs and spreadsheets to messaging tools and databases.`,
},
{
question: `What can I automate with ${name} in Sim?`,
answer:
topOpNames.length > 0
? `With Sim you can: ${topOpNames.join('; ')}${operations.length > 5 ? `; and ${operations.length - 5} more tools` : ''}. Each action runs inside an AI agent block, so you can combine ${name} with LLM reasoning, conditional logic, and data from any other connected service.`
: `Sim lets you automate ${name} workflows by connecting it to an AI agent that can read from it, write to it, and chain it together with other services — all driven by natural-language instructions instead of rigid rules.`,
},
{
question: `How do I connect ${name} to Sim?`,
answer: `Getting started takes under five minutes: (1) Create a free account at sim.ai. (2) Open a new workflow. (3) Drag a ${name} block onto the canvas. (4) ${authStep} (5) Choose the tool you want to use, wire it to the inputs you need, and click Run. Your automation is live.`,
},
...(topOpNames.length >= 2
? [
{
question: `How do I ${topOpNames[0].toLowerCase()} with ${name} in Sim?`,
answer: `Add a ${name} block to your workflow and select "${topOpNames[0]}" as the tool. Fill in the required fields — you can reference outputs from earlier steps, such as text generated by an AI agent or data fetched from another integration. No code is required.`,
},
]
: []),
...(pairs.length > 0
? [
{
question: `Can I connect ${name} to ${pairs[0].from === name ? pairs[0].to : pairs[0].from} with Sim?`,
answer: `Yes. ${pairs[0].description} In Sim, you set this up by adding both a ${name} block and a ${pairs[0].from === name ? pairs[0].to : pairs[0].from} block to the same workflow and connecting them through an AI agent that orchestrates the logic between them.`,
},
]
: []),
...(triggers.length > 0
? [
{
question: `Can ${name} trigger a Sim workflow automatically?`,
answer: `Yes. ${name} supports ${triggers.length} webhook trigger${triggers.length === 1 ? '' : 's'} that can instantly start a Sim workflow: ${triggers.map((t) => t.name).join(', ')}. No polling needed — the workflow fires the moment the event occurs in ${name}.`,
},
]
: []),
{
question: `What ${name} tools does Sim support?`,
answer:
operations.length > 0
? `Sim supports ${operations.length} ${name} tool${operations.length === 1 ? '' : 's'}: ${operations.map((o) => o.name).join(', ')}.`
: `Sim supports core ${name} tools for reading and writing data, triggering actions, and integrating with your other services. See the full list in the Sim documentation.`,
},
{
question: `Is the ${name} integration free to use?`,
answer: `Yes — Sim's free plan includes access to the ${name} integration and every other integration in the library. No credit card is needed to get started. Visit sim.ai to create your account.`,
},
]
return faqs
}
export async function generateStaticParams() {
return allIntegrations.map((i) => ({ slug: i.slug }))
}
export async function generateMetadata({
params,
}: {
params: Promise<{ slug: string }>
}): Promise<Metadata> {
const { slug } = await params
const integration = bySlug.get(slug)
if (!integration) return {}
const { name, description, operations } = integration
const opSample = operations
.slice(0, 3)
.map((o) => o.name)
.join(', ')
const metaDesc = `Automate ${name} with AI-powered workflows on Sim. ${description.slice(0, 100).trimEnd()}. Free to start.`
return {
title: `${name} Integration`,
description: metaDesc,
keywords: [
`${name} automation`,
`${name} integration`,
`automate ${name}`,
`connect ${name}`,
`${name} workflow`,
`${name} AI automation`,
...(opSample ? [`${name} ${opSample}`] : []),
'workflow automation',
'no-code automation',
'AI agent workflow',
],
openGraph: {
title: `${name} Integration — AI Workflow Automation | Sim`,
description: `Connect ${name} to ${INTEGRATION_COUNT - 1}+ tools using AI agents. ${description.slice(0, 100).trimEnd()}.`,
url: `https://sim.ai/integrations/${slug}`,
type: 'website',
images: [{ url: 'https://sim.ai/opengraph-image.png', width: 1200, height: 630 }],
},
twitter: {
card: 'summary_large_image',
title: `${name} Integration | Sim`,
description: `Automate ${name} with AI-powered workflows. Connect to ${INTEGRATION_COUNT - 1}+ tools. Free to start.`,
},
alternates: { canonical: `https://sim.ai/integrations/${slug}` },
}
}
export default async function IntegrationPage({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params
const integration = bySlug.get(slug)
if (!integration) notFound()
const { name, description, longDescription, bgColor, docsUrl, operations, triggers, authType } =
integration
const IconComponent = blockTypeToIconMap[integration.type]
const faqs = buildFAQs(integration)
const relatedSlugs = getRelatedSlugs(name, slug, operations)
const relatedIntegrations = relatedSlugs
.map((s) => bySlug.get(s))
.filter((i): i is Integration => i !== undefined)
const featuredPairs = getPairsFor(name)
const baseType = integration.type.replace(/_v\d+$/, '')
const matchingTemplates = TEMPLATES.filter(
(t) =>
t.integrationBlockTypes.includes(integration.type) ||
t.integrationBlockTypes.includes(baseType)
)
const breadcrumbJsonLd = {
'@context': 'https://schema.org',
'@type': 'BreadcrumbList',
itemListElement: [
{ '@type': 'ListItem', position: 1, name: 'Home', item: 'https://sim.ai' },
{
'@type': 'ListItem',
position: 2,
name: 'Integrations',
item: 'https://sim.ai/integrations',
},
{ '@type': 'ListItem', position: 3, name, item: `https://sim.ai/integrations/${slug}` },
],
}
const softwareAppJsonLd = {
'@context': 'https://schema.org',
'@type': 'SoftwareApplication',
name: `${name} Integration`,
description,
url: `https://sim.ai/integrations/${slug}`,
applicationCategory: 'BusinessApplication',
operatingSystem: 'Web',
featureList: operations.map((o) => o.name),
offers: { '@type': 'Offer', price: '0', priceCurrency: 'USD' },
}
const howToJsonLd = {
'@context': 'https://schema.org',
'@type': 'HowTo',
name: `How to automate ${name} with Sim`,
description: `Step-by-step guide to connecting ${name} to AI-powered workflows in Sim.`,
step: [
{
'@type': 'HowToStep',
position: 1,
name: 'Create a free Sim account',
text: 'Sign up at sim.ai — no credit card required.',
},
{
'@type': 'HowToStep',
position: 2,
name: `Add a ${name} block`,
text: `Open a workflow, drag a ${name} block onto the canvas, and authenticate with your ${name} credentials.`,
},
{
'@type': 'HowToStep',
position: 3,
name: 'Configure and run',
text: `Choose the operation you want, connect it to an AI agent, and run your workflow. Automate anything in ${name} without code.`,
},
],
}
const faqJsonLd = {
'@context': 'https://schema.org',
'@type': 'FAQPage',
mainEntity: faqs.map(({ question, answer }) => ({
'@type': 'Question',
name: question,
acceptedAnswer: { '@type': 'Answer', text: answer },
})),
}
return (
<>
<script
type='application/ld+json'
dangerouslySetInnerHTML={{ __html: JSON.stringify(breadcrumbJsonLd) }}
/>
<script
type='application/ld+json'
dangerouslySetInnerHTML={{ __html: JSON.stringify(softwareAppJsonLd) }}
/>
<script
type='application/ld+json'
dangerouslySetInnerHTML={{ __html: JSON.stringify(howToJsonLd) }}
/>
<script
type='application/ld+json'
dangerouslySetInnerHTML={{ __html: JSON.stringify(faqJsonLd) }}
/>
<div className='mx-auto max-w-[1200px] px-6 py-12 sm:px-8 md:px-12'>
{/* Breadcrumb */}
<nav
aria-label='Breadcrumb'
className='mb-10 flex items-center gap-2 text-[#555] text-[13px]'
>
<Link href='/' className='transition-colors hover:text-[#999]'>
Home
</Link>
<span aria-hidden='true'>/</span>
<Link href='/integrations' className='transition-colors hover:text-[#999]'>
Integrations
</Link>
<span aria-hidden='true'>/</span>
<span className='text-[#999]'>{name}</span>
</nav>
{/* Hero */}
<section aria-labelledby='integration-heading' className='mb-16'>
<div className='mb-6 flex items-center gap-5'>
<IntegrationIcon
bgColor={bgColor}
name={name}
Icon={IconComponent}
className='h-16 w-16 rounded-xl'
iconClassName='h-8 w-8'
fallbackClassName='text-[26px]'
aria-hidden='true'
/>
<div>
<p className='mb-0.5 text-[#555] text-[12px]'>Integration</p>
<h1
id='integration-heading'
className='font-[500] text-[#ECECEC] text-[36px] leading-tight sm:text-[44px]'
>
{name}
</h1>
</div>
</div>
<p className='mb-8 max-w-[700px] text-[#999] text-[17px] leading-[1.7]'>{description}</p>
{/* CTAs */}
<div className='flex flex-wrap gap-[8px]'>
<a
href='https://sim.ai'
className='inline-flex h-[32px] items-center rounded-[5px] border border-[#FFFFFF] bg-[#FFFFFF] px-[10px] font-[430] font-season text-[#1C1C1C] text-[14px] transition-colors hover:border-[#E0E0E0] hover:bg-[#E0E0E0]'
>
Start building free
</a>
<a
href={docsUrl}
target='_blank'
rel='noopener noreferrer'
className='inline-flex h-[32px] items-center gap-[6px] rounded-[5px] border border-[#3d3d3d] px-[10px] font-[430] font-season text-[#ECECEC] text-[14px] transition-colors hover:bg-[#2A2A2A]'
>
View docs
<svg
aria-hidden='true'
className='h-3 w-3'
fill='none'
stroke='currentColor'
strokeWidth={2}
viewBox='0 0 24 24'
>
<path d='M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6' />
<polyline points='15 3 21 3 21 9' />
<line x1='10' x2='21' y1='14' y2='3' />
</svg>
</a>
</div>
</section>
{/* Two-column layout */}
<div className='grid grid-cols-1 gap-16 lg:grid-cols-[1fr_300px]'>
{/* Main column */}
<div className='min-w-0 space-y-16'>
{/* Overview */}
{longDescription && (
<section aria-labelledby='overview-heading'>
<h2 id='overview-heading' className='mb-4 font-[500] text-[#ECECEC] text-[20px]'>
Overview
</h2>
<p className='text-[#999] text-[15px] leading-[1.8]'>{longDescription}</p>
</section>
)}
{/* How to automate — targets "how to connect X" queries */}
<section aria-labelledby='how-it-works-heading'>
<h2 id='how-it-works-heading' className='mb-6 font-[500] text-[#ECECEC] text-[20px]'>
How to automate {name} with Sim
</h2>
<ol className='space-y-4' aria-label='Steps to set up automation'>
{[
{
step: '01',
title: 'Create a free account',
body: 'Sign up at sim.ai in seconds. No credit card required. Your workspace is ready immediately.',
},
{
step: '02',
title: `Add a ${name} block`,
body:
authType === 'oauth'
? `Open a workflow, drag a ${name} block onto the canvas, and connect your account with one-click OAuth.`
: authType === 'api-key'
? `Open a workflow, drag a ${name} block onto the canvas, and paste in your ${name} API key.`
: `Open a workflow, drag a ${name} block onto the canvas, and authenticate your account.`,
},
{
step: '03',
title: 'Configure, connect, and run',
body: `Pick the tool you need, wire in an AI agent for reasoning or data transformation, and run. Your ${name} automation is live.`,
},
].map(({ step, title, body }) => (
<li key={step} className='flex gap-4'>
<span
className='mt-0.5 flex h-7 w-7 shrink-0 items-center justify-center rounded-full border border-[#3d3d3d] font-[500] text-[#555] text-[11px]'
aria-hidden='true'
>
{step}
</span>
<div>
<h3 className='mb-1 font-[500] text-[#ECECEC] text-[15px]'>{title}</h3>
<p className='text-[#999] text-[14px] leading-relaxed'>{body}</p>
</div>
</li>
))}
</ol>
</section>
{/* Triggers */}
{triggers.length > 0 && (
<section aria-labelledby='triggers-heading'>
<h2 id='triggers-heading' className='mb-2 font-[500] text-[#ECECEC] text-[20px]'>
Triggers
</h2>
<p className='mb-6 text-[#999] text-[14px]'>
These events in {name} can automatically start a Sim workflow — no polling
required.
</p>
<ul
className='grid grid-cols-1 gap-3 sm:grid-cols-2'
aria-label={`${name} triggers`}
>
{triggers.map((trigger) => (
<li
key={trigger.id}
className='rounded-lg border border-[#2A2A2A] bg-[#242424] p-4'
>
<div className='mb-2 flex items-center gap-2'>
<span className='inline-flex items-center gap-1 rounded-[4px] bg-[#2A2A2A] px-1.5 py-0.5 font-[500] text-[#ECECEC] text-[11px]'>
<svg
aria-hidden='true'
className='h-2.5 w-2.5'
fill='none'
stroke='currentColor'
strokeWidth={2.5}
viewBox='0 0 24 24'
>
<polygon points='13 2 3 14 12 14 11 22 21 10 12 10 13 2' />
</svg>
Trigger
</span>
</div>
<p className='font-[500] text-[#ECECEC] text-[13px]'>{trigger.name}</p>
{trigger.description && (
<p className='mt-1 text-[#999] text-[12px] leading-relaxed'>
{trigger.description}
</p>
)}
</li>
))}
</ul>
</section>
)}
{/* Popular workflows featuring this integration */}
{featuredPairs.length > 0 && (
<section aria-labelledby='workflows-heading'>
<h2 id='workflows-heading' className='mb-2 font-[500] text-[#ECECEC] text-[20px]'>
Popular workflows with {name}
</h2>
<p className='mb-6 text-[#999] text-[14px]'>
Common automation patterns teams build on Sim using {name}.
</p>
<ul
className='grid grid-cols-1 gap-4 sm:grid-cols-2'
aria-label='Popular workflow combinations'
>
{featuredPairs.map(({ from, to, headline, description: desc }) => {
const fromInt = byName.get(from)
const toInt = byName.get(to)
const FromIcon = fromInt ? blockTypeToIconMap[fromInt.type] : undefined
const ToIcon = toInt ? blockTypeToIconMap[toInt.type] : undefined
const fromBg = fromInt?.bgColor ?? '#6B7280'
const toBg = toInt?.bgColor ?? '#6B7280'
return (
<li key={`${from}-${to}`}>
<div className='h-full rounded-lg border border-[#2A2A2A] bg-[#242424] p-5'>
<div className='mb-3 flex items-center gap-2 text-[12px]'>
<span className='inline-flex items-center gap-1 rounded-[3px] bg-[#2A2A2A] px-1.5 py-0.5 font-[500] text-[#ECECEC]'>
{FromIcon && (
<IntegrationIcon
bgColor={fromBg}
name={from}
Icon={FromIcon}
as='span'
className='h-3.5 w-3.5 rounded-[2px]'
iconClassName='h-2.5 w-2.5'
aria-hidden='true'
/>
)}
{from}
</span>
<span className='text-[#555]' aria-hidden='true'>
→
</span>
<span className='inline-flex items-center gap-1 rounded-[3px] bg-[#2A2A2A] px-1.5 py-0.5 font-[500] text-[#ECECEC]'>
{ToIcon && (
<IntegrationIcon
bgColor={toBg}
name={to}
Icon={ToIcon}
as='span'
className='h-3.5 w-3.5 rounded-[2px]'
iconClassName='h-2.5 w-2.5'
aria-hidden='true'
/>
)}
{to}
</span>
</div>
<p className='mb-1 font-[500] text-[#ECECEC] text-[14px]'>{headline}</p>
<p className='text-[#999] text-[13px] leading-relaxed'>{desc}</p>
</div>
</li>
)
})}
</ul>
</section>
)}
{/* Workflow templates */}
{matchingTemplates.length > 0 && (
<section aria-labelledby='templates-heading'>
<h2 id='templates-heading' className='mb-2 font-[500] text-[#ECECEC] text-[20px]'>
Workflow templates
</h2>
<p className='mb-6 text-[#999] text-[14px]'>
Ready-to-use workflows featuring {name}. Click any to build it instantly.
</p>
<ul
className='grid grid-cols-1 gap-3 sm:grid-cols-2'
aria-label='Workflow templates'
>
{matchingTemplates.map((template) => {
const allTypes = [
integration.type,
...template.integrationBlockTypes.filter((bt) => bt !== integration.type),
]
return (
<li key={template.title}>
<TemplateCardButton prompt={template.prompt}>
{/* Integration icons */}
<div className='mb-3 flex items-center gap-1.5'>
{allTypes.map((bt) => {
const int = byType.get(bt)
const intName = int?.name ?? bt
return (
<IntegrationIcon
key={bt}
bgColor={int?.bgColor ?? '#6B7280'}
name={intName}
Icon={blockTypeToIconMap[bt]}
className='h-6 w-6 rounded-[5px]'
iconClassName='h-3.5 w-3.5'
fallbackClassName='text-[9px]'
title={intName}
aria-label={intName}
/>
)
})}
</div>
<p className='mb-3 font-[500] text-[#ECECEC] text-[13px] leading-snug'>
{template.title}
</p>
<span className='text-[#555] text-[12px] transition-colors group-hover:text-[#999]'>
Try this workflow →
</span>
</TemplateCardButton>
</li>
)
})}
</ul>
</section>
)}
{/* Tools */}
{operations.length > 0 && (
<section aria-labelledby='tools-heading'>
<h2 id='tools-heading' className='mb-2 font-[500] text-[#ECECEC] text-[20px]'>
Supported tools
</h2>
<p className='mb-6 text-[#999] text-[14px]'>
{operations.length} {name} tool{operations.length === 1 ? '' : 's'} available in
Sim
</p>
<ul
className='grid grid-cols-1 gap-2 sm:grid-cols-2'
aria-label={`${name} supported tools`}
>
{operations.map((op) => (
<li
key={op.name}
className='rounded-[6px] border border-[#2A2A2A] bg-[#242424] px-3.5 py-3'
>
<p className='font-[500] text-[#ECECEC] text-[13px]'>{op.name}</p>
{op.description && (
<p className='mt-0.5 text-[#555] text-[12px] leading-relaxed'>
{op.description}
</p>
)}
</li>
))}
</ul>
</section>
)}
{/* FAQ */}
<section aria-labelledby='faq-heading'>
<h2 id='faq-heading' className='mb-8 font-[500] text-[#ECECEC] text-[20px]'>
Frequently asked questions
</h2>
<IntegrationFAQ faqs={faqs} />
</section>
</div>
{/* Sidebar */}
<aside className='space-y-5' aria-label='Integration details'>
{/* Quick details */}
<div className='rounded-lg border border-[#2A2A2A] bg-[#242424] p-5'>
<h3 className='mb-4 font-[500] text-[#ECECEC] text-[14px]'>Details</h3>
<dl className='space-y-3 text-[13px]'>
{operations.length > 0 && (
<div>
<dt className='text-[#555]'>Tools</dt>
<dd className='text-[#ECECEC]'>{operations.length} supported</dd>
</div>
)}
{triggers.length > 0 && (
<div>
<dt className='text-[#555]'>Triggers</dt>
<dd className='text-[#ECECEC]'>{triggers.length} available</dd>
</div>
)}
<div>
<dt className='text-[#555]'>Auth</dt>
<dd className='text-[#ECECEC]'>
{authType === 'oauth'
? 'One-click OAuth'
: authType === 'api-key'
? 'API key'
: 'None required'}
</dd>
</div>
<div>
<dt className='text-[#555]'>Pricing</dt>
<dd className='text-[#ECECEC]'>Free to start</dd>
</div>
</dl>
<a
href='https://sim.ai'
className='mt-5 flex h-[32px] w-full items-center justify-center rounded-[5px] border border-[#FFFFFF] bg-[#FFFFFF] font-[430] font-season text-[#1C1C1C] text-[13px] transition-colors hover:border-[#E0E0E0] hover:bg-[#E0E0E0]'
>
Get started free
</a>
</div>
{/* Docs */}
<div className='rounded-lg border border-[#2A2A2A] bg-[#242424] p-5'>
<h3 className='mb-2 font-[500] text-[#ECECEC] text-[14px]'>Documentation</h3>
<p className='mb-4 text-[#999] text-[13px] leading-relaxed'>
Full API reference, authentication setup, and usage examples for {name}.
</p>
<a
href={docsUrl}
target='_blank'
rel='noopener noreferrer'
className='inline-flex items-center gap-1.5 text-[#999] text-[13px] transition-colors hover:text-[#ECECEC]'
>
docs.sim.ai
<svg
aria-hidden='true'
className='h-3 w-3'
fill='none'
stroke='currentColor'
strokeWidth={2}
viewBox='0 0 24 24'
>
<path d='M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6' />
<polyline points='15 3 21 3 21 9' />
<line x1='10' x2='21' y1='14' y2='3' />
</svg>
</a>
</div>
{/* Related integrations — internal linking for SEO */}
{relatedIntegrations.length > 0 && (
<div className='rounded-lg border border-[#2A2A2A] bg-[#242424] p-5'>
<h3 className='mb-4 font-[500] text-[#ECECEC] text-[14px]'>Related integrations</h3>
<ul className='space-y-2'>
{relatedIntegrations.map((rel) => (
<li key={rel.slug}>
<Link
href={`/integrations/${rel.slug}`}
className='flex items-center gap-2.5 rounded-[6px] p-1.5 text-[#999] text-[13px] transition-colors hover:bg-[#2A2A2A] hover:text-[#ECECEC]'
>
<IntegrationIcon
bgColor={rel.bgColor}
name={rel.name}
Icon={blockTypeToIconMap[rel.type]}
as='span'
className='h-6 w-6 rounded-[4px]'
iconClassName='h-3.5 w-3.5'
fallbackClassName='text-[10px]'
aria-hidden='true'
/>
{rel.name}
</Link>
</li>
))}
</ul>
<Link
href='/integrations'
className='mt-4 block text-[#555] text-[12px] transition-colors hover:text-[#999]'
>
All integrations →
</Link>
</div>
)}
</aside>
</div>
{/* Bottom CTA */}
<section
aria-labelledby='cta-heading'
className='mt-20 rounded-xl border border-[#2A2A2A] bg-[#242424] p-8 text-center sm:p-12'
>
<h2
id='cta-heading'
className='mb-3 font-[500] text-[#ECECEC] text-[28px] sm:text-[34px]'
>
Start automating {name} today
</h2>
<p className='mx-auto mb-8 max-w-[480px] text-[#999] text-[16px] leading-relaxed'>
Build your first AI workflow with {name} in minutes. Connect to every tool your team
uses. Free to start — no credit card required.
</p>
<a
href='https://sim.ai'
className='inline-flex h-[32px] items-center rounded-[5px] border border-[#FFFFFF] bg-[#FFFFFF] px-[10px] font-[430] font-season text-[#1C1C1C] text-[14px] transition-colors hover:border-[#E0E0E0] hover:bg-[#E0E0E0]'
>
Build for free →
</a>
</section>
</div>
</>
)
}
@@ -0,0 +1,55 @@
import type { ComponentType, SVGProps } from 'react'
import Link from 'next/link'
import { Badge } from '@/components/emcn'
import type { Integration } from '@/app/(landing)/integrations/data/types'
import { IntegrationIcon } from './integration-icon'
interface IntegrationCardProps {
integration: Integration
IconComponent?: ComponentType<SVGProps<SVGSVGElement>>
}
export function IntegrationCard({ integration, IconComponent }: IntegrationCardProps) {
const { slug, name, description, bgColor, operationCount, triggerCount } = integration
return (
<Link
href={`/integrations/${slug}`}
className='group flex flex-col rounded-lg border border-[#2A2A2A] bg-[#242424] p-4 transition-colors hover:border-[#3d3d3d] hover:bg-[#2A2A2A]'
aria-label={`${name} integration`}
>
<IntegrationIcon
bgColor={bgColor}
name={name}
Icon={IconComponent}
className='mb-3 h-10 w-10 rounded-lg'
aria-hidden='true'
/>
{/* Name */}
<h3 className='mb-1 font-[500] text-[#ECECEC] text-[14px] leading-snug'>{name}</h3>
{/* Description — clamped to 2 lines */}
<p className='mb-3 line-clamp-2 flex-1 text-[#999] text-[12px] leading-relaxed'>
{description}
</p>
{/* Footer row */}
<div className='flex flex-wrap items-center gap-1.5'>
{operationCount > 0 && (
<Badge className='border-0 bg-[#333] text-[#999] text-[11px]'>
{operationCount} {operationCount === 1 ? 'tool' : 'tools'}
</Badge>
)}
{triggerCount > 0 && (
<Badge className='border-0 bg-[#333] text-[#999] text-[11px]'>
{triggerCount} {triggerCount === 1 ? 'trigger' : 'triggers'}
</Badge>
)}
<span className='ml-auto text-[#555] text-[12px] transition-colors group-hover:text-[#999]'>
Learn more →
</span>
</div>
</Link>
)
}
@@ -0,0 +1,71 @@
'use client'
import { useMemo, useState } from 'react'
import { Input } from '@/components/emcn'
import { blockTypeToIconMap } from '@/app/(landing)/integrations/data/icon-mapping'
import type { Integration } from '@/app/(landing)/integrations/data/types'
import { IntegrationCard } from './integration-card'
interface IntegrationGridProps {
integrations: Integration[]
}
export function IntegrationGrid({ integrations }: IntegrationGridProps) {
const [query, setQuery] = useState('')
const filtered = useMemo(() => {
const q = query.trim().toLowerCase()
if (!q) return integrations
return integrations.filter(
(i) =>
i.name.toLowerCase().includes(q) ||
i.description.toLowerCase().includes(q) ||
i.operations.some(
(op) => op.name.toLowerCase().includes(q) || op.description.toLowerCase().includes(q)
) ||
i.triggers.some((t) => t.name.toLowerCase().includes(q))
)
}, [integrations, query])
return (
<div>
<div className='relative mb-8 max-w-[480px]'>
<svg
aria-hidden='true'
className='-translate-y-1/2 pointer-events-none absolute top-1/2 left-3 h-4 w-4 text-[#555]'
fill='none'
stroke='currentColor'
strokeWidth={2}
viewBox='0 0 24 24'
>
<circle cx={11} cy={11} r={8} />
<path d='m21 21-4.35-4.35' />
</svg>
<Input
type='search'
placeholder='Search integrations, tools, or triggers…'
value={query}
onChange={(e) => setQuery(e.target.value)}
className='pl-9'
aria-label='Search integrations'
/>
</div>
{filtered.length === 0 ? (
<p className='py-12 text-center text-[#555] text-[15px]'>
No integrations found for &ldquo;{query}&rdquo;
</p>
) : (
<div className='grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4'>
{filtered.map((integration) => (
<IntegrationCard
key={integration.type}
integration={integration}
IconComponent={blockTypeToIconMap[integration.type]}
/>
))}
</div>
)}
</div>
)
}
@@ -0,0 +1,54 @@
import type { ComponentType, ElementType, HTMLAttributes, SVGProps } from 'react'
import { cn } from '@/lib/core/utils/cn'
import { isLightBg } from '@/app/(landing)/integrations/data/utils'
interface IntegrationIconProps extends HTMLAttributes<HTMLElement> {
bgColor: string
/** Integration name — used for the fallback initial letter. */
name: string
/** Optional icon component. When absent, renders the first letter of `name`. */
Icon?: ComponentType<SVGProps<SVGSVGElement>> | null
/** Tailwind size + rounding classes for the container. Default: `h-10 w-10 rounded-lg` */
className?: string
/** Tailwind size classes for the icon SVG. Default: `h-5 w-5` */
iconClassName?: string
/** Tailwind text-size class for the fallback letter. Default: `text-[15px]` */
fallbackClassName?: string
/** Rendered HTML element. Default: `div` */
as?: ElementType
}
/**
* Colored icon box used across integration listing and detail pages.
* Renders an integration icon over a brand-colored background, falling back
* to the integration's initial letter when no icon is available.
*/
export function IntegrationIcon({
bgColor,
name,
Icon,
className,
iconClassName = 'h-5 w-5',
fallbackClassName = 'text-[15px]',
as: Tag = 'div',
...rest
}: IntegrationIconProps) {
const isLight = isLightBg(bgColor)
const fgColor = isLight ? 'text-[#1C1C1C]' : 'text-white'
return (
<Tag
className={cn('flex shrink-0 items-center justify-center', className)}
style={{ background: bgColor }}
{...rest}
>
{Icon ? (
<Icon className={cn(iconClassName, fgColor)} />
) : (
<span className={cn('font-[500] leading-none', fallbackClassName, fgColor)}>
{name.charAt(0)}
</span>
)}
</Tag>
)
}
@@ -0,0 +1,339 @@
// Auto-generated file - do not edit manually
// Generated by scripts/generate-docs.ts
// Maps block types to their icon component references for the integrations page
import type { ComponentType, SVGProps } from 'react'
import {
A2AIcon,
AhrefsIcon,
AirtableIcon,
AirweaveIcon,
AlgoliaIcon,
AmplitudeIcon,
ApifyIcon,
ApolloIcon,
ArxivIcon,
AsanaIcon,
AshbyIcon,
AttioIcon,
BrainIcon,
BrandfetchIcon,
BrowserUseIcon,
CalComIcon,
CalendlyIcon,
CirclebackIcon,
ClayIcon,
ClerkIcon,
CloudflareIcon,
ConfluenceIcon,
CursorIcon,
DatabricksIcon,
DatadogIcon,
DevinIcon,
DiscordIcon,
DocumentIcon,
DropboxIcon,
DsPyIcon,
DubIcon,
DuckDuckGoIcon,
DynamoDBIcon,
ElasticsearchIcon,
ElevenLabsIcon,
EnrichSoIcon,
EvernoteIcon,
ExaAIIcon,
EyeIcon,
FathomIcon,
FirecrawlIcon,
FirefliesIcon,
GammaIcon,
GithubIcon,
GitLabIcon,
GmailIcon,
GongIcon,
GoogleAdsIcon,
GoogleBigQueryIcon,
GoogleBooksIcon,
GoogleCalendarIcon,
GoogleContactsIcon,
GoogleDocsIcon,
GoogleDriveIcon,
GoogleFormsIcon,
GoogleGroupsIcon,
GoogleIcon,
GoogleMapsIcon,
GoogleMeetIcon,
GooglePagespeedIcon,
GoogleSheetsIcon,
GoogleSlidesIcon,
GoogleTasksIcon,
GoogleTranslateIcon,
GoogleVaultIcon,
GrafanaIcon,
GrainIcon,
GreenhouseIcon,
GreptileIcon,
HexIcon,
HubspotIcon,
HuggingFaceIcon,
HunterIOIcon,
ImageIcon,
IncidentioIcon,
IntercomIcon,
JinaAIIcon,
JiraIcon,
JiraServiceManagementIcon,
KalshiIcon,
LangsmithIcon,
LemlistIcon,
LinearIcon,
LinkedInIcon,
LinkupIcon,
LoopsIcon,
LumaIcon,
MailchimpIcon,
MailgunIcon,
MailServerIcon,
Mem0Icon,
MicrosoftDataverseIcon,
MicrosoftExcelIcon,
MicrosoftOneDriveIcon,
MicrosoftPlannerIcon,
MicrosoftSharepointIcon,
MicrosoftTeamsIcon,
MistralIcon,
MongoDBIcon,
MySQLIcon,
Neo4jIcon,
NotionIcon,
ObsidianIcon,
OnePasswordIcon,
OpenAIIcon,
OutlookIcon,
PackageSearchIcon,
PagerDutyIcon,
ParallelIcon,
PerplexityIcon,
PineconeIcon,
PipedriveIcon,
PolymarketIcon,
PostgresIcon,
PosthogIcon,
PulseIcon,
QdrantIcon,
RDSIcon,
RedditIcon,
RedisIcon,
ReductoIcon,
ResendIcon,
RevenueCatIcon,
S3Icon,
SalesforceIcon,
SearchIcon,
SendgridIcon,
SentryIcon,
SerperIcon,
ServiceNowIcon,
SftpIcon,
ShopifyIcon,
SimilarwebIcon,
SlackIcon,
SmtpIcon,
SQSIcon,
SshIcon,
STTIcon,
StagehandIcon,
StripeIcon,
SupabaseIcon,
TavilyIcon,
TelegramIcon,
TextractIcon,
TinybirdIcon,
TranslateIcon,
TrelloIcon,
TTSIcon,
TwilioIcon,
TypeformIcon,
UpstashIcon,
VercelIcon,
VideoIcon,
WealthboxIcon,
WebflowIcon,
WhatsAppIcon,
WikipediaIcon,
WordpressIcon,
xIcon,
YouTubeIcon,
ZendeskIcon,
ZepIcon,
ZoomIcon,
} from '@/components/icons'
type IconComponent = ComponentType<SVGProps<SVGSVGElement>>
export const blockTypeToIconMap: Record<string, IconComponent> = {
a2a: A2AIcon,
ahrefs: AhrefsIcon,
airtable: AirtableIcon,
airweave: AirweaveIcon,
algolia: AlgoliaIcon,
amplitude: AmplitudeIcon,
apify: ApifyIcon,
apollo: ApolloIcon,
arxiv: ArxivIcon,
asana: AsanaIcon,
ashby: AshbyIcon,
attio: AttioIcon,
brandfetch: BrandfetchIcon,
browser_use: BrowserUseIcon,
calcom: CalComIcon,
calendly: CalendlyIcon,
circleback: CirclebackIcon,
clay: ClayIcon,
clerk: ClerkIcon,
cloudflare: CloudflareIcon,
confluence_v2: ConfluenceIcon,
cursor_v2: CursorIcon,
databricks: DatabricksIcon,
datadog: DatadogIcon,
devin: DevinIcon,
discord: DiscordIcon,
dropbox: DropboxIcon,
dspy: DsPyIcon,
dub: DubIcon,
duckduckgo: DuckDuckGoIcon,
dynamodb: DynamoDBIcon,
elasticsearch: ElasticsearchIcon,
elevenlabs: ElevenLabsIcon,
enrich: EnrichSoIcon,
evernote: EvernoteIcon,
exa: ExaAIIcon,
fathom: FathomIcon,
file_v3: DocumentIcon,
firecrawl: FirecrawlIcon,
fireflies_v2: FirefliesIcon,
gamma: GammaIcon,
github_v2: GithubIcon,
gitlab: GitLabIcon,
gmail_v2: GmailIcon,
gong: GongIcon,
google_ads: GoogleAdsIcon,
google_bigquery: GoogleBigQueryIcon,
google_books: GoogleBooksIcon,
google_calendar_v2: GoogleCalendarIcon,
google_contacts: GoogleContactsIcon,
google_docs: GoogleDocsIcon,
google_drive: GoogleDriveIcon,
google_forms: GoogleFormsIcon,
google_groups: GoogleGroupsIcon,
google_maps: GoogleMapsIcon,
google_meet: GoogleMeetIcon,
google_pagespeed: GooglePagespeedIcon,
google_search: GoogleIcon,
google_sheets_v2: GoogleSheetsIcon,
google_slides_v2: GoogleSlidesIcon,
google_tasks: GoogleTasksIcon,
google_translate: GoogleTranslateIcon,
google_vault: GoogleVaultIcon,
grafana: GrafanaIcon,
grain: GrainIcon,
greenhouse: GreenhouseIcon,
greptile: GreptileIcon,
hex: HexIcon,
hubspot: HubspotIcon,
huggingface: HuggingFaceIcon,
hunter: HunterIOIcon,
image_generator: ImageIcon,
imap: MailServerIcon,
incidentio: IncidentioIcon,
intercom_v2: IntercomIcon,
jina: JinaAIIcon,
jira: JiraIcon,
jira_service_management: JiraServiceManagementIcon,
kalshi_v2: KalshiIcon,
knowledge: PackageSearchIcon,
langsmith: LangsmithIcon,
lemlist: LemlistIcon,
linear: LinearIcon,
linkedin: LinkedInIcon,
linkup: LinkupIcon,
loops: LoopsIcon,
luma: LumaIcon,
mailchimp: MailchimpIcon,
mailgun: MailgunIcon,
mem0: Mem0Icon,
memory: BrainIcon,
microsoft_dataverse: MicrosoftDataverseIcon,
microsoft_excel_v2: MicrosoftExcelIcon,
microsoft_planner: MicrosoftPlannerIcon,
microsoft_teams: MicrosoftTeamsIcon,
mistral_parse_v3: MistralIcon,
mongodb: MongoDBIcon,
mysql: MySQLIcon,
neo4j: Neo4jIcon,
notion_v2: NotionIcon,
obsidian: ObsidianIcon,
onedrive: MicrosoftOneDriveIcon,
onepassword: OnePasswordIcon,
openai: OpenAIIcon,
outlook: OutlookIcon,
pagerduty: PagerDutyIcon,
parallel_ai: ParallelIcon,
perplexity: PerplexityIcon,
pinecone: PineconeIcon,
pipedrive: PipedriveIcon,
polymarket: PolymarketIcon,
postgresql: PostgresIcon,
posthog: PosthogIcon,
pulse_v2: PulseIcon,
qdrant: QdrantIcon,
rds: RDSIcon,
reddit: RedditIcon,
redis: RedisIcon,
reducto_v2: ReductoIcon,
resend: ResendIcon,
revenuecat: RevenueCatIcon,
s3: S3Icon,
salesforce: SalesforceIcon,
search: SearchIcon,
sendgrid: SendgridIcon,
sentry: SentryIcon,
serper: SerperIcon,
servicenow: ServiceNowIcon,
sftp: SftpIcon,
sharepoint: MicrosoftSharepointIcon,
shopify: ShopifyIcon,
similarweb: SimilarwebIcon,
slack: SlackIcon,
smtp: SmtpIcon,
sqs: SQSIcon,
ssh: SshIcon,
stagehand: StagehandIcon,
stripe: StripeIcon,
stt_v2: STTIcon,
supabase: SupabaseIcon,
tavily: TavilyIcon,
telegram: TelegramIcon,
textract_v2: TextractIcon,
tinybird: TinybirdIcon,
translate: TranslateIcon,
trello: TrelloIcon,
tts: TTSIcon,
twilio_sms: TwilioIcon,
twilio_voice: TwilioIcon,
typeform: TypeformIcon,
upstash: UpstashIcon,
vercel: VercelIcon,
video_generator_v2: VideoIcon,
vision_v2: EyeIcon,
wealthbox: WealthboxIcon,
webflow: WebflowIcon,
whatsapp: WhatsAppIcon,
wikipedia: WikipediaIcon,
wordpress: WordpressIcon,
x: xIcon,
youtube: YouTubeIcon,
zendesk: ZendeskIcon,
zep: ZepIcon,
zoom: ZoomIcon,
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,117 @@
/**
* Curated popular workflow pairs used on both the /integrations listing page
* and individual /integrations/[slug] pages.
*
* Each pair targets specific long-tail search queries like "notion to slack automation".
* The headline and description are written to be both human-readable and keyword-rich.
*/
export interface WorkflowPair {
/** Integration name (must match `name` field in integrations.json) */
from: string
/** Integration name (must match `name` field in integrations.json) */
to: string
headline: string
description: string
}
export const POPULAR_WORKFLOWS: WorkflowPair[] = [
{
from: 'Slack',
to: 'Notion',
headline: 'Archive Slack conversations to Notion',
description:
'Capture important Slack messages as Notion pages or database entries — ideal for meeting notes, decision logs, and knowledge bases.',
},
{
from: 'Notion',
to: 'Slack',
headline: 'Notify your team from Notion',
description:
'Post Slack messages automatically when Notion pages are created or updated so the whole team stays aligned without manual check-ins.',
},
{
from: 'GitHub',
to: 'Jira',
headline: 'Link GitHub pull requests to Jira tickets',
description:
'Transition Jira issues when PRs are opened or merged, keeping your project board accurate without any manual updates.',
},
{
from: 'GitHub',
to: 'Linear',
headline: 'Sync GitHub events with Linear issues',
description:
'Create Linear issues from GitHub activity, update status on merge, and keep your engineering workflow tightly connected.',
},
{
from: 'Gmail',
to: 'Notion',
headline: 'Save incoming emails to Notion databases',
description:
'Extract structured data from Gmail and store it in Notion — ideal for lead capture, support tickets, and meeting scheduling.',
},
{
from: 'HubSpot',
to: 'Slack',
headline: 'Get HubSpot deal alerts in Slack',
description:
'Receive instant Slack notifications when HubSpot deals advance, contacts are created, or revenue milestones are hit.',
},
{
from: 'Google Sheets',
to: 'Slack',
headline: 'Send Slack messages from Google Sheets',
description:
'Watch a spreadsheet for new rows or changes, then post formatted Slack updates to keep stakeholders informed in real time.',
},
{
from: 'Salesforce',
to: 'Slack',
headline: 'Push Salesforce pipeline updates to Slack',
description:
'Alert your sales team in Slack when Salesforce opportunities advance, close, or need immediate attention.',
},
{
from: 'Airtable',
to: 'Gmail',
headline: 'Trigger Gmail from Airtable records',
description:
'Send personalised Gmail messages when Airtable records are created or updated — great for onboarding flows and follow-up sequences.',
},
{
from: 'Linear',
to: 'Slack',
headline: 'Linear issue updates in Slack',
description:
'Post Slack messages when Linear issues are created, assigned, or completed so your team is always in the loop.',
},
{
from: 'Jira',
to: 'Confluence',
headline: 'Auto-generate Confluence pages from Jira sprints',
description:
'Create Confluence documentation from Jira sprint data automatically, eliminating manual reporting at the end of every sprint.',
},
{
from: 'Google Sheets',
to: 'Notion',
headline: 'Sync Google Sheets data into Notion',
description:
'Transform spreadsheet rows into structured Notion database entries for richer documentation and cross-team project tracking.',
},
{
from: 'GitHub',
to: 'Slack',
headline: 'Get GitHub activity alerts in Slack',
description:
'Post Slack notifications for new PRs, commits, issues, or deployments so your engineering team never misses a critical event.',
},
{
from: 'HubSpot',
to: 'Gmail',
headline: 'Send personalised emails from HubSpot events',
description:
'Trigger Gmail messages when HubSpot contacts enter a lifecycle stage, ensuring timely and relevant outreach without manual effort.',
},
]
@@ -0,0 +1,37 @@
// Shared types for the integrations section of the landing site.
// Mirrors the shape written by scripts/generate-docs.ts → writeIntegrationsJson().
export type AuthType = 'oauth' | 'api-key' | 'none'
export interface TriggerInfo {
id: string
name: string
description: string
}
export interface OperationInfo {
name: string
description: string
}
export interface FAQItem {
question: string
answer: string
}
export interface Integration {
type: string
slug: string
name: string
description: string
longDescription: string
bgColor: string
iconName: string
docsUrl: string
operations: OperationInfo[]
operationCount: number
triggers: TriggerInfo[]
triggerCount: number
authType: AuthType
category: string
}
@@ -0,0 +1,15 @@
/**
* Utility helpers for the integrations landing pages.
* Shared across the listing grid, individual integration cards, and slug pages.
*/
/** bgColor values that are visually light and require dark icon text. */
const LIGHT_BG = new Set(['#e0e0e0', '#f5f5f5', '#ffffff', '#ececec', '#f0f0f0'])
/**
* Returns true when `bgColor` is a light color that requires dark foreground text.
* Handles gradient strings safely — they always use light foreground (white).
*/
export function isLightBg(bgColor: string): boolean {
return LIGHT_BG.has(bgColor.toLowerCase())
}
@@ -0,0 +1,43 @@
import Footer from '@/app/(home)/components/footer/footer'
import Navbar from '@/app/(home)/components/navbar/navbar'
export default function IntegrationsLayout({ children }: { children: React.ReactNode }) {
const orgJsonLd = {
'@context': 'https://schema.org',
'@type': 'Organization',
name: 'Sim',
url: 'https://sim.ai',
logo: 'https://sim.ai/logo/primary/small.png',
sameAs: ['https://x.com/simdotai'],
}
const websiteJsonLd = {
'@context': 'https://schema.org',
'@type': 'WebSite',
name: 'Sim',
url: 'https://sim.ai',
potentialAction: {
'@type': 'SearchAction',
target: 'https://sim.ai/search?q={search_term_string}',
'query-input': 'required name=search_term_string',
},
}
return (
<div className='dark flex min-h-screen flex-col bg-[#1C1C1C] font-[430] font-season text-[#ECECEC]'>
<script
type='application/ld+json'
dangerouslySetInnerHTML={{ __html: JSON.stringify(orgJsonLd) }}
/>
<script
type='application/ld+json'
dangerouslySetInnerHTML={{ __html: JSON.stringify(websiteJsonLd) }}
/>
<header>
<Navbar />
</header>
<main className='relative flex-1'>{children}</main>
<Footer />
</div>
)
}
@@ -0,0 +1,165 @@
import type { Metadata } from 'next'
import { IntegrationGrid } from './components/integration-grid'
import { blockTypeToIconMap } from './data/icon-mapping'
import integrations from './data/integrations.json'
import { POPULAR_WORKFLOWS } from './data/popular-workflows'
import type { Integration } from './data/types'
const allIntegrations = integrations as Integration[]
const INTEGRATION_COUNT = allIntegrations.length
/**
* Unique integration names that appear in popular workflow pairs.
* Used for metadata keywords so they stay in sync automatically.
*/
const TOP_NAMES = [...new Set(POPULAR_WORKFLOWS.flatMap((p) => [p.from, p.to]))].slice(0, 6)
export const metadata: Metadata = {
title: 'Integrations',
description: `Connect ${INTEGRATION_COUNT}+ apps and services with Sim's AI workflow automation. Build intelligent pipelines with ${TOP_NAMES.join(', ')}, and more.`,
keywords: [
'workflow automation integrations',
'AI workflow automation',
'no-code automation',
...TOP_NAMES.flatMap((n) => [`${n} integration`, `${n} automation`]),
...allIntegrations.slice(0, 20).map((i) => `${i.name} automation`),
],
openGraph: {
title: 'Integrations for AI Workflow Automation | Sim',
description: `Connect ${INTEGRATION_COUNT}+ apps with Sim. Build AI-powered pipelines that link ${TOP_NAMES.join(', ')}, and every tool your team uses.`,
url: 'https://sim.ai/integrations',
type: 'website',
images: [{ url: 'https://sim.ai/opengraph-image.png', width: 1200, height: 630 }],
},
twitter: {
card: 'summary_large_image',
title: 'Integrations | Sim',
description: `Connect ${INTEGRATION_COUNT}+ apps with Sim's AI workflow automation.`,
},
alternates: { canonical: 'https://sim.ai/integrations' },
}
export default function IntegrationsPage() {
const breadcrumbJsonLd = {
'@context': 'https://schema.org',
'@type': 'BreadcrumbList',
itemListElement: [
{ '@type': 'ListItem', position: 1, name: 'Home', item: 'https://sim.ai' },
{
'@type': 'ListItem',
position: 2,
name: 'Integrations',
item: 'https://sim.ai/integrations',
},
],
}
const itemListJsonLd = {
'@context': 'https://schema.org',
'@type': 'ItemList',
name: 'Sim AI Workflow Integrations',
description: `Complete list of ${INTEGRATION_COUNT}+ integrations available in Sim for building AI-powered workflow automation.`,
url: 'https://sim.ai/integrations',
numberOfItems: INTEGRATION_COUNT,
itemListElement: allIntegrations.map((integration, index) => ({
'@type': 'ListItem',
position: index + 1,
item: {
'@type': 'SoftwareApplication',
name: integration.name,
description: integration.description,
url: `https://sim.ai/integrations/${integration.slug}`,
applicationCategory: 'BusinessApplication',
featureList: integration.operations.map((o) => o.name),
},
})),
}
return (
<>
<script
type='application/ld+json'
dangerouslySetInnerHTML={{ __html: JSON.stringify(breadcrumbJsonLd) }}
/>
<script
type='application/ld+json'
dangerouslySetInnerHTML={{ __html: JSON.stringify(itemListJsonLd) }}
/>
<div className='mx-auto max-w-[1200px] px-6 py-16 sm:px-8 md:px-12'>
{/* Hero */}
<section aria-labelledby='integrations-heading' className='mb-16'>
<h1
id='integrations-heading'
className='mb-4 font-[500] text-[#ECECEC] text-[40px] leading-tight sm:text-[56px]'
>
Integrations
</h1>
<p className='max-w-[640px] text-[#999] text-[18px] leading-relaxed'>
Connect every tool your team uses. Build AI-powered workflows that automate tasks across{' '}
{TOP_NAMES.slice(0, 4).map((name, i, arr) => {
const integration = allIntegrations.find((int) => int.name === name)
const Icon = integration ? blockTypeToIconMap[integration.type] : undefined
return (
<span key={name} className='inline-flex items-center gap-[5px]'>
{Icon && (
<span
aria-hidden='true'
className='inline-flex shrink-0'
style={{ opacity: 0.65 }}
>
<Icon className='h-[0.85em] w-[0.85em]' />
</span>
)}
{name}
{i < arr.length - 1 ? ', ' : ''}
</span>
)
})}
{' and more.'}
</p>
</section>
{/* Searchable grid — client component */}
<section aria-labelledby='all-integrations-heading'>
<h2 id='all-integrations-heading' className='mb-8 font-[500] text-[#ECECEC] text-[24px]'>
All Integrations
</h2>
<IntegrationGrid integrations={allIntegrations} />
</section>
{/* Integration request */}
<div className='mt-16 flex flex-col items-start gap-3 border-[#2A2A2A] border-t pt-10 sm:flex-row sm:items-center sm:justify-between'>
<div>
<p className='font-[500] text-[#ECECEC] text-[15px]'>
Don&apos;t see the integration you need?
</p>
<p className='mt-0.5 text-[#555] text-[13px]'>
Let us know and we&apos;ll prioritize it.
</p>
</div>
<a
href='https://github.com/simstudioai/sim/issues/new?labels=integration+request&template=integration_request.md'
target='_blank'
rel='noopener noreferrer'
className='inline-flex h-[32px] shrink-0 items-center gap-[6px] rounded-[5px] border border-[#3d3d3d] px-[10px] font-[430] font-season text-[#ECECEC] text-[14px] transition-colors hover:bg-[#2A2A2A]'
>
Request an integration
<svg
aria-hidden='true'
className='h-3 w-3'
fill='none'
stroke='currentColor'
strokeWidth={2}
viewBox='0 0 24 24'
>
<path d='M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6' />
<polyline points='15 3 21 3 21 9' />
<line x1='10' x2='21' y1='14' y2='3' />
</svg>
</a>
</div>
</div>
</>
)
}
@@ -105,6 +105,8 @@ export interface TemplatePrompt {
title: string
prompt: string
image?: string
// Base block type keys from `blocks/registry.ts` for integrations used by this template.
integrationBlockTypes: string[]
modules: ModuleTag[]
category: Category
tags: Tag[]
@@ -126,6 +128,7 @@ export const TEMPLATES: TemplatePrompt[] = [
prompt:
'Create a self-healing CRM table that keeps track of all my customers by integrating with my existing data sources. Schedule a recurring job every morning to automatically pull updates from all relevant data sources and keep my CRM up to date.',
image: '/templates/crm-light.png',
integrationBlockTypes: [],
modules: ['tables', 'scheduled', 'workflows'],
category: 'popular',
tags: ['founder', 'sales', 'crm', 'sync', 'automation'],
@@ -137,6 +140,7 @@ export const TEMPLATES: TemplatePrompt[] = [
prompt:
'Create an agent that checks my Google Calendar each morning, researches every attendee and topic on the web, and prepares a brief for each meeting so I walk in fully prepared. Schedule it to run every weekday morning.',
image: '/templates/meeting-prep-dark.png',
integrationBlockTypes: ['google_calendar'],
modules: ['agent', 'scheduled', 'workflows'],
category: 'popular',
tags: ['founder', 'sales', 'research', 'automation'],
@@ -148,6 +152,7 @@ export const TEMPLATES: TemplatePrompt[] = [
prompt:
'Create a file of all my todos then go one by one and check off every time a todo is done. Look at my calendar and see what I have to do.',
image: '/templates/todo-list-light.png',
integrationBlockTypes: [],
modules: ['files', 'agent', 'workflows'],
category: 'popular',
tags: ['individual', 'automation'],
@@ -159,6 +164,7 @@ export const TEMPLATES: TemplatePrompt[] = [
prompt:
'Build an agent that takes a topic, searches the web for the latest information, summarizes key findings, and compiles them into a clean document I can review.',
image: '/templates/research-assistant-dark.png',
integrationBlockTypes: [],
modules: ['agent', 'files', 'workflows'],
category: 'popular',
tags: ['founder', 'research', 'content', 'individual'],
@@ -170,6 +176,7 @@ export const TEMPLATES: TemplatePrompt[] = [
prompt:
'Create a workflow that reads my Gmail inbox, identifies emails that need a response, and drafts contextual replies for each one. Schedule it to run every hour.',
image: '/templates/gmail-agent-dark.png',
integrationBlockTypes: ['gmail'],
modules: ['agent', 'workflows'],
category: 'popular',
tags: ['individual', 'communication', 'automation'],
@@ -181,6 +188,7 @@ export const TEMPLATES: TemplatePrompt[] = [
prompt:
'Create a table that tracks all my expenses by pulling transactions from my connected accounts. Categorize each expense automatically and generate a weekly summary report.',
image: '/templates/expense-tracker-light.png',
integrationBlockTypes: [],
modules: ['tables', 'scheduled', 'workflows'],
category: 'popular',
tags: ['finance', 'individual', 'reporting'],
@@ -193,6 +201,7 @@ export const TEMPLATES: TemplatePrompt[] = [
title: 'RFP and proposal drafter',
prompt:
'Create a knowledge base from my past proposals, case studies, and company information. Then build an agent that drafts responses to new RFPs by matching requirements to relevant past work, generating tailored sections, and compiling a complete proposal file.',
integrationBlockTypes: [],
modules: ['knowledge-base', 'files', 'agent'],
category: 'sales',
tags: ['sales', 'content', 'enterprise'],
@@ -202,6 +211,7 @@ export const TEMPLATES: TemplatePrompt[] = [
title: 'Competitive battle cards',
prompt:
'Create an agent that deep-researches each of my competitors using web search — their product features, pricing, positioning, strengths, and weaknesses — and generates a structured battle card document for each one that my sales team can reference during calls.',
integrationBlockTypes: [],
modules: ['agent', 'files', 'workflows'],
category: 'sales',
tags: ['sales', 'research', 'content'],
@@ -211,6 +221,7 @@ export const TEMPLATES: TemplatePrompt[] = [
title: 'QBR prep agent',
prompt:
'Build a workflow that compiles everything needed for a quarterly business review — pulling customer usage data, support ticket history, billing summary, and key milestones from my tables — and generates a polished QBR document ready to present.',
integrationBlockTypes: [],
modules: ['tables', 'files', 'agent', 'workflows'],
category: 'sales',
tags: ['sales', 'support', 'reporting'],
@@ -220,6 +231,7 @@ export const TEMPLATES: TemplatePrompt[] = [
title: 'CRM knowledge search',
prompt:
'Create a knowledge base connected to my Salesforce account so all deals, contacts, notes, and activities are automatically synced and searchable. Then build an agent I can ask things like "what\'s the history with Acme Corp?" or "who was involved in the last enterprise deal?" and get instant answers with CRM record citations.',
integrationBlockTypes: ['salesforce'],
modules: ['knowledge-base', 'agent'],
category: 'sales',
tags: ['sales', 'crm', 'research'],
@@ -229,6 +241,7 @@ export const TEMPLATES: TemplatePrompt[] = [
title: 'HubSpot deal search',
prompt:
'Create a knowledge base connected to my HubSpot account so all deals, contacts, and activity history are automatically synced and searchable. Then build an agent I can ask things like "what happened with the Stripe integration deal?" or "which deals closed last quarter over $50k?" and get answers with HubSpot record links.',
integrationBlockTypes: ['hubspot'],
modules: ['knowledge-base', 'agent'],
category: 'sales',
tags: ['sales', 'crm', 'research'],
@@ -238,6 +251,7 @@ export const TEMPLATES: TemplatePrompt[] = [
title: 'Lead enrichment pipeline',
prompt:
'Build a workflow that watches my leads table for new entries, enriches each lead with company size, funding, tech stack, and decision-maker contacts using Apollo and web search, then updates the table with the enriched information.',
integrationBlockTypes: ['apollo'],
modules: ['tables', 'agent', 'workflows'],
category: 'sales',
tags: ['sales', 'crm', 'automation', 'research'],
@@ -247,6 +261,7 @@ export const TEMPLATES: TemplatePrompt[] = [
title: 'Prospect researcher',
prompt:
'Create an agent that takes a company name, deep-researches them across the web, finds key decision-makers, recent news, funding rounds, and pain points, then compiles a prospect brief I can review before outreach.',
integrationBlockTypes: [],
modules: ['agent', 'files', 'workflows'],
category: 'sales',
tags: ['sales', 'research'],
@@ -256,6 +271,7 @@ export const TEMPLATES: TemplatePrompt[] = [
title: 'Outbound sequence builder',
prompt:
'Build a workflow that reads leads from my table, researches each prospect and their company on the web, writes a personalized cold email tailored to their role and pain points, and sends it via Gmail. Schedule it to run daily to process new leads automatically.',
integrationBlockTypes: ['gmail'],
modules: ['tables', 'agent', 'workflows'],
category: 'sales',
tags: ['sales', 'communication', 'automation'],
@@ -265,6 +281,7 @@ export const TEMPLATES: TemplatePrompt[] = [
title: 'Deal pipeline tracker',
prompt:
'Create a table with columns for deal name, stage, amount, close date, and next steps. Build a workflow that syncs open deals from Salesforce into this table daily, and sends me a Slack summary each morning of deals that need attention or are at risk of slipping.',
integrationBlockTypes: ['salesforce', 'slack'],
modules: ['tables', 'scheduled', 'agent', 'workflows'],
category: 'sales',
tags: ['sales', 'crm', 'monitoring', 'reporting'],
@@ -274,6 +291,7 @@ export const TEMPLATES: TemplatePrompt[] = [
title: 'Win/loss analyzer',
prompt:
'Build a workflow that pulls closed deals from HubSpot each week, analyzes patterns in wins vs losses — deal size, industry, sales cycle length, objections — and generates a report file with actionable insights on what to change. Schedule it to run every Monday.',
integrationBlockTypes: ['hubspot'],
modules: ['agent', 'files', 'scheduled', 'workflows'],
category: 'sales',
tags: ['sales', 'crm', 'analysis', 'reporting'],
@@ -283,6 +301,7 @@ export const TEMPLATES: TemplatePrompt[] = [
title: 'Sales call analyzer',
prompt:
'Build a workflow that pulls call transcripts from Gong after each sales call, identifies key objections raised, action items promised, and competitor mentions, updates the deal record in my CRM, and posts a call summary with next steps to the Slack deal channel.',
integrationBlockTypes: ['gong', 'slack'],
modules: ['agent', 'tables', 'workflows'],
category: 'sales',
tags: ['sales', 'analysis', 'communication'],
@@ -292,6 +311,7 @@ export const TEMPLATES: TemplatePrompt[] = [
title: 'Webflow lead capture pipeline',
prompt:
'Create a workflow that monitors new Webflow form submissions, enriches each lead with company and contact data using Apollo and web search, adds them to a tracking table with a lead score, and sends a Slack notification to the sales team for high-potential leads.',
integrationBlockTypes: ['webflow', 'apollo', 'slack'],
modules: ['tables', 'agent', 'workflows'],
category: 'sales',
tags: ['sales', 'crm', 'automation'],
@@ -303,6 +323,7 @@ export const TEMPLATES: TemplatePrompt[] = [
title: 'Customer support bot',
prompt:
'Create a knowledge base and connect it to my Notion or Google Docs so it stays synced with my product documentation automatically. Then build an agent that answers customer questions using it with sourced citations and deploy it as a chat endpoint.',
integrationBlockTypes: ['notion', 'google_docs'],
modules: ['knowledge-base', 'agent', 'workflows'],
category: 'support',
tags: ['support', 'communication', 'automation'],
@@ -312,6 +333,7 @@ export const TEMPLATES: TemplatePrompt[] = [
title: 'Slack Q&A bot',
prompt:
'Create a knowledge base connected to my Notion workspace so it stays synced with my company wiki. Then build a workflow that monitors Slack channels for questions and answers them using the knowledge base with source citations.',
integrationBlockTypes: ['notion', 'slack'],
modules: ['knowledge-base', 'agent', 'workflows'],
category: 'support',
tags: ['support', 'communication', 'team'],
@@ -321,6 +343,7 @@ export const TEMPLATES: TemplatePrompt[] = [
title: 'Customer feedback analyzer',
prompt:
'Build a scheduled workflow that pulls support tickets and conversations from Intercom daily, categorizes them by theme and sentiment, tracks trends in a table, and sends a weekly Slack report highlighting the top feature requests and pain points.',
integrationBlockTypes: ['intercom', 'slack'],
modules: ['tables', 'scheduled', 'agent', 'workflows'],
category: 'support',
tags: ['support', 'product', 'analysis', 'reporting'],
@@ -330,6 +353,7 @@ export const TEMPLATES: TemplatePrompt[] = [
title: 'Churn risk detector',
prompt:
'Create a workflow that monitors customer activity — support ticket frequency, response sentiment, usage patterns — scores each account for churn risk in a table, and triggers a Slack alert to the account team when a customer crosses the risk threshold.',
integrationBlockTypes: ['slack'],
modules: ['tables', 'scheduled', 'agent', 'workflows'],
category: 'support',
tags: ['support', 'sales', 'monitoring', 'analysis'],
@@ -339,6 +363,7 @@ export const TEMPLATES: TemplatePrompt[] = [
title: 'Discord community manager',
prompt:
'Create a knowledge base connected to my Google Docs or Notion with product documentation. Then build a workflow that monitors my Discord server for unanswered questions, answers them using the knowledge base, tracks common questions in a table, and sends a weekly community summary to Slack.',
integrationBlockTypes: ['discord', 'google_docs', 'notion', 'slack'],
modules: ['knowledge-base', 'tables', 'agent', 'scheduled', 'workflows'],
category: 'support',
tags: ['community', 'support', 'communication'],
@@ -348,6 +373,7 @@ export const TEMPLATES: TemplatePrompt[] = [
title: 'Survey response analyzer',
prompt:
'Create a workflow that pulls new Typeform responses daily, categorizes feedback by theme and sentiment, logs structured results to a table, and sends a Slack digest when a new batch of responses comes in with the key takeaways.',
integrationBlockTypes: ['typeform', 'slack'],
modules: ['tables', 'scheduled', 'agent', 'workflows'],
category: 'support',
tags: ['product', 'analysis', 'reporting'],
@@ -357,6 +383,7 @@ export const TEMPLATES: TemplatePrompt[] = [
title: 'Email knowledge search',
prompt:
'Create a knowledge base connected to my Gmail so all my emails are automatically synced, chunked, and searchable. Then build an agent I can ask things like "what did Sarah say about the pricing proposal?" or "find the contract John sent last month" and get instant answers with the original email cited.',
integrationBlockTypes: ['gmail'],
modules: ['knowledge-base', 'agent'],
category: 'support',
tags: ['individual', 'research', 'communication'],
@@ -366,6 +393,7 @@ export const TEMPLATES: TemplatePrompt[] = [
title: 'Support ticket knowledge search',
prompt:
'Create a knowledge base connected to my Zendesk account so all past tickets, resolutions, and agent notes are automatically synced and searchable. Then build an agent my support team can ask things like "how do we usually resolve the SSO login issue?" or "has anyone reported this billing bug before?" to find past solutions instantly.',
integrationBlockTypes: ['zendesk'],
modules: ['knowledge-base', 'agent'],
category: 'support',
tags: ['support', 'research', 'team'],
@@ -377,6 +405,7 @@ export const TEMPLATES: TemplatePrompt[] = [
title: 'Feature spec writer',
prompt:
'Create an agent that takes a rough feature idea or user story, researches how similar features work in competing products, and writes a complete product requirements document with user stories, acceptance criteria, edge cases, and technical considerations.',
integrationBlockTypes: [],
modules: ['agent', 'files', 'workflows'],
category: 'engineering',
tags: ['product', 'engineering', 'research', 'content'],
@@ -386,6 +415,7 @@ export const TEMPLATES: TemplatePrompt[] = [
title: 'Jira knowledge search',
prompt:
'Create a knowledge base connected to my Jira project so all tickets, comments, and resolutions are automatically synced and searchable. Then build an agent I can ask things like "how did we fix the auth timeout issue?" or "what was decided about the API redesign?" and get answers with ticket citations.',
integrationBlockTypes: ['jira'],
modules: ['knowledge-base', 'agent'],
category: 'engineering',
tags: ['engineering', 'research'],
@@ -395,6 +425,7 @@ export const TEMPLATES: TemplatePrompt[] = [
title: 'Linear knowledge search',
prompt:
'Create a knowledge base connected to my Linear workspace so all issues, comments, project updates, and decisions are automatically synced and searchable. Then build an agent I can ask things like "why did we deprioritize the mobile app?" or "what was the root cause of the checkout bug?" and get answers traced back to specific issues.',
integrationBlockTypes: ['linear'],
modules: ['knowledge-base', 'agent'],
category: 'engineering',
tags: ['engineering', 'research', 'product'],
@@ -404,6 +435,7 @@ export const TEMPLATES: TemplatePrompt[] = [
title: 'Bug triage agent',
prompt:
'Build an agent that monitors Sentry for new errors, automatically triages them by severity and affected users, creates Linear tickets for critical issues with full stack traces, and sends a Slack notification to the on-call channel.',
integrationBlockTypes: ['sentry', 'linear', 'slack'],
modules: ['agent', 'workflows'],
category: 'engineering',
tags: ['engineering', 'devops', 'automation'],
@@ -413,6 +445,7 @@ export const TEMPLATES: TemplatePrompt[] = [
title: 'PR review assistant',
prompt:
'Create a knowledge base connected to my GitHub repo so it stays synced with my style guide and coding standards. Then build a workflow that reviews new pull requests against it, checks for common issues and security vulnerabilities, and posts a review comment with specific suggestions.',
integrationBlockTypes: ['github'],
modules: ['knowledge-base', 'agent', 'workflows'],
category: 'engineering',
tags: ['engineering', 'automation'],
@@ -422,6 +455,7 @@ export const TEMPLATES: TemplatePrompt[] = [
title: 'Changelog generator',
prompt:
'Build a scheduled workflow that runs every Friday, pulls all merged PRs from GitHub for the week, categorizes changes as features, fixes, or improvements, and generates a user-facing changelog document with clear descriptions.',
integrationBlockTypes: ['github'],
modules: ['scheduled', 'agent', 'files', 'workflows'],
category: 'engineering',
tags: ['engineering', 'product', 'reporting', 'content'],
@@ -431,6 +465,7 @@ export const TEMPLATES: TemplatePrompt[] = [
title: 'Incident postmortem writer',
prompt:
'Create a workflow that when triggered after an incident, pulls the Slack thread from the incident channel, gathers relevant Sentry errors and deployment logs, and drafts a structured postmortem with timeline, root cause, and action items.',
integrationBlockTypes: ['slack', 'sentry'],
modules: ['agent', 'files', 'workflows'],
category: 'engineering',
tags: ['engineering', 'devops', 'analysis'],
@@ -440,6 +475,7 @@ export const TEMPLATES: TemplatePrompt[] = [
title: 'Documentation auto-updater',
prompt:
'Create a knowledge base connected to my GitHub repository so code and docs stay synced. Then build a scheduled weekly workflow that detects API changes, compares them against the knowledge base to find outdated documentation, and either updates Notion pages directly or creates Linear tickets for the needed changes.',
integrationBlockTypes: ['github', 'notion', 'linear'],
modules: ['scheduled', 'agent', 'workflows'],
category: 'engineering',
tags: ['engineering', 'sync', 'automation'],
@@ -449,6 +485,7 @@ export const TEMPLATES: TemplatePrompt[] = [
title: 'Incident response coordinator',
prompt:
'Create a knowledge base connected to my Confluence or Notion with runbooks and incident procedures. Then build a workflow triggered by PagerDuty incidents that searches the runbooks, gathers related Datadog alerts, identifies the on-call rotation, and posts a comprehensive incident brief to Slack.',
integrationBlockTypes: ['confluence', 'notion', 'pagerduty', 'datadog', 'slack'],
modules: ['knowledge-base', 'agent', 'workflows'],
category: 'engineering',
tags: ['devops', 'engineering', 'automation'],
@@ -458,6 +495,7 @@ export const TEMPLATES: TemplatePrompt[] = [
title: 'Sprint report generator',
prompt:
'Create a scheduled workflow that runs at the end of each sprint, pulls all completed, in-progress, and blocked Jira tickets, calculates velocity and carry-over, and generates a sprint summary document with charts and trends to share with the team.',
integrationBlockTypes: ['jira'],
modules: ['scheduled', 'agent', 'files', 'workflows'],
category: 'engineering',
tags: ['engineering', 'reporting', 'team'],
@@ -467,6 +505,7 @@ export const TEMPLATES: TemplatePrompt[] = [
title: 'Knowledge base sync',
prompt:
'Create a knowledge base connected to my Confluence workspace so all wiki pages are automatically synced and searchable. Then build a scheduled workflow that identifies stale pages not updated in 90 days and sends a Slack reminder to page owners to review them.',
integrationBlockTypes: ['confluence', 'slack'],
modules: ['knowledge-base', 'scheduled', 'agent', 'workflows'],
category: 'engineering',
tags: ['engineering', 'sync', 'team'],
@@ -478,6 +517,7 @@ export const TEMPLATES: TemplatePrompt[] = [
title: 'Long-form content writer',
prompt:
'Build a workflow that takes a topic or brief, researches it deeply across the web, generates a detailed outline, then writes a full long-form article with sections, examples, and a conclusion. Save the final draft as a document for review.',
integrationBlockTypes: [],
modules: ['agent', 'files', 'workflows'],
category: 'marketing',
tags: ['content', 'research', 'marketing'],
@@ -487,6 +527,7 @@ export const TEMPLATES: TemplatePrompt[] = [
title: 'Case study generator',
prompt:
'Create a knowledge base from my customer data and interview notes, then build a workflow that generates a polished case study file with the challenge, solution, results, and a pull quote — formatted and ready to publish.',
integrationBlockTypes: [],
modules: ['knowledge-base', 'files', 'agent'],
category: 'marketing',
tags: ['marketing', 'content', 'sales'],
@@ -496,6 +537,7 @@ export const TEMPLATES: TemplatePrompt[] = [
title: 'Social media content calendar',
prompt:
'Build a workflow that generates a full month of social media content for my brand. Research trending topics in my industry, create a table with post dates, platforms, copy drafts, and hashtags, then schedule a weekly refresh to keep the calendar filled with fresh ideas.',
integrationBlockTypes: [],
modules: ['tables', 'agent', 'scheduled', 'workflows'],
category: 'marketing',
tags: ['marketing', 'content', 'automation'],
@@ -505,6 +547,7 @@ export const TEMPLATES: TemplatePrompt[] = [
title: 'Multi-language content translator',
prompt:
'Create a workflow that takes a document or blog post and translates it into multiple target languages while preserving tone, formatting, and brand voice. Save each translation as a separate file and flag sections that may need human review for cultural nuance.',
integrationBlockTypes: [],
modules: ['files', 'agent', 'workflows'],
category: 'marketing',
tags: ['content', 'enterprise', 'automation'],
@@ -514,6 +557,7 @@ export const TEMPLATES: TemplatePrompt[] = [
title: 'Content repurposer',
prompt:
'Build a workflow that takes a YouTube video URL, pulls the video details and description, researches the topic on the web for additional context, and generates a Twitter thread, LinkedIn post, and blog summary optimized for each platform.',
integrationBlockTypes: ['youtube'],
modules: ['agent', 'files', 'workflows'],
category: 'marketing',
tags: ['marketing', 'content', 'automation'],
@@ -523,6 +567,7 @@ export const TEMPLATES: TemplatePrompt[] = [
title: 'Social mention tracker',
prompt:
'Create a scheduled workflow that monitors Reddit and X for mentions of my brand and competitors, scores each mention by sentiment and reach, logs them to a table, and sends a daily Slack digest of notable mentions.',
integrationBlockTypes: ['reddit', 'x', 'slack'],
modules: ['tables', 'scheduled', 'agent', 'workflows'],
category: 'marketing',
tags: ['marketing', 'monitoring', 'analysis'],
@@ -532,6 +577,7 @@ export const TEMPLATES: TemplatePrompt[] = [
title: 'SEO content brief generator',
prompt:
'Build a workflow that takes a target keyword, scrapes the top 10 ranking pages, analyzes their content structure and subtopics, then generates a detailed content brief with outline, word count target, questions to answer, and internal linking suggestions.',
integrationBlockTypes: ['firecrawl'],
modules: ['agent', 'files', 'workflows'],
category: 'marketing',
tags: ['marketing', 'content', 'research'],
@@ -541,6 +587,7 @@ export const TEMPLATES: TemplatePrompt[] = [
title: 'Newsletter curator',
prompt:
'Create a scheduled weekly workflow that scrapes my favorite industry news sites and blogs, picks the top stories relevant to my audience, writes summaries for each, and drafts a ready-to-send newsletter in Mailchimp.',
integrationBlockTypes: ['mailchimp'],
modules: ['scheduled', 'agent', 'files', 'workflows'],
category: 'marketing',
tags: ['marketing', 'content', 'communication'],
@@ -550,6 +597,7 @@ export const TEMPLATES: TemplatePrompt[] = [
title: 'LinkedIn content engine',
prompt:
'Build a workflow that scrapes my company blog for new posts, generates LinkedIn posts with hooks, insights, and calls-to-action optimized for engagement, and saves drafts as files for my review before posting to LinkedIn.',
integrationBlockTypes: ['linkedin'],
modules: ['agent', 'files', 'scheduled', 'workflows'],
category: 'marketing',
tags: ['marketing', 'content', 'automation'],
@@ -559,6 +607,7 @@ export const TEMPLATES: TemplatePrompt[] = [
title: 'Blog auto-publisher',
prompt:
'Build a workflow that takes a draft document, optimizes it for SEO by researching target keywords, formats it for WordPress with proper headings and meta description, and publishes it as a draft post for final review.',
integrationBlockTypes: ['wordpress'],
modules: ['agent', 'files', 'workflows'],
category: 'marketing',
tags: ['marketing', 'content', 'automation'],
@@ -570,6 +619,7 @@ export const TEMPLATES: TemplatePrompt[] = [
title: 'Personal knowledge assistant',
prompt:
'Create a knowledge base and connect it to my Google Drive, Notion, or Obsidian so all my notes, docs, and articles are automatically synced and embedded. Then build an agent that I can ask anything — it should answer with citations and deploy as a chat endpoint.',
integrationBlockTypes: ['google_drive', 'notion', 'obsidian'],
modules: ['knowledge-base', 'agent'],
category: 'productivity',
tags: ['individual', 'research', 'team'],
@@ -579,6 +629,7 @@ export const TEMPLATES: TemplatePrompt[] = [
title: 'Slack knowledge search',
prompt:
'Create a knowledge base connected to my Slack workspace so all channel conversations and threads are automatically synced and searchable. Then build an agent I can ask things like "what did the team decide about the launch date?" or "what was the outcome of the design review?" and get answers with links to the original messages.',
integrationBlockTypes: ['slack'],
modules: ['knowledge-base', 'agent'],
category: 'productivity',
tags: ['team', 'research', 'communication'],
@@ -588,6 +639,7 @@ export const TEMPLATES: TemplatePrompt[] = [
title: 'Notion knowledge search',
prompt:
'Create a knowledge base connected to my Notion workspace so all pages, databases, meeting notes, and wikis are automatically synced and searchable. Then build an agent I can ask things like "what\'s our refund policy?" or "what was decided in the Q3 planning doc?" and get instant answers with page links.',
integrationBlockTypes: ['notion'],
modules: ['knowledge-base', 'agent'],
category: 'productivity',
tags: ['team', 'research'],
@@ -597,6 +649,7 @@ export const TEMPLATES: TemplatePrompt[] = [
title: 'Google Drive knowledge search',
prompt:
'Create a knowledge base connected to my Google Drive so all documents, spreadsheets, and presentations are automatically synced and searchable. Then build an agent I can ask things like "find the board deck from last quarter" or "what were the KPIs in the marketing plan?" and get answers with doc links.',
integrationBlockTypes: ['google_drive'],
modules: ['knowledge-base', 'agent'],
category: 'productivity',
tags: ['individual', 'team', 'research'],
@@ -606,6 +659,7 @@ export const TEMPLATES: TemplatePrompt[] = [
title: 'Document summarizer',
prompt:
'Create a workflow that takes any uploaded document — PDF, contract, report, research paper — and generates a structured summary with key takeaways, action items, important dates, and a one-paragraph executive overview.',
integrationBlockTypes: [],
modules: ['files', 'agent', 'workflows'],
category: 'productivity',
tags: ['individual', 'analysis', 'team'],
@@ -615,6 +669,7 @@ export const TEMPLATES: TemplatePrompt[] = [
title: 'Bulk data classifier',
prompt:
'Build a workflow that takes a table of unstructured data — support tickets, feedback, survey responses, leads, or any text — runs each row through an agent to classify, tag, score, and enrich it, then writes the structured results back to the table.',
integrationBlockTypes: [],
modules: ['tables', 'agent', 'workflows'],
category: 'productivity',
tags: ['analysis', 'automation', 'team'],
@@ -624,6 +679,7 @@ export const TEMPLATES: TemplatePrompt[] = [
title: 'Automated narrative report',
prompt:
'Build a scheduled workflow that pulls key data from my tables every week, analyzes trends and anomalies, and writes a narrative report — not just charts and numbers, but written insights explaining what changed, why it matters, and what to do next. Save it as a document and send a summary to Slack.',
integrationBlockTypes: ['slack'],
modules: ['tables', 'scheduled', 'agent', 'files', 'workflows'],
category: 'productivity',
tags: ['founder', 'reporting', 'analysis'],
@@ -633,6 +689,7 @@ export const TEMPLATES: TemplatePrompt[] = [
title: 'Investor update writer',
prompt:
'Build a workflow that pulls key metrics from my tables — revenue, growth, burn rate, headcount, milestones — and drafts a concise investor update with highlights, lowlights, asks, and KPIs. Save it as a file I can review before sending. Schedule it to run on the first of each month.',
integrationBlockTypes: [],
modules: ['tables', 'scheduled', 'agent', 'files', 'workflows'],
category: 'productivity',
tags: ['founder', 'reporting', 'communication'],
@@ -642,6 +699,7 @@ export const TEMPLATES: TemplatePrompt[] = [
title: 'Email digest curator',
prompt:
'Create a scheduled daily workflow that searches the web for the latest articles, papers, and news on topics I care about, picks the top 5 most relevant pieces, writes a one-paragraph summary for each, and delivers a curated reading digest to my inbox or Slack.',
integrationBlockTypes: ['slack'],
modules: ['scheduled', 'agent', 'files', 'workflows'],
category: 'productivity',
tags: ['individual', 'research', 'content'],
@@ -651,6 +709,7 @@ export const TEMPLATES: TemplatePrompt[] = [
title: 'Knowledge extractor',
prompt:
'Build a workflow that takes raw meeting notes, brainstorm dumps, or research transcripts, extracts the key insights, decisions, and facts, organizes them by topic, and saves them into my knowledge base so they are searchable and reusable in future conversations.',
integrationBlockTypes: [],
modules: ['files', 'knowledge-base', 'agent', 'workflows'],
category: 'productivity',
tags: ['individual', 'team', 'research'],
@@ -660,6 +719,7 @@ export const TEMPLATES: TemplatePrompt[] = [
title: 'Weekly team digest',
prompt:
"Build a scheduled workflow that runs every Friday, pulls the week's GitHub commits, closed Linear issues, and key Slack conversations, then emails a formatted weekly summary to the team.",
integrationBlockTypes: ['github', 'linear', 'slack'],
modules: ['scheduled', 'agent', 'workflows'],
category: 'productivity',
tags: ['engineering', 'team', 'reporting'],
@@ -669,6 +729,7 @@ export const TEMPLATES: TemplatePrompt[] = [
title: 'Daily standup summary',
prompt:
'Create a scheduled workflow that reads the #standup Slack channel each morning, summarizes what everyone is working on, identifies blockers, and posts a structured recap to a Google Doc.',
integrationBlockTypes: ['slack', 'google_docs'],
modules: ['scheduled', 'agent', 'files', 'workflows'],
category: 'productivity',
tags: ['team', 'reporting', 'communication'],
@@ -678,6 +739,7 @@ export const TEMPLATES: TemplatePrompt[] = [
title: 'Email triage assistant',
prompt:
'Build a workflow that scans my Gmail inbox every hour, categorizes emails by urgency and type (action needed, FYI, follow-up), drafts replies for routine messages, and sends me a prioritized summary in Slack so I only open what matters. Schedule it to run hourly.',
integrationBlockTypes: ['gmail', 'slack'],
modules: ['agent', 'scheduled', 'workflows'],
category: 'productivity',
tags: ['individual', 'communication', 'automation'],
@@ -687,6 +749,7 @@ export const TEMPLATES: TemplatePrompt[] = [
title: 'Meeting notes to action items',
prompt:
'Create a workflow that takes meeting notes or a transcript, extracts action items with owners and due dates, creates tasks in Linear or Asana for each one, and posts a summary to the relevant Slack channel.',
integrationBlockTypes: ['linear', 'asana', 'slack'],
modules: ['agent', 'workflows'],
category: 'productivity',
tags: ['team', 'automation'],
@@ -696,6 +759,7 @@ export const TEMPLATES: TemplatePrompt[] = [
title: 'Weekly metrics report',
prompt:
'Build a scheduled workflow that pulls data from Stripe and my database every Monday, calculates key metrics like MRR, churn, new subscriptions, and failed payments, populates a Google Sheet, and Slacks the team a summary with week-over-week trends.',
integrationBlockTypes: ['stripe', 'google_sheets', 'slack'],
modules: ['scheduled', 'tables', 'agent', 'workflows'],
category: 'productivity',
tags: ['founder', 'finance', 'reporting'],
@@ -705,6 +769,7 @@ export const TEMPLATES: TemplatePrompt[] = [
title: 'Product analytics digest',
prompt:
'Create a scheduled weekly workflow that pulls key product metrics from Amplitude — active users, feature adoption rates, retention cohorts, and top events — generates an executive summary with week-over-week trends, and posts it to Slack.',
integrationBlockTypes: ['amplitude', 'slack'],
modules: ['scheduled', 'agent', 'workflows'],
category: 'productivity',
tags: ['product', 'reporting', 'analysis'],
@@ -714,6 +779,7 @@ export const TEMPLATES: TemplatePrompt[] = [
title: 'Scheduling follow-up automator',
prompt:
'Build a workflow that monitors new Calendly bookings, researches each attendee and their company, prepares a pre-meeting brief with relevant context, and sends a personalized confirmation email with an agenda and any prep materials.',
integrationBlockTypes: ['calendly'],
modules: ['agent', 'workflows'],
category: 'productivity',
tags: ['sales', 'research', 'automation'],
@@ -723,6 +789,7 @@ export const TEMPLATES: TemplatePrompt[] = [
title: 'SMS appointment reminders',
prompt:
'Create a scheduled workflow that checks Google Calendar each morning for appointments in the next 24 hours, and sends an SMS reminder to each attendee via Twilio with the meeting time, location, and any prep notes.',
integrationBlockTypes: ['google_calendar', 'twilio_sms'],
modules: ['scheduled', 'agent', 'workflows'],
category: 'productivity',
tags: ['individual', 'communication', 'automation'],
@@ -732,6 +799,7 @@ export const TEMPLATES: TemplatePrompt[] = [
title: 'Microsoft Teams daily brief',
prompt:
'Build a scheduled workflow that pulls updates from your project tools — GitHub commits, Jira ticket status changes, and calendar events — and posts a formatted daily brief to your Microsoft Teams channel each morning.',
integrationBlockTypes: ['github', 'jira', 'microsoft_teams'],
modules: ['scheduled', 'agent', 'workflows'],
category: 'productivity',
tags: ['team', 'reporting', 'enterprise'],
@@ -743,6 +811,7 @@ export const TEMPLATES: TemplatePrompt[] = [
title: 'Data cleanup agent',
prompt:
'Create a workflow that takes a messy table — inconsistent formatting, duplicates, missing fields, typos — and cleans it up by standardizing values, merging duplicates, filling gaps where possible, and flagging rows that need human review.',
integrationBlockTypes: [],
modules: ['tables', 'agent', 'workflows'],
category: 'operations',
tags: ['automation', 'analysis'],
@@ -752,6 +821,7 @@ export const TEMPLATES: TemplatePrompt[] = [
title: 'Training material generator',
prompt:
'Create a knowledge base from my product documentation, then build a workflow that generates training materials from it — onboarding guides, FAQ documents, step-by-step tutorials, and quiz questions. Schedule it to regenerate weekly so materials stay current as docs change.',
integrationBlockTypes: [],
modules: ['knowledge-base', 'files', 'agent', 'scheduled'],
category: 'operations',
tags: ['hr', 'content', 'team', 'automation'],
@@ -761,6 +831,7 @@ export const TEMPLATES: TemplatePrompt[] = [
title: 'SOP generator',
prompt:
'Create an agent that takes a brief description of any business process — from employee onboarding to incident response to content publishing — and generates a detailed standard operating procedure document with numbered steps, responsible roles, decision points, and checklists.',
integrationBlockTypes: [],
modules: ['files', 'agent'],
category: 'operations',
tags: ['team', 'enterprise', 'content'],
@@ -770,6 +841,7 @@ export const TEMPLATES: TemplatePrompt[] = [
title: 'Invoice processor',
prompt:
'Build a workflow that processes invoice PDFs from Gmail, extracts vendor name, amount, due date, and line items, then logs everything to a tracking table and sends a Slack alert for invoices due within 7 days.',
integrationBlockTypes: ['gmail', 'slack'],
modules: ['files', 'tables', 'agent', 'workflows'],
category: 'operations',
tags: ['finance', 'automation'],
@@ -779,6 +851,7 @@ export const TEMPLATES: TemplatePrompt[] = [
title: 'Contract analyzer',
prompt:
'Create a knowledge base from my standard contract terms, then build a workflow that reviews uploaded contracts against it — extracting key clauses like payment terms, liability caps, and termination conditions, flagging deviations, and outputting a summary to a table.',
integrationBlockTypes: [],
modules: ['knowledge-base', 'files', 'tables', 'agent'],
category: 'operations',
tags: ['legal', 'analysis'],
@@ -788,6 +861,7 @@ export const TEMPLATES: TemplatePrompt[] = [
title: 'Competitive intel monitor',
prompt:
'Build a scheduled workflow that scrapes competitor websites, pricing pages, and changelog pages weekly using Firecrawl, compares against previous snapshots, summarizes any changes, logs them to a tracking table, and sends a Slack alert for major updates.',
integrationBlockTypes: ['firecrawl', 'slack'],
modules: ['scheduled', 'tables', 'agent', 'workflows'],
category: 'operations',
tags: ['founder', 'product', 'monitoring', 'research'],
@@ -797,6 +871,7 @@ export const TEMPLATES: TemplatePrompt[] = [
title: 'Revenue operations dashboard',
prompt:
'Create a scheduled daily workflow that pulls payment data from Stripe, calculates MRR, net revenue, failed payments, and new subscriptions, logs everything to a table with historical tracking, and sends a daily Slack summary with trends and anomalies.',
integrationBlockTypes: ['stripe', 'slack'],
modules: ['tables', 'scheduled', 'agent', 'workflows'],
category: 'operations',
tags: ['finance', 'founder', 'reporting', 'monitoring'],
@@ -806,6 +881,7 @@ export const TEMPLATES: TemplatePrompt[] = [
title: 'E-commerce order monitor',
prompt:
'Build a workflow that monitors Shopify orders, flags high-value or unusual orders for review, tracks fulfillment status in a table, and sends daily inventory and sales summaries to Slack with restock alerts when items run low.',
integrationBlockTypes: ['shopify', 'slack'],
modules: ['tables', 'scheduled', 'agent', 'workflows'],
category: 'operations',
tags: ['ecommerce', 'monitoring', 'reporting'],
@@ -815,6 +891,7 @@ export const TEMPLATES: TemplatePrompt[] = [
title: 'Compliance document checker',
prompt:
'Create a knowledge base from my compliance requirements and policies, then build an agent that reviews uploaded policy documents and SOC 2 evidence against it, identifies gaps or outdated sections, and generates a remediation checklist file with priority levels.',
integrationBlockTypes: [],
modules: ['knowledge-base', 'files', 'agent'],
category: 'operations',
tags: ['legal', 'enterprise', 'analysis'],
@@ -824,6 +901,7 @@ export const TEMPLATES: TemplatePrompt[] = [
title: 'New hire onboarding automation',
prompt:
"Build a workflow that when triggered with a new hire's info, creates their accounts, sends a personalized welcome message in Slack, schedules 1:1s with their team on Google Calendar, shares relevant onboarding docs from the knowledge base, and tracks completion in a table.",
integrationBlockTypes: ['slack', 'google_calendar'],
modules: ['knowledge-base', 'tables', 'agent', 'workflows'],
category: 'operations',
tags: ['hr', 'automation', 'team'],
@@ -833,6 +911,7 @@ export const TEMPLATES: TemplatePrompt[] = [
title: 'Candidate screening assistant',
prompt:
'Create a knowledge base from my job descriptions and hiring criteria, then build a workflow that takes uploaded resumes, evaluates candidates against the requirements, scores them on experience, skills, and culture fit, and populates a comparison table with a summary and recommendation for each.',
integrationBlockTypes: [],
modules: ['knowledge-base', 'files', 'tables', 'agent'],
category: 'operations',
tags: ['hr', 'recruiting', 'analysis'],
@@ -842,6 +921,7 @@ export const TEMPLATES: TemplatePrompt[] = [
title: 'Recruiting pipeline automator',
prompt:
'Build a scheduled workflow that syncs open jobs and candidates from Greenhouse to a tracking table daily, flags candidates who have been in the same stage for more than 5 days, and sends a Slack summary to hiring managers with pipeline stats and bottlenecks.',
integrationBlockTypes: ['greenhouse', 'slack'],
modules: ['tables', 'scheduled', 'agent', 'workflows'],
category: 'operations',
tags: ['hr', 'recruiting', 'monitoring', 'reporting'],
@@ -851,6 +931,7 @@ export const TEMPLATES: TemplatePrompt[] = [
title: 'Infrastructure health report',
prompt:
'Create a scheduled daily workflow that queries Datadog for key infrastructure metrics — error rates, latency percentiles, CPU and memory usage — logs them to a table for trend tracking, and sends a morning Slack report highlighting any anomalies or degradations.',
integrationBlockTypes: ['datadog', 'slack'],
modules: ['tables', 'scheduled', 'agent', 'workflows'],
category: 'operations',
tags: ['devops', 'infrastructure', 'monitoring', 'reporting'],
@@ -860,6 +941,7 @@ export const TEMPLATES: TemplatePrompt[] = [
title: 'Airtable data sync',
prompt:
'Create a scheduled workflow that syncs records from my Airtable base into a Sim table every hour, keeping both in sync. Use an agent to detect changes, resolve conflicts, and flag any discrepancies for review in Slack.',
integrationBlockTypes: ['airtable', 'slack'],
modules: ['tables', 'scheduled', 'agent', 'workflows'],
category: 'operations',
tags: ['sync', 'automation'],
@@ -869,6 +951,7 @@ export const TEMPLATES: TemplatePrompt[] = [
title: 'Multi-source knowledge hub',
prompt:
'Create a knowledge base and connect it to Confluence, Notion, and Google Drive so all my company documentation is automatically synced, chunked, and embedded. Then deploy a Q&A agent that can answer questions across all sources with citations.',
integrationBlockTypes: ['confluence', 'notion', 'google_drive'],
modules: ['knowledge-base', 'scheduled', 'agent', 'workflows'],
category: 'operations',
tags: ['enterprise', 'team', 'sync', 'automation'],
@@ -878,6 +961,7 @@ export const TEMPLATES: TemplatePrompt[] = [
title: 'Customer 360 view',
prompt:
'Create a comprehensive customer table that aggregates data from my CRM, support tickets, billing history, and product usage into a single unified view per customer. Schedule it to sync daily and send a Slack alert when any customer shows signs of trouble across multiple signals.',
integrationBlockTypes: ['slack'],
modules: ['tables', 'scheduled', 'agent', 'workflows'],
category: 'operations',
tags: ['founder', 'sales', 'support', 'enterprise', 'sync'],
+20 -5
View File
@@ -69,7 +69,11 @@ import { getBaseUrl } from '@/lib/core/utils/urls'
import { processCredentialDraft } from '@/lib/credentials/draft-processor'
import { sendEmail } from '@/lib/messaging/email/mailer'
import { getFromEmailAddress, getPersonalEmailFrom } from '@/lib/messaging/email/utils'
import { quickValidateEmail } from '@/lib/messaging/email/validation'
import {
isDisposableEmailFull,
isDisposableMxBackend,
quickValidateEmail,
} from '@/lib/messaging/email/validation'
import { syncAllWebhooksForCredentialSet } from '@/lib/webhooks/utils.server'
import { SSO_TRUSTED_PROVIDERS } from '@/ee/sso/constants'
import { createAnonymousSession, ensureAnonymousUserExists } from './anonymous'
@@ -624,12 +628,23 @@ export const auth = betterAuth({
}
}
if (ctx.path.startsWith('/sign-up') && blockedSignupDomains) {
if (ctx.path.startsWith('/sign-up')) {
const requestEmail = ctx.body?.email?.toLowerCase()
if (requestEmail) {
const emailDomain = requestEmail.split('@')[1]
if (emailDomain && blockedSignupDomains.has(emailDomain)) {
throw new Error('Sign-ups from this email domain are not allowed.')
// Check manually blocked domains
if (blockedSignupDomains) {
const emailDomain = requestEmail.split('@')[1]
if (emailDomain && blockedSignupDomains.has(emailDomain)) {
throw new Error('Sign-ups from this email domain are not allowed.')
}
}
// Check disposable email domains (full list + MX backend check)
if (isDisposableEmailFull(requestEmail)) {
throw new Error('Sign-ups from disposable email addresses are not allowed.')
}
if (await isDisposableMxBackend(requestEmail)) {
throw new Error('Sign-ups from disposable email addresses are not allowed.')
}
}
}
+144 -8
View File
@@ -1,19 +1,41 @@
import { loggerMock } from '@sim/testing'
import { describe, expect, it, vi } from 'vitest'
import { quickValidateEmail, validateEmail } from '@/lib/messaging/email/validation'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import {
isDisposableEmailFull,
isDisposableMxBackend,
quickValidateEmail,
validateEmail,
} from '@/lib/messaging/email/validation'
vi.mock('@sim/logger', () => loggerMock)
const { mockResolveMx } = vi.hoisted(() => ({
mockResolveMx: vi.fn(
(
_domain: string,
callback: (err: Error | null, addresses: { exchange: string; priority: number }[]) => void
) => {
callback(null, [{ exchange: 'mail.example.com', priority: 10 }])
}
),
}))
vi.mock('dns', () => ({
resolveMx: (
_domain: string,
callback: (err: Error | null, addresses: { exchange: string; priority: number }[]) => void
) => {
callback(null, [{ exchange: 'mail.example.com', priority: 10 }])
},
resolveMx: mockResolveMx,
}))
describe('Email Validation', () => {
beforeEach(() => {
mockResolveMx.mockImplementation(
(
_domain: string,
callback: (err: Error | null, addresses: { exchange: string; priority: number }[]) => void
) => {
callback(null, [{ exchange: 'mail.example.com', priority: 10 }])
}
)
})
describe('validateEmail', () => {
it.concurrent('should validate a correct email', async () => {
const result = await validateEmail('user@example.com')
@@ -215,4 +237,118 @@ describe('Email Validation', () => {
expect(result.checks.domain).toBe(true)
})
})
describe('isDisposableEmailFull', () => {
it('should reject domains from the inline blocklist', () => {
expect(isDisposableEmailFull('user@sharebot.net')).toBe(true)
expect(isDisposableEmailFull('user@oakon.com')).toBe(true)
expect(isDisposableEmailFull('user@catchmail.io')).toBe(true)
expect(isDisposableEmailFull('user@salt.email')).toBe(true)
expect(isDisposableEmailFull('user@mail.gw')).toBe(true)
expect(isDisposableEmailFull('user@mailinator.com')).toBe(true)
})
it('should reject domains from the npm package list that are not in the inline list', () => {
expect(isDisposableEmailFull('user@0-mail.com')).toBe(true)
expect(isDisposableEmailFull('user@0-180.com')).toBe(true)
})
it('should allow legitimate email domains', () => {
expect(isDisposableEmailFull('user@gmail.com')).toBe(false)
expect(isDisposableEmailFull('user@company.com')).toBe(false)
expect(isDisposableEmailFull('user@outlook.com')).toBe(false)
})
it('should handle invalid input', () => {
expect(isDisposableEmailFull('')).toBe(false)
expect(isDisposableEmailFull('nodomain')).toBe(false)
expect(isDisposableEmailFull('user@')).toBe(false)
})
it('should be case-insensitive', () => {
expect(isDisposableEmailFull('user@MAILINATOR.COM')).toBe(true)
expect(isDisposableEmailFull('user@ShareBot.Net')).toBe(true)
})
})
describe('isDisposableMxBackend', () => {
it('should detect mail.gw MX backend', async () => {
mockResolveMx.mockImplementation(
(
_domain: string,
callback: (err: Error | null, addresses: { exchange: string; priority: number }[]) => void
) => {
callback(null, [{ exchange: 'in.mail.gw', priority: 10 }])
}
)
expect(await isDisposableMxBackend('user@some-random-domain.xyz')).toBe(true)
})
it('should detect catchmail.io MX backend', async () => {
mockResolveMx.mockImplementation(
(
_domain: string,
callback: (err: Error | null, addresses: { exchange: string; priority: number }[]) => void
) => {
callback(null, [{ exchange: 'smtp.catchmail.io', priority: 10 }])
}
)
expect(await isDisposableMxBackend('user@custom-domain.com')).toBe(true)
})
it('should handle trailing dot in MX exchange', async () => {
mockResolveMx.mockImplementation(
(
_domain: string,
callback: (err: Error | null, addresses: { exchange: string; priority: number }[]) => void
) => {
callback(null, [{ exchange: 'in.mail.gw.', priority: 10 }])
}
)
expect(await isDisposableMxBackend('user@trailing-dot.com')).toBe(true)
})
it('should allow legitimate MX backends', async () => {
mockResolveMx.mockImplementation(
(
_domain: string,
callback: (err: Error | null, addresses: { exchange: string; priority: number }[]) => void
) => {
callback(null, [{ exchange: 'aspmx.l.google.com', priority: 10 }])
}
)
expect(await isDisposableMxBackend('user@legitimate.com')).toBe(false)
})
it('should return false on DNS errors', async () => {
mockResolveMx.mockImplementation(
(_domain: string, callback: (err: Error | null, addresses: null) => void) => {
callback(new Error('ENOTFOUND'), null)
}
)
expect(await isDisposableMxBackend('user@nonexistent.invalid')).toBe(false)
})
it('should return false for invalid input', async () => {
expect(await isDisposableMxBackend('')).toBe(false)
expect(await isDisposableMxBackend('nodomain')).toBe(false)
})
})
describe('validateEmail with disposable MX backend', () => {
it('should reject emails with disposable MX backend even if domain is not in blocklist', async () => {
mockResolveMx.mockImplementation(
(
_domain: string,
callback: (err: Error | null, addresses: { exchange: string; priority: number }[]) => void
) => {
callback(null, [{ exchange: 'in.mail.gw', priority: 10 }])
}
)
const result = await validateEmail('user@unknown-disposable.xyz')
expect(result.isValid).toBe(false)
expect(result.reason).toBe('Disposable email addresses are not allowed')
expect(result.checks.disposable).toBe(false)
})
})
})
+117 -32
View File
@@ -14,29 +14,57 @@ export interface EmailValidationResult {
}
}
// Common disposable email domains (subset - can be expanded)
const DISPOSABLE_DOMAINS = new Set([
/** Common disposable domains for fast client-side checks (no heavy import needed) */
const DISPOSABLE_DOMAINS_INLINE = new Set([
'10minutemail.com',
'tempmail.org',
'guerrillamail.com',
'mailinator.com',
'yopmail.com',
'temp-mail.org',
'throwaway.email',
'getnada.com',
'10minutemail.net',
'temporary-mail.net',
'fakemailgenerator.com',
'sharklasers.com',
'guerrillamailblock.com',
'pokemail.net',
'spam4.me',
'tempail.com',
'tempr.email',
'catchmail.io',
'dispostable.com',
'emailondeck.com',
'fakemailgenerator.com',
'getnada.com',
'guerrillamail.com',
'guerrillamailblock.com',
'mail.gw',
'mailinator.com',
'oakon.com',
'pokemail.net',
'salt.email',
'sharebot.net',
'sharklasers.com',
'spam4.me',
'temp-mail.org',
'tempail.com',
'tempmail.org',
'tempr.email',
'temporary-mail.net',
'throwaway.email',
'yopmail.com',
])
/** Full disposable domain list from npm package (~5.3K domains), lazy-loaded server-side only */
let disposableDomainsFull: Set<string> | null = null
function getDisposableDomainsFull(): Set<string> {
if (!disposableDomainsFull) {
try {
// eslint-disable-next-line @typescript-eslint/no-require-imports
const domains = require('disposable-email-domains') as string[]
disposableDomainsFull = new Set(domains)
} catch {
logger.warn('Failed to load disposable-email-domains package')
disposableDomainsFull = new Set()
}
}
return disposableDomainsFull
}
/** MX hostnames used by known disposable email backends */
const DISPOSABLE_MX_BACKENDS = new Set(['in.mail.gw', 'smtp.catchmail.io', 'mx.yopmail.com'])
/** Per-domain MX result cache — avoids redundant DNS queries for concurrent or repeated sign-ups */
const mxCache = new Map<string, { result: boolean; expires: number }>()
/**
* Validates email syntax using RFC 5322 compliant regex
*/
@@ -47,12 +75,14 @@ function validateEmailSyntax(email: string): boolean {
}
/**
* Checks if domain has valid MX records (server-side only)
* Checks if domain has valid MX records and is not backed by a disposable email service (server-side only)
*/
async function checkMXRecord(domain: string): Promise<boolean> {
async function checkMXRecord(
domain: string
): Promise<{ exists: boolean; isDisposableBackend: boolean }> {
// Skip MX check on client-side (browser)
if (typeof window !== 'undefined') {
return true // Assume valid on client-side
return { exists: true, isDisposableBackend: false }
}
try {
@@ -61,19 +91,58 @@ async function checkMXRecord(domain: string): Promise<boolean> {
const resolveMx = promisify(dns.resolveMx)
const mxRecords = await resolveMx(domain)
return mxRecords && mxRecords.length > 0
if (!mxRecords || mxRecords.length === 0) {
return { exists: false, isDisposableBackend: false }
}
const isDisposableBackend = mxRecords.some((record: { exchange: string }) =>
DISPOSABLE_MX_BACKENDS.has(record.exchange.toLowerCase().replace(/\.$/, ''))
)
return { exists: true, isDisposableBackend }
} catch (error) {
logger.debug('MX record check failed', { domain, error })
return false
return { exists: false, isDisposableBackend: false }
}
}
/**
* Checks if email is from a known disposable email provider
* Checks against the full disposable email domain list (~5.3K domains server-side, inline list client-side)
*/
function isDisposableEmail(email: string): boolean {
export function isDisposableEmailFull(email: string): boolean {
const domain = email.split('@')[1]?.toLowerCase()
return domain ? DISPOSABLE_DOMAINS.has(domain) : false
if (!domain) return false
return DISPOSABLE_DOMAINS_INLINE.has(domain) || getDisposableDomainsFull().has(domain)
}
/**
* Checks if an email's MX records point to a known disposable email backend (server-side only)
*/
export async function isDisposableMxBackend(email: string): Promise<boolean> {
const domain = email.split('@')[1]?.toLowerCase()
if (!domain) return false
const now = Date.now()
const cached = mxCache.get(domain)
if (cached && cached.expires > now) return cached.result
let timeoutId: ReturnType<typeof setTimeout> | undefined
try {
const mxCheckPromise = checkMXRecord(domain)
const timeoutPromise = new Promise<{ exists: boolean; isDisposableBackend: boolean }>(
(_, reject) => {
timeoutId = setTimeout(() => reject(new Error('MX check timeout')), 5000)
}
)
const result = await Promise.race([mxCheckPromise, timeoutPromise])
mxCache.set(domain, { result: result.isDisposableBackend, expires: now + 5 * 60 * 1000 })
return result.isDisposableBackend
} catch {
mxCache.set(domain, { result: false, expires: now + 60 * 1000 })
return false
} finally {
clearTimeout(timeoutId)
}
}
/**
@@ -123,8 +192,8 @@ export async function validateEmail(email: string): Promise<EmailValidationResul
}
}
// 2. Check for disposable email first (more specific)
checks.disposable = !isDisposableEmail(email)
// 2. Check for disposable email against full list (server-side)
checks.disposable = !isDisposableEmailFull(email)
if (!checks.disposable) {
return {
isValid: false,
@@ -155,17 +224,33 @@ export async function validateEmail(email: string): Promise<EmailValidationResul
}
}
// 5. MX record check (with timeout)
// 5. MX record check (with timeout) — also detects disposable email backends
let mxTimeoutId: ReturnType<typeof setTimeout> | undefined
try {
const mxCheckPromise = checkMXRecord(domain)
const timeoutPromise = new Promise<boolean>((_, reject) =>
setTimeout(() => reject(new Error('MX check timeout')), 5000)
const timeoutPromise = new Promise<{ exists: boolean; isDisposableBackend: boolean }>(
(_, reject) => {
mxTimeoutId = setTimeout(() => reject(new Error('MX check timeout')), 5000)
}
)
checks.mxRecord = await Promise.race([mxCheckPromise, timeoutPromise])
const mxResult = await Promise.race([mxCheckPromise, timeoutPromise])
checks.mxRecord = mxResult.exists
if (mxResult.isDisposableBackend) {
checks.disposable = false
return {
isValid: false,
reason: 'Disposable email addresses are not allowed',
confidence: 'high',
checks,
}
}
} catch (error) {
logger.debug('MX record check failed or timed out', { domain, error })
checks.mxRecord = false
} finally {
clearTimeout(mxTimeoutId)
}
// Determine overall validity and confidence
@@ -225,7 +310,7 @@ export function quickValidateEmail(email: string): EmailValidationResult {
}
}
checks.disposable = !isDisposableEmail(email)
checks.disposable = !isDisposableEmailFull(email)
if (!checks.disposable) {
return {
isValid: false,
+1
View File
@@ -99,6 +99,7 @@
"csv-parse": "6.1.0",
"date-fns": "4.1.0",
"decimal.js": "10.6.0",
"disposable-email-domains": "1.0.62",
"drizzle-orm": "^0.44.5",
"encoding": "0.1.13",
"entities": "6.0.1",
+4
View File
@@ -1,5 +1,6 @@
{
"lockfileVersion": 1,
"configVersion": 0,
"workspaces": {
"": {
"name": "simstudio",
@@ -124,6 +125,7 @@
"csv-parse": "6.1.0",
"date-fns": "4.1.0",
"decimal.js": "10.6.0",
"disposable-email-domains": "1.0.62",
"drizzle-orm": "^0.44.5",
"encoding": "0.1.13",
"entities": "6.0.1",
@@ -2048,6 +2050,8 @@
"dingbat-to-unicode": ["dingbat-to-unicode@1.0.1", "", {}, "sha512-98l0sW87ZT58pU4i61wa2OHwxbiYSbuxsCBozaVnYX2iCnr3bLM3fIes1/ej7h1YdOKuKt/MLs706TVnALA65w=="],
"disposable-email-domains": ["disposable-email-domains@1.0.62", "", {}, "sha512-LBQvhRw7mznQTPoyZbsmYeNOZt1pN5aCsx4BAU/3siVFuiM9f2oyKzUaB8v1jbxFjE3aYqYiMo63kAL4pHgfWQ=="],
"dlv": ["dlv@1.1.3", "", {}, "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="],
"dockerfile-ast": ["dockerfile-ast@0.7.1", "", { "dependencies": { "vscode-languageserver-textdocument": "^1.0.8", "vscode-languageserver-types": "^3.17.3" } }, "sha512-oX/A4I0EhSkGqrFv0YuvPkBUSYp1XiY8O8zAKc8Djglx8ocz+JfOr8gP0ryRMC2myqvDLagmnZaU9ot1vG2ijw=="],
+376
View File
@@ -21,6 +21,11 @@ const BLOCKS_PATH = path.join(rootDir, 'apps/sim/blocks/blocks')
const DOCS_OUTPUT_PATH = path.join(rootDir, 'apps/docs/content/docs/en/tools')
const ICONS_PATH = path.join(rootDir, 'apps/sim/components/icons.tsx')
const DOCS_ICONS_PATH = path.join(rootDir, 'apps/docs/components/icons.tsx')
const LANDING_INTEGRATIONS_DATA_PATH = path.join(
rootDir,
'apps/sim/app/(landing)/integrations/data'
)
const TRIGGERS_PATH = path.join(rootDir, 'apps/sim/triggers')
if (!fs.existsSync(DOCS_OUTPUT_PATH)) {
fs.mkdirSync(DOCS_OUTPUT_PATH, { recursive: true })
@@ -43,9 +48,39 @@ interface BlockConfig {
tools?: {
access?: string[]
}
operations?: OperationInfo[]
docsLink?: string
[key: string]: any
}
interface TriggerInfo {
id: string
name: string
description: string
}
interface OperationInfo {
name: string
description: string
}
interface IntegrationEntry {
type: string
slug: string
name: string
description: string
longDescription: string
bgColor: string
iconName: string
docsUrl: string
operations: OperationInfo[]
operationCount: number
triggers: TriggerInfo[]
triggerCount: number
authType: 'oauth' | 'api-key' | 'none'
category: string
}
/**
* Copy the icons.tsx file from the main sim app to the docs app
* This ensures icons are rendered consistently across both apps
@@ -207,6 +242,333 @@ ${mappingEntries}
}
}
/**
* Extract operation options from the subBlock with id: 'operation' (if present).
* Returns { label, id } pairs — label is the display name, id is the option's id field
* (used to construct the tool ID as `{blockType}_{id}`).
* Parses the subBlocks array using brace/bracket counting to safely traverse
* the nested structure without eval or a full AST parser.
*/
function extractOperationsFromContent(blockContent: string): { label: string; id: string }[] {
const subBlocksMatch = /subBlocks\s*:\s*\[/.exec(blockContent)
if (!subBlocksMatch) return []
// Locate the opening '[' of the subBlocks array
const arrayStart = subBlocksMatch.index + subBlocksMatch[0].length - 1
let bracketCount = 1
let pos = arrayStart + 1
while (pos < blockContent.length && bracketCount > 0) {
if (blockContent[pos] === '[') bracketCount++
else if (blockContent[pos] === ']') bracketCount--
pos++
}
const subBlocksContent = blockContent.substring(arrayStart + 1, pos - 1)
// Iterate over top-level objects in the subBlocks array, looking for id: 'operation'
let i = 0
while (i < subBlocksContent.length) {
if (subBlocksContent[i] === '{') {
let braceCount = 1
let j = i + 1
while (j < subBlocksContent.length && braceCount > 0) {
if (subBlocksContent[j] === '{') braceCount++
else if (subBlocksContent[j] === '}') braceCount--
j++
}
const objContent = subBlocksContent.substring(i, j)
if (/\bid\s*:\s*['"]operation['"]/.test(objContent)) {
const optionsMatch = /options\s*:\s*\[/.exec(objContent)
if (!optionsMatch) return []
const optArrayStart = optionsMatch.index + optionsMatch[0].length - 1
let bc = 1
let op = optArrayStart + 1
while (op < objContent.length && bc > 0) {
if (objContent[op] === '[') bc++
else if (objContent[op] === ']') bc--
op++
}
const optionsContent = objContent.substring(optArrayStart + 1, op - 1)
// Extract { label, id } pairs from each option object
const pairs: { label: string; id: string }[] = []
const optionObjectRegex = /\{[^{}]*\}/g
let m
while ((m = optionObjectRegex.exec(optionsContent)) !== null) {
const optObj = m[0]
const labelMatch = /label\s*:\s*['"]([^'"]+)['"]/.exec(optObj)
const idMatch = /\bid\s*:\s*['"]([^'"]+)['"]/.exec(optObj)
if (labelMatch) {
pairs.push({ label: labelMatch[1], id: idMatch ? idMatch[1] : '' })
}
}
return pairs
}
i = j
} else {
i++
}
}
return []
}
/**
* 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<Map<string, string>> {
const toolsDir = path.join(rootDir, 'apps/sim/tools')
const map = new Map<string, string>()
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])
}
}
} catch {
// Non-fatal: descriptions will be empty strings
}
return map
}
/**
* Detect the authentication type from block content.
* Returns 'oauth' if the block uses oauth-input credentials,
* 'api-key' if it uses a plain API key field, or 'none' otherwise.
*/
function extractAuthType(blockContent: string): 'oauth' | 'api-key' | 'none' {
if (/type\s*:\s*['"]oauth-input['"]/.test(blockContent)) return 'oauth'
if (/\bid\s*:\s*['"](?:apiKey|api_key|accessToken)['"]/.test(blockContent)) return 'api-key'
return 'none'
}
/**
* Extract the list of trigger IDs from the block's `triggers.available` array.
* Handles blocks that declare `triggers: { enabled: true, available: [...] }`.
*/
function extractTriggersAvailable(blockContent: string): string[] {
const triggersMatch = /\btriggers\s*:\s*\{/.exec(blockContent)
if (!triggersMatch) return []
const start = triggersMatch.index + triggersMatch[0].length - 1
let braceCount = 1
let pos = start + 1
while (pos < blockContent.length && braceCount > 0) {
if (blockContent[pos] === '{') braceCount++
else if (blockContent[pos] === '}') braceCount--
pos++
}
const triggersContent = blockContent.substring(start, pos)
if (!/enabled\s*:\s*true/.test(triggersContent)) return []
const availableMatch = /available\s*:\s*\[/.exec(triggersContent)
if (!availableMatch) return []
const arrayStart = availableMatch.index + availableMatch[0].length - 1
let bracketCount = 1
let ap = arrayStart + 1
while (ap < triggersContent.length && bracketCount > 0) {
if (triggersContent[ap] === '[') bracketCount++
else if (triggersContent[ap] === ']') bracketCount--
ap++
}
const arrayContent = triggersContent.substring(arrayStart + 1, ap - 1)
const ids: string[] = []
const idRegex = /['"]([^'"]+)['"]/g
let m
while ((m = idRegex.exec(arrayContent)) !== null) {
ids.push(m[1])
}
return ids
}
/**
* Scan all trigger definition files and build a registry mapping trigger IDs
* to their human-readable name and description.
*/
async function buildTriggerRegistry(): Promise<Map<string, TriggerInfo>> {
const registry = new Map<string, TriggerInfo>()
const SKIP = new Set(['index.ts', 'registry.ts', 'types.ts', 'constants.ts', 'utils.ts'])
const triggerFiles = (await glob(`${TRIGGERS_PATH}/**/*.ts`)).filter(
(f) => !SKIP.has(path.basename(f)) && !f.includes('.test.')
)
for (const file of triggerFiles) {
try {
const content = fs.readFileSync(file, 'utf-8')
// Each trigger file exports a single TriggerConfig with id, name, description
const idMatch = /\bid\s*:\s*['"]([^'"]+)['"]/.exec(content)
const nameMatch = /\bname\s*:\s*['"]([^'"]+)['"]/.exec(content)
const descMatch = /\bdescription\s*:\s*['"]([^'"]+)['"]/.exec(content)
if (idMatch && nameMatch) {
registry.set(idMatch[1], {
id: idMatch[1],
name: nameMatch[1],
description: descMatch?.[1] ?? '',
})
}
} catch {
// skip unreadable files silently
}
}
console.log(`✓ Loaded ${registry.size} trigger definitions`)
return registry
}
/**
* Write the icon mapping TypeScript file for the landing integrations page.
* Mirrors writeIconMapping but targets the sim app so it imports from @/components/icons.
*/
function writeIntegrationsIconMapping(iconMapping: Record<string, string>): void {
try {
if (!fs.existsSync(LANDING_INTEGRATIONS_DATA_PATH)) {
fs.mkdirSync(LANDING_INTEGRATIONS_DATA_PATH, { recursive: true })
}
const iconMappingPath = path.join(LANDING_INTEGRATIONS_DATA_PATH, 'icon-mapping.ts')
const iconNames = [...new Set(Object.values(iconMapping))].sort()
const imports = iconNames.map((icon) => ` ${icon},`).join('\n')
const mappingEntries = Object.entries(iconMapping)
.sort(([a], [b]) => a.localeCompare(b))
.map(([blockType, iconName]) => ` ${blockType}: ${iconName},`)
.join('\n')
const content = `// Auto-generated file - do not edit manually
// Generated by scripts/generate-docs.ts
// Maps block types to their icon component references for the integrations page
import type { ComponentType, SVGProps } from 'react'
import {
${imports}
} from '@/components/icons'
type IconComponent = ComponentType<SVGProps<SVGSVGElement>>
export const blockTypeToIconMap: Record<string, IconComponent> = {
${mappingEntries}
}
`
fs.writeFileSync(iconMappingPath, content)
console.log('✓ Integration icon mapping written to landing app')
} catch (error) {
console.error('Error writing integration icon mapping:', error)
}
}
/**
* Collect all integration entries from block definitions and write integrations.json
* to the landing integrations page data directory.
* Applies the same visibility filters as the docs generation pipeline.
*/
async function writeIntegrationsJson(iconMapping: Record<string, string>): Promise<void> {
try {
if (!fs.existsSync(LANDING_INTEGRATIONS_DATA_PATH)) {
fs.mkdirSync(LANDING_INTEGRATIONS_DATA_PATH, { recursive: true })
}
const triggerRegistry = await buildTriggerRegistry()
const toolDescMap = await buildToolDescriptionMap()
const integrations: IntegrationEntry[] = []
const seenBaseTypes = new Set<string>()
const blockFiles = (await glob(`${BLOCKS_PATH}/*.ts`)).sort()
for (const blockFile of blockFiles) {
const fileContent = fs.readFileSync(blockFile, 'utf-8')
const configs = extractAllBlockConfigs(fileContent)
for (const config of configs) {
const blockType = config.type
// Apply the same filters as docs/icon-mapping generation
if (
blockType.includes('_trigger') ||
blockType.includes('_webhook') ||
blockType.includes('rss') ||
(config.category === 'blocks' && blockType !== 'memory' && blockType !== 'knowledge') ||
blockType === 'evaluator' ||
blockType === 'number' ||
blockType === 'webhook' ||
blockType === 'schedule' ||
blockType === 'mcp' ||
blockType === 'generic_webhook'
) {
continue
}
// Deduplicate by stripped base type
const baseType = stripVersionSuffix(blockType)
if (seenBaseTypes.has(baseType)) continue
seenBaseTypes.add(baseType)
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
const operations: OperationInfo[] = rawOps.map(({ label, id }) => {
const toolId = `${baseType}_${id}`
const desc = toolDescMap.get(toolId) || toolDescMap.get(id) || ''
return { name: label, description: desc }
})
const triggerIds: string[] = (config as any).triggerIds || []
const triggers: TriggerInfo[] = triggerIds
.map((id) => triggerRegistry.get(id))
.filter((t): t is TriggerInfo => t !== undefined)
const docsUrl = (config as any).docsLink || `https://docs.sim.ai/tools/${baseType}`
const slug = config.name
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-|-$/g, '')
// Detect auth type from the original block file content
const blockFileContent = fs.readFileSync(blockFile, 'utf-8')
const authType = extractAuthType(blockFileContent)
integrations.push({
type: blockType,
slug,
name: config.name,
description: config.description,
longDescription: config.longDescription || '',
bgColor: config.bgColor || '#6B7280',
iconName,
docsUrl,
operations,
operationCount: operations.length,
triggers,
triggerCount: triggers.length,
authType,
category: config.category,
})
}
}
// Sort alphabetically by name for a predictable, crawl-friendly order
integrations.sort((a, b) => a.name.localeCompare(b.name))
const jsonPath = path.join(LANDING_INTEGRATIONS_DATA_PATH, 'integrations.json')
fs.writeFileSync(jsonPath, JSON.stringify(integrations, null, 2))
console.log(`✓ Integration data written: ${integrations.length} integrations → ${jsonPath}`)
} catch (error) {
console.error('Error writing integrations JSON:', error)
}
}
/**
* Extract ALL block configs from a file, filtering out hidden blocks
*/
@@ -355,6 +717,13 @@ function extractBlockConfigFromContent(
}
}
const operations = extractOperationsFromContent(blockContent)
const triggerIds = extractTriggersAvailable(blockContent)
const docsLink =
extractStringPropertyFromContent(blockContent, 'docsLink', true) ||
(baseConfig as any)?.docsLink ||
`https://docs.sim.ai/tools/${stripVersionSuffix(blockType)}`
return {
type: blockType,
name,
@@ -367,6 +736,9 @@ function extractBlockConfigFromContent(
tools: {
access: finalToolsAccess.length > 0 ? finalToolsAccess : baseConfig?.tools?.access || [],
},
operations: operations.length > 0 ? operations : (baseConfig as any)?.operations || [],
triggerIds: triggerIds.length > 0 ? triggerIds : (baseConfig as any)?.triggerIds || [],
docsLink,
}
} catch (error) {
console.error(`Error extracting block configuration for ${blockName}:`, error)
@@ -2442,6 +2814,10 @@ async function generateAllBlockDocs() {
const iconMapping = await generateIconMapping()
writeIconMapping(iconMapping)
// Generate landing integrations page data (JSON + icon mapping)
await writeIntegrationsJson(iconMapping)
writeIntegrationsIconMapping(iconMapping)
// Get hidden and visible block types before generating docs
const { hiddenTypes, visibleDisplayNames } = await getHiddenAndVisibleBlockTypes()
console.log(`Found ${hiddenTypes.size} hidden blocks: ${[...hiddenTypes].join(', ')}`)