diff --git a/apps/sim/app/(landing)/studio/post-grid.tsx b/apps/sim/app/(landing)/studio/post-grid.tsx
new file mode 100644
index 0000000000..22c49032bc
--- /dev/null
+++ b/apps/sim/app/(landing)/studio/post-grid.tsx
@@ -0,0 +1,90 @@
+'use client'
+
+import Image from 'next/image'
+import Link from 'next/link'
+import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
+
+interface Author {
+ id: string
+ name: string
+ avatarUrl?: string
+ url?: string
+}
+
+interface Post {
+ slug: string
+ title: string
+ description: string
+ date: string
+ ogImage: string
+ author: Author
+ authors?: Author[]
+ featured?: boolean
+}
+
+export function PostGrid({ posts }: { posts: Post[] }) {
+ return (
+
+ {posts.map((p, index) => (
+
+
+ {/* Image container with fixed aspect ratio to prevent layout shift */}
+
+
+
+
+
+ {new Date(p.date).toLocaleDateString('en-US', {
+ month: 'short',
+ day: 'numeric',
+ year: 'numeric',
+ })}
+
+
{p.title}
+
{p.description}
+
+
+ {(p.authors && p.authors.length > 0 ? p.authors : [p.author])
+ .slice(0, 3)
+ .map((author, idx) => (
+
+
+
+ {author?.name.slice(0, 2)}
+
+
+ ))}
+
+
+ {(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'
+ : ''}
+ >
+ )}
+
+
+
+
+
+ ))}
+
+ )
+}
diff --git a/apps/sim/app/_shell/providers/theme-provider.tsx b/apps/sim/app/_shell/providers/theme-provider.tsx
index dd1564e020..a6bc5444e0 100644
--- a/apps/sim/app/_shell/providers/theme-provider.tsx
+++ b/apps/sim/app/_shell/providers/theme-provider.tsx
@@ -12,6 +12,7 @@ export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
pathname === '/' ||
pathname.startsWith('/login') ||
pathname.startsWith('/signup') ||
+ pathname.startsWith('/reset-password') ||
pathname.startsWith('/sso') ||
pathname.startsWith('/terms') ||
pathname.startsWith('/privacy') ||
diff --git a/apps/sim/app/_styles/globals.css b/apps/sim/app/_styles/globals.css
index d51362e9f6..f67e27e375 100644
--- a/apps/sim/app/_styles/globals.css
+++ b/apps/sim/app/_styles/globals.css
@@ -759,3 +759,24 @@ input[type="search"]::-ms-clear {
--surface-elevated: #202020;
}
}
+
+/**
+ * Remove backticks from inline code in prose (Tailwind Typography default)
+ */
+.prose code::before,
+.prose code::after {
+ content: none !important;
+}
+
+/**
+ * Remove underlines from heading anchor links in prose
+ */
+.prose h1 a,
+.prose h2 a,
+.prose h3 a,
+.prose h4 a,
+.prose h5 a,
+.prose h6 a {
+ text-decoration: none !important;
+ color: inherit !important;
+}
diff --git a/apps/sim/app/api/auth/accounts/route.ts b/apps/sim/app/api/auth/accounts/route.ts
index 418a04c027..5739586c38 100644
--- a/apps/sim/app/api/auth/accounts/route.ts
+++ b/apps/sim/app/api/auth/accounts/route.ts
@@ -32,7 +32,17 @@ export async function GET(request: NextRequest) {
.from(account)
.where(and(...whereConditions))
- return NextResponse.json({ accounts })
+ // Use the user's email as the display name (consistent with credential selector)
+ const userEmail = session.user.email
+
+ const accountsWithDisplayName = accounts.map((acc) => ({
+ id: acc.id,
+ accountId: acc.accountId,
+ providerId: acc.providerId,
+ displayName: userEmail || acc.providerId,
+ }))
+
+ return NextResponse.json({ accounts: accountsWithDisplayName })
} catch (error) {
logger.error('Failed to fetch accounts', { error })
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
diff --git a/apps/sim/app/api/auth/forget-password/route.test.ts b/apps/sim/app/api/auth/forget-password/route.test.ts
index 1f70d3111c..8dc57e18e3 100644
--- a/apps/sim/app/api/auth/forget-password/route.test.ts
+++ b/apps/sim/app/api/auth/forget-password/route.test.ts
@@ -6,6 +6,10 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { createMockRequest, setupAuthApiMocks } from '@/app/api/__test-utils__/utils'
+vi.mock('@/lib/core/utils/urls', () => ({
+ getBaseUrl: vi.fn(() => 'https://app.example.com'),
+}))
+
describe('Forget Password API Route', () => {
beforeEach(() => {
vi.resetModules()
@@ -15,7 +19,7 @@ describe('Forget Password API Route', () => {
vi.clearAllMocks()
})
- it('should send password reset email successfully', async () => {
+ it('should send password reset email successfully with same-origin redirectTo', async () => {
setupAuthApiMocks({
operations: {
forgetPassword: { success: true },
@@ -24,7 +28,7 @@ describe('Forget Password API Route', () => {
const req = createMockRequest('POST', {
email: 'test@example.com',
- redirectTo: 'https://example.com/reset',
+ redirectTo: 'https://app.example.com/reset',
})
const { POST } = await import('@/app/api/auth/forget-password/route')
@@ -39,12 +43,36 @@ describe('Forget Password API Route', () => {
expect(auth.auth.api.forgetPassword).toHaveBeenCalledWith({
body: {
email: 'test@example.com',
- redirectTo: 'https://example.com/reset',
+ redirectTo: 'https://app.example.com/reset',
},
method: 'POST',
})
})
+ it('should reject external redirectTo URL', async () => {
+ setupAuthApiMocks({
+ operations: {
+ forgetPassword: { success: true },
+ },
+ })
+
+ const req = createMockRequest('POST', {
+ email: 'test@example.com',
+ redirectTo: 'https://evil.com/phishing',
+ })
+
+ const { POST } = await import('@/app/api/auth/forget-password/route')
+
+ const response = await POST(req)
+ const data = await response.json()
+
+ expect(response.status).toBe(400)
+ expect(data.message).toBe('Redirect URL must be a valid same-origin URL')
+
+ const auth = await import('@/lib/auth')
+ expect(auth.auth.api.forgetPassword).not.toHaveBeenCalled()
+ })
+
it('should send password reset email without redirectTo', async () => {
setupAuthApiMocks({
operations: {
diff --git a/apps/sim/app/api/auth/forget-password/route.ts b/apps/sim/app/api/auth/forget-password/route.ts
index b1fbf03bbc..6844c40c61 100644
--- a/apps/sim/app/api/auth/forget-password/route.ts
+++ b/apps/sim/app/api/auth/forget-password/route.ts
@@ -1,6 +1,7 @@
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { auth } from '@/lib/auth'
+import { isSameOrigin } from '@/lib/core/utils/validation'
import { createLogger } from '@/lib/logs/console/logger'
export const dynamic = 'force-dynamic'
@@ -13,10 +14,15 @@ const forgetPasswordSchema = z.object({
.email('Please provide a valid email address'),
redirectTo: z
.string()
- .url('Redirect URL must be a valid URL')
.optional()
.or(z.literal(''))
- .transform((val) => (val === '' ? undefined : val)),
+ .transform((val) => (val === '' || val === undefined ? undefined : val))
+ .refine(
+ (val) => val === undefined || (z.string().url().safeParse(val).success && isSameOrigin(val)),
+ {
+ message: 'Redirect URL must be a valid same-origin URL',
+ }
+ ),
})
export async function POST(request: NextRequest) {
diff --git a/apps/sim/app/api/workflows/[id]/execute/route.ts b/apps/sim/app/api/workflows/[id]/execute/route.ts
index 2b9cd8beaf..0500887873 100644
--- a/apps/sim/app/api/workflows/[id]/execute/route.ts
+++ b/apps/sim/app/api/workflows/[id]/execute/route.ts
@@ -11,6 +11,7 @@ import { processInputFileFields } from '@/lib/execution/files'
import { preprocessExecution } from '@/lib/execution/preprocessing'
import { createLogger } from '@/lib/logs/console/logger'
import { LoggingSession } from '@/lib/logs/execution/logging-session'
+import { ALL_TRIGGER_TYPES } from '@/lib/logs/types'
import { executeWorkflowCore } from '@/lib/workflows/executor/execution-core'
import { type ExecutionEvent, encodeSSEEvent } from '@/lib/workflows/executor/execution-events'
import { PauseResumeManager } from '@/lib/workflows/executor/human-in-the-loop-manager'
@@ -30,7 +31,7 @@ const logger = createLogger('WorkflowExecuteAPI')
const ExecuteWorkflowSchema = z.object({
selectedOutputs: z.array(z.string()).optional().default([]),
- triggerType: z.enum(['api', 'webhook', 'schedule', 'manual', 'chat']).optional(),
+ triggerType: z.enum(ALL_TRIGGER_TYPES).optional(),
stream: z.boolean().optional(),
useDraftState: z.boolean().optional(),
input: z.any().optional(),
diff --git a/apps/sim/app/api/workspaces/[id]/notifications/[notificationId]/route.ts b/apps/sim/app/api/workspaces/[id]/notifications/[notificationId]/route.ts
index a7bca617d1..799d148a64 100644
--- a/apps/sim/app/api/workspaces/[id]/notifications/[notificationId]/route.ts
+++ b/apps/sim/app/api/workspaces/[id]/notifications/[notificationId]/route.ts
@@ -6,13 +6,14 @@ import { z } from 'zod'
import { getSession } from '@/lib/auth'
import { encryptSecret } from '@/lib/core/security/encryption'
import { createLogger } from '@/lib/logs/console/logger'
+import { ALL_TRIGGER_TYPES } from '@/lib/logs/types'
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
import { MAX_EMAIL_RECIPIENTS, MAX_WORKFLOW_IDS } from '../constants'
const logger = createLogger('WorkspaceNotificationAPI')
const levelFilterSchema = z.array(z.enum(['info', 'error']))
-const triggerFilterSchema = z.array(z.enum(['api', 'webhook', 'schedule', 'manual', 'chat']))
+const triggerFilterSchema = z.array(z.enum(ALL_TRIGGER_TYPES))
const alertRuleSchema = z.enum([
'consecutive_failures',
diff --git a/apps/sim/app/api/workspaces/[id]/notifications/route.ts b/apps/sim/app/api/workspaces/[id]/notifications/route.ts
index 9eb99ed5fe..b1aa69ae0a 100644
--- a/apps/sim/app/api/workspaces/[id]/notifications/route.ts
+++ b/apps/sim/app/api/workspaces/[id]/notifications/route.ts
@@ -7,6 +7,7 @@ import { z } from 'zod'
import { getSession } from '@/lib/auth'
import { encryptSecret } from '@/lib/core/security/encryption'
import { createLogger } from '@/lib/logs/console/logger'
+import { ALL_TRIGGER_TYPES } from '@/lib/logs/types'
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
import { MAX_EMAIL_RECIPIENTS, MAX_NOTIFICATIONS_PER_TYPE, MAX_WORKFLOW_IDS } from './constants'
@@ -14,7 +15,7 @@ const logger = createLogger('WorkspaceNotificationsAPI')
const notificationTypeSchema = z.enum(['webhook', 'email', 'slack'])
const levelFilterSchema = z.array(z.enum(['info', 'error']))
-const triggerFilterSchema = z.array(z.enum(['api', 'webhook', 'schedule', 'manual', 'chat']))
+const triggerFilterSchema = z.array(z.enum(ALL_TRIGGER_TYPES))
const alertRuleSchema = z.enum([
'consecutive_failures',
@@ -80,7 +81,7 @@ const createNotificationSchema = z
workflowIds: z.array(z.string()).max(MAX_WORKFLOW_IDS).default([]),
allWorkflows: z.boolean().default(false),
levelFilter: levelFilterSchema.default(['info', 'error']),
- triggerFilter: triggerFilterSchema.default(['api', 'webhook', 'schedule', 'manual', 'chat']),
+ triggerFilter: triggerFilterSchema.default([...ALL_TRIGGER_TYPES]),
includeFinalOutput: z.boolean().default(false),
includeTraceSpans: z.boolean().default(false),
includeRateLimits: z.boolean().default(false),
diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/logs-toolbar/components/notifications/components/slack-channel-selector/slack-channel-selector.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/logs-toolbar/components/notifications/components/slack-channel-selector/slack-channel-selector.tsx
index 67583aa340..0370641997 100644
--- a/apps/sim/app/workspace/[workspaceId]/logs/components/logs-toolbar/components/notifications/components/slack-channel-selector/slack-channel-selector.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/logs/components/logs-toolbar/components/notifications/components/slack-channel-selector/slack-channel-selector.tsx
@@ -104,6 +104,8 @@ export function SlackChannelSelector({
disabled={disabled || channels.length === 0}
isLoading={isLoading}
error={fetchError}
+ searchable
+ searchPlaceholder='Search channels...'
/>
{selectedChannel && !fetchError && (
diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/logs-toolbar/components/notifications/notifications.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/logs-toolbar/components/notifications/notifications.tsx
index a112df9e6c..81bedb0393 100644
--- a/apps/sim/app/workspace/[workspaceId]/logs/components/logs-toolbar/components/notifications/notifications.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/logs/components/logs-toolbar/components/notifications/notifications.tsx
@@ -22,6 +22,7 @@ import { SlackIcon } from '@/components/icons'
import { Skeleton } from '@/components/ui'
import { cn } from '@/lib/core/utils/cn'
import { createLogger } from '@/lib/logs/console/logger'
+import { ALL_TRIGGER_TYPES, type TriggerType } from '@/lib/logs/types'
import { quickValidateEmail } from '@/lib/messaging/email/validation'
import {
type NotificationSubscription,
@@ -43,7 +44,6 @@ const PRIMARY_BUTTON_STYLES =
type NotificationType = 'webhook' | 'email' | 'slack'
type LogLevel = 'info' | 'error'
-type TriggerType = 'api' | 'webhook' | 'schedule' | 'manual' | 'chat'
type AlertRule =
| 'none'
| 'consecutive_failures'
@@ -84,7 +84,6 @@ interface NotificationSettingsProps {
}
const LOG_LEVELS: LogLevel[] = ['info', 'error']
-const TRIGGER_TYPES: TriggerType[] = ['api', 'webhook', 'schedule', 'manual', 'chat']
function formatAlertConfigLabel(config: {
rule: AlertRule
@@ -137,7 +136,7 @@ export function NotificationSettings({
workflowIds: [] as string[],
allWorkflows: true,
levelFilter: ['info', 'error'] as LogLevel[],
- triggerFilter: ['api', 'webhook', 'schedule', 'manual', 'chat'] as TriggerType[],
+ triggerFilter: [...ALL_TRIGGER_TYPES] as TriggerType[],
includeFinalOutput: false,
includeTraceSpans: false,
includeRateLimits: false,
@@ -207,7 +206,7 @@ export function NotificationSettings({
workflowIds: [],
allWorkflows: true,
levelFilter: ['info', 'error'],
- triggerFilter: ['api', 'webhook', 'schedule', 'manual', 'chat'],
+ triggerFilter: [...ALL_TRIGGER_TYPES],
includeFinalOutput: false,
includeTraceSpans: false,
includeRateLimits: false,
@@ -768,7 +767,7 @@ export function NotificationSettings({
({
value: acc.id,
- label: acc.accountId,
+ label: acc.displayName || 'Slack Workspace',
}))}
value={formData.slackAccountId}
onChange={(value) => {
@@ -859,7 +858,7 @@ export function NotificationSettings({
({
+ options={ALL_TRIGGER_TYPES.map((trigger) => ({
label: trigger.charAt(0).toUpperCase() + trigger.slice(1),
value: trigger,
}))}
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/tool-call/tool-call.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/tool-call/tool-call.tsx
index 40600da75b..0c61ea0b8a 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/tool-call/tool-call.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/tool-call/tool-call.tsx
@@ -101,6 +101,9 @@ const ACTION_VERBS = [
'Generated',
'Rendering',
'Rendered',
+ 'Sleeping',
+ 'Slept',
+ 'Resumed',
] as const
/**
@@ -580,6 +583,11 @@ export function ToolCall({ toolCall: toolCallProp, toolCallId, onStateChange }:
(toolCall.state === (ClientToolCallState.executing as any) ||
toolCall.state === ('executing' as any))
+ const showWake =
+ toolCall.name === 'sleep' &&
+ (toolCall.state === (ClientToolCallState.executing as any) ||
+ toolCall.state === ('executing' as any))
+
const handleStateChange = (state: any) => {
forceUpdate({})
onStateChange?.(state)
@@ -1102,6 +1110,37 @@ export function ToolCall({ toolCall: toolCallProp, toolCallId, onStateChange }:
Move to Background
+ ) : showWake ? (
+
+
+
) : null}
)
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/short-input/short-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/short-input/short-input.tsx
index 25d59184cc..3bca388f34 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/short-input/short-input.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/short-input/short-input.tsx
@@ -90,6 +90,7 @@ export function ShortInput({
blockId,
triggerId: undefined,
isPreview,
+ useWebhookUrl,
})
const wandHook = useWand({
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/trigger-save/trigger-save.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/trigger-save/trigger-save.tsx
index 3d29b1b921..ab9f43f080 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/trigger-save/trigger-save.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/trigger-save/trigger-save.tsx
@@ -74,6 +74,7 @@ export function TriggerSave({
blockId,
triggerId: effectiveTriggerId,
isPreview,
+ useWebhookUrl: true, // to store the webhook url in the store
})
const triggerConfig = useSubBlockStore((state) => state.getValue(blockId, 'triggerConfig'))
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-node-utilities.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-node-utilities.ts
index cbb8c9e113..6f5de152a1 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-node-utilities.ts
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-node-utilities.ts
@@ -6,6 +6,61 @@ import { getBlock } from '@/blocks/registry'
const logger = createLogger('NodeUtilities')
+/**
+ * Estimates block dimensions based on block type.
+ * Uses subblock count to estimate height for blocks that haven't been measured yet.
+ *
+ * @param blockType - The type of block (e.g., 'condition', 'agent')
+ * @returns Estimated width and height for the block
+ */
+export function estimateBlockDimensions(blockType: string): { width: number; height: number } {
+ const blockConfig = getBlock(blockType)
+ const subBlockCount = blockConfig?.subBlocks?.length ?? 3
+ // Many subblocks are conditionally rendered (advanced mode, provider-specific, etc.)
+ // Use roughly half the config count as a reasonable estimate, capped between 3-7 rows
+ const estimatedRows = Math.max(3, Math.min(Math.ceil(subBlockCount / 2), 7))
+ const hasErrorRow = blockType !== 'starter' && blockType !== 'response' ? 1 : 0
+
+ const height =
+ BLOCK_DIMENSIONS.HEADER_HEIGHT +
+ BLOCK_DIMENSIONS.WORKFLOW_CONTENT_PADDING +
+ (estimatedRows + hasErrorRow) * BLOCK_DIMENSIONS.WORKFLOW_ROW_HEIGHT
+
+ return {
+ width: BLOCK_DIMENSIONS.FIXED_WIDTH,
+ height: Math.max(height, BLOCK_DIMENSIONS.MIN_HEIGHT),
+ }
+}
+
+/**
+ * Clamps a position to keep a block fully inside a container's content area.
+ * Content area starts after the header and padding, and ends before the right/bottom padding.
+ *
+ * @param position - Raw position relative to container origin
+ * @param containerDimensions - Container width and height
+ * @param blockDimensions - Block width and height
+ * @returns Clamped position that keeps block inside content area
+ */
+export function clampPositionToContainer(
+ position: { x: number; y: number },
+ containerDimensions: { width: number; height: number },
+ blockDimensions: { width: number; height: number }
+): { x: number; y: number } {
+ const { width: containerWidth, height: containerHeight } = containerDimensions
+ const { width: blockWidth, height: blockHeight } = blockDimensions
+
+ // Content area bounds (where blocks can be placed)
+ const minX = CONTAINER_DIMENSIONS.LEFT_PADDING
+ const minY = CONTAINER_DIMENSIONS.HEADER_HEIGHT + CONTAINER_DIMENSIONS.TOP_PADDING
+ const maxX = containerWidth - CONTAINER_DIMENSIONS.RIGHT_PADDING - blockWidth
+ const maxY = containerHeight - CONTAINER_DIMENSIONS.BOTTOM_PADDING - blockHeight
+
+ return {
+ x: Math.max(minX, Math.min(position.x, Math.max(minX, maxX))),
+ y: Math.max(minY, Math.min(position.y, Math.max(minY, maxY))),
+ }
+}
+
/**
* Hook providing utilities for node position, hierarchy, and dimension calculations
*/
@@ -21,7 +76,7 @@ export function useNodeUtilities(blocks: Record