Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
UserCircle,
Users,
} from 'lucide-react'
import { isBillingEnabled } from '@/lib/environment'
import { getEnv } from '@/lib/env'
import { cn } from '@/lib/utils'
import { useSubscriptionStore } from '@/stores/subscription/store'

Expand Down Expand Up @@ -98,6 +98,9 @@ export function SettingsNavigation({
const { getSubscriptionStatus } = useSubscriptionStore()
const subscription = getSubscriptionStatus()

// Get billing status
const isBillingEnabled = getEnv('NEXT_PUBLIC_BILLING_ENABLED') || false
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: getEnv returns string | undefined but you're using || false which could result in truthy strings like 'false' being treated as true. Consider using the isTruthy utility from @/lib/env for proper boolean conversion.


const navigationItems = allNavigationItems.filter((item) => {
if (item.hideWhenBillingDisabled && !isBillingEnabled) {
return false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { useEffect, useRef, useState } from 'react'
import { X } from 'lucide-react'
import { Button, Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui'
import { isBillingEnabled } from '@/lib/environment'
import { getEnv } from '@/lib/env'
import { createLogger } from '@/lib/logs/console/logger'
import { cn } from '@/lib/utils'
import {
Expand Down Expand Up @@ -44,6 +44,9 @@ export function SettingsModal({ open, onOpenChange }: SettingsModalProps) {
const { activeOrganization } = useOrganizationStore()
const hasLoadedInitialData = useRef(false)

// Get billing status
const isBillingEnabled = getEnv('NEXT_PUBLIC_BILLING_ENABLED') || false

useEffect(() => {
async function loadAllSettings() {
if (!open) return
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { HelpCircle, LibraryBig, ScrollText, Search, Settings, Shapes } from 'lu
import { useParams, usePathname, useRouter } from 'next/navigation'
import { Button, ScrollArea, Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui'
import { useSession } from '@/lib/auth-client'
import { isBillingEnabled } from '@/lib/environment'
import { getEnv } from '@/lib/env'
import { createLogger } from '@/lib/logs/console/logger'
import { generateWorkspaceName } from '@/lib/naming'
import { cn } from '@/lib/utils'
Expand Down Expand Up @@ -195,6 +195,9 @@ export function Sidebar() {
const userPermissions = useUserPermissionsContext()
const isLoading = workflowsLoading || sessionLoading

// Get billing status
const isBillingEnabled = getEnv('NEXT_PUBLIC_BILLING_ENABLED') || false
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: Type safety concern: getEnv() returns string | undefined, but isBillingEnabled is used as boolean. Consider using a proper boolean conversion like getEnv('NEXT_PUBLIC_BILLING_ENABLED') === 'true' instead of relying on falsy check with || false.

Suggested change
const isBillingEnabled = getEnv('NEXT_PUBLIC_BILLING_ENABLED') || false
const isBillingEnabled = getEnv('NEXT_PUBLIC_BILLING_ENABLED') === 'true'


// Add state to prevent multiple simultaneous workflow creations
const [isCreatingWorkflow, setIsCreatingWorkflow] = useState(false)
// Add state to prevent multiple simultaneous workspace creations
Expand Down
37 changes: 12 additions & 25 deletions apps/sim/executor/handlers/workflow/workflow-handler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,13 +111,9 @@ describe('WorkflowBlockHandler', () => {
'parent-workflow-id_sub_child-workflow-id_workflow-block-1'
)

const result = await handler.execute(mockBlock, inputs, mockContext)
expect(result).toEqual({
success: false,
error:
'Cyclic workflow dependency detected: parent-workflow-id_sub_child-workflow-id_workflow-block-1',
childWorkflowName: 'child-workflow-id',
})
await expect(handler.execute(mockBlock, inputs, mockContext)).rejects.toThrow(
'Error in child workflow "child-workflow-id": Cyclic workflow dependency detected: parent-workflow-id_sub_child-workflow-id_workflow-block-1'
)
})

it('should enforce maximum depth limit', async () => {
Expand All @@ -130,12 +126,9 @@ describe('WorkflowBlockHandler', () => {
'level1_sub_level2_sub_level3_sub_level4_sub_level5_sub_level6_sub_level7_sub_level8_sub_level9_sub_level10_sub_level11',
}

const result = await handler.execute(mockBlock, inputs, deepContext)
expect(result).toEqual({
success: false,
error: 'Maximum workflow nesting depth of 10 exceeded',
childWorkflowName: 'child-workflow-id',
})
await expect(handler.execute(mockBlock, inputs, deepContext)).rejects.toThrow(
'Error in child workflow "child-workflow-id": Maximum workflow nesting depth of 10 exceeded'
)
})

it('should handle child workflow not found', async () => {
Expand All @@ -147,25 +140,19 @@ describe('WorkflowBlockHandler', () => {
statusText: 'Not Found',
})

const result = await handler.execute(mockBlock, inputs, mockContext)
expect(result).toEqual({
success: false,
error: 'Child workflow non-existent-workflow not found',
childWorkflowName: 'non-existent-workflow',
})
await expect(handler.execute(mockBlock, inputs, mockContext)).rejects.toThrow(
'Error in child workflow "non-existent-workflow": Child workflow non-existent-workflow not found'
)
})

it('should handle fetch errors gracefully', async () => {
const inputs = { workflowId: 'child-workflow-id' }

mockFetch.mockRejectedValueOnce(new Error('Network error'))

const result = await handler.execute(mockBlock, inputs, mockContext)
expect(result).toEqual({
success: false,
error: 'Child workflow child-workflow-id not found',
childWorkflowName: 'child-workflow-id',
})
await expect(handler.execute(mockBlock, inputs, mockContext)).rejects.toThrow(
'Error in child workflow "child-workflow-id": Child workflow child-workflow-id not found'
)
})
})

Expand Down
20 changes: 15 additions & 5 deletions apps/sim/executor/handlers/workflow/workflow-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,12 @@ export class WorkflowBlockHandler implements BlockHandler {
duration
)

// If the child workflow failed, throw an error to trigger proper error handling in the parent
if ((mappedResult as any).success === false) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: Type assertion to any bypasses TypeScript safety. Consider defining a proper interface for the mapped result structure.

Context Used: Context - Avoid using type assertions to 'any' in TypeScript. Instead, ensure proper type definitions are used to maintain type safety. (link)

const childError = (mappedResult as any).error || 'Unknown error'
throw new Error(`Error in child workflow "${childWorkflowName}": ${childError}`)
}

return mappedResult
} catch (error: any) {
logger.error(`Error executing child workflow ${workflowId}:`, error)
Expand All @@ -128,11 +134,15 @@ export class WorkflowBlockHandler implements BlockHandler {
const workflowMetadata = workflows[workflowId]
const childWorkflowName = workflowMetadata?.name || workflowId

return {
success: false,
error: error?.message || 'Child workflow execution failed',
childWorkflowName,
} as Record<string, any>
// Enhance error message with child workflow context
const originalError = error.message || 'Unknown error'

// Check if error message already has child workflow context to avoid duplication
if (originalError.startsWith('Error in child workflow')) {
throw error // Re-throw as-is to avoid duplication
}

throw new Error(`Error in child workflow "${childWorkflowName}": ${originalError}`)
}
}

Expand Down
15 changes: 10 additions & 5 deletions apps/sim/executor/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1448,7 +1448,7 @@ describe('Executor', () => {
}
)

it.concurrent('should surface child workflow failure in result without throwing', async () => {
it.concurrent('should propagate errors from child workflows to parent workflow', async () => {
const workflow = {
version: '1.0',
blocks: [
Expand Down Expand Up @@ -1488,12 +1488,17 @@ describe('Executor', () => {

const result = await executor.execute('test-workflow-id')

// Verify that child workflow failure is surfaced in the overall result
// Verify that child workflow errors propagate to parent
expect(result).toBeDefined()
if ('success' in result) {
// With reverted behavior, parent execution may still be considered successful overall,
// but the workflow block output should capture the failure. Only assert structure here.
expect(typeof result.success).toBe('boolean')
// The workflow should fail due to child workflow failure
expect(result.success).toBe(false)
expect(result.error).toBeDefined()

// Error message should indicate it came from a child workflow
if (result.error && typeof result.error === 'string') {
expect(result.error).toContain('Error in child workflow')
}
}
})
})
Expand Down
4 changes: 4 additions & 0 deletions apps/sim/lib/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,9 @@ export const env = createEnv({

// Asset Storage
NEXT_PUBLIC_BLOB_BASE_URL: z.string().url().optional(), // Base URL for Vercel Blob storage (CDN assets)

// Billing
NEXT_PUBLIC_BILLING_ENABLED: z.boolean().optional(), // Enable billing enforcement and usage tracking (client-side)

// Google Services - For client-side Google integrations
NEXT_PUBLIC_GOOGLE_CLIENT_ID: z.string().optional(), // Google OAuth client ID for browser auth
Expand Down Expand Up @@ -216,6 +219,7 @@ export const env = createEnv({
NEXT_PUBLIC_VERCEL_URL: process.env.NEXT_PUBLIC_VERCEL_URL,
NEXT_PUBLIC_SENTRY_DSN: process.env.NEXT_PUBLIC_SENTRY_DSN,
NEXT_PUBLIC_BLOB_BASE_URL: process.env.NEXT_PUBLIC_BLOB_BASE_URL,
NEXT_PUBLIC_BILLING_ENABLED: process.env.NEXT_PUBLIC_BILLING_ENABLED,
NEXT_PUBLIC_GOOGLE_CLIENT_ID: process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID,
NEXT_PUBLIC_RB2B_KEY: process.env.NEXT_PUBLIC_RB2B_KEY,
NEXT_PUBLIC_GOOGLE_API_KEY: process.env.NEXT_PUBLIC_GOOGLE_API_KEY,
Expand Down