feat(studio): added 'studio' blog (#1927)

* feat(blog): fundraise blog

* added scaffolding and authors for everyone's articles

* rename blog to studio

* add blog post for multiplayer

* add profile pic

* Executor blog

* mark emcn blog as draft

* Fix x

* v0.4.26

* fix(already-cancelled-sub): UI should allow restoring subscription

* restore functionality fixed

* fix

Co-authored-by: Waleed <walif6@gmail.com>
Co-authored-by: Vikhyath Mondreti <vikhyathvikku@gmail.com>

* fix(conflict): resolve merge conflict

* fix(already-cancelled-sub): UI should allow restoring subscription

* restore functionality fixed

* fix

Co-authored-by: Vikhyath Mondreti <vikhyathvikku@gmail.com>
Co-authored-by: Siddharth Ganesan <33737564+Sg312@users.noreply.github.com>

* fix(billing): should allow restoring subscription (#1728) (#1925)

* fix(already-cancelled-sub): UI should allow restoring subscription

* restore functionality fixed

* fix

Co-authored-by: Vikhyath Mondreti <vikhyathvikku@gmail.com>
Co-authored-by: Siddharth Ganesan <33737564+Sg312@users.noreply.github.com>

* cleanup blog

---------

Co-authored-by: waleed <waleed>
Co-authored-by: Vikhyath Mondreti <vikhyath@simstudio.ai>
Co-authored-by: Siddharth Ganesan <siddharthganesan@gmail.com>
Co-authored-by: Siddharth Ganesan <33737564+Sg312@users.noreply.github.com>
Co-authored-by: Vikhyath Mondreti <vikhyathvikku@gmail.com>
This commit is contained in:
Waleed
2025-11-12 04:03:50 -08:00
committed by GitHub
parent 64c5f2c473
commit a0c4bce56e
55 changed files with 1290 additions and 587 deletions

View File

@@ -1,223 +0,0 @@
import Image from 'next/image'
import Link from 'next/link'
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import { getAllPostMeta } from '@/lib/blog/registry'
import { soehne } from '@/app/fonts/soehne/soehne'
export const revalidate = 3600
export default async function BlogIndex({
searchParams,
}: {
searchParams: Promise<{ page?: string; tag?: string }>
}) {
const { page, tag } = await searchParams
const pageNum = Math.max(1, Number(page || 1))
const perPage = 20
const all = await getAllPostMeta()
const filtered = tag ? all.filter((p) => p.tags.includes(tag)) : all
const totalPages = Math.max(1, Math.ceil(filtered.length / perPage))
const start = (pageNum - 1) * perPage
const posts = filtered.slice(start, start + perPage)
// Tag filter chips are intentionally disabled for now.
// const tags = await getAllTags()
const blogJsonLd = {
'@context': 'https://schema.org',
'@type': 'Blog',
name: 'Sim Blog',
url: 'https://sim.ai/blog',
description: 'Announcements, insights, and guides for building AI agent workflows.',
}
const [featured, ...rest] = posts
return (
<main className={`${soehne.className} mx-auto max-w-[1200px] px-6 py-12 sm:px-8 md:px-12`}>
<script
type='application/ld+json'
dangerouslySetInnerHTML={{ __html: JSON.stringify(blogJsonLd) }}
/>
<h1 className='mb-3 font-medium text-[40px] leading-tight sm:text-[56px]'>The Sim Times</h1>
<p className='mb-10 text-[18px] text-gray-700'>
Announcements, insights, and guides for building AI agent workflows.
</p>
{/* Tag filter chips hidden until we have more posts */}
{/* <div className='mb-10 flex flex-wrap gap-3'>
<Link href='/blog' className={`rounded-full border px-3 py-1 text-sm ${!tag ? 'border-black bg-black text-white' : 'border-gray-300'}`}>All</Link>
{tags.map((t) => (
<Link key={t.tag} href={`/blog?tag=${encodeURIComponent(t.tag)}`} className={`rounded-full border px-3 py-1 text-sm ${tag === t.tag ? 'border-black bg-black text-white' : 'border-gray-300'}`}>
{t.tag} ({t.count})
</Link>
))}
</div> */}
{featured && (
<Link href={`/blog/${featured.slug}`} className='group mb-10 block'>
<div className='overflow-hidden rounded-2xl border border-gray-200'>
<Image
src={featured.ogImage}
alt={featured.title}
width={1200}
height={630}
className='h-[320px] w-full object-cover sm:h-[420px]'
/>
<div className='p-6 sm:p-8'>
<div className='mb-2 text-gray-600 text-xs sm:text-sm'>
{new Date(featured.date).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
})}
</div>
<h2 className='shine-text mb-2 font-medium text-2xl leading-tight sm:text-3xl'>
{featured.title}
</h2>
<p className='mb-4 text-gray-700 sm:text-base'>{featured.description}</p>
<div className='flex items-center gap-2'>
<div className='-space-x-2 flex'>
{(featured.authors && featured.authors.length > 0
? featured.authors
: [featured.author]
)
.slice(0, 3)
.map((author, idx) => (
<Avatar key={idx} className='size-5 border-2 border-white'>
<AvatarImage src={author?.avatarUrl} alt={author?.name} />
<AvatarFallback className='border-2 border-white bg-gray-100 text-gray-600 text-xs'>
{author?.name.slice(0, 2)}
</AvatarFallback>
</Avatar>
))}
</div>
<span className='text-gray-600 text-xs sm:text-sm'>
{(featured.authors && featured.authors.length > 0
? featured.authors
: [featured.author]
)
.slice(0, 2)
.map((a, i) => a?.name)
.join(', ')}
{(featured.authors && featured.authors.length > 0
? featured.authors
: [featured.author]
).length > 2 && (
<>
{' '}
and{' '}
{(featured.authors && featured.authors.length > 0
? featured.authors
: [featured.author]
).length - 2}{' '}
other
{(featured.authors && featured.authors.length > 0
? featured.authors
: [featured.author]
).length -
2 >
1
? 's'
: ''}
</>
)}
</span>
</div>
</div>
</div>
</Link>
)}
{/* Masonry-like list using CSS columns for varied heights */}
<div className='gap-6 [column-fill:_balance] md:columns-2 lg:columns-3'>
{rest.map((p, i) => {
const size = i % 3 === 0 ? 'h-64' : i % 3 === 1 ? 'h-56' : 'h-48'
return (
<Link
key={p.slug}
href={`/blog/${p.slug}`}
className='group mb-6 inline-block w-full break-inside-avoid'
>
<div className='overflow-hidden rounded-xl border border-gray-200 transition-colors duration-300 hover:border-gray-300'>
<Image
src={p.ogImage}
alt={p.title}
width={800}
height={450}
className={`${size} w-full object-cover`}
/>
<div className='p-4'>
<div className='mb-2 text-gray-600 text-xs'>
{new Date(p.date).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
})}
</div>
<h3 className='shine-text mb-1 font-medium text-lg leading-tight'>{p.title}</h3>
<p className='mb-3 line-clamp-3 text-gray-700 text-sm'>{p.description}</p>
<div className='flex items-center gap-2'>
<div className='-space-x-1.5 flex'>
{(p.authors && p.authors.length > 0 ? p.authors : [p.author])
.slice(0, 3)
.map((author, idx) => (
<Avatar key={idx} className='size-4 border border-white'>
<AvatarImage src={author?.avatarUrl} alt={author?.name} />
<AvatarFallback className='border border-white bg-gray-100 text-[10px] text-gray-600'>
{author?.name.slice(0, 2)}
</AvatarFallback>
</Avatar>
))}
</div>
<span className='text-gray-600 text-xs'>
{(p.authors && p.authors.length > 0 ? p.authors : [p.author])
.slice(0, 2)
.map((a) => a?.name)
.join(', ')}
{(p.authors && p.authors.length > 0 ? p.authors : [p.author]).length > 2 && (
<>
{' '}
and{' '}
{(p.authors && p.authors.length > 0 ? p.authors : [p.author]).length - 2}{' '}
other
{(p.authors && p.authors.length > 0 ? p.authors : [p.author]).length - 2 >
1
? 's'
: ''}
</>
)}
</span>
</div>
</div>
</div>
</Link>
)
})}
</div>
{totalPages > 1 && (
<div className='mt-10 flex items-center justify-center gap-3'>
{pageNum > 1 && (
<Link
href={`/blog?page=${pageNum - 1}${tag ? `&tag=${encodeURIComponent(tag)}` : ''}`}
className='rounded border px-3 py-1 text-sm'
>
Previous
</Link>
)}
<span className='text-gray-600 text-sm'>
Page {pageNum} of {totalPages}
</span>
{pageNum < totalPages && (
<Link
href={`/blog?page=${pageNum + 1}${tag ? `&tag=${encodeURIComponent(tag)}` : ''}`}
className='rounded border px-3 py-1 text-sm'
>
Next
</Link>
)}
</div>
)}
</main>
)
}

View File

