# Workspace forks

> How do I create workspace forks for parallel development workflows in Windmill?

Workspace forks allow users to create independent copies of a workspace, enabling parallel development workflows similar to git branches and GitHub forks.

:::tip Forks are stage 2 of the collaboration guide

Adopting workspace forks corresponds to **stage 2** of the [collaboration and deployment stages](../23_canonical_deployment_setups/index.mdx#stage-2--add-forks-and-the-merge-ui) guide. If you are setting up collaboration for a team, that guide explains how forks fit alongside protection rulesets, git sync, and multi-workspace promotion.

:::

## How workspace forks work

Workspace forks create a complete copy of your workspace, including scripts, flows, apps, resources, variables, and [data tables](../../core_concepts/11_persistent_storage/data_tables.mdx). Each fork operates independently and changes can later be merged to a parent workspace through a couple of methods.

Key features:
- **Independent development**: Make changes without affecting the parent workspace
- **Git integration**: Automatically creates corresponding git branches using the git sync workflow
- **Team collaboration**: Multiple developers can work on separate forks simultaneously
- **Data table forking**: Data tables are included in the fork, giving each fork its own isolated copy of the data
- **DuckLake data environments**: Pipelines that materialize [DuckLake](../../core_concepts/11_persistent_storage/ducklake.mdx) tables write to a fork-scoped namespace and read not-yet-materialized tables from the parent (see [Fork data environments for DuckLake](#fork-data-environments-for-ducklake))
- **CLI merge**: Use `wmill workspace merge` to merge changes from a fork back to the parent workspace

![Workspace Forks and Git Sync](/images/workspace_forks.png)

## Example feature development workflow

1. **Create fork**: Fork the main workspace for new feature development
2. **Develop**: Make changes in the forked workspace
3. **Create PR**: Use github/gitlab to create a pull request
4. **Review & merge**: Team reviews changes before merging to main branch
5. **Sync back changes**: Changes are automatically synced to the workspace linked to the main branch

## Managing workspace forks

### Prerequisites

- On self-hosted Windmill, forks work on any edition. On Community Edition, forks count toward the global 3-workspace limit; [Enterprise Edition](/pricing) removes that cap.
- On [Windmill Cloud](https://app.windmill.dev/), forking is available for workspaces on a paid (Team or Enterprise) plan only. See [Forks on Windmill Cloud](#forks-on-windmill-cloud) below.
- [Git sync](../11_git_sync/index.mdx) configured (optional, required only for git-based workflows)
- For git sync workflows, a way to sync changes back from the git remote to Windmill: either [automatic fork sync](../11_git_sync/index.mdx#automatic-fork-sync) (recommended, no CI needed) or the [CI/CD actions](../9_deploy_gh_gl/index.mdx#github-actions-setup)

### Forks on Windmill Cloud

On [Windmill Cloud](https://app.windmill.dev/), forking is available for workspaces on a paid (Team or Enterprise) plan. Free-tier workspaces cannot create forks - upgrade the workspace to a paid plan first (see [plans & how to upgrade](../../enterprise/1_plans_details/index.mdx#upgrading-to-team-edition)).

A fork inherits its parent (root) workspace's plan and usage limits rather than getting its own:

- **Shared plan**: a fork takes on the premium status of its root workspace. You don't subscribe a fork separately.
- **Shared usage limits**: a fork's executions are metered into the root workspace's usage, so all forks in a family draw from the same pool and count toward the root's usage-based bill. Billing stays with the parent - a fork never has its own invoice.
- **Per-seat fork cap**: the number of forks a workspace family can have is capped at 5 per paid (developer) seat of the root workspace (for example, 2 seats allow up to 10 forks). Delete a fork or add seats to free up capacity.

If a workspace is later downgraded to the free tier, its existing forks keep working under the root's shared free-tier limits, but no new forks can be created until it is upgraded again.

On self-hosted Windmill, none of these cloud limits apply - forks are governed only by your edition's workspace limit (see [prerequisites](#prerequisites) above).

### Fork creation

#### From the UI

1. Navigate to the workspace you want to fork.
2. Click on the Workspace menu on the sidebar.
3. Click on `Fork current workspace`.
4. Configure the name and id and then press on the `Fork workspace` button.

#### From the CLI

1. Enter the local repo being synced with the workspace you want to fork.
2. Run `wmill workspace fork` and follow the prompts to name your workspace. This uses your `workspaces` settings in `wmill.yaml` to determine which workspace to fork from based on the branch you are on.
3. The fork will be created on your instance and is ready to be worked on.
4. Run the command shown on your terminal to create the corresponding branch and start adding edits to your newly created fork!

<!-- TODO: Add step-by-step screenshots of the fork creation process -->

### Deleting a fork

Workspace forks can be deleted by their creator, or by any workspace admin.

#### From the UI

Just press the cogwheel on the sidebar and select the option to delete the workspace fork.

#### From the CLI

1. Find the workspace profile corresponding to the workspace fork using the `wmill workspace` command.
2. If missing, add it using `wmill workspace add`
3. Now run `wmill workspace delete-fork <profile_name>`

### Permissions and access

Forks can be created by any user of the workspace. Roles and access will be the same as in the original workspace. All users of the original workspace can join the fork at any time, which will make it appear as another workspace on their menu.

On [Windmill Cloud](https://app.windmill.dev/), creating a fork additionally requires the workspace to be on a paid plan and to have fork capacity left under the [per-seat cap](#forks-on-windmill-cloud).

#### Add admins and developers to new forks

By default, a new fork's only member is its creator.
To have every new fork start with the workspace's admins and developers, each with the role they hold in the workspace, a workspace admin can enable `Add admins and developers to new forks` in `Workspace settings` -> `Users` -> `Fork members`.
Members whose [role](../../core_concepts/16_roles_and_permissions/index.mdx#workspace-roles) cannot write code, disabled users and service accounts are not added.

The setting is off by default and only applies when a fork is created: existing forks keep their members.
Forks inherit the setting, so forks of a fork follow it too.
A [dev workspace](../26_dev_workspaces/index.mdx#creating-a-dev-workspace) created with `Copy members` enabled gets all members of the parent instead.

#### Managing members of a fork you created

The creator of a fork can manage developers on it without being an admin of it. From the fork's workspace settings, under `Members`, they can:

- add a developer or admin of the parent workspace to the fork, as a developer;
- remove any non-admin member of the fork.

Because a fork is a full copy of its parent - variables and secrets included - this right is deliberately narrow. As a fork creator, you cannot:

- add anyone who is not already an admin of the parent workspace or a member whose role there can write code (the picker only lists eligible parent members);
- grant the admin role or any other role than developer, or change the role of an existing member;
- invite someone by email;
- remove an admin of the fork.

Anything outside these bounds - and every other workspace setting - still requires a workspace admin. If the fork's creator is an admin, the usual full [workspace settings](../../core_concepts/16_roles_and_permissions/index.mdx) apply instead.

## Git sync integration

When workspace forks are used with [git sync](../11_git_sync/index.mdx), Windmill will automatically create a git branch on the same repository as
the parent workspace, and keep track of changes there. External changes to the branch are then synced back to the forked workspace. Once the changes are ready, a PR can be opened to sync back changes to the original workspace.

There are two ways to sync external branch changes back into fork workspaces:

- [Automatic fork sync](../11_git_sync/index.mdx#automatic-fork-sync) (recommended): enable "Automatically sync forks with git branches" on the parent workspace's repository in the Git sync settings. Windmill then deploys changes on each fork's `wm-fork/**` branch into the matching fork workspace, with no CI setup and no per-fork configuration. Fork workspaces show a read-only "managed in the parent workspace" line on their repository card. [Enterprise Edition](/pricing) only.
- [CI/CD actions](../9_deploy_gh_gl/index.mdx#github-actions-setup): set up the fork-specific actions, in particular the `push-on-merge-to-forks.yaml` action.

Similarly, the pull request that merges a fork's changes back into the tracked branch can be opened by Windmill itself when an item is deployed in the fork ("Open a pull request when an item is deployed in a fork" on the parent's repository, see [In-app pull request creation](../11_git_sync/index.mdx#in-app-pull-request-creation)), or by the `open-pr-on-fork-commit` GitHub Action.
On GitHub, that pull request can be gated on the fork's [CI tests](../24_ci_tests/index.mdx) with the ["Windmill CI tests" check](../11_git_sync/index.mdx#ci-tests-check), Enterprise Edition only.

### Fork and branch naming

Forks workspace IDs are always prefixed by `wm-fork-`. The associated branch name is always prefixed by `wm-fork/<originalBranch>/` where `<originalBranch>` is the branch that is associated with the original branch was worked from.

For example:

| Workspace ID | Default or selected branch of repo associated with workspace | Fork Workspace ID | Branch associated to fork |
|--------------|--------------------------------------------------------------|-------------------|---------------------------|
| windmill     | main                                                         | wm-fork-feature1  | wm-fork/main/feature1     |

This naming convention is what enables Windmill to match branches to workspaces without having to reconfigure it for each branch. This is why fork workspace ids and branch names cannot be changed.

## Synchronization and merging

### Simple edits, one item at a time

A fork deploys into the workspace it was forked from. That comes from the lineage itself, so there is nothing to configure: you can deploy any item to the parent workspace directly from the UI by clicking it and selecting `Deploy to staging/prod`. Learn more about [deploying to prod from the UI](../../core_concepts/12_staging_prod/index.md).

### Merge workspaces from the UI (Merge UI)

You can merge changes from a forked workspace back to its parent workspace directly from the UI without requiring git sync. This provides a simpler workflow for teams that don't need full git integration.

To merge a fork:

1. Navigate to the forked workspace
2. On the home page, a banner will show you the diff with its parent workspace. Click `Review & Deploy Changes`
3. Review the changes and confirm the merge

#### Preselecting items via URL

The review page (Compare & Deploy, at `/forks/compare` in the workspace) selects all changed items by default. To link to a review of specific items only, add an `?items=` URL parameter with comma-separated `kind:path` entries:

```text
/forks/compare?items=script:f/marketing/sync,trigger_schedule:u/alice/daily
```

Kinds follow the draft item kinds: `script`, `flow`, `app`, `raw_app`, `resource`, `variable`, and `trigger_*` variants such as `trigger_schedule` or `trigger_http`. An optional `?mode=draft|fork` parameter forces the drafts or fork comparison view; when omitted, the page picks automatically. This makes the selection linkable from external tools; the [AI chat](../../core_concepts/22_ai_generation/index.mdx#windmill-ai-chat) uses it to open the page with only its own edits preselected.

#### Items behind and conflicts

If it is detected that some of the differences come in fact from the original workspace, these changes will be shown as 'behind'. Before deploying, all behind changes must be updated into the fork and properly tested. When an items has changes that are both ahead and behind, it will be marked as a conflict and will be warned to the user. The user must then review the diff and decide how to edit the item to resolve the conflicting changes. Until deployed or updated however, the item will still be marked as a conflict. For more granular conflict detection and better tracked resolution, you can choose to merge the workspaces through a git workflow instead.

The merge will transfer all selected scripts, flows, apps, resources, and variables from the fork to the parent workspace.

#### Deletions and renames

Deleting an item in the fork, or renaming it, is a change the merge carries to the parent like any other:

- A deleted item is listed with a red `Removes in <parent>` badge. Removals are never part of a bulk selection - you tick them one by one - so a merge cannot delete something in the parent by accident.
- A rename merges as two rows: the new path as `New`, and the old path as `Removes in <parent>`, so the parent is not left with a copy under the former name.

Only a removal the fork itself made is offered. An item can be missing from a fork for other reasons - the parent created it after the fork was made, or a [git sync](../11_git_sync/index.mdx) pull reverted a deploy in the fork - and those never show up as deletions to merge into the parent. They stay listed in the update direction as `New`, so updating the fork brings them in.

Windmill tells the two apart by what wrote to the fork: `wmill sync push` (which the [automatic fork sync](../11_git_sync/index.mdx#automatic-fork-sync) runs on your behalf) and `wmill workspace merge --direction to-fork` apply a state computed elsewhere, so what they change in the fork is never read back as the fork's own decision.

#### Triggers and schedules

When a fork is created, it clones the parent's [triggers](../../triggers/index.mdx) and [schedules](../../core_concepts/1_scheduling/index.mdx), but always as disabled, so nothing fires or competes with the parent automatically. You re-enable in the fork only what you need to test. [Native triggers](../../triggers/11_native_triggers/index.mdx) (such as Nextcloud, Google Drive and GitHub) and non-workspaced [HTTP](../../triggers/2_http_routing/index.mdx) and [email](../../triggers/1_email_triggers/index.mdx) triggers are not cloned, because their external endpoints cannot be safely duplicated.

Re-enabling a trigger or schedule that also exists in the parent shows a warning: the two would share the same upstream ([Kafka](../../triggers/5_kafka_triggers/index.mdx) consumer group, [Postgres](../../triggers/4_postgres_triggers/index.mdx) replication slot, message queue, schedule, etc.) and compete for events. You can confirm to enable it anyway.

A trigger's or schedule's enabled/disabled state is owned by the parent workspace. When you sync or merge a fork back, the parent's state is preserved, so toggling a trigger in the fork never flips it in the parent and trigger state produces no spurious diff or conflict when [merging through git](#git-sync-based-merging). Any trigger you create in the fork is still surfaced in the [merge UI](#merge-workspaces-from-the-ui-merge-ui) so you can deploy, disable or delete it.

#### Merging into a workspace outside the lineage

Compare & Deploy targets the parent workspace by default. To deploy into another workspace instead - a one-off migration into a workspace that is not the parent, for instance - click the destination badge in the merge header (`into: <workspace>`) to open the target picker and select a workspace. The picker lists the workspaces you are a member of, minus this one and its own forks, and offers a way back to the parent. The same view opens from `Workspace settings` -> `Dev workspace` -> `Merge into another workspace`, and a workspace with no parent at all lands directly on the picker. The target can also be set through the URL:

```text
/forks/compare?target=<workspace id>
```

Merging outside the lineage requires being an admin of both workspaces.

Windmill only tracks changes continuously between a workspace and its parent, so an unrelated pair starts with nothing to show. The page offers a `Compute diff` action that enumerates every item on both sides and compares them. This is expensive on large workspaces, which is why it is an explicit action rather than something the page does on load. The result is kept for later visits: the card then shows when the diff was computed, next to a `Recompute` button. Anything created or changed on either side since that scan is missing from the list until you recompute.

Such a comparison is one-way, from the current workspace into the target. Nothing recorded which side a change came from, so the update direction and [behind changes and conflicts](#items-behind-and-conflicts) are not available - only deploying from here into the target is.

An item that exists only in the target is listed as `Removes in target`: deploying it deletes the item there. Those rows are never picked up by `select all` and have to be ticked one by one, and a recompute unticks any row that has just turned into a removal.

{/* TODO: screenshot of the Compare & Deploy header with the destination badge open on the target picker, and the merge card showing "Diff computed <date>", the Recompute button and the one-way banner. */}

### Git sync based merging

When git sync is properly setup, the workspace is synchronized with one of the branches in your repo, and the forks are also synchronized with their own branches. It is then possible your preferred git workflow to merge into or out of these branches to sync, so you can update the fork, or deploy changes into the parent workspace.

## Fork data environments for DuckLake

When the parent workspace has [DuckLake](../../core_concepts/11_persistent_storage/ducklake.mdx) lakes configured, a fork gets its own isolated data environment for each lake by default. Running a [pipeline](../../core_concepts/63_pipelines/index.mdx) that [materializes](../../core_concepts/63_pipelines/materialization.mdx) DuckLake tables in a fork writes to a fork-scoped namespace (a separate metadata schema in the same catalog database, and a dedicated `__wm_forks/<fork id>/...` prefix in the bucket) instead of the parent workspace's tables. This applies transparently: scripts keep the same `ducklake://<lake>/<table>` URIs and annotations, and the redirect covers managed materialization, raw SQL, previews and backfills alike.

This also applies to [dev workspaces](../26_dev_workspaces/index.mdx), which are persistent forks.

### Reading parent data (defer)

For every table the parent workspace has materialized but the fork has not, the fork sees a read-only view over the parent's current data, in the style of dbt's defer. You can iterate on a single pipeline step in a fork without first rebuilding everything upstream: consumers read the parent's data until the fork materializes the table itself, at which point they switch to the fork's own copy. For an [SCD2 history](../../core_concepts/63_pipelines/materialization.mdx#scd2-history-slowly-changing-dimensions) table, the `<table>_current` companion view is deferred as well.

Parent lakes are attached read-only in a fork, so a fork job cannot write the parent's data through DuckLake even accidentally.

### Isolated vs shared with parent

When the parent workspace has lakes configured, the fork-creation dialog shows a `Ducklake data environment` section with a per-lake choice:

- **Isolated** (default): the fork gets the isolated data environment described above.
- **Shared with parent**: the fork reads and writes the parent's lake directly, with no isolation. Useful for forks that must operate on production data, such as a prod-equivalent backfill; a warning is shown since pipeline runs in the fork will modify the parent's tables.

The choice is stamped per lake on the fork and is visible in the fork's workspace settings under `Object storage (S3)` -> Ducklake, where each lake shows an isolated or shared-with-parent chip. Forks created through the API without an explicit choice default to isolated.

Lakes using a MySQL catalog cannot be isolated (the fork-scoped metadata schema requires a PostgreSQL catalog): pipeline runs against such a lake fail with an explicit error in an isolated fork, while sharing the lake with the parent works.

{/* TODO: screenshot of the fork-creation dialog showing the "Ducklake data environment" section with per-lake Isolated / Shared with parent choice and the warning callout on shared. */}

### Fork status in the pipeline graph

In a fork, each DuckLake asset in the [pipeline graph](../../core_concepts/63_pipelines/index.mdx#the-pipeline-graph) carries a chip showing where its data currently comes from:

- an amber `deferred` chip: the fork has not materialized this table yet, reads are deferred to the parent's data;
- an emerald `fork` chip: the fork has materialized its own copy in its isolated namespace.

Selecting the asset shows a matching banner in the details pane.

{/* TODO: screenshot of a fork's pipeline graph with amber "deferred" and emerald "fork" chips on asset nodes, and the details-pane banner. */}

### Cleanup

Deleting a fork also offers to drop its forked DuckLake namespaces: the fork-scoped metadata schema and the `__wm_forks/<fork id>/...` files in the bucket. The same cleanup is available as an API endpoint, `POST /w/<workspace>/workspaces/drop_forked_ducklake_namespaces`, which requires the same permission as deleting the workspace. It is safe to retry if a previous attempt failed partway. Lakes shared with the parent have nothing to clean up.

## Dev workspaces

A dev workspace is a persistent fork that serves as a team's standing editable environment, paired with a "prod" workspace that can be locked so changes only reach it through promotion. Unlike an ordinary fork, a dev workspace keeps a plain, git-branch-safe workspace ID (no `wm-fork-` prefix), and there is at most one per workspace. Use a dev workspace when you want a long-lived dev/prod pair rather than the create-merge-delete lifecycle of an ordinary fork.

## Related features

### Workspace-specific tags and forks

When [workspace-specific default tags](../../core_concepts/9_worker_groups/index.mdx#set-tags-to-assign-specific-queues) are enabled (the instance setting that appends the workspace id to every job tag, e.g. `python3-myworkspace`), forked workspaces automatically resolve to their **parent workspace's tag**. This means fork jobs are routed to the same worker group as the parent workspace — there is no need to provision separate workers for every fork.

For example, if the parent workspace is `staging` and a fork `wm-fork-feature1` runs a Python script, the job tag will be `python3-staging` (not `python3-wm-fork-feature1`).

The same holds for [`$workspace` in an explicit tag](../../core_concepts/9_worker_groups/index.mdx#dynamic-tag), independently of that instance setting: a script tagged `normal-$workspace` that runs on `normal-staging` in the parent also runs on `normal-staging` in a fork of it. Resolution walks up until it reaches a workspace an admin actually names, so a fork of a fork skips the intermediate generated id, and a fork of a [dev workspace](../26_dev_workspaces/index.mdx) resolves to that dev workspace rather than all the way to prod.

#### Dedicated fork workers (optional)

If you want to route fork jobs to their own worker pool — for instance to isolate fork workloads from the parent — you can enable the **"Fork workspace tag: append -fork suffix"** toggle in the instance settings under the **Default Tags** section. When enabled, fork jobs are tagged with the parent workspace id plus a `-fork` suffix (e.g. `python3-staging-fork`), so all forks of a given parent share a common dedicated tag. You can then provision a worker group listening to `*-staging-fork` tags to handle all fork jobs for that parent.

### OIDC tokens in forks

Job [OIDC](../../core_concepts/29_oidc/index.mdx) tokens generated in a fork carry the fork's own workspace id in the `workspace` claim, plus a `fork_parent_workspace` claim containing the parent workspace id. External OIDC consumers such as Vault can bind policies to `fork_parent_workspace` so a single policy covers a workspace and all its forks. See [Token payload reference](../../core_concepts/29_oidc/index.mdx#token-payload-reference).
