Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
90f3eed
chore(deps): bump nodemailer to 10.0.10 and patch vulnerable undici c…
waleedlatif1 Sep 29, 2026
07c3ffe
refactor: remove dead code and enforce code quality gates (#8395)
waleedlatif1 Sep 29, 2026
66e5705
fix(tables): match unique JSON values exactly and validate row writes…
waleedlatif1 Sep 29, 2026
46e9b5b
fix(selectors): rebind Copilot principals for nested domain use cases…
icecrasher321 Sep 29, 2026
52cc796
fix(ui): clean up stale workflow state and provider policy handling (…
waleedlatif1 Sep 29, 2026
bb4d256
feat(forks): opt-in fork sync for new workflows (#8318)
mzxchandra Sep 29, 2026
61a5169
fix(tables): write the cell state when a resumed run throws (#8401)
waleedlatif1 Sep 29, 2026
b08cd5f
fix(function): report code-placeholder compiler invariants as server …
waleedlatif1 Sep 29, 2026
4f6c2d3
fix(knowledge): shrink the member lifecycle budget tests so they stop…
waleedlatif1 Sep 29, 2026
4e39944
docs(library): update sim-vs-dify-open-source-ai-workspace-vs-llm-app…
icecrasher321 Sep 29, 2026
e715c5b
fix(knowledge): name embedding quota, key, and deadline failures in k…
waleedlatif1 Sep 29, 2026
c47ed6c
fix(logs): keep compacted child span trees shaped as trees (#8403)
waleedlatif1 Sep 29, 2026
85b96fb
fix(forks): route new workflows through one row builder and harden li…
waleedlatif1 Sep 29, 2026
7e11748
improvement(search): accept several same-kind native queries per acco…
waleedlatif1 Sep 29, 2026
7c8f465
docs(library): update how-to-turn-a-workflow-into-a-reusable-mcp-tool…
icecrasher321 Sep 29, 2026
e758353
docs(library): update apache-2-0-vs-fair-code (#8412)
icecrasher321 Sep 29, 2026
863f99e
feat(library): Best AI Workflow Builders for Small Teams in 2026 (#8413)
icecrasher321 Sep 29, 2026
f77845b
feat(library): Best ChatGPT alternatives for building AI agents and a…
icecrasher321 Sep 29, 2026
ea69187
docs(library): update best-ai-agents-for-executive-assistant-tasks (#…
icecrasher321 Sep 29, 2026
ed8b482
docs(library): update best-ai-agents-for-scheduling-and-calendar-mana…
icecrasher321 Sep 29, 2026
5a1a4f3
fix(tables): settle a resume whose pause cannot be saved as failed, a…
waleedlatif1 Sep 29, 2026
3533353
test(forks): assert the sync-default audit fan-out against real audit…
waleedlatif1 Sep 29, 2026
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
2 changes: 1 addition & 1 deletion .github/workflows/test-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -377,7 +377,7 @@ jobs:
- name: Install ripgrep
run: command -v rg || (sudo apt-get update && sudo apt-get install -y ripgrep)

# Runs the setup CLI's Bun tests plus each workspace's Vitest suite,
# Runs the root scripts and each workspace's Vitest suite,
# without `--coverage`.
- name: Run tests
env:
Expand Down
52 changes: 38 additions & 14 deletions apps/docs/content/docs/platform/enterprise/forks.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ On Sim Cloud, your organization may also need the feature turned on for your acc

### 1. Open Forks

Go to **Settings → Organization → Workspace forks** in the workspace you want to fork from (or manage).
Go to **Settings → Workspace → Workspace forks** in the workspace you want to fork from (or manage).

<Image src="/static/enterprise/forks-list.png" alt="Workspace Forks settings page showing Parent and Forks sections with Docs, See activity, and Create fork actions" width={900} height={369} />

Expand Down Expand Up @@ -57,12 +57,12 @@ Everything under **Copy resources** starts **selected**. That is usually what yo
Click **Fork**. The child workspace is created immediately. Deployed workflows land as **drafts** in the child. Large content (table rows, knowledge base files, file blobs) may finish copying in the background — watch **Activity** on the source workspace.

<Callout type="info">
Only **deployed** workflows are forked. Drafts and undeployed work stay in the parent. If the parent has nothing deployed, the child starts with a blank starter workflow.
Only **deployed** workflows are forked, and only the ones that are [synced](#synced-workflows). Drafts and undeployed work stay in the parent. If there is nothing to copy, the child starts with a blank starter workflow.
</Callout>

### 3. Open the parent edge (from the child)

Open the **child** workspace → **Settings → Organization → Workspace forks**. On the **Parent** row, open the menu and choose **Edit mappings**.
Open the **child** workspace → **Settings → Workspace → Workspace forks**. On the **Parent** row, open the menu and choose **Edit mappings**.

Child rows (when you are on the parent) only offer **Open workspace** and **Disconnect** — mapping and sync are owned by the child configuring how it relates to its parent.

Expand Down Expand Up @@ -117,16 +117,35 @@ On success you will see a toast such as **Pushed to "…"** or **Pulled from "

---

## Excluded workflows
## Synced workflows

The **Excluded workflows** section on the Forks page lists this workspace's deployed workflows in their sidebar folder structure. Check a workflow — or a whole folder at once — to keep it out of forking entirely. Think of it as a `.gitignore` for syncs:
The **Synced workflows** section on the Forks page lists this workspace's deployed workflows in their sidebar folder structure, each with a checkbox. **Checked means the workflow syncs.** Uncheck one — or a whole folder at once — to keep it out of forking entirely. Think of an unchecked workflow as `.gitignore`d:

- **Never sent** — pushes from this workspace do not carry it, the other side pulling from this workspace does not receive it, and creating a new fork does not copy it
- **Never touched** — a sync into this workspace will not overwrite or archive it, even if its counterpart was deleted on the other side
- **Never touched** — a sync into this workspace will not overwrite or archive it, even if its counterpart was deleted on the other side. It stays deployed and keeps serving, and a previously-synced counterpart on the other side keeps running on its last deployed version.

The setting belongs to **this workspace's copy** only. Excluding a workflow here does not exclude its counterpart in the parent or a fork — each workspace manages its own list. If the pair has synced before, the link between them is kept, so un-excluding later resumes updating the same counterpart instead of creating a duplicate.
The checkbox list belongs to **this workspace's copy** only. Unchecking a workflow here does not unsync its counterpart in the parent or a fork — each workspace manages its own list. If the pair has synced before, the link between them is kept, so re-checking later resumes updating the same counterpart instead of creating a duplicate.

**Example:** a staging fork excludes `Scratch experiment` so it can never reach production, and production excludes `Billing hotfix` so no push from staging can ever overwrite it.
On the sync page, unsynced workflows still appear in the **Deployed workflows** list, greyed out, with a tooltip naming which workspace they are unsynced in. The sync will not touch them.

**Example:** a staging fork leaves `Scratch experiment` unchecked so it can never reach production, and production leaves `Billing hotfix` unchecked so no push from staging can ever overwrite it.

### Sync new workflows by default

Above the list, **Sync new workflows by default** decides where a **newly created** workflow starts:

| Setting | A new workflow… |
|---------|-----------------|
| **Sync** (default) | joins fork sync — it arrives checked and syncs as soon as you deploy it |
| **Don't sync** | starts outside fork sync — it arrives unchecked and only syncs after you check it |

Three things to know:

- **It applies to the whole fork lineage.** The toggle writes every workspace in the lineage — the root, every ancestor, every descendant — so a parent and its forks can never disagree about what "new" means. Any workspace admin in the lineage can change it, and each workspace whose value changes gets its own audit entry naming the workspace the change came from. A new fork inherits the value at creation.
- **It is forward-only.** Flipping it never moves an existing workflow in or out of sync. The checkbox list above stays the record of what syncs.
- **"New" means genuinely new.** Creating, duplicating, or importing a workflow takes this setting, as does the blank starter workflow a fork gets when there is nothing to copy. A workflow that arrives as a **copy** — from a fork, or from a push or pull — ignores this setting. Only synced workflows are copied, and they always arrive synced, so a workflow you deliberately synced never lands unsynced on the other side.

**Example:** a template workspace turns this off so every scratch workflow the team creates stays local, then checks only the handful meant to reach the forks.

---

Expand Down Expand Up @@ -155,6 +174,8 @@ Expand a row for names of workflows and resources that were created, updated, or
|--------|-----|
| See Forks / create a fork | Admin on this workspace (+ feature available) |
| Sync / edit mappings | Admin on **both** sides of the edge |
| Check / uncheck **Synced workflows** | Admin on the workspace those workflows live in |
| Change **Sync new workflows by default** | Admin on any one workspace in the lineage — the change applies to every member |
| Rollback | Admin on the workspace the sync landed in |
| Disconnect | Admin on **this** side only (you can disconnect even without access to the other workspace) |
| Open the other workspace | You must be a member of that workspace |
Expand All @@ -169,9 +190,9 @@ How each resource behaves at **fork** time vs **sync** time. Use this when you a

| Resource | Fork | Sync |
|----------|------|------|
| Deployed workflows | Copied as drafts (unless excluded) | Updated / created / archived (force overwrite) |
| Deployed workflows | Copied as drafts when [synced](#synced-workflows) | Updated / created / archived (force overwrite) |
| Undeployed workflows | Not copied | Not synced |
| [Excluded workflows](#excluded-workflows) | Never | Never — not sent, not overwritten, not archived |
| [Unsynced workflows](#synced-workflows) | Never | Never — not sent, not overwritten, not archived |
| Files | Optional copy (default on) | Map or copy |
| File folders referenced by workflows | Mirrored with their ancestor folders, even when empty | Map by canonical path |
| Tables | Optional copy (default on) | Map or copy |
Expand All @@ -191,11 +212,11 @@ How each resource behaves at **fork** time vs **sync** time. Use this when you a

### Workflows

Only **deployed** workflows move. Deploy is the commit; sync is the force push/pull of those commits. Workflows marked [excluded](#excluded-workflows) never move in either direction.
Only **deployed** workflows move, and only the ones checked under [Synced workflows](#synced-workflows). An unsynced workflow never moves in either direction.

| Feature | Behavior |
|---|----------|
| **Fork** | Each deployed workflow becomes a **draft** in the child. Run history is not copied. Only folders that contain a copied workflow are kept. |
| **Fork** | Each synced deployed workflow becomes a **draft** in the child. Run history is not copied. Only folders that contain a copied workflow are kept. |
| **Sync** | The change list shows what will be updated, created, or archived. The target is overwritten for those workflows. |

**Example:** Parent has `Support triage` deployed and `WIP experiment` as a draft. The fork gets only `Support triage` as a draft. A later push updates the child from the parent’s latest deploy of `Support triage`.
Expand Down Expand Up @@ -370,13 +391,16 @@ Schedules, webhooks, and triggers are not live in the child until you **deploy**
- **Rollback ≠ undo copies** — Workflow versions roll back; copied resources can remain as orphans.
- **Disconnect is permanent** — You cannot “reconnect” the same edge; you would fork again into a new workspace.
- **No grandparent sync** — Only the direct parent↔child pair.
- **The sync default is lineage-wide** — **Sync new workflows by default** is one shared setting for the whole lineage, so changing it from a fork also changes it in the parent and every sibling fork.

---

<FAQ items={[
{ question: "Why is Sync greyed out?", answer: "Usually a blocking reference, an unmapped credential or secret, or a required dependent field (label, channel, document, …) still empty. Open Blocking sync and the mapping sections — each row explains what to fix. Sync also stays disabled while details are loading or if loading failed (reload the page)." },
{ question: "Is sync a merge?", answer: "No. Deploy is like a commit; sync is a force push or force pull of deployed workflows onto the target. Use Rollback only for the last sync into a workspace, and remember copied resources may remain." },
{ question: "Who can disconnect a fork I cannot open?", answer: "Any admin on your side of the edge. Disconnect does not require access to the other workspace — so you are not stuck if the other side lost membership." }
{ question: "Who can disconnect a fork I cannot open?", answer: "Any admin on your side of the edge. Disconnect does not require access to the other workspace — so you are not stuck if the other side lost membership." },
{ question: "I deployed a new workflow and sync ignored it. Why?", answer: "Sync new workflows by default is off for this fork lineage, so the workflow was created outside fork sync. Open Settings → Workspace → Workspace forks and check it under Synced workflows. Turning the toggle back on only affects workflows created after that — it never moves an existing one." },
{ question: "Does turning Sync new workflows by default off stop my current syncs?", answer: "No. It is forward-only and never rewrites an existing workflow's checkbox, so everything already synced keeps syncing. It also applies to every workspace in the fork lineage, not just the one you changed it from." }
]} />

---
Expand All @@ -389,4 +413,4 @@ Self-hosted deployments turn Forks on with an environment variable instead of th
|----------|-------------|
| `FORKING_ENABLED`, `NEXT_PUBLIC_FORKING_ENABLED` | Enables workspace forking when billing is not used as the entitlement gate |

Once enabled, use the same **Settings → Organization → Workspace forks** UI as Sim Cloud. Only workspace admins can manage forks.
Once enabled, use the same **Settings → Workspace → Workspace forks** UI as Sim Cloud. Only workspace admins can manage forks.
27 changes: 12 additions & 15 deletions apps/sim/app/api/superuser/import-workflow/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { loadCopilotChatMessages } from '@/lib/mothership/chat/lifecycle'
import { appendCopilotChatMessages } from '@/lib/mothership/chat/messages-store'
import { verifyEffectiveSuperUser } from '@/lib/permissions/super-user'
import { parseWorkflowJson } from '@/lib/workflows/operations/import-export'
import { buildNewWorkflowRow } from '@/lib/workflows/persistence/new-workflow-row'
import {
loadWorkflowFromNormalizedTables,
saveWorkflowToNormalizedTables,
Expand Down Expand Up @@ -129,27 +130,23 @@ export const POST = withRouteHandler(async (request: NextRequest) => {

// Create new workflow record
const newWorkflowId = generateId()
const now = new Date()
const dedupedName = await deduplicateWorkflowName(
`[Debug Import] ${sourceWorkflow.name}`,
targetWorkspaceId,
null
)

await db.insert(workflow).values({
id: newWorkflowId,
userId: session.user.id,
workspaceId: targetWorkspaceId,
folderId: null,
name: dedupedName,
description: sourceWorkflow.description,
lastSynced: now,
createdAt: now,
updatedAt: now,
isDeployed: false, // Never copy deployment status
runCount: 0,
variables: sourceWorkflow.variables || {},
})
await db.insert(workflow).values(
await buildNewWorkflowRow(db, {
id: newWorkflowId,
userId: session.user.id,
workspaceId: targetWorkspaceId,
folderId: null,
name: dedupedName,
description: sourceWorkflow.description,
variables: sourceWorkflow.variables || {},
})
)

// Save using existing persistence logic
const saveResult = await saveWorkflowToNormalizedTables(newWorkflowId, importedData, {
Expand Down
26 changes: 11 additions & 15 deletions apps/sim/app/api/v1/admin/workflows/import/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { adminV1ImportWorkflowContract } from '@/lib/api/contracts/v1/admin'
import { parseRequest } from '@/lib/api/server'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { parseWorkflowJson } from '@/lib/workflows/operations/import-export'
import { buildNewWorkflowRow } from '@/lib/workflows/persistence/new-workflow-row'
import { prepareWorkflowStateForPersistence } from '@/lib/workflows/persistence/prepare-state'
import { saveWorkflowToNormalizedTables } from '@/lib/workflows/persistence/utils'
import { deduplicateWorkflowName } from '@/lib/workflows/utils'
Expand Down Expand Up @@ -112,23 +113,18 @@ export const POST = withRouteHandler(
)

const workflowId = generateId()
const now = new Date()
const dedupedName = await deduplicateWorkflowName(workflowName, workspaceId, folderId || null)

await db.insert(workflow).values({
id: workflowId,
userId: workspaceData.ownerId,
workspaceId,
folderId: folderId || null,
name: dedupedName,
description: workflowDescription,
lastSynced: now,
createdAt: now,
updatedAt: now,
isDeployed: false,
runCount: 0,
variables: {},
})
await db.insert(workflow).values(
await buildNewWorkflowRow(db, {
id: workflowId,
userId: workspaceData.ownerId,
workspaceId,
folderId: folderId || null,
name: dedupedName,
description: workflowDescription,
})
)

/**
* Same normalization the editor and the v1 import API run, via the one
Expand Down
26 changes: 11 additions & 15 deletions apps/sim/app/api/v1/admin/workspaces/[id]/import/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import {
extractWorkflowsFromZip,
parseWorkflowJson,
} from '@/lib/workflows/operations/import-export'
import { buildNewWorkflowRow } from '@/lib/workflows/persistence/new-workflow-row'
import { prepareWorkflowStateForPersistence } from '@/lib/workflows/persistence/prepare-state'
import { saveWorkflowToNormalizedTables } from '@/lib/workflows/persistence/utils'
import { deduplicateWorkflowName } from '@/lib/workflows/utils'
Expand Down Expand Up @@ -347,23 +348,18 @@ async function importSingleWorkflow(
}

const workflowId = generateId()
const now = new Date()
const dedupedName = await deduplicateWorkflowName(workflowName, workspaceId, targetFolderId)

await db.insert(workflow).values({
id: workflowId,
userId: ownerId,
workspaceId,
folderId: targetFolderId,
name: dedupedName,
description: workflowData.metadata?.description || 'Imported via Admin API',
lastSynced: now,
createdAt: now,
updatedAt: now,
isDeployed: false,
runCount: 0,
variables: {},
})
await db.insert(workflow).values(
await buildNewWorkflowRow(db, {
id: workflowId,
userId: ownerId,
workspaceId,
folderId: targetFolderId,
name: dedupedName,
description: workflowData.metadata?.description || 'Imported via Admin API',
})
)

/**
* Same normalization the editor, the v1 import API and the single-workflow
Expand Down
27 changes: 27 additions & 0 deletions apps/sim/app/api/workspaces/[id]/fork/sync-default/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { updateForkSyncDefaultContract } from '@/lib/api/contracts/workspace-fork'
import {
defineInternalJsonRoute,
internalRateLimits,
internalSessionAuth,
} from '@/lib/api/server/routes'
import { internalForkErrorPolicy } from '@/ee/workspace-forking/api/route-policies'
import { forkOperations } from '@/ee/workspace-forking/application/operations'
import { setForkSyncDefault } from '@/ee/workspace-forking/application/sync-default'

export const PUT = defineInternalJsonRoute({
contract: updateForkSyncDefaultContract,
auth: internalSessionAuth,
operation: forkOperations.syncDefault,
/**
* Rate-limited, unlike sibling fork routes: it writes the whole lineage under the coarsest
* fork lock, so looping it could starve fork creation lineage-wide.
*/
rateLimit: internalRateLimits.user({ bucketName: 'workspace-fork-sync-default' }),
errorPolicy: internalForkErrorPolicy,
mapInput: ({ params, body }) => ({ workspaceId: params.id, ...body }),
present: ({ excludeNewWorkflows, changedWorkspaces }) => ({
excludeNewWorkflows,
workspacesUpdated: changedWorkspaces.length,
}),
useCase: setForkSyncDefault,
})
Loading
Loading