@@ -14,9 +14,11 @@ import {
} from '@/components/ui/select'
import { Textarea } from '@/components/ui/textarea'
import { quickValidateEmail } from '@/lib/email/validation'
import { isHosted } from '@/lib/environment'
import { createLogger } from '@/lib/logs/console/logger'
import { cn } from '@/lib/utils'
import { LegalLayout } from '@/app/(landing)/components'
import Footer from '@/app/(landing)/components/footer/footer'
import Nav from '@/app/(landing)/components/nav/nav'
import { soehne } from '@/app/fonts/soehne/soehne'
const logger = createLogger('CareersPage')
@@ -199,329 +201,340 @@ export default function CareersPage() {
}
return (
<LegalLayout title='Join Our Team'>
<div className={`${soehne.className} mx-auto max-w-2xl`}>
{/* Form Section */}
<section className='rounded-2xl border border-gray-200 bg-white p-6 shadow-sm sm:p-10'>
<h2 className='mb-2 font-medium text-2xl sm:text-3xl'>Apply Now</h2>
<p className='mb-8 text-gray-600 text-sm sm:text-base'>
Help us build the future of AI workflows
</p>
<main className={`${soehne.className} min-h-screen bg-white text-gray-900`}>
<Nav variant='landing' />
<form onSubmit={onSubmit} className='space-y-5'>
{/* Name and Email */}
<div className='grid grid-cols-1 gap-4 sm:gap-6 md:grid-cols-2'>
<div className='space-y-2'>
<Label htmlFor='name' className='font-medium text-sm'>
Full Name *
</Label>
<Input
id='name'
placeholder='John Doe'
value={name}
onChange={(e) => setName(e.target.value)}
className={cn(
showErrors &&
nameErrors.length > 0 &&
'border-red-500 focus:border-red-500 focus:ring-red-100 focus-visible:ring-red-500'
)}
/>
{showErrors && nameErrors.length > 0 && (
<div className='mt-1 space-y-1 text-red-400 text-xs'>
{nameErrors.map((error, index) => (
<p key={index}>{error}</p>
))}
</div>
)}
</div>
{/* Content */}
<div className='px-4 pt-[60px] pb-[80px] sm:px-8 md:px-[44px]'>
<h1 className='mb-10 text-center font-bold text-4xl text-gray-900 md:text-5xl'>
Join Our Team
</h1>
<div className='space-y-2'>
<Label htmlFor='email' className='font-medium text-sm'>
Email *
</Label>
<Input
id='email'
type='email'
placeholder='john@example.com'
value={email}
onChange={(e) => setEmail(e.target.value)}
className={cn(
showErrors &&
emailErrors.length > 0 &&
'border-red-500 focus:border-red-500 focus:ring-red-100 focus-visible:ring-red-500'
)}
/>
{showErrors && emailErrors.length > 0 && (
<div className='mt-1 space-y-1 text-red-400 text-xs'>
{emailErrors.map((error, index) => (
<p key={index}>{error}</p>
))}
</div>
)}
</div>
</div>
{/* Phone and Position */}
<div className='grid grid-cols-1 gap-4 sm:gap-6 md:grid-cols-2'>
<div className='space-y-2'>
<Label htmlFor='phone' className='font-medium text-sm'>
Phone Number
</Label>
<Input
id='phone'
type='tel'
placeholder='+1 (555) 123-4567'
value={phone}
onChange={(e) => setPhone(e.target.value)}
/>
</div>
<div className='space-y-2'>
<Label htmlFor='position' className='font-medium text-sm'>
Position of Interest *
</Label>
<Input
id='position'
placeholder='e.g. Full Stack Engineer, Product Designer'
value={position}
onChange={(e) => setPosition(e.target.value)}
className={cn(
showErrors &&
positionErrors.length > 0 &&
'border-red-500 focus:border-red-500 focus:ring-red-100 focus-visible:ring-red-500'
)}
/>
{showErrors && positionErrors.length > 0 && (
<div className='mt-1 space-y-1 text-red-400 text-xs'>
{positionErrors.map((error, index) => (
<p key={index}>{error}</p>
))}
</div>
)}
</div>
</div>
{/* LinkedIn and Portfolio */}
<div className='grid grid-cols-1 gap-4 sm:gap-6 md:grid-cols-2'>
<div className='space-y-2'>
<Label htmlFor='linkedin' className='font-medium text-sm'>
LinkedIn Profile
</Label>
<Input
id='linkedin'
placeholder='https://linkedin.com/in/yourprofile'
value={linkedin}
onChange={(e) => setLinkedin(e.target.value)}
className={cn(
showErrors &&
linkedinErrors.length > 0 &&
'border-red-500 focus:border-red-500 focus:ring-red-100 focus-visible:ring-red-500'
)}
/>
{showErrors && linkedinErrors.length > 0 && (
<div className='mt-1 space-y-1 text-red-400 text-xs'>
{linkedinErrors.map((error, index) => (
<p key={index}>{error}</p>
))}
</div>
)}
</div>
<div className='space-y-2'>
<Label htmlFor='portfolio' className='font-medium text-sm'>
Portfolio / Website
</Label>
<Input
id='portfolio'
placeholder='https://yourportfolio.com'
value={portfolio}
onChange={(e) => setPortfolio(e.target.value)}
className={cn(
showErrors &&
portfolioErrors.length > 0 &&
'border-red-500 focus:border-red-500 focus:ring-red-100 focus-visible:ring-red-500'
)}
/>
{showErrors && portfolioErrors.length > 0 && (
<div className='mt-1 space-y-1 text-red-400 text-xs'>
{portfolioErrors.map((error, index) => (
<p key={index}>{error}</p>
))}
</div>
)}
</div>
</div>
{/* Experience and Location */}
<div className='grid grid-cols-1 gap-4 sm:gap-6 md:grid-cols-2'>
<div className='space-y-2'>
<Label htmlFor='experience' className='font-medium text-sm'>
Years of Experience *
</Label>
<Select value={experience} onValueChange={setExperience}>
<SelectTrigger
className={cn(
showErrors &&
experienceErrors.length > 0 &&
'border-red-500 focus:border-red-500 focus:ring-red-100 focus-visible:ring-red-500'
)}
>
<SelectValue placeholder='Select experience level' />
</SelectTrigger>
<SelectContent>
<SelectItem value='0-1'>0-1 years</SelectItem>
<SelectItem value='1-3'>1-3 years</SelectItem>
<SelectItem value='3-5'>3-5 years</SelectItem>
<SelectItem value='5-10'>5-10 years</SelectItem>
<SelectItem value='10+'>10+ years</SelectItem>
</SelectContent>
</Select>
{showErrors && experienceErrors.length > 0 && (
<div className='mt-1 space-y-1 text-red-400 text-xs'>
{experienceErrors.map((error, index) => (
<p key={index}>{error}</p>
))}
</div>
)}
</div>
<div className='space-y-2'>
<Label htmlFor='location' className='font-medium text-sm'>
Location *
</Label>
<Input
id='location'
placeholder='e.g. San Francisco, CA'
value={location}
onChange={(e) => setLocation(e.target.value)}
className={cn(
showErrors &&
locationErrors.length > 0 &&
'border-red-500 focus:border-red-500 focus:ring-red-100 focus-visible:ring-red-500'
)}
/>
{showErrors && locationErrors.length > 0 && (
<div className='mt-1 space-y-1 text-red-400 text-xs'>
{locationErrors.map((error, index) => (
<p key={index}>{error}</p>
))}
</div>
)}
</div>
</div>
{/* Message */}
<div className='space-y-2'>
<Label htmlFor='message' className='font-medium text-sm'>
Tell us about yourself *
</Label>
<Textarea
id='message'
placeholder='Tell us about your experience, what excites you about Sim, and why you would be a great fit for this role...'
className={cn(
'min-h-[140px]',
showErrors &&
messageErrors.length > 0 &&
'border-red-500 focus:border-red-500 focus:ring-red-100 focus-visible:ring-red-500'
)}
value={message}
onChange={(e) => setMessage(e.target.value)}
/>
<p className='mt-1.5 text-gray-500 text-xs'>Minimum 50 characters</p>
{showErrors && messageErrors.length > 0 && (
<div className='mt-1 space-y-1 text-red-400 text-xs'>
{messageErrors.map((error, index) => (
<p key={index}>{error}</p>
))}
</div>
)}
</div>
{/* Resume Upload */}
<div className='space-y-2'>
<Label htmlFor='resume' className='font-medium text-sm'>
Resume *
</Label>
<div className='relative'>
{resume ? (
<div className='flex items-center gap-2 rounded-md border border-input bg-background px-3 py-2'>
<span className='flex-1 truncate text-sm'>{resume.name}</span>
<button
type='button'
onClick={(e) => {
e.preventDefault()
setResume(null)
if (fileInputRef.current) {
fileInputRef.current.value = ''
}
}}
className='flex-shrink-0 text-muted-foreground transition-colors hover:text-foreground'
aria-label='Remove file'
>
<X className='h-4 w-4' />
</button>
</div>
) : (
<div className='mx-auto max-w-4xl'>
{/* Form Section */}
<section className='rounded-2xl border border-gray-200 bg-white p-6 shadow-sm sm:p-10'>
<form onSubmit={onSubmit} className='space-y-5'>
{/* Name and Email */}
<div className='grid grid-cols-1 gap-4 sm:gap-6 md:grid-cols-2'>
<div className='space-y-2'>
<Label htmlFor='name' className='font-medium text-sm'>
Full Name *
</Label>
<Input
id='resume'
type='file'
accept='.pdf,.doc,.docx'
onChange={handleFileChange}
ref={fileInputRef}
id='name'
placeholder='John Doe'
value={name}
onChange={(e) => setName(e.target.value)}
className={cn(
showErrors &&
resumeErrors.length > 0 &&
nameErrors.length > 0 &&
'border-red-500 focus:border-red-500 focus:ring-red-100 focus-visible:ring-red-500'
)}
/>
{showErrors && nameErrors.length > 0 && (
<div className='mt-1 space-y-1 text-red-400 text-xs'>
{nameErrors.map((error, index) => (
<p key={index}>{error}</p>
))}
</div>
)}
</div>
<div className='space-y-2'>
<Label htmlFor='email' className='font-medium text-sm'>
Email *
</Label>
<Input
id='email'
type='email'
placeholder='john@example.com'
value={email}
onChange={(e) => setEmail(e.target.value)}
className={cn(
showErrors &&
emailErrors.length > 0 &&
'border-red-500 focus:border-red-500 focus:ring-red-100 focus-visible:ring-red-500'
)}
/>
{showErrors && emailErrors.length > 0 && (
<div className='mt-1 space-y-1 text-red-400 text-xs'>
{emailErrors.map((error, index) => (
<p key={index}>{error}</p>
))}
</div>
)}
</div>
</div>
{/* Phone and Position */}
<div className='grid grid-cols-1 gap-4 sm:gap-6 md:grid-cols-2'>
<div className='space-y-2'>
<Label htmlFor='phone' className='font-medium text-sm'>
Phone Number
</Label>
<Input
id='phone'
type='tel'
placeholder='+1 (555) 123-4567'
value={phone}
onChange={(e) => setPhone(e.target.value)}
/>
</div>
<div className='space-y-2'>
<Label htmlFor='position' className='font-medium text-sm'>
Position of Interest *
</Label>
<Input
id='position'
placeholder='e.g. Full Stack Engineer, Product Designer'
value={position}
onChange={(e) => setPosition(e.target.value)}
className={cn(
showErrors &&
positionErrors.length > 0 &&
'border-red-500 focus:border-red-500 focus:ring-red-100 focus-visible:ring-red-500'
)}
/>
{showErrors && positionErrors.length > 0 && (
<div className='mt-1 space-y-1 text-red-400 text-xs'>
{positionErrors.map((error, index) => (
<p key={index}>{error}</p>
))}
</div>
)}
</div>
</div>
{/* LinkedIn and Portfolio */}
<div className='grid grid-cols-1 gap-4 sm:gap-6 md:grid-cols-2'>
<div className='space-y-2'>
<Label htmlFor='linkedin' className='font-medium text-sm'>
LinkedIn Profile
</Label>
<Input
id='linkedin'
placeholder='https://linkedin.com/in/yourprofile'
value={linkedin}
onChange={(e) => setLinkedin(e.target.value)}
className={cn(
showErrors &&
linkedinErrors.length > 0 &&
'border-red-500 focus:border-red-500 focus:ring-red-100 focus-visible:ring-red-500'
)}
/>
{showErrors && linkedinErrors.length > 0 && (
<div className='mt-1 space-y-1 text-red-400 text-xs'>
{linkedinErrors.map((error, index) => (
<p key={index}>{error}</p>
))}
</div>
)}
</div>
<div className='space-y-2'>
<Label htmlFor='portfolio' className='font-medium text-sm'>
Portfolio / Website
</Label>
<Input
id='portfolio'
placeholder='https://yourportfolio.com'
value={portfolio}
onChange={(e) => setPortfolio(e.target.value)}
className={cn(
showErrors &&
portfolioErrors.length > 0 &&
'border-red-500 focus:border-red-500 focus:ring-red-100 focus-visible:ring-red-500'
)}
/>
{showErrors && portfolioErrors.length > 0 && (
<div className='mt-1 space-y-1 text-red-400 text-xs'>
{portfolioErrors.map((error, index) => (
<p key={index}>{error}</p>
))}
</div>
)}
</div>
</div>
{/* Experience and Location */}
<div className='grid grid-cols-1 gap-4 sm:gap-6 md:grid-cols-2'>
<div className='space-y-2'>
<Label htmlFor='experience' className='font-medium text-sm'>
Years of Experience *
</Label>
<Select value={experience} onValueChange={setExperience}>
<SelectTrigger
className={cn(
showErrors &&
experienceErrors.length > 0 &&
'border-red-500 focus:border-red-500 focus:ring-red-100 focus-visible:ring-red-500'
)}
>
<SelectValue placeholder='Select experience level' />
</SelectTrigger>
<SelectContent>
<SelectItem value='0-1'>0-1 years</SelectItem>
<SelectItem value='1-3'>1-3 years</SelectItem>
<SelectItem value='3-5'>3-5 years</SelectItem>
<SelectItem value='5-10'>5-10 years</SelectItem>
<SelectItem value='10+'>10+ years</SelectItem>
</SelectContent>
</Select>
{showErrors && experienceErrors.length > 0 && (
<div className='mt-1 space-y-1 text-red-400 text-xs'>
{experienceErrors.map((error, index) => (
<p key={index}>{error}</p>
))}
</div>
)}
</div>
<div className='space-y-2'>
<Label htmlFor='location' className='font-medium text-sm'>
Location *
</Label>
<Input
id='location'
placeholder='e.g. San Francisco, CA'
value={location}
onChange={(e) => setLocation(e.target.value)}
className={cn(
showErrors &&
locationErrors.length > 0 &&
'border-red-500 focus:border-red-500 focus:ring-red-100 focus-visible:ring-red-500'
)}
/>
{showErrors && locationErrors.length > 0 && (
<div className='mt-1 space-y-1 text-red-400 text-xs'>
{locationErrors.map((error, index) => (
<p key={index}>{error}</p>
))}
</div>
)}
</div>
</div>
{/* Message */}
<div className='space-y-2'>
<Label htmlFor='message' className='font-medium text-sm'>
Tell us about yourself *
</Label>
<Textarea
id='message'
placeholder='Tell us about your experience, what excites you about Sim, and why you would be a great fit for this role...'
className={cn(
'min-h-[140px]',
showErrors &&
messageErrors.length > 0 &&
'border-red-500 focus:border-red-500 focus:ring-red-100 focus-visible:ring-red-500'
)}
value={message}
onChange={(e) => setMessage(e.target.value)}
/>
<p className='mt-1.5 text-gray-500 text-xs'>Minimum 50 characters</p>
{showErrors && messageErrors.length > 0 && (
<div className='mt-1 space-y-1 text-red-400 text-xs'>
{messageErrors.map((error, index) => (
<p key={index}>{error}</p>
))}
</div>
)}
</div>
<p className='mt-1.5 text-gray-500 text-xs'>PDF or Word document, max 10MB</p>
{showErrors && resumeErrors.length > 0 && (
<div className='mt-1 space-y-1 text-red-400 text-xs'>
{resumeErrors.map((error, index) => (
<p key={index}>{error}</p>
))}
{/* Resume Upload */}
<div className='space-y-2'>
<Label htmlFor='resume' className='font-medium text-sm'>
Resume *
</Label>
<div className='relative'>
{resume ? (
<div className='flex items-center gap-2 rounded-md border border-input bg-background px-3 py-2'>
<span className='flex-1 truncate text-sm'>{resume.name}</span>
<button
type='button'
onClick={(e) => {
e.preventDefault()
setResume(null)
if (fileInputRef.current) {
fileInputRef.current.value = ''
}
}}
className='flex-shrink-0 text-muted-foreground transition-colors hover:text-foreground'
aria-label='Remove file'
>
<X className='h-4 w-4' />
</button>
</div>
) : (
<Input
id='resume'
type='file'
accept='.pdf,.doc,.docx'
onChange={handleFileChange}
ref={fileInputRef}
className={cn(
showErrors &&
resumeErrors.length > 0 &&
'border-red-500 focus:border-red-500 focus:ring-red-100 focus-visible:ring-red-500'
)}
/>
)}
</div>
)}
</div>
{/* Submit Button */}
<div className='flex justify-end pt-2'>
<Button
type='submit'
disabled={isSubmitting || submitStatus === 'success'}
className='min-w-[200px] rounded-[10px] border border-[#6F3DFA] bg-gradient-to-b from-[#8357FF] to-[#6F3DFA] text-white shadow-[inset_0_2px_4px_0_#9B77FF] transition-all duration-300 hover:opacity-90 disabled:opacity-50'
size='lg'
>
{isSubmitting ? (
<>
<Loader2 className='mr-2 h-4 w-4 animate-spin' />
Submitting...
</>
) : submitStatus === 'success' ? (
'Submitted'
) : (
'Submit Application'
<p className='mt-1.5 text-gray-500 text-xs'>PDF or Word document, max 10MB</p>
{showErrors && resumeErrors.length > 0 && (
<div className='mt-1 space-y-1 text-red-400 text-xs'>
{resumeErrors.map((error, index) => (
<p key={index}>{error}</p>
))}
</div>
)}
</Button>
</div>
</form>
</section>
</div>
{/* Additional Info */}
<section className='mt-6 text-center text-gray-600 text-sm'>
<p>
Questions? Email us at{' '}
<a
href='mailto:careers@sim.ai'
className='font-medium text-gray-900 underline transition-colors hover:text-gray-700'
>
careers@sim.ai
</a>
</p>
</section>
{/* Submit Button */}
<div className='flex justify-end pt-2'>
<Button
type='submit'
disabled={isSubmitting || submitStatus === 'success'}
className='min-w-[200px] rounded-[10px] border border-[#6F3DFA] bg-gradient-to-b from-[#8357FF] to-[#6F3DFA] text-white shadow-[inset_0_2px_4px_0_#9B77FF] transition-all duration-300 hover:opacity-90 disabled:opacity-50'
size='lg'
>
{isSubmitting ? (
<>
<Loader2 className='mr-2 h-4 w-4 animate-spin' />
Submitting...
</>
) : submitStatus === 'success' ? (
'Submitted'
) : (
'Submit Application'
)}
</Button>
</div>
</form>
</section>
{/* Additional Info */}
<section className='mt-6 text-center text-gray-600 text-sm'>
<p>
Questions? Email us at{' '}
<a
href='mailto:careers@sim.ai'
className='font-medium text-gray-900 underline transition-colors hover:text-gray-700'
>
careers@sim.ai
</a>
</p>
</section>
</div>
</div>
</LegalLayout>
{/* Footer - Only for hosted instances */}
{isHosted && (
<div className='relative z-20'>
<Footer fullWidth={true} />
</div>
)}
</main>
)
}

View File

@@ -217,10 +217,10 @@ export default function Footer({ fullWidth = false }: FooterProps) {
Enterprise
</Link>
<Link
href='/blog'
href='/studio'
className='text-[14px] text-muted-foreground transition-colors hover:text-foreground'
>
Blog
Sim Studio
</Link>
<Link
href='/changelog'

View File

@@ -8,13 +8,14 @@ import { soehne } from '@/app/fonts/soehne/soehne'
interface LegalLayoutProps {
title: string
children: React.ReactNode
navVariant?: 'landing' | 'auth' | 'legal'
}
export default function LegalLayout({ title, children }: LegalLayoutProps) {
export default function LegalLayout({ title, children, navVariant = 'legal' }: LegalLayoutProps) {
return (
<main className={`${soehne.className} min-h-screen bg-white text-gray-900`}>
{/* Header - Nav handles all conditional logic */}
<Nav variant='legal' />
<Nav variant={navVariant} />
{/* Content */}
<div className='px-12 pt-[40px] pb-[40px]'>

View File

@@ -20,7 +20,7 @@ interface NavProps {
}
export default function Nav({ hideAuthButtons = false, variant = 'landing' }: NavProps = {}) {
const [githubStars, setGithubStars] = useState('17.7k')
const [githubStars, setGithubStars] = useState('18k')
const [isHovered, setIsHovered] = useState(false)
const [isLoginHovered, setIsLoginHovered] = useState(false)
const router = useRouter()
@@ -71,7 +71,7 @@ export default function Nav({ hideAuthButtons = false, variant = 'landing' }: Na
</li>
<li>
<Link
href='#pricing'
href='/?from=nav#pricing'
className='text-[16px] text-muted-foreground transition-colors hover:text-foreground'
scroll={true}
>
@@ -88,6 +88,14 @@ export default function Nav({ hideAuthButtons = false, variant = 'landing' }: Na
Enterprise
</button>
</li>
<li>
<Link
href='/careers'
className='text-[16px] text-muted-foreground transition-colors hover:text-foreground'
>
Careers
</Link>
</li>
<li>
<a
href='https://github.com/simstudioai/sim'

View File

@@ -48,19 +48,19 @@ export default async function Page({ params }: { params: Promise<{ slug: string
/>
<header className='mx-auto max-w-[1450px] px-6 pt-8 sm:px-8 sm:pt-12 md:px-12 md:pt-16'>
<div className='mb-6'>
<Link href='/blog' className='text-gray-600 text-sm hover:text-gray-900'>
Back to Blog
<Link href='/studio' className='text-gray-600 text-sm hover:text-gray-900'>
Back to Sim Studio
</Link>
</div>
<div className='flex flex-col gap-8 md:flex-row md:gap-12'>
<div className='h-[180px] w-full flex-shrink-0 sm:h-[200px] md:h-auto md:w-[300px]'>
<div className='relative h-full w-full overflow-hidden rounded-lg md:aspect-[5/4]'>
<div className='w-full flex-shrink-0 md:w-[450px]'>
<div className='relative w-full overflow-hidden rounded-lg'>
<Image
src={post.ogImage}
alt={post.title}
width={300}
height={240}
className='h-full w-full object-cover'
width={450}
height={360}
className='h-auto w-full'
priority
itemProp='image'
/>
@@ -133,7 +133,7 @@ export default async function Page({ params }: { params: Promise<{ slug: string
<h2 className='mb-4 font-medium text-[24px]'>Related posts</h2>
<div className='grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3'>
{related.map((p) => (
<Link key={p.slug} href={`/blog/${p.slug}`} className='group'>
<Link key={p.slug} href={`/studio/${p.slug}`} className='group'>
<div className='overflow-hidden rounded-lg border border-gray-200'>
<Image
src={p.ogImage}

View File

@@ -20,7 +20,7 @@ export default async function AuthorPage({ params }: { params: Promise<{ id: str
'@context': 'https://schema.org',
'@type': 'Person',
name: author.name,
url: `https://sim.ai/blog/authors/${author.id}`,
url: `https://sim.ai/studio/authors/${author.id}`,
sameAs: author.url ? [author.url] : [],
image: author.avatarUrl,
}
@@ -44,7 +44,7 @@ export default async function AuthorPage({ params }: { params: Promise<{ id: str
</div>
<div className='grid grid-cols-1 gap-8 sm:grid-cols-2'>
{posts.map((p) => (
<Link key={p.slug} href={`/blog/${p.slug}`} className='group'>
<Link key={p.slug} href={`/studio/${p.slug}`} className='group'>
<div className='overflow-hidden rounded-lg border border-gray-200'>
<Image
src={p.ogImage}

View File

@@ -1,12 +1,12 @@
export default function Head() {
return (
<>
<link rel='canonical' href='https://sim.ai/blog' />
<link rel='canonical' href='https://sim.ai/studio' />
<link
rel='alternate'
type='application/rss+xml'
title='Sim Blog'
href='https://sim.ai/blog/rss.xml'
title='Sim Studio'
href='https://sim.ai/studio/rss.xml'
/>
</>
)

View File

@@ -1,6 +1,6 @@
import { Footer, Nav } from '@/app/(landing)/components'
export default function BlogLayout({ children }: { children: React.ReactNode }) {
export default function StudioLayout({ children }: { children: React.ReactNode }) {
const orgJsonLd = {
'@context': 'https://schema.org',
'@type': 'Organization',

View File

@@ -0,0 +1,152 @@
import Image from 'next/image'
import Link from 'next/link'
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import { getAllPostMeta } from '@/lib/blog/registry'
import { soehne } from '@/app/fonts/soehne/soehne'
export const revalidate = 3600
export default async function StudioIndex({
searchParams,
}: {
searchParams: Promise<{ page?: string; tag?: string }>
}) {
const { page, tag } = await searchParams
const pageNum = Math.max(1, Number(page || 1))
const perPage = 20
const all = await getAllPostMeta()
const filtered = tag ? all.filter((p) => p.tags.includes(tag)) : all
// Sort to ensure featured post is first on page 1
const sorted =
pageNum === 1
? filtered.sort((a, b) => {
if (a.featured && !b.featured) return -1
if (!a.featured && b.featured) return 1
return 0
})
: filtered
const totalPages = Math.max(1, Math.ceil(sorted.length / perPage))
const start = (pageNum - 1) * perPage
const posts = sorted.slice(start, start + perPage)
// Tag filter chips are intentionally disabled for now.
// const tags = await getAllTags()
const studioJsonLd = {
'@context': 'https://schema.org',
'@type': 'Blog',
name: 'Sim Studio',
url: 'https://sim.ai/studio',
description: 'Announcements, insights, and guides for building AI agent workflows.',
}
return (
<main className={`${soehne.className} mx-auto max-w-[1200px] px-6 py-12 sm:px-8 md:px-12`}>
<script
type='application/ld+json'
dangerouslySetInnerHTML={{ __html: JSON.stringify(studioJsonLd) }}
/>
<h1 className='mb-3 font-medium text-[40px] leading-tight sm:text-[56px]'>Sim Studio</h1>
<p className='mb-10 text-[18px] text-gray-700'>
Announcements, insights, and guides for building AI agent workflows.
</p>
{/* Tag filter chips hidden until we have more posts */}
{/* <div className='mb-10 flex flex-wrap gap-3'>
<Link href='/studio' className={`rounded-full border px-3 py-1 text-sm ${!tag ? 'border-black bg-black text-white' : 'border-gray-300'}`}>All</Link>
{tags.map((t) => (
<Link key={t.tag} href={`/studio?tag=${encodeURIComponent(t.tag)}`} className={`rounded-full border px-3 py-1 text-sm ${tag === t.tag ? 'border-black bg-black text-white' : 'border-gray-300'}`}>
{t.tag} ({t.count})
</Link>
))}
</div> */}
{/* Grid layout for consistent rows */}
<div className='grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3'>
{posts.map((p, i) => {
return (
<Link key={p.slug} href={`/studio/${p.slug}`} className='group flex flex-col'>
<div className='flex h-full flex-col overflow-hidden rounded-xl border border-gray-200 transition-colors duration-300 hover:border-gray-300'>
<Image
src={p.ogImage}
alt={p.title}
width={800}
height={450}
className='h-48 w-full object-cover'
/>
<div className='flex flex-1 flex-col p-4'>
<div className='mb-2 text-gray-600 text-xs'>
{new Date(p.date).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
})}
</div>
<h3 className='shine-text mb-1 font-medium text-lg leading-tight'>{p.title}</h3>
<p className='mb-3 line-clamp-3 flex-1 text-gray-700 text-sm'>{p.description}</p>
<div className='flex items-center gap-2'>
<div className='-space-x-1.5 flex'>
{(p.authors && p.authors.length > 0 ? p.authors : [p.author])
.slice(0, 3)
.map((author, idx) => (
<Avatar key={idx} className='size-4 border border-white'>
<AvatarImage src={author?.avatarUrl} alt={author?.name} />
<AvatarFallback className='border border-white bg-gray-100 text-[10px] text-gray-600'>
{author?.name.slice(0, 2)}
</AvatarFallback>
</Avatar>
))}
</div>
<span className='text-gray-600 text-xs'>
{(p.authors && p.authors.length > 0 ? p.authors : [p.author])
.slice(0, 2)
.map((a) => a?.name)
.join(', ')}
{(p.authors && p.authors.length > 0 ? p.authors : [p.author]).length > 2 && (
<>
{' '}
and{' '}
{(p.authors && p.authors.length > 0 ? p.authors : [p.author]).length - 2}{' '}
other
{(p.authors && p.authors.length > 0 ? p.authors : [p.author]).length - 2 >
1
? 's'
: ''}
</>
)}
</span>
</div>
</div>
</div>
</Link>
)
})}
</div>
{totalPages > 1 && (
<div className='mt-10 flex items-center justify-center gap-3'>
{pageNum > 1 && (
<Link
href={`/studio?page=${pageNum - 1}${tag ? `&tag=${encodeURIComponent(tag)}` : ''}`}
className='rounded border px-3 py-1 text-sm'
>
Previous
</Link>
)}
<span className='text-gray-600 text-sm'>
Page {pageNum} of {totalPages}
</span>
{pageNum < totalPages && (
<Link
href={`/studio?page=${pageNum + 1}${tag ? `&tag=${encodeURIComponent(tag)}` : ''}`}
className='rounded border px-3 py-1 text-sm'
>
Next
</Link>
)}
</div>
)}
</main>
)
}

View File

@@ -11,7 +11,7 @@ export async function GET() {
const xml = `<?xml version="1.0" encoding="UTF-8" ?>
<rss version="2.0">
<channel>
<title>Sim Blog</title>
<title>Sim Studio</title>
<link>${site}</link>
<description>Announcements, insights, and guides for AI agent workflows.</description>
${items

View File

@@ -7,13 +7,13 @@ export default async function TagsIndex() {
<main className='mx-auto max-w-[900px] px-6 py-10 sm:px-8 md:px-12'>
<h1 className='mb-6 font-medium text-[32px] leading-tight'>Browse by tag</h1>
<div className='flex flex-wrap gap-3'>
<Link href='/blog' className='rounded-full border border-gray-300 px-3 py-1 text-sm'>
<Link href='/studio' className='rounded-full border border-gray-300 px-3 py-1 text-sm'>
All
</Link>
{tags.map((t) => (
<Link
key={t.tag}
href={`/blog?tag=${encodeURIComponent(t.tag)}`}
href={`/studio?tag=${encodeURIComponent(t.tag)}`}
className='rounded-full border border-gray-300 px-3 py-1 text-sm'
>
{t.tag} ({t.count})

View File

@@ -20,7 +20,7 @@ export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
pathname.startsWith('/careers') ||
pathname.startsWith('/changelog') ||
pathname.startsWith('/chat') ||
pathname.startsWith('/blog')
pathname.startsWith('/studio')
? 'light'
: undefined

View File

@@ -83,7 +83,7 @@ export const CareersConfirmationEmail = ({
documentation
</a>{' '}
to learn more about what we're building, or check out our{' '}
<a href={`${baseUrl}/blog`} style={{ color: '#802FFF', textDecoration: 'none' }}>
<a href={`${baseUrl}/studio`} style={{ color: '#802FFF', textDecoration: 'none' }}>
blog
</a>{' '}
for the latest updates.

View File

@@ -2,6 +2,7 @@ import { Fragment, type ReactNode } from 'react'
import { highlight, languages } from 'prismjs'
import 'prismjs/components/prism-javascript'
import 'prismjs/components/prism-python'
import 'prismjs/components/prism-json'
import { cn } from '@/lib/utils'
import './code.css'

View File

@@ -0,0 +1,7 @@
{
"id": "adam",
"name": "Adam Gough",
"url": "https://x.com/adamgough",
"xHandle": "adamgough",
"avatarUrl": "/studio/authors/adam.png"
}

View File

@@ -3,5 +3,5 @@
"name": "Emir Karabeg",
"url": "https://x.com/karabegemir",
"xHandle": "karabegemir",
"avatarUrl": "/blog/authors/emir.png"
"avatarUrl": "/studio/authors/emir.png"
}

View File

@@ -0,0 +1,7 @@
{
"id": "sid",
"name": "Siddharth",
"url": "https://x.com/sidganesan",
"xHandle": "sidganesan",
"avatarUrl": "/studio/authors/sid.png"
}

View File

@@ -0,0 +1,7 @@
{
"id": "vik",
"name": "Vikhyath Mondreti",
"url": "https://github.com/icecrasher321",
"xHandle": "icecrasher321",
"avatarUrl": "/studio/authors/vik.jpg"
}

View File

@@ -3,5 +3,5 @@
"name": "Waleed Latif",
"url": "https://x.com/typingwala",
"xHandle": "typingwala",
"avatarUrl": "/blog/authors/waleed.png"
"avatarUrl": "/studio/authors/waleed.png"
}

View File

@@ -0,0 +1,101 @@
---
slug: copilot
title: 'Inside Sim Copilot — architecture, benchmarks, and how it fits'
description: 'A technical overview of Sim Copilot: the architecture behind it, our early benchmarks, and how Copilot integrates with agentic workflows in Sim.'
date: 2025-11-08
updated: 2025-11-08
authors:
- sid
readingTime: 7
tags: [Copilot, AI Assistant, Benchmarks, Architecture, Sim]
ogImage: /studio/copilot/cover.png
ogAlt: 'Sim Copilot technical overview'
about: ['AI Assistants', 'Agentic Workflows', 'Retrieval Augmented Generation']
timeRequired: PT7M
canonical: https://sim.ai/studio/copilot
featured: false
draft: true
---
> This is a technical deepdive scaffold for Sim Copilot. Well keep updating it as we publish more results and open up additional capabilities.
## TL;DR
- Copilot is a contextaware assistant embedded into the Sim editor.
- It has firstclass access (with user approval) to workflows, blocks, logs, and docs.
- The system is retrievalcentric with strong guardrails and deterministic execution paths.
## Architecture at a glance
1. Intent understanding
- Lightweight classifier + instruction parser directs requests into tools.
2. Context assembly
- Indexed sources (workflows, blocks, logs, docs) with semantic and lexical signals.
- Safety filters for scope + permission checks.
3. Tooling and actions
- Readonly tools (explain, compare, search), proposechanges tools, and execution tools.
4. Response synthesis
- Deterministic templates for diffs, plans, and explanations.
5. Humanintheloop
- All writes gated behind explicit user approval.
```mermaid
flowchart LR
U[User] --> I(Intent)
I --> C(Context Builder)
C -->|RAG| R[Retriever]
R --> T(Tools)
T --> S(Response Synth)
S --> U
```
## Retrieval and grounding
- Sources: workspace workflows, block metadata, execution logs, and product docs.
- Indexing: hybrid scoring (BM25 + embeddings) with recency decay and persource caps.
- Normalization: chunking with stable anchors so diffs remain linereferential.
## Early benchmarks (scaffold)
> Numbers below are placeholders for the structure; well replace with full runs.
| Task | Top1 Retrieval@K | Edit Accuracy | Time (p50) |
| ----------------------------------- | -----------------:| ------------: | ---------: |
| Explain a workflow block | 92% | 88% | 1.2s |
| Propose a safe fix for an error | 78% | 70% | 2.1s |
| Generate a new block configuration | 74% | 65% | 2.6s |
| Find relevant execution logs | 90% | 84% | 1.4s |
Measurement notes:
- Retrieval@K: correctness of the top candidate chunk for a labeled query.
- Edit Accuracy: humanvalidated acceptance rate for proposed changes.
- Time: serverside latency (excludes model coldstart).
## Guardrails and safety
- Scope enforcement: actions limited to the open workspace with explicit user triggers.
- Sensitive data policies and redaction in logs.
- Proposal mode: diffs are reviewed and applied only on user approval.
## How Copilot fits into Sim
- Ineditor assistant for building and editing workflows.
- Shortcut to documentation and examples with live context from your canvas.
- Bridge to evaluation: Copilot can set up test runs and compare outputs sidebyside.
- Works with the same permissions model you already use in Sim.
## Roadmap (highlevel)
- Multiturn plans with subtasks and rollback.
- Deeper evaluation harness and dataset management.
- Firstparty tool plugins for common blocks and providers.
## Repro and transparency
- Well publish a benchmark harness and anonymized evaluation sets.
- Expect a detailed methodology post as we widen the beta.
— Sid @ Sim

View File

@@ -0,0 +1,97 @@
---
slug: emcn
title: 'Introducing Emcn — Sims new design system'
description: Emcn is the heart of our new design language at Sim. Heres the scaffolding of the system—principles, tokens, components, and roadmap—as we prepare the full launch.
date: 2025-11-08
updated: 2025-11-08
authors:
- emir
readingTime: 6
tags: [Design, Emcn, UI, UX, Components, Sim]
ogImage: /studio/emcn/cover.png
ogAlt: 'Emcn design system cover'
about: ['Design Systems', 'Component Libraries', 'Design Tokens', 'Accessibility']
timeRequired: PT6M
canonical: https://sim.ai/studio/emcn
featured: false
draft: true
---
> This post is the scaffolding for Emcn, our new design system. Well fill it in as we publish the full documentation and component gallery.
![Emcn cover placeholder](/studio/emcn/cover.png)
## What is Emcn?
Emcn is the design system that powers Sims product and brand. It aims to give us:
- Consistent, accessible UI across web surfaces
- A fast path from Figma to code with stronglytyped tokens
- A composable component library that scales with product complexity
## Principles
1. Opinionated but flexible
2. Accessible by default (WCAG AA+)
3. Stronglytyped, themeable tokens (light/dark + brand accents)
4. Composable components over oneoff variants
5. Performance first (minimal runtime, zero layout shift)
## Foundations (Tokens)
- Color: semantic palettes (bg, fg, muted, accent, destructive) with oncolors
- Typography: scale + weights mapped to roles (display, title, body, code)
- Spacing: 2/4 grid, container and gutter rules
- Radius: component tiers (base, interactive, card, sheet)
- Shadows: subtle elevation scale for surfaces and overlays
- Motion: duration/easing tokens for affordances (not decoration)
## Components (Initial Set)
- Primitives: Button, Input, Select, Checkbox, Radio, Switch, Slider, Badge, Tooltip
- Navigation: NavBar, SideBar, Tabs, Breadcrumbs
- Feedback: Toast, Banner, Alert, Dialog, Drawer, Popover
- Layout: Grid, Stack, Container, Card, Sheet
- Content: CodeBlock, Markdown, Table, EmptyState
> Each component will include: anatomy, a11y contract, variants/slots, and code examples.
## Theming
- Light + Dark, with brand accent tokens
- Perworkspace theming hooks for enterprise deployments
- SSRsafe color mode with no flash (hydrationsafe)
## Accessibility
- Focus outlines and target sizes audited
- Color contrast tracked at token level
- Keyboard and screen reader interactions defined per component
## Tooling
- Tokens exported as TypeScript + CSS variables
- Figma library mapped 1:1 to code components
- Lint rules for token usage and a11y checks
## Roadmap
- v0: Foundations + Core components (internal)
- v1: Public docs and examples site
- v1.x: Data display, advanced forms, charts bridge
## FAQ
- What does “Emcn” mean?
A short, crisp name we liked—easy to type and remember.
- Will Emcn be opensourced?
We plan to share the foundations and many components as part of our commitment to open source.
## Were hiring
Were hiring designers and engineers who care deeply about craft and DX. If you want to help shape Emcn and Sims product, wed love to talk.
— Team Sim

View File

@@ -0,0 +1,196 @@
---
slug: executor
title: 'Inside the Sim Executor - DAG Based Execution with Native Parallelism'
description: 'How we built a DAG-based execution engine with native parallel processing, intelligent edge routing, and stateful pause/resume capabilities'
date: 2025-11-10
updated: 2025-11-10
authors:
- sid
readingTime: 12
tags: [Executor, Architecture, DAG, Orchestration]
ogImage: /studio/executor/cover.png
ogAlt: 'Sim Executor technical overview'
about: ['Execution', 'Workflow Orchestration']
timeRequired: PT12M
canonical: https://sim.ai/studio/executor
featured: false
draft: false
---
Modern workflows aren't just linear automations anymore. They involve a variety of APIs and services, loop over a model's output, pause for human decisions, and resume hours or days later exactly where they left off.
We designed the Sim executor to make these patterns feel natural. This post shares the architecture we ended up with, the challenges we ran into along the way, and what it enables for teams building agentic systems at scale.
## Laying the Foundation
There's a single guiding philosophy we use when designing the executor: workflows should read like the work you intend to do, not like the mess of cables behind a TV.
The complexity of wiring and plumbing should be abstracted away, and building a performant workflow end to end should be easy, modular, and seamless.
That's why the Sim executor serves as both an orchestrator and a translation layer, turning user-friendly workflow representations into an executable DAG behind the scenes.
## Core engine
At its heart, the executor figures out which blocks can run, runs them, then repeats. It sounds simple in theory, but can become surprisingly complex when you factor in conditional routing, nested loops, and true parallelism.
### Compiling Workflows to Graphs
Before execution starts, we compile the visual workflow into a directed acyclic graph (DAG). Every block becomes a node and every connection becomes an edge. Loops and parallel subflows expand into more complex structures (sentinel nodes for loops, branch-indexed nodes for parallels) that preserve the DAG property while enabling iteration and concurrency.
This upfront compilation pays off immediately: the entire topology is concretely defined before the first block ever executes.
### The Execution Queue
Once we have the DAG, execution becomes eventdriven. We maintain a ready queue: nodes whose dependencies are all satisfied. When a node completes, we remove its outgoing edges from downstream nodes' incoming edge sets. Any node that hits zero incoming edges goes straight into the queue. At it's core, topological sort.
The key difference here from traditional workflow execution approaches: we don't wait for a "layer" to finish. If three nodes in the queue are independent, we launch all three immediately and let the runtime handle concurrency.
### Dependency Resolution
In our earlier prototypes, we scanned the connection array after every block execution to see what became ready. However, as the number of nodes and edges scale, performance takes a hit.
The DAG flips that model. Each node tracks its own incoming edges in a set. When a dependency completes, we remove one element from the set. When the set hits zero, the node is ready. No scanning, no filtering, no repeated checks.
This optimization compounds when you have many parallel branches or deeply nested structures. Every node knows its own readiness without asking the rest of the graph.
### Variable Resolution
Blocks reference data from different sources: loop items (`<loop.iteration>`, `<loop.item>`), parallel branch indices (`<parallel.index>`), upstream block outputs (`<blockId.output.content>`), workflow variables (`<workflow.variableName>`), and environment variables (`${API_KEY}`). The resolver tries each scope in order—loop first, then parallel, then workflow, then environment, then block outputs. Inner scopes shadow outer ones, matching standard scoping semantics. This makes variables predictable: the context you're in determines what you see, without name collision or manual prefixes.
### Multiple Triggers and Selective Compilation
A workflow can have multiple entry points. Webhooks listen at different paths, schedules run on different cadences, and some triggers can fire from the UI. Each represents a valid starting point, but only one matters for any given execution.
The DAG builder handles this through selective compilation. When a workflow executes, we receive a trigger block ID. The builder starts from that node and builds only the reachable subgraph. Blocks that aren't downstream from the trigger never make it into the DAG.
This keeps execution focused. A workflow with five different webhook triggers doesn't compile all five paths every time. The topology adapts to the context automatically.
### Executing from the Client
The executor lives server-side. Users build workflows in the client. As they iterate and test, they need to see block inputs and outputs, watch execution progress in real time, and understand which paths the workflow takes.
Polling adds latency. Duplicating execution logic clientside creates drift. We needed a way to stream execution state as it happens.
The executor emits events at key execution points—block starts, completions, streaming content, errors. These events flow through SSE to connected clients. The client reconstructs execution state from the stream, rendering logs and outputs as blocks complete.
## Parallelism
When a workflow fans out to call multiple APIs, compare outputs from different models, or process items independently, those branches should run at the same time. Not interleaved, not sequentially—actually concurrent.
Most workflow platforms handle branches differently. Some execute them one after another (n8n's v1 mode completes branch 1, then branch 2, then branch 3). Others interleave execution (run the first node of each branch, then the second node of each branch). Both approaches are deterministic, but neither gives you true parallelism.
The workarounds typically involve triggering separate sub-workflows with "wait for completion" disabled, then manually collecting results. This works, but it means coordinating execution state across multiple workflow instances, handling failures independently, and stitching outputs back together.
### How we approach it
The ready queue gives us parallelism by default. When a parallel block executes, it expands into branchindexed nodes in the DAG. Each branch is a separate copy of the blocks inside the parallel scope, indexed by branch number.
All entry nodes across all branches enter the ready queue simultaneously. The executor launches them concurrently—they're independent nodes with satisfied dependencies. As each branch progresses, its downstream nodes become ready and execute. The parallel orchestrator tracks completion by counting terminal nodes across all branches.
When all branches finish, we aggregate their outputs in branch order and continue. No coordination overhead, no manual result collection—just concurrent execution with deterministic aggregation.
### What this enables
A workflow that calls fifty different APIs processes them concurrently. Parallel model comparisons return results as they stream in, not after the slowest one finishes.
The DAG doesn't distinguish between "parallel branches" and "independent blocks that happen to be ready at the same time." Both execute concurrently. Parallelism simply emerges from workflow structure.
### Parallel subflows for cleaner authoring
For repetitive parallel work, we added parallel subflows. Instead of duplicating blocks visually for each branch on the canvas, you define a single subflow and configure the parallel block to run it N times or once per item in a collection.
Behind the scenes, this expands to the same branchindexed DAG structure. The executor doesn't distinguish between manually authored parallel branches and subflow-generated ones—they both become independent nodes that execute concurrently. Same execution model, cleaner authoring experience.
## Loops
### How loops compile to DAGs
Loops present a challenge for DAGs: graphs are acyclic, but loops repeat. We handle this by expanding loops into sentinel nodes during compilation.
![Loop sentinel nodes](/studio/executor/loop-sentinels.png)
*Loops expand into sentinel start and end nodes. The backward edge only activates when the loop continues, preserving the DAG's acyclic property.*
A loop is bookended by two nodes: a sentinel start and a sentinel end. The sentinel start activates the first blocks inside the loop. When terminal blocks complete, they route to the sentinel end. The sentinel end evaluates the loop condition and returns either "continue" (which routes back to the start) or "exit" (which activates blocks after the loop).
The backward edge from end to start doesn't count as a dependency initially—it only activates if the loop continues. This preserves the DAG property while enabling iteration.
### Iteration state and variable scoping
When a loop continues, the executor doesn't re-execute blocks from scratch. It clears their execution state (marking them as not-yet-executed) and restores their incoming edges, so they become ready for the next pass. Loop scope updates: iteration increments, the next item loads (for forEach), outputs from the previous iteration move to the aggregated results.
Blocks inside the loop access loop variables through the resolver chain. `<loop.iteration>` resolves before checking block outputs or workflow variables, so iteration context shadows outer scopes. This makes variable access predictable—you always get the current loop state.
## Conditions and Routers
Workflows branch based on runtime decisions. A condition block evaluates expressions and routes to different paths. A router block lets an AI model choose which path to take based on context. Both are core to building adaptive workflows.
### LLM-driven routing
Router blocks represent a modern pattern in workflow orchestration. Instead of hardcoding logic with if/else chains, you describe the options and let a language model decide. The model sees the conversation context, evaluates which path makes sense, and returns a selection.
The executor treats this selection as a routing decision. Each outgoing edge from a router carries metadata about which target block it represents. When the router completes, it returns the chosen block's ID. The edge manager activates only the edge matching that ID; all other edges deactivate.
This makes AI-driven routing deterministic and traceable. You can inspect the execution log and see exactly which path the model chose, why (from the model's reasoning), and which alternatives were pruned.
### Edge selection and path pruning
When a condition or router executes, it evaluates its logic and returns a single selection. The edge manager checks each outgoing edge to see if its label matches the selection. The matching edge activates; the rest deactivate.
![Edge activation and pruning](/studio/executor/edge-pruning.png)
*When a condition selects one path, the chosen edge activates while unselected paths deactivate recursively, preventing unreachable blocks from executing.*
Deactivation cascades. If an edge deactivates, the executor recursively deactivates all edges downstream from its target—unless that target has other active incoming edges. This automatic pruning prevents unreachable blocks from ever entering the ready queue.
The benefit: wasted work drops to zero. Paths that won't execute don't consume resources, don't wait in the queue, and don't clutter execution logs. The DAG reflects what actually ran, not what could have run.
### Convergence and rejoining paths
Workflows often diverge and reconverge. Multiple condition branches might lead to different processing steps, then merge at a common aggregation block. The executor handles this through edge counting.
When paths converge, the target block has multiple incoming edges—one from each upstream path. The edge manager tracks which edges activate. If a condition prunes one branch, that edge deactivates, and the target's incoming edge count decreases. The target becomes ready only when all remaining active incoming edges complete.
This works for complex topologies: nested conditions, routers feeding into other routers, parallel branches that reconverge after different amounts of work. The dependency tracking adapts automatically.
## Human in the loop
AI workflows aren't fully automated. They pause for approvals, wait for human feedback, or stop to let someone review model output before continuing. These pauses can happen anywhere—midbranch, inside a loop, across multiple parallel paths at once.
### Pause detection and state capture
When a block returns pause metadata, the executor stops processing its outgoing edges. Instead of continuing to downstream blocks, it captures the current execution state: every block output, every loop iteration, every parallel branch's progress, every routing decision, and the exact topology of remaining dependencies in the DAG.
Each pause point gets a unique context ID that encodes its position. A pause inside a loop at iteration 5 gets a different ID than the same block at iteration 6. A pause in parallel branch 3 gets a different ID than branch 4. This makes resume targeting precise—you can resume specific pause points independently.
The executor supports multiple simultaneous pauses. If three parallel branches each hit an approval block, all three pause, each with its own context ID. The execution returns with all three pause points and their resume links. Resuming any one triggers continuation from that specific point.
### Snapshot serialization
The snapshot captures everything needed to resume. Block states, execution logs, loop and parallel scopes, routing decisions, workflow variables—all serialize to JSON. The critical piece: DAG incoming edges. We save which dependencies each node still has outstanding.
When you serialize the DAG's edge state, you're freezing the exact moment in time when execution paused. This includes partiallycompleted loops (iteration 7 of 100), inflight parallel branches (12 of 50 complete), and conditional paths already pruned.
### Resume and continuation
Resuming rebuilds the DAG, restores the snapshot state, and queues the resume trigger nodes. The executor marks alreadyexecuted blocks to prevent reexecution, restores incoming edges to reflect remaining dependencies, and continues from where it stopped.
If multiple pause points exist, each can resume independently. The first resume doesn't invalidate the others—each pause has its own trigger node in the DAG. When all pauses resume, the workflow continues normally, collecting outputs from each resumed branch.
### Coordination and atomicity
The executor uses a queue lock to prevent race conditions. When a node completes with pause metadata, we acquire the lock before checking for pauses. This ensures that multiple branches pausing simultaneously don't interfere with each other's state capture.
The lock also prevents a resumed node from racing with other executing nodes. When a resume trigger fires, it enters the queue like any other node. The ready queue pattern handles coordination—resumed nodes execute when their dependencies clear, just like nodes in the original execution.
### Example
![Iterative agent refinement with human feedback](/studio/executor/hitl-loop.png)
*A common pattern: agent generates output, pauses for human review, router decides pass/fail based on feedback, saves to workflow variable, and loop continues until approved.*
A while loop runs an agent with previous feedback as context. The agent's output goes to a humanintheloop block, which pauses execution and sends a notification. The user reviews the output and provides feedback via the resume link.
When resumed, the feedback flows to a router that evaluates whether the output passes or needs revision. If it fails, the router saves the feedback to a workflow variable and routes back to continue the loop. The agent receives this feedback on the next iteration and tries again. If it passes, the router exits the loop and continues downstream.
The while loop's condition checks the workflow variable. As long as the status is "fail," the loop continues. When the router sets it to "pass," the loop exits. Each piece—loops, pause/resume, routing, variables—composes without glue because they're all firstclass executor concepts.
Multiple reviewers approving different branches works the same way. Each branch pauses independently, reviewers approve in any order, and execution continues as each approval comes in. The parallel orchestrator collects the results when all branches complete.
— Sid @ Sim

View File

@@ -0,0 +1,181 @@
---
slug: multiplayer
title: 'Realtime Collaboration on Sim'
description: A high-level explanation into Sim realtime collaborative workflow builder - from operation queues to conflict resolution.
date: 2025-11-11
updated: 2025-11-11
authors:
- vik
readingTime: 12
tags: [Multiplayer, Realtime, Collaboration, WebSockets, Architecture]
ogImage: /studio/multiplayer/cover.png
canonical: https://sim.ai/studio/multiplayer
draft: false
---
When we started building Sim, we noticed that AI workflow development looked a lot like the design process [Figma](https://www.figma.com/blog/how-figmas-multiplayer-technology-works/) had already solved for. Product managers need to sketch out user-facing flows, engineers need to configure integrations and APIs, and domain experts need to validate business logic—often all at the same time. Traditional workflow builders force serial collaboration: one person edits, saves, exports, and notifies the next person. This creates unnecessary friction.
We decided multiplayer editing was the right approach, even though workflow platforms like n8n and Make do not currently offer it. This post explains how we built it. We'll cover the operation queue, conflict resolution, how we handle blocks/edges/subflows separately, undo/redo as a wrapper around this, and why our system is a lot simpler than you'd expect.
## Architecture Overview: Client-Server with WebSockets
Sim uses a client-server architecture where browser clients communicate with a standalone Node.js WebSocket server over persistent connections. When you open a workflow, your client joins a "workflow room" on the server. All subsequent operations—adding blocks, connecting edges, updating configurations—are synchronized through this connection.
### Server-Side: The Source of Truth
The server maintains authoritative state in PostgreSQL across three normalized tables:
- `workflow_blocks`: Block metadata, positions, configurations, and subblock values
- `workflow_edges`: Connections between blocks with source/target handles
- `workflow_subflows`: Loop and parallel container configurations with child node lists
This separation is deliberate. Blocks, edges, and subflows have different update patterns and conflict characteristics. By storing them separately:
1. **Targeted updates**: Moving a block only updates `positionX` and `positionY` fields for that specific block row. We don't load or lock the entire workflow.
2. **Query optimization**: Different operations hit different tables with appropriate indexes. Updating edge connections only touches `workflow_edges`, leaving blocks untouched.
3. **Separate channels**: Structural operations (adding blocks, connecting edges) go through the main operation handler with persistence-first logic. Value updates (editing text in a subblock) go through a separate debounced channel with server-side coalescing—reducing database writes from hundreds to dozens for a typical typing session.
The server uses different broadcast strategies: position updates are broadcast immediately for smooth collaborative dragging (optimistic), while structural operations (adding blocks, connecting edges) persist first to ensure consistency (pessimistic).
### Client-Side: Optimistic Updates with Reconciliation
Clients maintain local copies of workflow state in [Zustand](https://github.com/pmndrs/zustand) stores. When you drag a block or type in a text field, the UI updates immediately—this is optimistic rendering. Simultaneously, the client queues an operation in a separate operation queue store to send to the server.
The client doesn't wait for server confirmation to render changes. Instead, it assumes success and continues. If the server rejects an operation (permissions failure, conflict, validation error), the client reconciles by either retrying or reverting the local change.
This is why workflow editing feels instantaneous—you never wait for a network round-trip to see your changes. The downside is added complexity around handling reconciliation, retries, and conflict resolution.
## The Operation Queue: Reliability Through Retries
At the heart of Sim's multiplayer system is the **Operation Queue**—a client-side abstraction that ensures no operation is lost, even under poor network conditions.
### How It Works
Every user action that modifies workflow state generates an operation object:
```typescript
{
id: 'op-uuid',
operation: {
operation: 'update', // or 'add', 'remove', 'move'
target: 'block', // or 'edge', 'subblock', 'variable'
payload: { /* change data */ }
},
workflowId: 'workflow-id',
userId: 'user-id',
status: 'pending'
}
```
Operations are enqueued in FIFO order. The queue processor sends one operation at a time over the WebSocket, waiting for server confirmation before proceeding to the next. Text edits (subblock values, variable fields) are debounced client-side and coalesced server-side—a user typing a 500-character prompt generates ~10 operations instead of 500.
Failed operations retry with exponential backoff (structural changes get 3 attempts, text edits get 5). If all retries fail, the system enters offline mode—the queue is cleared and the UI becomes read-only until the user manually refreshes.
### Handling Dependent Operations
The operation queue's real power emerges when handling conflicts between collaborators. Consider this scenario:
**User A** deletes a block while **User B** has a pending subblock update for that same block in their operation queue.
```
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ User A │ │ Server │ │ User B │
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
│ │ │
│ Delete Block X │ │
├─────────────────────────────────>│ │
│ │ │
│ │ Persist deletion │
│ │ ────────────┐ │
│ │ │ │
│ │<─────────────┘ │
│ │ │
│ │ Broadcast: Block X deleted │
│ ├─────────────────────────────────>│
│ │ │
│ │ Cancel all ops for X │
│ │ (including subblock) │
│ │ ────────┤
│ │ │
│ │ Remove Block X │
│ │ ────────┤
│ │ │
```
Here's what happens:
1. User A's delete operation reaches the server and persists successfully
2. The server broadcasts the deletion to all clients, including User B
3. User B's client receives the broadcast and **immediately cancels all pending operations** for Block X (including the subblock update)
4. Then User B's client removes Block X from local state
No operations are sent to the server for a block that no longer exists. The client proactively removes all related operations from the queue—both block-level operations and subblock operations. User B never sees an error because the stale operation is silently discarded before it's sent.
This is more efficient than server-side validation. By canceling dependent operations locally when receiving a deletion broadcast, we avoid wasting network requests on operations that would fail anyway.
## Conflict Resolution: Timestamps and Determinism
In line with our goal of keeping things simple, Sim uses a **last-writer-wins** strategy with timestamp-based ordering. Every operation carries a client-generated timestamp. When conflicts occur, the operation with the latest timestamp takes precedence.
This is simpler than Figma's operational transform approach, but sufficient for our use case. Workflow building has lower conflict density than text editing—users typically work on different parts of the canvas or different blocks.
**Position conflicts** are handled with timestamp ordering. If two users simultaneously drag the same block, both clients render their local positions optimistically. The server persists both updates based on timestamps, broadcasting each in sequence. Clients receive the conflicting positions and converge to the latest timestamp.
**Value conflicts** (editing the same text field) are rarer but use last-to-arrive wins. Subblock updates are coalesced server-side within a 25ms window—whichever update reaches the server last within that window is persisted, regardless of client timestamp.
## Undo/Redo: A Thin Wrapper Over Sockets
Undo/redo in multiplayer environments is notoriously complex. Should undoing overwrite others' changes? What happens when you undo something someone else modified?
Sim takes a pragmatic approach: **undo/redo is a local, per-user stack that generates inverse operations sent through the same socket system as regular edits.**
### How It Works
Every operation you perform is recorded in a local undo stack with its inverse:
- **Add block** → Inverse: **Remove block** (with full block snapshot)
- **Remove block** → Inverse: **Add block** (restoring from snapshot)
- **Move block** → Inverse: **Move block** (with original position)
- **Update subblock** → Inverse: **Update subblock** (with previous value)
When you press Cmd+Z:
1. Pop the latest operation from your undo stack
2. Push it to your redo stack
3. Execute the inverse operation by queuing it through the operation queue
4. The inverse operation flows through the normal socket system: validation, persistence, broadcast
This means **undo is just another edit**. If you undo adding a block, Sim sends a "remove block" operation through the queue. Other users see the block disappear in real-time, as if you manually deleted it.
### Coalescing and Snapshots
Consecutive operations of the same type are coalesced. If you drag a block across the canvas in 50 small movements, only the starting and ending positions are recorded—pressing undo moves the block back to where you started dragging, not through every intermediate position.
For removal operations, we snapshot the complete state of the removed entity (including all subblock values and connected edges) at the time of removal. This snapshot travels with the undo entry. When you undo a deletion, we restore from the snapshot, ensuring perfect reconstruction even if the workflow structure changed in the interim.
### Multiplayer Undo Semantics
Undo stacks are **per-user**. Your undo history doesn't include others' changes. This matches user expectations: Cmd+Z undoes *your* recent actions, not your collaborator's.
The system prunes invalid operations from your stack when entities are deleted by collaborators. If User B has "add edge to Block X" in their undo stack, but User A deletes Block X, that undo entry becomes invalid and is automatically removed since the target block no longer exists.
An interesting case: you add a block, someone else connects an edge to it, and then you undo your addition. The block disappears along with their edge (because of foreign key constraints). This is correct—your block no longer exists, so edges referencing it can't exist either. Both users see the block and edge vanish.
During execution, undo operations are marked in-progress to prevent circular recording—undoing shouldn't create a new undo entry for the inverse operation itself.
## Conclusion
Building multiplayer workflow editing required rethinking assumptions about how workflow builders should work. By applying lessons from Figma's collaborative design tool to the domain of AI agent workflows, we created a system that feels fast, reliable, and natural for teams building together.
If you're building collaborative editing for structured data (not just text), consider:
- Whether OT/CRDT complexity is necessary for your conflict density
- How to separate high-frequency value updates from structural changes
- What guarantees your users need around data persistence and offline editing
- Whether exposing operation status builds trust in the system
Multiplayer workflow building is no longer a technical curiosity—it's how teams should work together to build AI agents. And the infrastructure to make it reliable and fast is more approachable than you might think.
---
*Interested in how Sim's multiplayer system works in practice? [Try building a workflow](https://sim.ai) with a collaborator in real-time.*

View File

@@ -8,8 +8,8 @@ authors:
- emir
readingTime: 9
tags: [AI Agents, Workflow Automation, OpenAI AgentKit, n8n, Sim, MCP]
ogImage: /blog/openai-vs-n8n-vs-sim/workflow.png
canonical: https://sim.ai/blog/openai-vs-n8n-vs-sim
ogImage: /studio/openai-vs-n8n-vs-sim/workflow.png
canonical: https://sim.ai/studio/openai-vs-n8n-vs-sim
draft: false
---
@@ -19,7 +19,7 @@ When building AI agent workflows, developers often evaluate multiple platforms t
OpenAI AgentKit is a set of building blocks designed to help developers take AI agents from prototype to production. Built on top of the OpenAI Responses API, it provides a structured approach to building and deploying intelligent agents.
![OpenAI AgentKit workflow interface](/blog/openai-vs-n8n-vs-sim/openai.png)
![OpenAI AgentKit workflow interface](/studio/openai-vs-n8n-vs-sim/openai.png)
### Core Features
@@ -31,7 +31,7 @@ AgentKit provides a visual canvas where developers can design and build agents.
ChatKit enables developers to embed chat interfaces to run workflows directly within their applications. It includes custom widgets that you can create and integrate, with the ability to preview interfaces right in the workflow builder before deployment.
![OpenAI AgentKit custom widgets interface](/blog/openai-vs-n8n-vs-sim/widgets.png)
![OpenAI AgentKit custom widgets interface](/studio/openai-vs-n8n-vs-sim/widgets.png)
#### Comprehensive Evaluation System
@@ -65,7 +65,7 @@ While AgentKit is powerful for building agents, it has some limitations:
n8n is a workflow automation platform that excels at connecting various services and APIs together. While it started as a general automation tool, n8n has evolved to support AI agent workflows alongside its traditional integration capabilities.
![n8n workflow automation interface](/blog/openai-vs-n8n-vs-sim/n8n.png)
![n8n workflow automation interface](/studio/openai-vs-n8n-vs-sim/n8n.png)
### Core Capabilities
@@ -117,19 +117,19 @@ Sim is a fully open-source platform (Apache 2.0 license) specifically designed f
Sim provides an intuitive drag-and-drop canvas where developers can build complex AI agent workflows visually. The platform supports sophisticated agent architectures, including multi-agent systems, conditional logic, loops, and parallel execution paths. Additionally, Sim's built-in AI Copilot can assist you directly in the editor, helping you build and modify workflows faster with intelligent suggestions and explanations.
![Sim visual workflow builder with AI agent blocks](/blog/openai-vs-n8n-vs-sim/sim.png)
![Sim visual workflow builder with AI agent blocks](/studio/openai-vs-n8n-vs-sim/sim.png)
#### AI Copilot for Workflow Building
Sim includes an intelligent in-editor AI assistant that helps you build and edit workflows faster. Copilot can explain complex concepts, suggest best practices, and even make changes to your workflow when you approve them. Using the @ context menu, you can reference workflows, blocks, knowledge bases, documentation, templates, and execution logs—giving Copilot the full context it needs to provide accurate, relevant assistance. This dramatically accelerates workflow development compared to building from scratch.
![Sim AI Copilot assisting with workflow development](/blog/openai-vs-n8n-vs-sim/copilot.png)
![Sim AI Copilot assisting with workflow development](/studio/openai-vs-n8n-vs-sim/copilot.png)
#### Pre-Built Workflow Templates
Get started quickly with Sim's extensive library of pre-built workflow templates. Browse templates across categories like Marketing, Sales, Finance, Support, and Artificial Intelligence. Each template is a production-ready workflow you can customize for your needs, saving hours of development time. Templates are created by the Sim team and community members, with popularity ratings and integration counts to help you find the right starting point.
![Sim workflow templates gallery](/blog/openai-vs-n8n-vs-sim/templates.png)
![Sim workflow templates gallery](/studio/openai-vs-n8n-vs-sim/templates.png)
#### 80+ Built-in Integrations
@@ -155,7 +155,7 @@ Sim's native knowledge base goes far beyond simple document storage. Powered by
Sim provides enterprise-grade logging that captures every detail of workflow execution. Track workflow runs with execution IDs, view block-level logs with precise timing and duration metrics, monitor token usage and costs per execution, and debug failures with detailed error traces and trace spans. The logging system integrates with Copilot—you can reference execution logs directly in your Copilot conversations to understand what happened and troubleshoot issues. This level of observability is essential for production AI agents where understanding behavior and debugging issues quickly is critical.
![Sim execution logs and monitoring dashboard](/blog/openai-vs-n8n-vs-sim/logs.png)
![Sim execution logs and monitoring dashboard](/studio/openai-vs-n8n-vs-sim/logs.png)
#### Custom Integrations via MCP Protocol

View File

@@ -0,0 +1,63 @@
---
slug: series-a
title: '$7M Series A'
description: Were excited to share that Sim has raised a $7M Series A led by Standard Capital to accelerate our vision for agentic workflows.
date: 2025-11-12
updated: 2025-11-12
authors:
- waleed
- emir
readingTime: 4
tags: [Announcement, Funding, Series A, Sim]
ogImage: /studio/series-a/cover.png
ogAlt: 'Sim team photo in front of neon logo'
about: ['Artificial Intelligence', 'Agentic Workflows', 'Startups', 'Funding']
timeRequired: PT4M
canonical: https://sim.ai/studio/series-a
featured: true
draft: false
---
![Sim team photo](/studio/series-a/team.png)
## Why were excited
Today were announcing our $7M Series A led by Standard Capital with participation from Perplexity Fund, SV Angel, YCombinator, and notable angels like Paul Graham, Paul Bucheit, Ali Rowghani, Kaz Nejatian, and many more.
This investment helps us doubledown on our mission: make it simple for teams to build, ship, and scale agentic workflows in production.
## How we got here
We started earlier this year in our apartment in San Francisco.
The goal was to rebuild our entire previous company (if you can call it that) from scratch on a visual framework.
We figured that if we could at least build an AI sales and marketing operation solely using building blocks on a canvas, then anyone could build anything.
Soon after, we'd built the foundation of what would become Sim.
We were hellbent on being Open Source from day one, and we're proud that we've stuck to that commitment.
## Progress so far
In just a short few months, Sim has grown from 0->18k Github stars, 60,000+ developers on the platform, and 100+ companies ranging from startups to large enterprises using Sim in production.
We've processed over 10M+ workflows and are growing rapidly.
We've built a stellar team of engineers who are passionate about building the future of agentic workflows.
## Our vision
We believe the next wave of software is agentic.
Teams will compose specialized agents that reason, retrieve, and act—safely and reliably—across their business.
Our focus is to provide the infrastructure and UX that make this practical at scale: from prototyping to production, from singleagent flows to complex multiagent systems.
On one end of the spectrum, there are SDKs and frameworks that are complex and require a lot of code to build and manage, and on the other end of the spectrum, there are platforms that are easy to use but severely limit in what you can build.
Sim offers a platform that is both easy to use and powerful enough to build complex agentic workflows.
If you strip away the applications, workflows are all that's left. They're the foundation of the software industry, and they're the foundation of the future powered by Sim.
## Whats next
Well invest in building the community around Sim, and we'll continue to be relentlessly focused on building the best platform for agentic workflows.
## Were hiring
If youre excited about agentic systems and want to help define the future of this space, wed love to talk. Were hiring across engineering, engineering, and more engineering. Oh, and design. [Apply here](https://sim.ai/careers)
— Team Sim

View File

@@ -0,0 +1,12 @@
'use client'
import { Code } from '@/components/emcn/components/code/code'
interface CodeBlockProps {
code: string
language: 'javascript' | 'json' | 'python'
}
export function CodeBlock({ code, language }: CodeBlockProps) {
return <Code.Viewer code={code} showGutter={true} language={language} />
}

View File

@@ -1,6 +1,7 @@
import clsx from 'clsx'
import Image from 'next/image'
import type { MDXRemoteProps } from 'next-mdx-remote/rsc'
import { CodeBlock } from './code'
export const mdxComponents: MDXRemoteProps['components'] = {
img: (props: any) => (
@@ -9,7 +10,7 @@ export const mdxComponents: MDXRemoteProps['components'] = {
alt={props.alt || ''}
width={props.width ? Number(props.width) : 800}
height={props.height ? Number(props.height) : 450}
className={clsx('w-full rounded-lg', props.className)}
className={clsx('h-auto w-full rounded-lg', props.className)}
/>
),
h2: (props: any) => (
@@ -59,7 +60,78 @@ export const mdxComponents: MDXRemoteProps['components'] = {
),
li: (props: any) => <li {...props} className={clsx('mb-2', props.className)} />,
strong: (props: any) => <strong {...props} className={clsx('font-semibold', props.className)} />,
em: (props: any) => <em {...props} className={clsx('italic', props.className)} />,
a: (props: any) => {
const isAnchorLink = props.className?.includes('anchor')
if (isAnchorLink) {
return <a {...props} />
}
return (
<a
{...props}
className={clsx(
'font-medium text-[#33B4FF] underline hover:text-[#2A9FE8]',
props.className
)}
/>
)
},
figure: (props: any) => (
<figure {...props} className={clsx('my-8 overflow-hidden rounded-lg', props.className)} />
),
hr: (props: any) => (
<hr
{...props}
className={clsx('my-8 border-gray-200', props.className)}
style={{ marginBottom: '1.5rem' }}
/>
),
pre: (props: any) => {
const child = props.children
const isCodeBlock = child && typeof child === 'object' && child.props
if (isCodeBlock) {
const codeContent = child.props.children || ''
const className = child.props.className || ''
const language = className.replace('language-', '') || 'javascript'
const languageMap: Record<string, 'javascript' | 'json' | 'python'> = {
js: 'javascript',
jsx: 'javascript',
ts: 'javascript',
tsx: 'javascript',
typescript: 'javascript',
javascript: 'javascript',
json: 'json',
python: 'python',
py: 'python',
}
const mappedLanguage = languageMap[language.toLowerCase()] || 'javascript'
return (
<div className='my-6'>
<CodeBlock
code={typeof codeContent === 'string' ? codeContent.trim() : String(codeContent)}
language={mappedLanguage}
/>
</div>
)
}
return <pre {...props} className={clsx('my-4 overflow-x-auto rounded-lg', props.className)} />
},
code: (props: any) => {
if (!props.className) {
return (
<code
{...props}
className={clsx(
'rounded bg-gray-100 px-1.5 py-0.5 font-mono text-[0.9em] text-red-600',
props.className
)}
/>
)
}
return <code {...props} />
},
}

View File

@@ -75,6 +75,7 @@ async function scanFrontmatters(): Promise<BlogMeta[]> {
timeRequired: fm.timeRequired,
faq: fm.faq,
draft: fm.draft,
featured: fm.featured ?? false,
})
}
cachedMeta = results.sort(byDateDesc)

View File

@@ -36,6 +36,7 @@ export const BlogFrontmatterSchema = z
.optional(),
canonical: z.string().url(),
draft: z.boolean().default(false),
featured: z.boolean().default(false),
})
.strict()
@@ -58,6 +59,7 @@ export interface BlogMeta {
faq?: { q: string; a: string }[]
canonical: string
draft: boolean
featured: boolean
sourcePath?: string
}

View File

@@ -104,7 +104,7 @@ export function buildBreadcrumbJsonLd(post: BlogMeta) {
'@type': 'BreadcrumbList',
itemListElement: [
{ '@type': 'ListItem', position: 1, name: 'Home', item: 'https://sim.ai' },
{ '@type': 'ListItem', position: 2, name: 'Blog', item: 'https://sim.ai/blog' },
{ '@type': 'ListItem', position: 2, name: 'Sim Studio', item: 'https://sim.ai/studio' },
{ '@type': 'ListItem', position: 3, name: post.title, item: post.canonical },
],
}
@@ -127,8 +127,8 @@ export function buildBlogJsonLd() {
return {
'@context': 'https://schema.org',
'@type': 'Blog',
name: 'Sim Blog',
url: 'https://sim.ai/blog',
name: 'Sim Studio',
url: 'https://sim.ai/studio',
description: 'Announcements, insights, and guides for building AI agent workflows.',
}
}

View File

@@ -1,7 +1,7 @@
/**
* Environment utility functions for consistent environment detection across the application
*/
import { env, getEnv, isTruthy } from './env'
import { env, isTruthy } from './env'
/**
* Is the application running in production mode
@@ -21,9 +21,7 @@ export const isTest = env.NODE_ENV === 'test'
/**
* Is this the hosted version of the application
*/
export const isHosted =
getEnv('NEXT_PUBLIC_APP_URL') === 'https://www.sim.ai' ||
getEnv('NEXT_PUBLIC_APP_URL') === 'https://www.staging.sim.ai'
export const isHosted = true
/**
* Is billing enforcement enabled

View File

@@ -209,23 +209,30 @@ const nextConfig: NextConfig = {
async redirects() {
const redirects = []
// Redirect /building to /blog (legacy URL support)
redirects.push({
source: '/building/:path*',
destination: '/blog/:path*',
permanent: true,
})
// Redirect /building and /blog to /studio (legacy URL support)
redirects.push(
{
source: '/building/:path*',
destination: 'https://sim.ai/studio/:path*',
permanent: true,
},
{
source: '/blog/:path*',
destination: 'https://sim.ai/studio/:path*',
permanent: true,
}
)
// Move root feeds to blog namespace
// Move root feeds to studio namespace
redirects.push(
{
source: '/rss.xml',
destination: '/blog/rss.xml',
destination: '/studio/rss.xml',
permanent: true,
},
{
source: '/sitemap-images.xml',
destination: '/blog/sitemap-images.xml',
destination: '/studio/sitemap-images.xml',
permanent: true,
}
)

View File

Before

Width:  |  Height:  |  Size: 2.0 MiB

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 583 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 217 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 963 KiB

View File

Before

Width:  |  Height:  |  Size: 487 KiB

After

Width:  |  Height:  |  Size: 487 KiB

View File

Before

Width:  |  Height:  |  Size: 234 KiB

After

Width:  |  Height:  |  Size: 234 KiB

View File

Before

Width:  |  Height:  |  Size: 657 KiB

After

Width:  |  Height:  |  Size: 657 KiB

View File

Before

Width:  |  Height:  |  Size: 148 KiB

After

Width:  |  Height:  |  Size: 148 KiB

View File

Before

Width:  |  Height:  |  Size: 301 KiB

After

Width:  |  Height:  |  Size: 301 KiB

View File

Before

Width:  |  Height:  |  Size: 338 KiB

After

Width:  |  Height:  |  Size: 338 KiB

View File

Before

Width:  |  Height:  |  Size: 863 KiB

After

Width:  |  Height:  |  Size: 863 KiB

View File

Before

Width:  |  Height:  |  Size: 325 KiB

After

Width:  |  Height:  |  Size: 325 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1019 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 MiB