chore: import upstream snapshot with attribution
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:20:55 +08:00
commit d25d482dc2
13754 changed files with 4996608 additions and 0 deletions
@@ -0,0 +1,25 @@
'use client'
import { Chip } from '@sim/emcn'
import { AuthModal } from '@/app/(landing)/components/auth-modal/auth-modal'
import { trackLandingCta } from '@/app/(landing)/track-landing-cta'
interface IntegrationCtaButtonProps {
children: React.ReactNode
label: string
}
export function IntegrationCtaButton({ children, label }: IntegrationCtaButtonProps) {
return (
<AuthModal defaultView='signup' source='integrations'>
<Chip
variant='primary'
onClick={() =>
trackLandingCta({ label, section: 'integrations', destination: 'auth_modal' })
}
>
{children}
</Chip>
</AuthModal>
)
}
@@ -0,0 +1,38 @@
'use client'
import { cn } from '@sim/emcn'
import { useRouter } from 'next/navigation'
import { LandingPromptStorage } from '@/lib/core/utils/browser-storage'
import { trackLandingCta } from '@/app/(landing)/track-landing-cta'
interface TemplateCardButtonProps {
/**
* Curated template prompt, already rewritten to `@`-mention form by the
* page's server-side `mentionifyPromptForNames` (registry-free, so the
* landing client bundle never pulls the full block registry). Stored verbatim
* for the home input to consume after signup.
*/
prompt: string
className?: string
children: React.ReactNode
}
export function TemplateCardButton({ prompt, className, children }: TemplateCardButtonProps) {
const router = useRouter()
function savePromptAndNavigate() {
LandingPromptStorage.store(prompt)
trackLandingCta({ label: 'Template card', section: 'integrations', destination: '/signup' })
router.push('/signup')
}
return (
<button
type='button'
onClick={savePromptAndNavigate}
className={cn('w-full text-left', className)}
>
{children}
</button>
)
}
@@ -0,0 +1,9 @@
import { Loader } from '@sim/emcn'
export default function IntegrationDetailLoading() {
return (
<div className='flex min-h-[60vh] items-center justify-center bg-[var(--bg)]'>
<Loader animate className='size-6 text-[var(--text-muted)]' />
</div>
)
}
@@ -0,0 +1,48 @@
import { notFound } from 'next/navigation'
import integrationsJson from '@/lib/integrations/integrations.json'
import type { AuthType, Integration } from '@/lib/integrations/types'
import { createLandingOgImage } from '@/app/(landing)/og-utils'
export const contentType = 'image/png'
export const size = {
width: 1200,
height: 630,
}
/** Raw catalog JSON, not the barrel - keeps `@/blocks/registry` out of the OG bundle. */
const integrations = integrationsJson.integrations as readonly Integration[]
const bySlug = new Map(integrations.map((i) => [i.slug, i]))
const AUTH_LABEL: Record<AuthType, string> = {
oauth: 'One-click OAuth',
'api-key': 'API key auth',
none: 'No auth required',
}
export default async function Image({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params
const integration = bySlug.get(slug)
if (!integration) {
notFound()
}
const pills = [
integration.operationCount > 0
? `${integration.operationCount} tool${integration.operationCount === 1 ? '' : 's'}`
: null,
integration.triggerCount > 0
? `${integration.triggerCount} real-time trigger${integration.triggerCount === 1 ? '' : 's'}`
: null,
AUTH_LABEL[integration.authType],
'Free to start',
].filter((pill): pill is string => pill !== null)
return createLandingOgImage({
eyebrow: 'Sim integration',
title: `${integration.name} Integration`,
subtitle: integration.description,
pills,
domainLabel: `sim.ai/integrations/${slug}`,
})
}
@@ -0,0 +1,969 @@
import { ChipLink } from '@sim/emcn'
import { truncate } from '@sim/utils/string'
import type { Metadata } from 'next'
import Image from 'next/image'
import Link from 'next/link'
import { notFound } from 'next/navigation'
import { SITE_URL } from '@/lib/core/utils/urls'
import {
type AuthType,
blockTypeToIconMap,
type FAQItem,
formatIntegrationType,
INTEGRATIONS,
INTEGRATIONS_UPDATED_AT,
type Integration,
} from '@/lib/integrations'
import { BackLink } from '@/app/(landing)/components'
import { JsonLd } from '@/app/(landing)/components/json-ld'
import { LandingFAQ } from '@/app/(landing)/components/landing-faq'
import { ShareButton } from '@/app/(landing)/components/share-button'
import { IntegrationCtaButton } from '@/app/(landing)/integrations/(shell)/[slug]/components/integration-cta-button'
import { TemplateCardButton } from '@/app/(landing)/integrations/(shell)/[slug]/components/template-card-button'
import { IntegrationIcon } from '@/app/(landing)/integrations/components/integration-icon'
import { INTEGRATION_SEO } from '@/app/(landing)/integrations/data/seo-content'
import { getTemplatesForBlock } from '@/blocks/registry'
const allIntegrations = INTEGRATIONS
const INTEGRATION_COUNT = allIntegrations.length
const baseUrl = SITE_URL
/** Fast O(1) lookups - avoids repeated linear scans inside render loops. */
const bySlug = new Map(allIntegrations.map((i) => [i.slug, i]))
const byType = new Map(allIntegrations.map((i) => [i.type, i]))
export const dynamicParams = false
/**
* Returns up to `limit` related integration slugs.
*
* Scoring (additive):
* +3 per shared operation name - strongest signal (same capability)
* +2 per shared operation word - weaker signal (e.g. both have "create" ops)
* +2 same integration category - topical relevance (both CRMs, both devops)
* +1 same auth type - comparable setup experience
*
* Every integration gets a score, so the sidebar always has suggestions.
* Ties are broken by alphabetical slug order for determinism.
*/
function getRelatedSlugs(
slug: string,
operations: Integration['operations'],
authType: AuthType,
integrationType: Integration['integrationType'],
limit = 6
): string[] {
const currentOpNames = new Set(operations.map((o) => o.name.toLowerCase()))
const currentOpWords = new Set(
operations.flatMap((o) =>
o.name
.toLowerCase()
.split(/\s+/)
.filter((w) => w.length > 3)
)
)
return allIntegrations
.reduce<Array<{ slug: string; score: number }>>((scored, i) => {
if (i.slug === slug) return scored
const sharedNames = i.operations.filter((o) =>
currentOpNames.has(o.name.toLowerCase())
).length
const sharedWords = i.operations.filter((o) =>
o.name
.toLowerCase()
.split(/\s+/)
.some((w) => w.length > 3 && currentOpWords.has(w))
).length
const sameCategory = i.integrationType === integrationType ? 2 : 0
const sameAuth = i.authType === authType ? 1 : 0
scored.push({
slug: i.slug,
score: sharedNames * 3 + sharedWords * 2 + sameCategory + sameAuth,
})
return scored
}, [])
.sort((a, b) => b.score - a.score || a.slug.localeCompare(b.slug))
.slice(0, limit)
.map(({ slug: s }) => s)
}
const AUTH_STEP: Record<AuthType, (name: string) => string> = {
oauth: (name) =>
`Connect your ${name} account with one-click OAuth, with no credentials to copy.`,
'api-key': (name) =>
`Paste your ${name} API key to authenticate. You can find it in your ${name} account settings.`,
none: () => 'No authentication is needed, so the block works as soon as you drop it in.',
}
/** Human-readable catalog refresh date for the visible last-updated line. */
const UPDATED_AT_DISPLAY = new Date(`${INTEGRATIONS_UPDATED_AT}T00:00:00Z`).toLocaleDateString(
'en-US',
{ year: 'numeric', month: 'long', day: 'numeric', timeZone: 'UTC' }
)
/**
* Ensures autogenerated prose can be safely composed with a following sentence.
*/
function sentenceWithTerminalPunctuation(value: string): string {
const trimmedValue = value.trim()
return /[.!?]$/.test(trimmedValue) ? trimmedValue : `${trimmedValue}.`
}
function escapeRegex(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
}
/**
* Server-side rewrite of bare integration names in a curated template prompt
* to `@`-mention form (`Slack` → `@Slack`) so the prompt chips with brand
* icons once it is populated into the Mothership home input after signup -
* the home auto-mention pipeline only chips token-starting `@` mentions, so
* curated prompts must opt in.
*
* Unlike the workspace surface, which calls `mentionifyIntegrations` from
* `@/blocks/integration-matcher`, this runs only over the handful of names a
* template actually references (its owner + `otherBlockTypes`) and lives in a
* Server Component, so it never pulls the full block/tool/icon registry into
* the landing client bundle. Whole-token, longest-first matching with
* lookarounds mirrors the canonical matcher; idempotent on already-prefixed
* names.
*/
function mentionifyPromptForNames(prompt: string, names: readonly string[]): string {
const unique = Array.from(new Set(names.filter((n) => n.trim().length >= 2))).sort(
(a, b) => b.length - a.length
)
if (unique.length === 0) return prompt
const regex = new RegExp(
`(?<![A-Za-z0-9_@])(${unique.map(escapeRegex).join('|')})(?![A-Za-z0-9_])`,
'gi'
)
return prompt.replace(regex, (match) => `@${match}`)
}
/** Lowercases only the first character so acronyms in tool names survive. */
function lowercaseFirst(value: string): string {
return value.charAt(0).toLowerCase() + value.slice(1)
}
/**
* The ordered integration-icon chain for a template card - one tile per block
* type in flow order, separated by arrows. Resolves each type through its
* versioned aliases so v2/v3 blocks reuse the base icon and name.
*/
function TemplateIconRow({ allTypes }: { allTypes: string[] }) {
return (
<>
{allTypes.map((bt, idx) => {
const resolvedBt = byType.get(bt)
? bt
: byType.get(`${bt}_v2`)
? `${bt}_v2`
: byType.get(`${bt}_v3`)
? `${bt}_v3`
: bt
const int = byType.get(resolvedBt)
const ToolIcon = blockTypeToIconMap[resolvedBt]
return (
<span key={bt} className='inline-flex items-center gap-1.5'>
{idx > 0 && (
<span className='text-[11px] text-[var(--text-subtle)]' aria-hidden='true'>
</span>
)}
<IntegrationIcon
bgColor={int?.bgColor ?? 'var(--surface-active)'}
name={int?.name ?? bt}
Icon={ToolIcon}
as='span'
className='size-6 rounded-[4px]'
iconClassName='size-3.5'
fallbackClassName='text-[10px]'
aria-hidden='true'
/>
</span>
)
})}
</>
)
}
/** Joins items into readable prose: "a", "a and b", or "a, b, and c". */
function toProseList(items: string[]): string {
if (items.length <= 1) return items[0] ?? ''
if (items.length === 2) return `${items[0]} and ${items[1]}`
return `${items.slice(0, -1).join(', ')}, and ${items[items.length - 1]}`
}
/** "a" vs "an" for a service name; U-names read as "you", so they take "a". */
function articleFor(name: string): string {
return /^[aeio]/i.test(name) ? 'an' : 'a'
}
/**
* Generates the per-integration FAQ. Answers lead with a direct answer and
* carry integration-specific facts; catalog-generic questions live once on
* the /integrations index FAQ instead of repeating across every page.
*/
function buildFAQs(integration: Integration, relatedNames: string[]): FAQItem[] {
const { name, description, operations, triggers, authType } = integration
const faqDescription = sentenceWithTerminalPunctuation(description)
const opCount = operations.length
const triggerCount = triggers.length
const topOpNames = operations.slice(0, 5).map((o) => o.name)
const firstOp = operations[0]
const firstTrigger = triggers[0]
const pairings = relatedNames.slice(0, 2)
const toolsPhrase = `${opCount} ${name} tool${opCount === 1 ? '' : 's'}`
const triggersPhrase = `${triggerCount} real-time trigger${triggerCount === 1 ? '' : 's'}`
const capabilityPhrase = [
opCount > 0 ? toolsPhrase : null,
triggerCount > 0 ? triggersPhrase : null,
]
.filter((part): part is string => part !== null)
.join(' and ')
const triggerNames = triggers.map((t) => t.name)
const triggerListPhrase =
triggerCount > 6
? `${triggerNames.slice(0, 6).join(', ')}, and ${triggerCount - 6} more`
: toProseList(triggerNames)
const firstTriggerWhen = firstTrigger?.description.match(/^trigger workflow (when .+)$/i)?.[1]
const connectFinalStep = firstOp
? `Pick a tool such as "${firstOp.name}", wire up its inputs, and click Run, and your agent is live.`
: triggerCount > 0
? `Choose the ${name} event you want to listen for, and your agent runs automatically from then on.`
: `Configure the block's inputs and click Run, and your agent is live.`
const faqs: FAQItem[] = [
{
question: `What is Sim's ${name} integration?`,
answer: `Sim's ${name} integration ${capabilityPhrase ? `adds ${capabilityPhrase} to` : `connects ${name} to`} the AI agents you build in Sim's visual workflow builder — you build it all visually. ${faqDescription}${
pairings.length === 2
? ` Teams often pair ${name} with ${pairings[0]} and ${pairings[1]} in the same agent.`
: ''
}`,
},
...(opCount > 0
? [
{
question: `What can I automate with ${name} in Sim?`,
answer: `You can ${toProseList(topOpNames.map(lowercaseFirst))} with ${name} in Sim${
opCount > 5 ? `, plus ${opCount - 5} more ${name} tools listed on this page` : ''
}. ${opCount === 1 ? 'It runs' : 'Each runs'} as a tool inside an AI agent block, so an agent can chain ${name} with ${
pairings.length === 2
? `services like ${pairings[0]} and ${pairings[1]}`
: 'any other connected service'
} and apply LLM reasoning between steps.`,
},
]
: []),
{
question: `How do I connect ${name} to Sim?`,
answer: `Connecting ${name} takes about five minutes: (1) Create a free account at sim.ai. (2) Create an agent in your workspace. (3) Drag ${articleFor(name)} ${name} block onto the workflow builder. (4) ${AUTH_STEP[authType](name)} (5) ${connectFinalStep}`,
},
...(firstOp && opCount >= 2
? [
{
question: `How do I ${lowercaseFirst(firstOp.name)} with ${name} in Sim?`,
answer: `Add ${articleFor(name)} ${name} block to your agent and select "${firstOp.name}" as the tool.${
firstOp.description ? ` ${sentenceWithTerminalPunctuation(firstOp.description)}` : ''
} Fill in the required fields. Inputs can reference outputs from earlier steps, such as text generated by an AI block or data fetched from another integration, and you build it all visually.`,
},
]
: []),
...(triggerCount > 0
? [
{
question: `How do I trigger a Sim agent from ${name} automatically?`,
answer: `Add ${articleFor(name)} ${name} trigger block to your agent and copy its generated webhook URL into ${name}'s webhook settings. Sim supports ${triggersPhrase} for ${name}: ${triggerListPhrase}. Once configured, every matching ${name} event starts your agent instantly, no polling, no delay.`,
},
{
question: `What data does Sim receive when a ${name} event triggers an agent?`,
answer: `Sim receives the full event payload ${name} sends, typically the record or object that changed, plus metadata like the event type and timestamp.${
firstTriggerWhen
? ` For example, the "${firstTrigger.name}" trigger fires ${sentenceWithTerminalPunctuation(firstTriggerWhen)}`
: ''
} Every field in the payload is available as a variable you can pass to AI blocks, conditions, or other integrations.`,
},
]
: []),
]
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 categoryLabel = formatIntegrationType(integration.integrationType)
const seo = INTEGRATION_SEO[slug]
const metaDesc =
seo?.description ??
`Automate ${name} with AI agents in Sim. ${sentenceWithTerminalPunctuation(truncate(description, 100))} Free to start.`
return {
// A hand-authored SEO title is rendered verbatim (it carries its own brand
// suffix); otherwise the bare name flows through the root `%s | Sim` template.
title: seo?.title ? { absolute: seo.title } : `${name} Integration`,
description: metaDesc,
keywords: seo?.keywords ?? [
`${name} automation`,
`${name} integration`,
`automate ${name}`,
`connect ${name}`,
`${name} AI agent`,
`${name} AI automation`,
...(opSample ? [`${name} ${opSample}`] : []),
`${categoryLabel} integration`,
...(integration.tags ?? []).map((tag) => `${name} ${tag.replace(/-/g, ' ')}`),
...(integration.triggerCount > 0 ? [`${name} webhook`, `${name} trigger`] : []),
'AI workspace integrations',
'AI agent integrations',
'AI agent builder',
],
// og:image/twitter:image come from the sibling opengraph-image.tsx -
// Next serves it at a hash-suffixed URL, so hardcoding it here 404s.
openGraph: {
title: seo?.title ?? `${name} Integration | Sim AI Workspace`,
description:
seo?.description ??
`Connect ${name} to ${INTEGRATION_COUNT - 1}+ tools using AI agents. ${sentenceWithTerminalPunctuation(truncate(description, 100))}`,
url: `${baseUrl}/integrations/${slug}`,
type: 'website',
},
twitter: {
card: 'summary_large_image',
title: seo?.title ?? `${name} Integration | Sim`,
description:
seo?.description ??
`Automate ${name} with AI agents in Sim. Connect to ${INTEGRATION_COUNT - 1}+ tools. Free to start.`,
},
alternates: { canonical: `${baseUrl}/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 landingContent = integration.landingContent
const seo = INTEGRATION_SEO[slug]
const overviewBody = seo?.overview ?? longDescription
const IconComponent = blockTypeToIconMap[integration.type]
const categoryLabel = formatIntegrationType(integration.integrationType)
const relatedSlugs = getRelatedSlugs(slug, operations, authType, integration.integrationType)
const relatedIntegrations = relatedSlugs
.map((s) => bySlug.get(s))
.filter((i): i is Integration => i !== undefined)
const faqs = buildFAQs(
integration,
relatedIntegrations.map((i) => i.name)
)
const matchingTemplates = getTemplatesForBlock(integration.type)
const breadcrumbJsonLd = {
'@context': 'https://schema.org',
'@type': 'BreadcrumbList',
itemListElement: [
{ '@type': 'ListItem', position: 1, name: 'Home', item: baseUrl },
{
'@type': 'ListItem',
position: 2,
name: 'Integrations',
item: `${baseUrl}/integrations`,
},
{ '@type': 'ListItem', position: 3, name, item: `${baseUrl}/integrations/${slug}` },
],
}
const softwareAppJsonLd = {
'@context': 'https://schema.org',
'@type': 'SoftwareApplication',
name: `${name} Integration`,
description,
url: `${baseUrl}/integrations/${slug}`,
applicationCategory: 'BusinessApplication',
applicationSubCategory: categoryLabel,
operatingSystem: 'Web',
featureList: operations.map((o) => o.name),
...(integration.tags?.length
? { keywords: integration.tags.map((tag) => tag.replace(/-/g, ' ')).join(', ') }
: {}),
dateModified: INTEGRATIONS_UPDATED_AT,
offers: { '@type': 'Offer', price: '0', priceCurrency: 'USD' },
}
const faqJsonLd = {
'@context': 'https://schema.org',
'@type': 'FAQPage',
mainEntity: faqs.map(({ question, answer }) => ({
'@type': 'Question',
name: question,
acceptedAnswer: { '@type': 'Answer', text: answer },
})),
}
return (
<section className='bg-[var(--bg)]'>
<JsonLd data={breadcrumbJsonLd} />
<JsonLd data={softwareAppJsonLd} />
<JsonLd data={faqJsonLd} />
{/* Hero */}
<div className='mx-auto w-full max-w-[1460px] px-20 pt-[112px] max-sm:px-5 max-sm:pt-20 max-lg:px-8'>
<div className='mb-6'>
<BackLink href='/integrations' label='Back to Integrations' />
</div>
{/* Hero content */}
<div className='mb-6 flex items-center gap-5'>
<IntegrationIcon
bgColor={bgColor}
name={name}
Icon={IconComponent}
className='size-12 rounded-xl border border-[var(--border-1)]'
iconClassName='size-6'
fallbackClassName='text-[20px]'
aria-hidden='true'
/>
<div>
<h1
id='integration-heading'
className='text-[28px] text-[var(--text-primary)] leading-[110%] tracking-[-0.02em] sm:text-[36px] lg:text-[44px]'
>
{seo?.h1 ?? name}
</h1>
</div>
</div>
<p className='mb-3 max-w-[700px] text-[var(--text-body)] text-base leading-[150%] tracking-[0.02em]'>
{seo?.tagline ?? description}
</p>
<p className='sr-only'>
{name} is a {categoryLabel} integration for Sim, the AI workspace where teams build and
deploy AI agents. Sim&apos;s {name} integration provides{' '}
{[
operations.length > 0
? `${operations.length} ${name} tool${operations.length === 1 ? '' : 's'}`
: null,
triggers.length > 0
? `${triggers.length} real-time trigger${triggers.length === 1 ? '' : 's'}`
: null,
]
.filter((part): part is string => part !== null)
.join(' and ') || `a ${name} connection`}{' '}
that AI agents can use inside Sim&apos;s visual workflow builder.{' '}
{authType === 'oauth'
? `${name} connects with one-click OAuth.`
: authType === 'api-key'
? `${name} connects with an API key.`
: `${name} requires no authentication.`}{' '}
Free to start at sim.ai.
</p>
{/* CTAs */}
<div className='flex flex-wrap gap-2'>
<IntegrationCtaButton label='Start building free'>
Start building free
</IntegrationCtaButton>
<ChipLink
href={docsUrl}
target='_blank'
rel='noopener noreferrer'
className='border border-[var(--border-1)]'
>
View docs
</ChipLink>
<ShareButton url={`${baseUrl}/integrations/${slug}`} title={`${name} Integration`} />
</div>
<p className='mt-5 text-[var(--text-muted)] text-xs'>
Last updated <time dateTime={INTEGRATIONS_UPDATED_AT}>{UPDATED_AT_DISPLAY}</time>
</p>
</div>
{/* Full-width divider */}
<div className='mt-8 h-px w-full bg-[var(--border)]' />
{/* Border-railed content */}
<div className='mx-20 max-w-[1300px] border-[var(--border)] border-x max-sm:mx-5 max-lg:mx-8 min-[1460px]:mx-auto'>
{/* Overview */}
{overviewBody && (
<>
<section aria-labelledby='overview-heading' className='px-6 py-10'>
<h2
id='overview-heading'
className='mb-4 text-[20px] text-[var(--text-primary)] leading-[100%] tracking-[-0.02em]'
>
Overview
</h2>
<p className='text-[15px] text-[var(--text-body)] leading-[150%] tracking-[0.02em]'>
{overviewBody}
</p>
</section>
<div className='h-px w-full bg-[var(--border)]' />
</>
)}
{/* Install / Add to workspace (integration-specific) */}
{landingContent?.install && (
<>
<section aria-labelledby='install-heading' className='px-6 py-10'>
<h2
id='install-heading'
className='mb-4 text-[20px] text-[var(--text-primary)] leading-[100%] tracking-[-0.02em]'
>
{landingContent.install.heading}
</h2>
<p className='mb-6 max-w-[700px] text-[15px] text-[var(--text-body)] leading-[150%] tracking-[0.02em]'>
{landingContent.install.intro}
</p>
<ol className='space-y-4' aria-label={`Steps to add ${name}`}>
{landingContent.install.steps.map((item, index) => (
<li key={item.title} className='flex gap-4'>
<span
className='mt-0.5 flex size-7 shrink-0 items-center justify-center rounded-full border border-[var(--border-1)] text-[11px] text-[var(--text-muted)]'
aria-hidden='true'
>
{String(index + 1).padStart(2, '0')}
</span>
<div>
<h3 className='mb-1 text-[15px] text-[var(--text-primary)] tracking-[-0.02em]'>
{item.title}
</h3>
<p className='text-[14px] text-[var(--text-body)] leading-[150%] tracking-[0.02em]'>
{item.body}
</p>
</div>
</li>
))}
</ol>
<div className='mt-8 flex flex-wrap gap-2'>
<IntegrationCtaButton label={`Add to ${name}`}>Add to {name}</IntegrationCtaButton>
</div>
</section>
<div className='h-px w-full bg-[var(--border)]' />
</>
)}
{/* Privacy & data (integration-specific) */}
{landingContent?.privacy && (
<>
<section aria-labelledby='privacy-heading' className='px-6 py-10'>
<h2
id='privacy-heading'
className='mb-4 text-[20px] text-[var(--text-primary)] leading-[100%] tracking-[-0.02em]'
>
Privacy & data
</h2>
<p className='max-w-[700px] text-[15px] text-[var(--text-body)] leading-[150%] tracking-[0.02em]'>
{landingContent.privacy.body}{' '}
<Link
href={landingContent.privacy.href}
className='text-[var(--text-primary)] underline underline-offset-2 hover:text-[var(--text-primary)]'
>
Privacy Policy
</Link>
.
</p>
</section>
<div className='h-px w-full bg-[var(--border)]' />
</>
)}
{/* AI-generated content disclaimer (integration-specific) */}
{landingContent?.aiDisclaimer && (
<>
<section aria-labelledby='ai-disclaimer-heading' className='px-6 py-10'>
<h2
id='ai-disclaimer-heading'
className='mb-4 text-[20px] text-[var(--text-primary)] leading-[100%] tracking-[-0.02em]'
>
AI-generated content
</h2>
<p className='max-w-[700px] text-[15px] text-[var(--text-body)] leading-[150%] tracking-[0.02em]'>
{landingContent.aiDisclaimer}
</p>
</section>
<div className='h-px w-full bg-[var(--border)]' />
</>
)}
{/* How to automate */}
<section aria-labelledby='how-it-works-heading' className='px-6 py-10'>
<h2
id='how-it-works-heading'
className='mb-6 text-[20px] text-[var(--text-primary)] leading-[100%] tracking-[-0.02em]'
>
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 ${articleFor(name)} ${name} block`,
body:
authType === 'oauth'
? `Open your workspace, drag ${articleFor(name)} ${name} block onto the workflow builder, and connect your account with one-click OAuth.`
: authType === 'api-key'
? `Open your workspace, drag ${articleFor(name)} ${name} block onto the workflow builder, and paste in your ${name} API key.`
: `Open your workspace, drag ${articleFor(name)} ${name} block onto the workflow builder. No authentication is needed.`,
},
{
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 size-7 shrink-0 items-center justify-center rounded-full border border-[var(--border-1)] text-[11px] text-[var(--text-muted)]'
aria-hidden='true'
>
{step}
</span>
<div>
<h3 className='mb-1 text-[15px] text-[var(--text-primary)] tracking-[-0.02em]'>
{title}
</h3>
<p className='text-[14px] text-[var(--text-body)] leading-[150%] tracking-[0.02em]'>
{body}
</p>
</div>
</li>
))}
</ol>
</section>
<div className='h-px w-full bg-[var(--border)]' />
{/* Triggers - rows */}
{triggers.length > 0 && (
<section aria-labelledby='triggers-heading'>
<div className='px-6 pt-10 pb-4'>
<div className='mb-2 flex items-center gap-2.5'>
<span className='relative flex size-2' aria-hidden='true'>
<span className='absolute inline-flex h-full w-full animate-ping rounded-full bg-emerald-400 opacity-75' />
<span className='relative inline-flex size-2 rounded-full bg-emerald-500' />
</span>
<h2
id='triggers-heading'
className='text-[20px] text-[var(--text-primary)] leading-[100%] tracking-[-0.02em]'
>
Real-time triggers
</h2>
</div>
<p className='text-[14px] text-[var(--text-body)] leading-[150%] tracking-[0.02em]'>
{seo?.triggersIntro ?? (
<>
Connect {articleFor(name)} {name} webhook to Sim and your agent runs the instant
an event happens, no polling, no delay.
</>
)}
</p>
</div>
<div className='h-px w-full bg-[var(--border)]' />
{triggers.map((trigger) => (
<div key={trigger.id}>
<div className='flex items-start gap-4 px-6 py-4'>
<div className='flex min-w-0 flex-1 flex-col gap-0.5'>
<p className='text-[14px] text-[var(--text-primary)] leading-snug tracking-[-0.02em]'>
{trigger.name}
</p>
{trigger.description && (
<p className='text-[12px] text-[var(--text-muted)] leading-[150%]'>
{trigger.description}
</p>
)}
</div>
</div>
<div className='h-px w-full bg-[var(--border)]' />
</div>
))}
</section>
)}
{/* Workflow templates - horizontal cards */}
{matchingTemplates.length > 0 && (
<section aria-labelledby='templates-heading'>
<div className='px-6 pt-10 pb-4'>
<h2
id='templates-heading'
className='mb-2 text-[20px] text-[var(--text-primary)] leading-[100%] tracking-[-0.02em]'
>
Agent templates
</h2>
<p className='text-[14px] text-[var(--text-body)] tracking-[0.02em]'>
{seo?.templatesIntro ??
`Ready-to-use templates featuring ${name}. Click any to build it instantly.`}
</p>
</div>
<div className='h-px w-full bg-[var(--border)]' />
{(() => {
const isOdd = matchingTemplates.length % 2 === 1
const pairedTemplates = isOdd ? matchingTemplates.slice(0, -1) : matchingTemplates
const lastTemplate = isOdd ? matchingTemplates[matchingTemplates.length - 1] : null
const resolveTypes = (template: (typeof matchingTemplates)[number]) => [
integration.type,
...template.otherBlockTypes,
]
const resolveDisplayName = (bt: string): string | null => {
const resolvedBt = byType.get(bt)
? bt
: byType.get(`${bt}_v2`)
? `${bt}_v2`
: byType.get(`${bt}_v3`)
? `${bt}_v3`
: bt
return byType.get(resolvedBt)?.name ?? null
}
/**
* The curated template prompt rewritten so the integrations it
* references chip in the home input after signup. Computed
* server-side from the template's own integration set - never the
* full registry - so the visible card text stays raw while the
* stored prompt opts into mention treatment.
*/
const storedPrompt = (template: (typeof matchingTemplates)[number]) =>
mentionifyPromptForNames(
template.prompt,
resolveTypes(template)
.map(resolveDisplayName)
.filter((n): n is string => n !== null)
)
return (
<>
{/* Paired rows of 2 */}
{Array.from({ length: Math.ceil(pairedTemplates.length / 2) }, (_, rowIdx) => {
const row = pairedTemplates.slice(rowIdx * 2, rowIdx * 2 + 2)
return (
<div key={rowIdx}>
<nav
aria-label={`Template row ${rowIdx + 1}`}
className='flex flex-col sm:flex-row'
>
{row.map((template) => (
<TemplateCardButton
key={template.title}
prompt={storedPrompt(template)}
className='group flex flex-1 flex-col gap-4 border-[var(--border)] border-t p-6 transition-colors first:border-t-0 hover:bg-[var(--surface-hover)] sm:border-t-0 sm:border-l sm:first:border-l-0'
>
<div className='flex items-center gap-1.5'>
<TemplateIconRow allTypes={resolveTypes(template)} />
</div>
<div className='flex flex-col gap-2'>
<h3 className='text-[14px] text-[var(--text-primary)] leading-snug tracking-[-0.02em]'>
{template.title}
</h3>
<p className='line-clamp-2 text-[var(--text-muted)] text-sm leading-[150%]'>
{template.prompt}
</p>
</div>
</TemplateCardButton>
))}
</nav>
<div className='h-px w-full bg-[var(--border)]' />
</div>
)
})}
{/* Last template as a full-width row when odd */}
{lastTemplate && (
<>
<TemplateCardButton
prompt={storedPrompt(lastTemplate)}
className='group/link flex items-center gap-4 px-6 py-4 transition-colors hover:bg-[var(--surface-hover)]'
>
<div className='flex items-center gap-1.5'>
<TemplateIconRow allTypes={resolveTypes(lastTemplate)} />
</div>
<div className='flex min-w-0 flex-1 flex-col gap-0.5'>
<h3 className='text-[14px] text-[var(--text-primary)] leading-snug tracking-[-0.02em]'>
{lastTemplate.title}
</h3>
<p className='line-clamp-1 text-[12px] text-[var(--text-muted)] leading-[150%]'>
{lastTemplate.prompt}
</p>
</div>
</TemplateCardButton>
<div className='h-px w-full bg-[var(--border)]' />
</>
)}
</>
)
})()}
</section>
)}
{/* Supported tools - rows */}
{operations.length > 0 && (
<section aria-labelledby='tools-heading'>
<div className='px-6 pt-10 pb-4'>
<h2
id='tools-heading'
className='mb-2 text-[20px] text-[var(--text-primary)] leading-[100%] tracking-[-0.02em]'
>
Supported tools
</h2>
<p className='text-[14px] text-[var(--text-body)] tracking-[0.02em]'>
{operations.length} {name} tool{operations.length === 1 ? '' : 's'} available in Sim
{seo?.toolsSubtitleSuffix ?? ''}
</p>
</div>
<div className='h-px w-full bg-[var(--border)]' />
{operations.map((op) => (
<div key={op.name}>
<div className='flex items-start gap-4 px-6 py-4'>
<div className='flex min-w-0 flex-1 flex-col gap-0.5'>
<p className='text-[14px] text-[var(--text-primary)] leading-snug tracking-[-0.02em]'>
{op.name}
</p>
{op.description && (
<p className='text-[12px] text-[var(--text-muted)] leading-[150%]'>
{op.description}
</p>
)}
</div>
</div>
<div className='h-px w-full bg-[var(--border)]' />
</div>
))}
</section>
)}
{/* FAQ - full width */}
<section aria-labelledby='faq-heading' className='px-6 py-10'>
<h2
id='faq-heading'
className='mb-8 text-[20px] text-[var(--text-primary)] leading-[100%] tracking-[-0.02em]'
>
Frequently asked questions
</h2>
<LandingFAQ faqs={faqs} />
</section>
<div className='h-px w-full bg-[var(--border)]' />
{/* Related integrations - horizontal cards with vertical dividers (blog featured pattern) */}
{relatedIntegrations.length > 0 && (
<>
<nav aria-label='Related integrations' className='flex flex-col sm:flex-row'>
{relatedIntegrations.slice(0, 4).map((rel) => (
<Link
key={rel.slug}
href={`/integrations/${rel.slug}`}
className='group flex flex-1 flex-col gap-4 border-[var(--border)] border-t p-6 transition-colors first:border-t-0 hover:bg-[var(--surface-hover)] sm:border-t-0 sm:border-l sm:first:border-l-0'
>
<IntegrationIcon
bgColor={rel.bgColor}
name={rel.name}
Icon={blockTypeToIconMap[rel.type]}
as='span'
className='size-10 rounded-xl border border-[var(--border-1)]'
aria-hidden='true'
/>
<div className='flex flex-col gap-2'>
<h3 className='text-[var(--text-primary)] text-lg leading-tight tracking-[-0.01em]'>
{rel.name}
</h3>
<p className='line-clamp-2 text-[var(--text-muted)] text-sm leading-[150%]'>
{rel.description}
</p>
</div>
</Link>
))}
</nav>
<div className='h-px w-full bg-[var(--border)]' />
</>
)}
{/* Bottom CTA */}
<section aria-labelledby='cta-heading' className='px-6 py-16 text-center'>
<div className='mx-auto mb-6 flex items-center justify-center gap-3'>
<Image
src='/brandbook/logo/small.png'
alt='Sim'
width={56}
height={56}
className='shrink-0 rounded-xl'
unoptimized
/>
<div className='flex items-center gap-2'>
<span className='h-px w-5 bg-[var(--border-1)]' aria-hidden='true' />
<span
className='flex size-7 items-center justify-center rounded-full border border-[var(--border-1)]'
aria-hidden='true'
>
<svg
className='size-3.5 text-[var(--text-muted)]'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth={2}
strokeLinecap='round'
>
<path d='M5 12h14' />
<path d='M12 5v14' />
</svg>
</span>
<span className='h-px w-5 bg-[var(--border-1)]' aria-hidden='true' />
</div>
<IntegrationIcon
bgColor={bgColor}
name={name}
Icon={IconComponent}
className='size-14 rounded-xl'
iconClassName='size-7'
fallbackClassName='text-[22px]'
aria-hidden='true'
/>
</div>
<h2
id='cta-heading'
className='mb-3 text-[28px] text-[var(--text-primary)] leading-[100%] tracking-[-0.02em] sm:text-[34px]'
>
Start automating {name} today
</h2>
<p className='mx-auto mb-8 max-w-[480px] text-[var(--text-body)] text-base leading-[150%] tracking-[0.02em]'>
Build your first AI agent with {name} in minutes. Connect to every tool your team uses.
Free to start, no credit card required.
</p>
<IntegrationCtaButton label='Build for free'>Build for free</IntegrationCtaButton>
</section>
</div>
{/* Closing full-width divider */}
<div className='-mt-px h-px w-full bg-[var(--border)]' />
</section>
)
}
@@ -0,0 +1,10 @@
import type { ReactNode } from 'react'
/**
* Integrations route segment. The shared landing layout owns the chrome (navbar,
* footer, site-wide JSON-LD, scroll port); this layout only provides the
* `<main>` landmark. Pages emit their own page-specific JSON-LD.
*/
export default function IntegrationsLayout({ children }: { children: ReactNode }) {
return <main id='main-content'>{children}</main>
}
@@ -0,0 +1,30 @@
import integrationsJson from '@/lib/integrations/integrations.json'
import type { Integration } from '@/lib/integrations/types'
import { createLandingOgImage } from '@/app/(landing)/og-utils'
export const contentType = 'image/png'
export const size = {
width: 1200,
height: 630,
}
/** Raw catalog JSON, not the barrel - keeps `@/blocks/registry` out of the OG bundle. */
const integrations = integrationsJson.integrations as readonly Integration[]
const TOTAL_TOOL_COUNT = integrations.reduce((sum, i) => sum + i.operationCount, 0)
const OAUTH_COUNT = integrations.filter((i) => i.authType === 'oauth').length
const TRIGGER_INTEGRATION_COUNT = integrations.filter((i) => i.triggerCount > 0).length
export default async function Image() {
return createLandingOgImage({
eyebrow: 'Sim integrations directory',
title: 'Integrations',
subtitle: `Connect ${integrations.length} apps and services to AI agents in Sim's workflow builder, visually, conversationally, or with code.`,
pills: [
`${integrations.length} integrations`,
`${TOTAL_TOOL_COUNT}+ tools`,
`${OAUTH_COUNT} OAuth apps`,
`${TRIGGER_INTEGRATION_COUNT} with real-time triggers`,
],
domainLabel: 'sim.ai/integrations',
})
}
@@ -0,0 +1,253 @@
import type { Metadata } from 'next'
import type { SearchParams } from 'nuqs/server'
import { SITE_URL } from '@/lib/core/utils/urls'
import {
blockTypeToIconMap,
type FAQItem,
INTEGRATIONS,
type Integration,
POPULAR_WORKFLOWS,
} from '@/lib/integrations'
import { withFilteredNoindex } from '@/lib/landing/seo'
import { JsonLd } from '@/app/(landing)/components/json-ld'
import { LandingFAQ } from '@/app/(landing)/components/landing-faq'
import { IntegrationCard } from '@/app/(landing)/integrations/components/integration-card'
import { IntegrationGrid } from '@/app/(landing)/integrations/components/integration-grid'
import { RequestIntegrationModal } from '@/app/(landing)/integrations/components/request-integration-modal'
import { integrationsSearchParamsCache } from '@/app/(landing)/integrations/search-params'
const allIntegrations = INTEGRATIONS
const INTEGRATION_COUNT = allIntegrations.length
const OAUTH_COUNT = allIntegrations.filter((i) => i.authType === 'oauth').length
const TRIGGER_INTEGRATION_COUNT = allIntegrations.filter((i) => i.triggerCount > 0).length
const TOTAL_TOOL_COUNT = allIntegrations.reduce((sum, i) => sum + i.operationCount, 0)
/**
* Catalog-level FAQ. Questions that read the same for every integration live
* here exactly once instead of repeating across all per-integration pages.
*/
const CATALOG_FAQS: FAQItem[] = [
{
question: 'How do integrations work in Sim?',
answer: `Each integration is a block you drag onto Sim's workflow builder. Together, Sim's ${INTEGRATION_COUNT} integrations expose ${TOTAL_TOOL_COUNT}+ tools that AI agents can call. ${OAUTH_COUNT} connect with one-click OAuth, and the rest use an API key or no authentication at all. Wire blocks together, add an AI agent block for reasoning, and run.`,
},
{
question: 'Are Sim integrations free to use?',
answer: `Yes. Sim's free plan includes every integration in the library, all ${INTEGRATION_COUNT} of them, with no credit card required. Create an account at sim.ai and start building.`,
},
{
question: 'Can an AI agent decide when to use an integration?',
answer: `Yes. This is the core of Sim. You give an agent access to integration tools and describe the goal in plain language; the agent decides which tools to call, in what order, and how to handle the results. Automations adapt to context instead of breaking when inputs change.`,
},
{
question: 'Can external events trigger my agents automatically?',
answer: `Yes. ${TRIGGER_INTEGRATION_COUNT} Sim integrations include real-time webhook triggers. Add a trigger block to your agent, copy its webhook URL into the external service, and every matching event starts your agent instantly, no polling, no delay.`,
},
{
question: 'How many integrations does Sim support?',
answer: `Sim supports ${INTEGRATION_COUNT} integrations across messaging, CRMs, databases, developer tools, AI providers, and more, and the catalog grows continually. If a tool you need is missing, request it below and we'll prioritize it.`,
},
]
/**
* 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)
const baseUrl = SITE_URL
const INTEGRATIONS_BREADCRUMB_JSON_LD = {
'@context': 'https://schema.org',
'@type': 'BreadcrumbList',
itemListElement: [
{ '@type': 'ListItem', position: 1, name: 'Home', item: baseUrl },
{
'@type': 'ListItem',
position: 2,
name: 'Integrations',
item: `${baseUrl}/integrations`,
},
],
}
/** Curated featured integrations - high-recognition services shown as cards. */
const FEATURED_SLUGS = ['slack', 'notion', 'github', 'gmail'] as const
const bySlug = new Map(allIntegrations.map((i) => [i.slug, i]))
const featured = FEATURED_SLUGS.map((s) => bySlug.get(s)).filter(
(i): i is Integration => i !== undefined
)
/**
* `q`/`category` render a genuinely different server-rendered list (see
* search-params.ts), so filtered URLs are noindexed rather than
* self-canonicalized — keeps the single indexable URL as the bare catalog
* page instead of asking Google to index every filter permutation.
*/
export async function generateMetadata({
searchParams,
}: {
searchParams: Promise<SearchParams>
}): Promise<Metadata> {
const { q, category } = await integrationsSearchParamsCache.parse(searchParams)
const isFiltered = Boolean(q || category)
return withFilteredNoindex(
{
title: 'Integrations',
description: `Connect ${INTEGRATION_COUNT}+ apps and services in Sim's AI workspace. Build agents that automate real work with ${TOP_NAMES.join(', ')}, and more.`,
keywords: [
'AI workspace integrations',
'AI agent integrations',
'AI agent builder integrations',
...TOP_NAMES.flatMap((n) => [`${n} integration`, `${n} automation`]),
...allIntegrations.slice(0, 20).map((i) => `${i.name} automation`),
],
// og:image/twitter:image come from the sibling opengraph-image.tsx -
// Next serves it at a hash-suffixed URL, so hardcoding it here 404s.
openGraph: {
title: 'Integrations | Sim AI Workspace',
description: `Connect ${INTEGRATION_COUNT}+ apps in Sim's AI workspace. Build agents that link ${TOP_NAMES.join(', ')}, and every tool your team uses.`,
url: `${baseUrl}/integrations`,
type: 'website',
},
twitter: {
card: 'summary_large_image',
title: 'Integrations | Sim',
description: `Connect ${INTEGRATION_COUNT}+ apps in Sim's AI workspace.`,
},
alternates: { canonical: `${baseUrl}/integrations` },
},
isFiltered
)
}
export default async function IntegrationsPage({
searchParams,
}: {
searchParams: Promise<SearchParams>
}) {
await integrationsSearchParamsCache.parse(searchParams)
const itemListJsonLd = {
'@context': 'https://schema.org',
'@type': 'ItemList',
name: 'Sim AI Workflow Integrations',
description: `Complete list of ${INTEGRATION_COUNT}+ integrations available in Sim's AI workspace for building and deploying AI agents.`,
url: `${baseUrl}/integrations`,
numberOfItems: INTEGRATION_COUNT,
itemListElement: allIntegrations.map((integration, index) => ({
'@type': 'ListItem',
position: index + 1,
item: {
'@type': 'SoftwareApplication',
name: integration.name,
description: integration.description,
url: `${baseUrl}/integrations/${integration.slug}`,
applicationCategory: 'BusinessApplication',
featureList: integration.operations.map((o) => o.name),
},
})),
}
const faqJsonLd = {
'@context': 'https://schema.org',
'@type': 'FAQPage',
mainEntity: CATALOG_FAQS.map(({ question, answer }) => ({
'@type': 'Question',
name: question,
acceptedAnswer: { '@type': 'Answer', text: answer },
})),
}
return (
<section className='bg-[var(--bg)]'>
<JsonLd data={INTEGRATIONS_BREADCRUMB_JSON_LD} />
<JsonLd data={itemListJsonLd} />
<JsonLd data={faqJsonLd} />
{/* Hero */}
<div className='mx-auto w-full max-w-[1460px] px-20 pt-[112px] max-sm:px-5 max-sm:pt-20 max-lg:px-8'>
<div className='flex flex-col gap-4 xl:flex-row xl:items-end xl:justify-between'>
<h1
id='integrations-heading'
className='text-balance text-[28px] text-[var(--text-primary)] leading-[100%] tracking-[-0.02em] lg:text-[40px]'
>
Integrations
</h1>
<p className='text-[var(--text-muted)] text-sm leading-[150%] tracking-[0.02em] lg:text-base'>
Connect every tool your team uses. Build agents that automate real work across{' '}
{INTEGRATION_COUNT} apps and services.
</p>
</div>
</div>
{/* Full-width divider */}
<div className='mt-8 h-px w-full bg-[var(--border)]' />
{/* Border-railed content */}
<div className='mx-auto w-full max-w-[1460px]'>
<div className='mx-20 border-[var(--border)] border-x max-sm:mx-5 max-lg:mx-8'>
{/* Featured integrations - top */}
{featured.length > 0 && (
<>
<nav aria-label='Featured integrations' className='flex flex-col sm:flex-row'>
{featured.map((integration) => (
<IntegrationCard
key={integration.type}
integration={integration}
IconComponent={blockTypeToIconMap[integration.type]}
/>
))}
</nav>
<div className='h-px w-full bg-[var(--border)]' />
</>
)}
{/* All Integrations - search, filters, rows */}
<section aria-labelledby='all-integrations-heading'>
<div className='px-6 pt-10 pb-4'>
<h2
id='all-integrations-heading'
className='mb-2 text-[20px] text-[var(--text-primary)] leading-[100%] tracking-[-0.02em] lg:text-[24px]'
>
All Integrations
</h2>
</div>
<IntegrationGrid integrations={allIntegrations} />
</section>
{/* FAQ */}
<section aria-labelledby='integrations-faq-heading' className='px-6 py-10'>
<h2
id='integrations-faq-heading'
className='mb-8 text-[20px] text-[var(--text-primary)] leading-[100%] tracking-[-0.02em]'
>
Frequently asked questions
</h2>
<LandingFAQ faqs={CATALOG_FAQS} />
</section>
<div className='h-px w-full bg-[var(--border)]' />
{/* Integration request */}
<div className='flex flex-col items-start gap-3 p-6 sm:flex-row sm:items-center sm:justify-between'>
<div>
<p className='text-[15px] text-[var(--text-primary)] tracking-[-0.02em]'>
Don&apos;t see the integration you need?
</p>
<p className='mt-0.5 text-[var(--text-muted)] text-xs uppercase tracking-[0.1em]'>
Let us know and we&apos;ll prioritize it.
</p>
</div>
<RequestIntegrationModal />
</div>
</div>
</div>
{/* Closing full-width divider */}
<div className='-mt-px h-px w-full bg-[var(--border)]' />
</section>
)
}
@@ -0,0 +1,87 @@
import type { ComponentType, SVGProps } from 'react'
import { memo } from 'react'
import Link from 'next/link'
import type { Integration } from '@/lib/integrations'
import { ChevronArrow } from '@/app/(landing)/components/chevron-arrow'
import { IntegrationIcon } from '@/app/(landing)/integrations/components/integration-icon'
const HOVER_BG = 'transition-colors hover:bg-[var(--surface-hover)]' as const
interface IntegrationItemProps {
integration: Integration
IconComponent?: ComponentType<SVGProps<SVGSVGElement>>
}
/**
* Featured integration card - matches blog featured post pattern.
* Used in flex rows separated by border-l dividers.
*/
export function IntegrationCard({ integration, IconComponent }: IntegrationItemProps) {
const { slug, name, description, bgColor } = integration
return (
<Link
href={`/integrations/${slug}`}
className={`group/link flex flex-1 flex-col gap-4 border-[var(--border)] border-t p-6 first:border-t-0 sm:border-t-0 sm:border-l sm:first:border-l-0 ${HOVER_BG}`}
>
<IntegrationIcon
bgColor={bgColor}
name={name}
Icon={IconComponent}
className='size-10 rounded-xl border border-[var(--border-1)]'
aria-hidden='true'
/>
<div className='flex flex-col gap-2'>
<h3 className='text-[var(--text-primary)] text-lg leading-tight tracking-[-0.01em]'>
{name}
</h3>
<p className='line-clamp-2 text-[var(--text-muted)] text-sm leading-[150%]'>
{description}
</p>
</div>
</Link>
)
}
/**
* Integration list row - matches blog remaining post pattern.
* Each row followed by an h-px divider.
*/
export const IntegrationRow = memo(function IntegrationRow({
integration,
IconComponent,
}: IntegrationItemProps) {
const { slug, name, description, bgColor } = integration
return (
<>
<Link
href={`/integrations/${slug}`}
className={`group/link flex items-center gap-4 px-6 py-4 ${HOVER_BG}`}
aria-label={`${name} integration`}
>
<IntegrationIcon
bgColor={bgColor}
name={name}
Icon={IconComponent}
className='size-8 shrink-0 rounded-xl border border-[var(--border-1)]'
iconClassName='size-4'
fallbackClassName='text-[13px]'
aria-hidden='true'
/>
<div className='flex min-w-0 flex-1 flex-col gap-0.5'>
<h3 className='text-[14px] text-[var(--text-primary)] leading-snug tracking-[-0.02em]'>
{name}
</h3>
<p className='line-clamp-1 hidden text-[12px] text-[var(--text-muted)] leading-[150%] sm:block'>
{description}
</p>
</div>
<ChevronArrow />
</Link>
<div className='h-px w-full bg-[var(--border)]' />
</>
)
})
@@ -0,0 +1,127 @@
'use client'
import { useMemo } from 'react'
import { ChipInput, Search } from '@sim/emcn'
import { debounce, useQueryStates } from 'nuqs'
import { blockTypeToIconMap, formatIntegrationType, type Integration } from '@/lib/integrations'
import { IntegrationRow } from '@/app/(landing)/integrations/components/integration-card'
import {
integrationsParsers,
integrationsUrlKeys,
} from '@/app/(landing)/integrations/search-params'
/** Debounce window for writing the search term to the URL (filtering is instant). */
const SEARCH_DEBOUNCE_MS = 300
const PILL_BASE =
'rounded-[5px] border border-[var(--border-1)] px-[9px] py-0.5 text-small text-[var(--text-primary)] transition-colors' as const
const PILL_ACTIVE = 'bg-[var(--surface-active)]' as const
const PILL_INACTIVE = 'hover:bg-[var(--surface-hover)]' as const
interface IntegrationGridProps {
integrations: readonly Integration[]
}
export function IntegrationGrid({ integrations }: IntegrationGridProps) {
const [{ q: query, category }, setParams] = useQueryStates(
integrationsParsers,
integrationsUrlKeys
)
const activeCategory = category || null
// Category facets and a per-integration lowercased search index, derived once
// from the (stable) integration list instead of rebuilt on every keystroke.
// The index keeps each searchable field as its own entry so matching stays
// identical to a per-field `includes` (no cross-field boundary matches).
const { availableCategories, searchIndex } = useMemo(() => {
const counts = new Map<string, number>()
const searchIndex = new Map<string, string[]>()
for (const i of integrations) {
if (i.integrationType) {
counts.set(i.integrationType, (counts.get(i.integrationType) || 0) + 1)
}
searchIndex.set(i.type, [
i.name.toLowerCase(),
i.description.toLowerCase(),
...i.operations.flatMap((op) => [op.name.toLowerCase(), op.description.toLowerCase()]),
...i.triggers.map((t) => t.name.toLowerCase()),
])
}
return {
availableCategories: Array.from(counts.entries())
.sort((a, b) => b[1] - a[1])
.map(([key]) => key),
searchIndex,
}
}, [integrations])
const q = query.trim().toLowerCase()
const filtered = useMemo(
() =>
integrations.filter((i) => {
if (activeCategory && i.integrationType !== activeCategory) return false
if (!q) return true
return searchIndex.get(i.type)?.some((field) => field.includes(q)) ?? false
}),
[integrations, searchIndex, q, activeCategory]
)
return (
<div>
<div className='mb-6 flex flex-col gap-4 px-6 sm:flex-row sm:items-center'>
<div className='max-w-[480px] flex-1'>
<ChipInput
icon={Search}
type='search'
placeholder='Search integrations, tools, or triggers…'
value={query}
onChange={(e) =>
setParams({ q: e.target.value }, { limitUrlUpdates: debounce(SEARCH_DEBOUNCE_MS) })
}
aria-label='Search integrations'
/>
</div>
</div>
<div className='mb-6 flex flex-wrap gap-2 px-6'>
<button
type='button'
onClick={() => setParams({ category: '' })}
className={`${PILL_BASE} ${activeCategory === null ? PILL_ACTIVE : PILL_INACTIVE}`}
>
All
</button>
{availableCategories.map((cat) => (
<button
key={cat}
type='button'
onClick={() => setParams({ category: activeCategory === cat ? '' : cat })}
className={`${PILL_BASE} ${activeCategory === cat ? PILL_ACTIVE : PILL_INACTIVE}`}
>
{formatIntegrationType(cat)}
</button>
))}
</div>
<div className='h-px w-full bg-[var(--border)]' />
{filtered.length === 0 ? (
<p className='py-12 text-center text-[15px] text-[var(--text-muted)]'>
No integrations found
{query ? <> for &ldquo;{query}&rdquo;</> : null}
{activeCategory ? <> in {formatIntegrationType(activeCategory)}</> : null}
</p>
) : (
<div>
{filtered.map((integration) => (
<IntegrationRow
key={integration.type}
integration={integration}
IconComponent={blockTypeToIconMap[integration.type]}
/>
))}
</div>
)}
</div>
)
}
@@ -0,0 +1,51 @@
import type { ComponentType, ElementType, HTMLAttributes, SVGProps } from 'react'
import { cn } from '@sim/emcn'
import { getTileIconColorClass } from '@/blocks/icon-color'
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: `size-10 rounded-xl` */
className?: string
/** Tailwind size classes for the icon SVG. Default: `size-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 = 'size-5',
fallbackClassName = 'text-[15px]',
as: Tag = 'div',
...rest
}: IntegrationIconProps) {
return (
<Tag
className={cn('flex shrink-0 items-center justify-center', className)}
style={{ background: bgColor }}
{...rest}
>
{Icon ? (
<Icon className={cn(iconClassName, getTileIconColorClass(bgColor))} />
) : (
<span className={cn('leading-none', getTileIconColorClass(bgColor), fallbackClassName)}>
{name.charAt(0)}
</span>
)}
</Tag>
)
}
@@ -0,0 +1,155 @@
'use client'
import { useCallback, useState } from 'react'
import {
Chip,
ChipModal,
ChipModalBody,
ChipModalError,
ChipModalField,
ChipModalFooter,
ChipModalHeader,
} from '@sim/emcn'
import { requestJson } from '@/lib/api/client/request'
import { integrationRequestContract } from '@/lib/api/contracts/common'
type SubmitStatus = 'idle' | 'submitting' | 'success' | 'error'
export function RequestIntegrationModal() {
const [open, setOpen] = useState(false)
const [status, setStatus] = useState<SubmitStatus>('idle')
const [integrationName, setIntegrationName] = useState('')
const [email, setEmail] = useState('')
const [useCase, setUseCase] = useState('')
const resetForm = useCallback(() => {
setIntegrationName('')
setEmail('')
setUseCase('')
setStatus('idle')
}, [])
const handleOpenChange = useCallback(
(nextOpen: boolean) => {
setOpen(nextOpen)
if (!nextOpen) resetForm()
},
[resetForm]
)
const handleSubmit = useCallback(async () => {
if (!integrationName.trim() || !email.trim()) return
setStatus('submitting')
try {
await requestJson(integrationRequestContract, {
body: {
integrationName: integrationName.trim(),
email: email.trim(),
useCase: useCase.trim() || undefined,
},
})
setStatus('success')
setTimeout(() => setOpen(false), 1500)
} catch {
setStatus('error')
}
}, [integrationName, email, useCase])
const canSubmit = integrationName.trim() && email.trim() && status === 'idle'
return (
<>
<Chip className='border border-[var(--border-1)]' onClick={() => setOpen(true)}>
Request an integration
</Chip>
<ChipModal open={open} onOpenChange={handleOpenChange} srTitle='Request an Integration'>
<ChipModalHeader onClose={() => handleOpenChange(false)}>
Request an Integration
</ChipModalHeader>
<ChipModalBody>
{status === 'success' ? (
<div className='flex flex-col items-center gap-3 py-6 text-center'>
<div className='flex size-10 items-center justify-center rounded-full bg-[var(--brand-accent)]/10'>
<svg
className='size-5 text-[var(--brand-accent)]'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth={2}
strokeLinecap='round'
strokeLinejoin='round'
>
<polyline points='20 6 9 17 4 12' />
</svg>
</div>
<p className='text-[14px] text-[var(--text-primary)]'>
Request submitted. We&apos;ll follow up at <span>{email}</span>.
</p>
</div>
) : (
<>
<ChipModalField
type='input'
title='Integration name'
value={integrationName}
onChange={(value) => setIntegrationName(value)}
placeholder='e.g. Stripe, HubSpot, Snowflake'
maxLength={200}
autoComplete='off'
required
/>
<ChipModalField
type='email'
title='Your email'
value={email}
onChange={(value) => setEmail(value)}
placeholder='you@company.com'
autoComplete='email'
required
/>
<ChipModalField
type='textarea'
title={
<>
Use case <span className='text-[var(--text-subtle)]'>(optional)</span>
</>
}
value={useCase}
onChange={(value) => setUseCase(value)}
placeholder='What would you automate with this integration?'
rows={3}
maxLength={2000}
/>
{status === 'error' && (
<ChipModalError>Something went wrong. Please try again.</ChipModalError>
)}
</>
)}
</ChipModalBody>
{status === 'success' ? (
<ChipModalFooter
onCancel={() => handleOpenChange(false)}
primaryAction={{ label: 'Done', onClick: () => handleOpenChange(false) }}
/>
) : (
<ChipModalFooter
onCancel={() => setOpen(false)}
cancelDisabled={status === 'submitting'}
primaryAction={{
label: status === 'submitting' ? 'Submitting...' : 'Submit request',
onClick: handleSubmit,
disabled: !canSubmit && status !== 'error',
}}
/>
)}
</ChipModal>
</>
)
}
@@ -0,0 +1,43 @@
/**
* Hand-authored, integration-specific landing content, keyed by integration
* slug. This is a pure-data generation input: `scripts/generate-docs.ts` reads
* it and bakes the matching entry into `integrations.json`, so the landing page
* consumes a single source (`integration.landingContent`) with no render-time
* augmentation. Has no app imports so the build script can import it safely.
*/
import type { IntegrationLandingContent } from '@/app/(landing)/integrations/data/types'
export const INTEGRATION_LANDING_CONTENT: Record<string, IntegrationLandingContent> = {
slack: {
install: {
heading: 'Add Sim to your Slack workspace',
intro:
'Sim connects to Slack through Slacks official OAuth flow. The “Add to Slack” button lives inside your Sim account (after sign-in). Connect from there and the Sim bot is installed in your Slack workspace. The steps below show exactly how to reach it.',
steps: [
{
title: 'Create your free Sim account',
body: 'Sign up at sim.ai. No credit card required.',
},
{
title: 'Add a Slack block',
body: 'Open a workflow, drag in a Slack block, and open its credential dropdown.',
},
{
title: 'Connect Slack',
body: 'Click Connect Slack, choose your workspace, and approve the requested permissions. This installs the Sim bot in your Slack workspace.',
},
{
title: 'Invite the bot and build',
body: 'Invite the Sim bot to the channels it should act in, pick a Slack action, wire it into your agent, and run.',
},
],
},
privacy: {
body: 'Sim requests only the Slack permissions its actions and triggers need, and never shows private channel names or messages to people who are not members of those channels in Slack.',
href: '/privacy',
},
aiDisclaimer:
'Sim agents use AI models to generate messages and responses sent to Slack. AI-generated content can be inaccurate or incomplete, so review automated outputs before relying on them, especially for important communications.',
},
}
@@ -0,0 +1,257 @@
/**
* Per-integration SEO/GEO overrides keyed by integration slug, authored from the
* SEO team's on-site recommendations. Consumed at render time by the integration
* page (`integrations/(shell)/[slug]/page.tsx`): both `generateMetadata` (title,
* description, keywords) and the page body (H1, hero tagline, overview prose, and
* the triggers/templates/tools section intros).
*
* This is NOT baked into `integrations.json` and never touches
* `scripts/generate-docs.ts`; it augments the generated catalog purely at render
* time. Any slug absent here renders the generated defaults unchanged. Has no app
* runtime imports beyond the type, so it stays a cheap, reviewable data source.
*/
import type { IntegrationSeoContent } from '@/app/(landing)/integrations/data/types'
export const INTEGRATION_SEO: Record<string, IntegrationSeoContent> = {
github: {
title: 'GitHub Workflow Automation | Sim',
description:
'Build GitHub automation in Sim. Use GitHub workflow automation for pull requests, pushes, issues, and releases with AI agents.',
keywords: ['github automation', 'github workflow automation', 'github integration'],
h1: 'GitHub Workflow Automation and Integration',
tagline: 'Build GitHub automation and trigger AI workflows from GitHub events.',
overview:
'Use Sims GitHub integration to run GitHub automation inside one AI workspace. Get pull request details, post pull request and issue comments, fetch repository and commit data, and trigger workflows from pull requests, comments, pushes, releases, and GitHub Actions events. Build GitHub workflow automation for code reviews, changelog generation, engineering digests, release operations, and documentation updates.',
triggersIntro:
'Connect a GitHub webhook to Sim and your agent runs the instant an event happens, no polling, no delay.',
},
outlook: {
title: 'Outlook Automation with Sim',
description:
'Build Outlook automation in Sim. Send, draft, route, and manage emails with AI agents using Outlook integration.',
keywords: ['outlook automation', 'outlook integration'],
h1: 'Outlook Automation and Integration',
tagline: 'Build Outlook automation for inboxes, drafts, replies, and email workflows.',
overview:
'Integrate Outlook into your workflow and run Outlook automation inside Sim. Send, read, draft, forward, and move email messages, or trigger workflows when new emails arrive. This Outlook integration helps teams automate inbox operations, email routing, follow-ups, and agent-driven communication from one AI workspace.',
toolsSubtitleSuffix:
'. Use Sim with Outlook to power Outlook automation for sending, reading, drafting, forwarding, organizing, and triggering email workflows',
},
box: {
title: 'Box Workflow Automation | Box Automation with Sim',
description:
'Build Box workflow automation in Sim. Run Box automation for files, folders, search, and Box Sign with AI agents. Free to start.',
keywords: ['box workflow automation', 'box automation', 'box integration'],
h1: 'Box Integrations for Workflow Automation',
tagline: 'Build Box workflow automation for files, folders, and e-signatures.',
overview:
'Use Sims workflow builder to run Box automation in one AI workspace. This Box integration lets you upload and download files, search content, create folders, send documents for e-signature, track signing status, and connect Box to the rest of your stack with AI agents.',
},
slack: {
title: 'Slack Workflow Automation with Sim',
description:
'Build Slack workflow automation in Sim. Send messages, manage channels, and trigger agents with real-time Slack integration.',
keywords: [
'slack workflow builder',
'slack automation',
'slack workflow automation',
'slack integration',
],
h1: 'Slack Integrations for Workflow Automation',
tagline:
'Build Slack workflow automation in Sim. Send, update, delete, and read messages; manage channels, users, canvases, and modals; and trigger AI agents from mentions, messages, and reactions in real time.',
overview:
'Use Sim as your Slack integration for team communication and operations. Build Slack automation that routes requests, posts alerts, summarises threads, updates tickets, and keeps work moving. Sim supports messages, reactions, canvases, views, channel and user lookups, file downloads, and real-time Slack workflows in one workspace.',
triggersIntro:
'Connect the Slack Webhook trigger to Sim and run Slack workflow automation the moment a mention, message, or reaction happens, no polling, no delay.',
templatesIntro:
'Ready-to-use Slack automation templates for Q&A bots, sales alerts, incident response, standups, digests, and CRM updates. Click any template to launch a workflow faster.',
},
airtable: {
title: 'Airtable Automation with Sim',
description:
'Build Airtable workflow automation in Sim. Sync records and run an Airtable integration with AI agents.',
keywords: ['airtable automation', 'airtable workflow automation', 'airtable ai integration'],
h1: 'Airtable Workflow Automation and AI Integration',
tagline: 'Build Airtable automation for records, schema, syncs, and workflow triggers.',
overview:
'Use this Airtable integration to run Airtable workflow automation in Sim. List bases and tables, inspect schema, read records, create new rows, update one or many records, and launch workflows when Airtable data changes.',
triggersIntro:
'Connect Airtable webhooks to Sim and launch Airtable workflow automation when records are created, updated, or deleted. Pass changed fields into AI agents for enrichment, routing, validation, or notifications.',
templatesIntro:
'Start from Airtable automation templates for data sync, enrichment, reporting, and record-driven workflows.',
},
pipedrive: {
title: 'Pipedrive Workflow Automation with Sim',
description:
'Build Pipedrive workflow automation in Sim. Run Pipedrive automation for deals, leads, activities, and pipelines.',
keywords: [
'pipedrive workflow automation',
'pipedrive automation',
'pipedrive integration',
'pipedrive crm integration',
],
h1: 'Pipedrive Workflow Automation',
tagline:
'Build Pipedrive CRM workflow automation for deals, leads, pipelines, activities, files, and mail threads in Sims workspace.',
overview:
'Use Sim for Pipedrive workflow automation across deals, contacts, leads, sales pipeline stages, projects, activities, files, and communications with powerful CRM capabilities. This Pipedrive integration helps sales teams route leads, update CRM records, trigger follow-ups, and keep pipeline data in sync from one AI workspace.',
toolsSubtitleSuffix:
'. Use Sim with Pipedrive for deals, leads, activities, projects, pipelines, files, and mail threads, from creating deals and leads to updating activities, retrieving files, and tracking pipeline progress',
},
confluence: {
title: 'Confluence Workflow Automation with Sim',
description:
'Build Confluence workflow automation in Sim. Read, update and trigger pages, comments, spaces and knowledge flows.',
keywords: ['confluence automation', 'confluence workflow automation'],
h1: 'Confluence Workflow Automation',
tagline:
'Build Confluence automation for pages, comments, attachments, labels and knowledge workflows with AI agents.',
overview:
'Integrate Confluence into the workflow with Sim and run Confluence automation across your knowledge base. Read, create, update and delete pages, manage comments, attachments, labels and spaces, search content, and trigger downstream actions the moment Confluence changes.',
triggersIntro:
'Connect a Confluence webhook to Sim to start workflows and your agent runs the instant an event happens, no polling, no delay.',
templatesIntro:
'Ready-to-use templates for Confluence knowledge workflows. Click any template to build it instantly.',
toolsSubtitleSuffix:
' for Confluence automation across pages, blog posts, comments, attachments, labels, spaces, tasks and users',
},
jira: {
title: 'Jira Automation with Sim',
description:
'Build Jira automation in Sim. Create, update, assign, and transition issues with AI agents and real-time triggers.',
keywords: ['jira automation', 'jira integration'],
h1: 'Jira Automation with Sim',
tagline:
'Build Jira automation for issues, comments, worklogs, and status changes with AI agents and real-time triggers.',
overview:
'Integrate Jira into Sims workflow and run Jira automation to create, update, assign, and transition issues, search with JQL, manage comments and attachments, and trigger workflows from Jira webhook events. Sim and Jira integration helps teams automate project updates, sprint reporting, triage, and issue routing in one AI workspace.',
triggersIntro:
'Connect Jira webhooks to Sim and your agent runs the instant an event, comments, worklogs, sprints, projects, or releases change, no polling, no delay.',
},
salesforce: {
title: 'Salesforce CRM Automation with Sim',
description:
'Automate Salesforce CRM with AI agents in Sim. Streamline workflows, sync data, and trigger actions automatically.',
keywords: ['salesforce automation', 'salesforce workflow automation', 'salesforce integration'],
h1: 'Salesforce CRM Automation',
tagline: 'Interact with Salesforce CRM or trigger workflows from Salesforce events.',
overview:
'Integrate Salesforce CRM into your workflow. Automate your CRM by managing accounts, contacts, leads, opportunities, cases, and tasks, all with powerful workflow automation. Use Sims AI agents to keep your Salesforce data in sync, automate follow-ups, and eliminate manual CRM work.',
triggersIntro:
'Connect a Salesforce webhook to Sim and your agent runs the instant an event happens, no polling, no delay. For example, trigger a workflow when a new Salesforce record is created or an opportunity stage changes, and let Sim handle the automation immediately.',
},
hubspot: {
title: 'HubSpot CRM Automation with Sim',
description:
'Build HubSpot automation workflows in Sim. Automate CRM updates, triggers, and follow-ups with AI agents. Free to start.',
keywords: [
'hubspot automation',
'hubspot workflow',
'hubspot automation workflows',
'hubspot integration',
],
h1: 'HubSpot CRM Automation',
tagline: 'Build HubSpot automation workflows with AI agents in Sim.',
overview:
'Integrate HubSpot into your workflow and run HubSpot automation inside Sim. Create, update, and manage CRM records, or trigger agents and workflows from HubSpot events. This HubSpot integration helps teams automate follow-ups, sync CRM data, and build faster HubSpot automation workflows from one AI workspace.',
triggersIntro:
'Connect a HubSpot webhook to Sim and trigger workflows the moment CRM activity happens. Run agents when records are created or updated, then automate routing, enrichment, follow-ups, and downstream actions without manual work.',
},
notion: {
title: 'Notion Automation with Sim',
description:
'Build Notion automation workflows in Sim. Create, update, and manage Notion pages with AI agents.',
keywords: ['notion automation', 'notion workflow automation', 'notion integration'],
h1: 'Notion Automation with Sim',
tagline: 'Build Notion workflow automation with AI agents in Sim.',
overview:
'Integrate Notion into your workflow and run Notion automation inside Sim. Create, update, and manage Notion pages with AI agents, or connect Notion to larger workflows across your stack. This Notion integration helps teams organize knowledge, automate page updates, and build faster Notion workflow automation from one AI workspace.',
},
supabase: {
title: 'Supabase Automation with Sim',
description:
'Build Supabase automation in Sim. Connect your database to AI workflows and automate Supabase actions. Free to start.',
keywords: ['supabase automation', 'supabase integration'],
h1: 'Supabase Automation',
tagline: 'Build Supabase automation with AI agents in Sim.',
overview:
'Integrate Supabase into your workflow and run Supabase automation inside Sim. Connect your database to AI agents, trigger workflows from app activity, and automate data operations across your stack. This Supabase integration helps teams move faster by connecting backend data, workflows, and AI in one workspace.',
},
linkedin: {
title: 'LinkedIn Automation with Sim',
description:
'Build LinkedIn automation workflows in Sim. Share posts, manage your presence, and connect LinkedIn to AI agents. Free to start.',
keywords: [
'linkedin automation',
'linkedin workflow',
'linkedin workflow automation',
'linkedin integration',
],
h1: 'LinkedIn Automation',
tagline: 'Build LinkedIn workflow automation with AI agents in Sim.',
overview:
'Integrate LinkedIn into your workflows and run LinkedIn automation inside Sim. Share posts to your personal feed, access LinkedIn profile information, and connect LinkedIn to the rest of your AI workspace. This LinkedIn integration helps teams manage their LinkedIn presence, publish content faster, and build repeatable LinkedIn workflows without manual posting.',
},
attio: {
title: 'Attio Automation with Sim',
description:
'Build Attio automation in Sim. Manage CRM records, notes, tasks, lists, comments, and webhooks with AI agents. Free to start.',
keywords: ['attio automation', 'attio integration'],
h1: 'Attio Automation',
tagline:
'Build Attio automation with AI agents in Sim. Manage records, notes, tasks, lists, comments, and more in Attio CRM.',
overview:
'Connect Attio to Sim and run Attio automation across your CRM workflows. Manage records, notes, tasks, lists, list entries, comments, workspace members, and webhooks from one AI workspace. This Attio integration helps teams automate CRM updates, trigger agents from Attio events, and connect customer data to the rest of their go-to-market stack.',
},
lemlist: {
title: 'Lemlist Automation with Sim',
description:
'Build Lemlist automation in Sim. Manage outreach activities, leads, replies, and emails with AI agents. Free to start.',
keywords: ['lemlist automation', 'lemlist integration'],
h1: 'Lemlist Automation',
tagline:
'Build Lemlist automation with AI agents in Sim. Manage outreach activities, leads, replies, and emails through Lemlist.',
overview:
'Integrate Lemlist into your workflow and run Lemlist automation inside Sim. Retrieve campaign activities and replies, get lead information, and send emails through the Lemlist inbox. This Lemlist integration helps teams automate outreach workflows, track lead engagement, respond to campaign activity, and connect Lemlist to the rest of their sales and marketing stack.',
},
linear: {
title: 'Linear Automation with Sim',
description:
'Build Linear automation in Sim. Manage issues, projects, cycles, comments, and product workflows with AI agents. Free to start.',
keywords: ['linear automation', 'linear workflow'],
h1: 'Linear Automation',
tagline:
'Build Linear automation with AI agents in Sim. Manage issues, projects, cycles, comments, and product workflows.',
overview:
'Integrate Linear into your workflow and run Linear automation inside Sim. Manage issues, comments, projects, labels, workflow states, cycles, attachments, customers, and more from one AI workspace. This Linear workflow setup helps product and engineering teams automate bug triage, issue creation, project updates, sprint reporting, and cross-tool workflows triggered by Linear events.',
},
apollo: {
title: 'Apollo CRM Automation with Sim',
description:
'Build Apollo CRM automation in Sim. Search, enrich, manage contacts, and run Apollo.io workflows with AI agents. Free to start.',
keywords: [
'apollo crm integration',
'apollo crm automation',
'apollo io workflow',
'apollo io automation',
],
h1: 'Apollo CRM Automation',
tagline:
'Build Apollo.io automation with AI agents in Sim. Search, enrich, and manage contacts, accounts, opportunities, and sales workflows.',
overview:
'Integrate Apollo into your workflow and run Apollo CRM automation inside Sim. Search for people and companies, enrich contact and account data, manage CRM contacts and accounts, add contacts to sequences, create tasks, and update opportunities from one AI workspace. This Apollo CRM integration helps sales and growth teams automate prospecting, lead enrichment, contact management, and outbound workflows without manual CRM work.',
},
datadog: {
title: 'Datadog Automation with Sim',
description:
'Build Datadog workflow automation in Sim. Monitor apps, logs, metrics, incidents, and alerts with AI agents. Free to start.',
keywords: ['datadog workflow automation', 'datadog automation'],
h1: 'Datadog Automation',
tagline:
'Build Datadog workflow automation in Sim. Monitor apps, logs, metrics, incidents, and alerts with AI agents.',
overview:
'Integrate Datadog into your workflow and run Datadog automation inside Sim. Monitor infrastructure, applications, logs, metrics, incidents, dashboards, and alerts from one AI workspace. This Datadog integration helps engineering and operations teams automate observability workflows, investigate issues faster, route incidents, summarize alerts, and connect monitoring data to the rest of their stack.',
},
}
@@ -0,0 +1,66 @@
// Shared types for the integrations section of the landing site.
// Mirrors the shape written by scripts/generate-docs.ts → writeIntegrationsJson().
export interface IntegrationInstallStep {
title: string
body: string
}
export interface IntegrationLandingContent {
/**
* Install walkthrough for OAuth apps whose connection lives behind sign-in.
* Provides the "Add to {app}" instructions that app marketplaces require
* when the install button sits behind a login.
*/
install?: {
heading: string
intro: string
steps: IntegrationInstallStep[]
}
/** Short data-handling summary shown next to a privacy-policy link. */
privacy?: {
body: string
href: string
}
/**
* Disclaimer about AI-generated content, required by some marketplaces for
* apps with an AI component (e.g. Slack's AI-components guideline).
*/
aiDisclaimer?: string
}
/**
* Hand-authored, per-integration SEO/GEO overrides keyed by slug. Unlike
* {@link IntegrationLandingContent}, this is consumed at render time directly by
* the integration page (`integrations/(shell)/[slug]/page.tsx`) and is NOT baked
* into `integrations.json` - it never touches the build/generation pipeline.
*
* Every field is optional: when absent, the page falls back to its generated
* default (the bare integration name for the H1, the auto-built title/meta, the
* block's `longDescription` for the overview, etc.). Provide only the fields a
* given integration needs to tune.
*/
export interface IntegrationSeoContent {
/** Absolute `<title>` rendered verbatim (e.g. `GitHub Workflow Automation | Sim`). */
title?: string
/** Meta description (≤160 chars), overriding the generated one. */
description?: string
/** Focus keywords, replacing the generated keyword list when present. */
keywords?: string[]
/** Visible `<h1>` text, overriding the bare integration name. */
h1?: string
/** Short keyword-rich tagline under the H1, overriding the default short description. */
tagline?: string
/** Overview-section body prose, overriding the generated `longDescription`. */
overview?: string
/** Real-time-triggers intro paragraph, overriding the generated default. */
triggersIntro?: string
/** Agent-templates intro paragraph, overriding the generated default. */
templatesIntro?: string
/**
* Text appended to the `"{n} {name} tool(s) available in Sim"` subtitle (e.g.
* `" for Confluence automation across pages, blog posts, …"`). Keeps the tool
* count dynamic while letting authors extend the line with keyword context.
*/
toolsSubtitleSuffix?: string
}
@@ -0,0 +1,26 @@
import { ChipLink } from '@sim/emcn'
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: 'Page Not Found',
robots: { index: false, follow: true },
}
export default function IntegrationsNotFound() {
return (
<main
id='main-content'
className='mx-auto flex min-h-[60vh] w-full max-w-[1460px] flex-col items-center justify-center gap-3 px-20 py-24 text-center max-sm:px-5 max-lg:px-8'
>
<h1 className='text-balance text-[40px] text-[var(--text-primary)] leading-[110%] tracking-[-0.02em]'>
Integration not found
</h1>
<p className='text-[var(--text-muted)] text-lg'>
The integration you&apos;re looking for doesn&apos;t exist or has been moved.
</p>
<ChipLink variant='primary' href='/integrations' className='mt-3'>
Browse integrations
</ChipLink>
</main>
)
}
@@ -0,0 +1,30 @@
import { createSearchParamsCache, parseAsString } from 'nuqs/server'
/**
* Co-located, typed URL query params for the integrations catalog. Shared by the
* client grid (`useQueryStates`) and the server page (`integrationsSearchParamsCache`)
* so the filtered view is server-rendered for shareable, crawlable `?category=`/`?q=`
* URLs — the same SSR pattern the blog index uses.
*
* - `q` is the search filter; its URL write is debounced on the setter, never
* written per keystroke.
* - `category` filters by integration type (`''` = all).
*/
export const integrationsParsers = {
q: parseAsString.withDefault(''),
category: parseAsString.withDefault(''),
}
/** Filter/search view-state: clean URLs, no back-stack churn. */
export const integrationsUrlKeys = {
history: 'replace',
clearOnDefault: true,
} as const
/**
* Parsing this in the page's server component opts the route into dynamic
* rendering, which populates `useSearchParams` during the server render — so the
* client grid's `useQueryStates` filters server-side and the initial HTML ships
* the filtered catalog (crawlable, shareable), with no post-hydration swap.
*/
export const integrationsSearchParamsCache = createSearchParamsCache(integrationsParsers)