Skip to content

Commit 5e9f998

Browse files
committed
no deployed workflow case
1 parent 055cb47 commit 5e9f998

4 files changed

Lines changed: 62 additions & 5 deletions

File tree

apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/fork-workspace-modal/fork-workspace-modal.tsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ interface ForkWorkspaceModalProps {
2828
sourceWorkspaceName: string
2929
}
3030

31-
type ResourceKey = keyof GetForkResourcesResponse
31+
type ResourceKey = Exclude<keyof GetForkResourcesResponse, 'deployedWorkflowCount'>
3232
type ResourceSelection = Record<ResourceKey, Set<string>>
3333

3434
const RESOURCE_KINDS: ReadonlyArray<{ key: ResourceKey; label: string }> = [
@@ -188,6 +188,13 @@ export function ForkWorkspaceModal({
188188
[resources.data]
189189
)
190190

191+
// A fork always produces a usable workspace: deployed workflows are copied, and
192+
// when the source has none, create-fork seeds a blank starter workflow (plus any
193+
// selected resources). So forking is never blocked - we just set expectations when
194+
// there are no deployed workflows to carry over.
195+
const noDeployedWorkflows =
196+
Boolean(resources.data) && (resources.data?.deployedWorkflowCount ?? 0) === 0
197+
191198
const handleSubmit = () => {
192199
const trimmed = name.trim()
193200
if (!trimmed || isForking) return
@@ -258,12 +265,17 @@ export function ForkWorkspaceModal({
258265
disabled={isForking}
259266
/>
260267
))}
261-
<p className='text-[var(--text-secondary)] text-xs'>
268+
<p className='text-[var(--text-muted)] text-caption'>
262269
Unselected resources leave their workflow fields empty in the fork.
263270
</p>
264271
</div>
265272
</ChipModalField>
266273
) : null}
274+
{noDeployedWorkflows ? (
275+
<p className='px-2 text-[var(--text-muted)] text-caption'>
276+
No deployed workflows to copy — your fork will start with a blank workflow.
277+
</p>
278+
) : null}
267279
<ChipModalError>{error ?? undefined}</ChipModalError>
268280
</ChipModalBody>
269281
<ChipModalFooter

apps/sim/lib/api/contracts/workspace-fork.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ export const getForkResourcesContract = defineRouteContract({
121121
customTools: z.array(forkCopyableResourceSchema),
122122
skills: z.array(forkCopyableResourceSchema),
123123
mcpServers: z.array(forkCopyableResourceSchema),
124+
deployedWorkflowCount: z.number().int(),
124125
}),
125126
},
126127
})

apps/sim/lib/workspaces/fork/create-fork.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import { db } from '@sim/db'
2-
import { permissions, workspace } from '@sim/db/schema'
2+
import { permissions, workflow, workspace } from '@sim/db/schema'
33
import { createLogger } from '@sim/logger'
44
import type { PermissionType } from '@sim/platform-authz/workspace'
55
import { generateId } from '@sim/utils/id'
66
import { and, eq } from 'drizzle-orm'
77
import type { Workspace } from '@/lib/api/contracts/workspaces'
88
import { isTriggerDevEnabled } from '@/lib/core/config/env-flags'
99
import { runDetached } from '@/lib/core/utils/background'
10+
import { buildDefaultWorkflowArtifacts } from '@/lib/workflows/defaults'
11+
import { saveWorkflowToNormalizedTables } from '@/lib/workflows/persistence/utils'
1012
import {
1113
type ForkContentCopyPayload,
1214
runForkContentCopy,
@@ -209,6 +211,30 @@ export async function createFork(params: CreateForkParams): Promise<CreateForkRe
209211
workflowsCopied += 1
210212
}
211213

214+
// A fork carries only DEPLOYED workflows. When the source has none (e.g. it was
215+
// itself just forked and never redeployed), seed a default workflow so the child
216+
// is a usable workspace rather than a blank one with no workflow at all - the same
217+
// starter "New workspace" creates. Any copied resources still land alongside it.
218+
if (workflowsCopied === 0) {
219+
const defaultWorkflowId = generateId()
220+
await tx.insert(workflow).values({
221+
id: defaultWorkflowId,
222+
userId,
223+
workspaceId: childWorkspaceId,
224+
folderId: null,
225+
name: 'default-agent',
226+
description: 'Your first workflow - start building here!',
227+
lastSynced: now,
228+
createdAt: now,
229+
updatedAt: now,
230+
isDeployed: false,
231+
runCount: 0,
232+
variables: {},
233+
})
234+
const { workflowState } = buildDefaultWorkflowArtifacts()
235+
await saveWorkflowToNormalizedTables(defaultWorkflowId, workflowState, tx)
236+
}
237+
212238
const seedEntries: ForkMappingUpsert[] = []
213239
for (const [sourceWorkflowId, childWorkflowId] of workflowIdMap.entries()) {
214240
seedEntries.push({

apps/sim/lib/workspaces/fork/mapping/resources.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@ import {
55
mcpServers,
66
skill,
77
userTableDefinitions,
8+
workflow,
89
workspaceEnvironment,
910
workspaceFiles,
1011
} from '@sim/db/schema'
11-
import { and, eq, isNull, sql } from 'drizzle-orm'
12+
import { and, count, eq, isNull, sql } from 'drizzle-orm'
1213
import type { DbOrTx } from '@/lib/db/types'
1314
import type { ForkResourceType } from '@/lib/workspaces/fork/mapping/mapping-store'
1415
import type { ForkRemapKind } from '@/lib/workspaces/fork/remap/remap-references'
@@ -123,6 +124,12 @@ export interface ForkCopyableResources {
123124
customTools: ForkResourceCandidate[]
124125
skills: ForkResourceCandidate[]
125126
mcpServers: ForkResourceCandidate[]
127+
/**
128+
* Count of deployed workflows that the fork would copy. The fork modal disables
129+
* the action (with a reason) when this is 0 and there are no copyable resources,
130+
* since the fork would otherwise produce an empty workspace.
131+
*/
132+
deployedWorkflowCount: number
126133
}
127134

128135
/**
@@ -134,7 +141,7 @@ export async function listForkCopyableResources(
134141
executor: DbOrTx,
135142
workspaceId: string
136143
): Promise<ForkCopyableResources> {
137-
const [files, tables, kbs, tools, skills, servers] = await Promise.all([
144+
const [files, tables, kbs, tools, skills, servers, deployed] = await Promise.all([
138145
executor
139146
.select({
140147
id: workspaceFiles.id,
@@ -174,6 +181,16 @@ export async function listForkCopyableResources(
174181
.from(mcpServers)
175182
.where(and(eq(mcpServers.workspaceId, workspaceId), isNull(mcpServers.deletedAt)))
176183
.limit(CANDIDATE_LIMIT),
184+
executor
185+
.select({ value: count() })
186+
.from(workflow)
187+
.where(
188+
and(
189+
eq(workflow.workspaceId, workspaceId),
190+
eq(workflow.isDeployed, true),
191+
isNull(workflow.archivedAt)
192+
)
193+
),
177194
])
178195
return {
179196
files,
@@ -182,6 +199,7 @@ export async function listForkCopyableResources(
182199
customTools: tools,
183200
skills,
184201
mcpServers: servers,
202+
deployedWorkflowCount: deployed[0]?.value ?? 0,
185203
}
186204
}
187205

0 commit comments

Comments
 (0)