Lightning.Projects.Sandboxes (Lightning v2.16.3)
View SourceManage sandbox projects - isolated copies of existing projects for safe experimentation.
Sandboxes allow developers to test changes, experiment with workflows, and collaborate without affecting production projects. They share credentials with their parent but maintain separate workflow execution environments.
What gets copied to a sandbox
- Project settings: retention policies, concurrency limits, MFA requirements
- Workflow structure: jobs, triggers (disabled), edges, and node positions
- Credentials: references to parent credentials (no secrets duplicated)
- Keychain metadata: cloned for jobs that use them
- Version history: latest workflow version per workflow
- Optional dataclips: named clips of specific types can be selectively copied
Operations
provision/3- Create a new sandbox from a parent projectmerge/4- Merge a sandbox into its target (workflows + collections)update_sandbox/3- Update sandbox name, color, or environmentschedule_sandbox_deletion/2- Soft-delete a sandbox and its descendants; they remain in the database for a grace period before being purgedcancel_scheduled_sandbox_deletion/2- Restore a scheduled sandbox subtree while it is still within its grace period (admin recovery path)delete_sandbox/2- Hard-delete a sandbox and all its descendants immediately (used by the Oban purge worker after the grace period elapses)
Authorization
- Provisioning: Requires
:editor,:admin, or:ownerrole on the parent project, or superuser - Merge: Requires
:editor,:admin, or:ownerrole on the target project, or superuser - Updates/Deletion: Requires
:owneror:adminrole on the sandbox itself,or `:owner` or `:admin` on the root project, or superuser
Transaction safety
All operations are performed within database transactions to ensure consistency. Failed operations leave no partial state behind.
Summary
Types
Attributes for creating a new sandbox via provision/3.
Functions
Clears the scheduled deletion on a sandbox subtree, restoring it to active use.
Deletes a sandbox and all its descendant projects.
Merges a sandbox into its target project.
Returns true when user has an :admin or :owner role on any ancestor
of project, walking the parent chain.
Creates a new sandbox project by cloning from a parent project.
Schedules a sandbox and its entire descendant subtree for deletion.
Synchronises collection names from a sandbox to its merge target.
Updates a sandbox project's basic attributes.
Types
@type provision_attrs() :: %{ :name => String.t(), optional(:color) => String.t() | nil, optional(:env) => String.t() | nil, optional(:dataclip_ids) => [Ecto.UUID.t()] }
Attributes for creating a new sandbox via provision/3.
Required
:name- Sandbox name (must be unique within the parent project)
Optional
:color- UI color hex string (e.g."#336699"):env- Environment identifier (e.g."staging","dev"):dataclip_ids- UUIDs of dataclips to copy (only copies named dataclips of types:global,:saved_input, or:http_request)
The sandbox's project_users are derived from the parent project: every
parent user is copied across with their role preserved, except the parent
owner who is demoted to :admin. The actor is then set as the sandbox
owner (replacing any other role they may have had on the parent). To add
a user to the sandbox who is not on the parent, call
Lightning.Projects.add_project_users/3 after provision returns — that
path goes through the seat-limit check.
Functions
@spec cancel_scheduled_sandbox_deletion( Lightning.Projects.Project.t() | Ecto.UUID.t(), Lightning.Accounts.User.t() ) :: {:ok, Lightning.Projects.Project.t()} | {:error, :unauthorized | :not_found | term()} | Lightning.Extensions.UsageLimiting.error()
Clears the scheduled deletion on a sandbox subtree, restoring it to active use.
Walks every descendant of sandbox and clears scheduled_deletion on any
row that has it set. Triggers are not automatically re-enabled: this is an
admin recovery path and the operator decides whether the subtree should
resume firing triggers.
Cascade semantics
The cancel clears scheduled_deletion on every descendant that has it set,
regardless of whether the schedule originated from this subtree's parent
or from a separate scheduling action on the descendant itself. Any row
whose scheduled_deletion is already nil is left alone.
Limit
Restoring a sandbox moves it back into the active count, so the same
usage-limit action that gates new sandbox creation also gates restore.
When the active-sandbox count is already at the limit, restore is
refused with {:error, :too_many_sandboxes, message}; the operator
needs to delete an active sandbox first.
Parameters
sandbox- Sandbox project to restore (or sandbox ID as string)actor- User performing the action (needs:delete_sandboxpermission)
Returns
{:ok, restored_sandbox}- Sandbox subtree restored{:error, :unauthorized}- Actor lacks permission on the sandbox{:error, :not_found}- Sandbox ID not found (when using a string ID)Lightning.Extensions.UsageLimiting.error()- Limit reached
@spec delete_sandbox( Lightning.Projects.Project.t() | Ecto.UUID.t(), Lightning.Accounts.User.t() ) :: {:ok, Lightning.Projects.Project.t()} | {:error, :unauthorized | :not_found | term()}
Deletes a sandbox and all its descendant projects.
Parameters
sandbox- Sandbox project to delete (or sandbox ID as string)actor- User performing the deletion (needs:owneror:adminrole on sandbox)
Returns
{:ok, deleted_sandbox}- Successfully deleted sandbox and descendants{:error, :unauthorized}- Actor lacks permission on sandbox{:error, :not_found}- Sandbox ID not found (when using string ID){:error, reason}- Database or other deletion error
Example
{:ok, deleted} = Sandboxes.delete_sandbox(sandbox, user)
@spec merge( Lightning.Projects.Project.t(), Lightning.Projects.Project.t(), Lightning.Accounts.User.t(), map() ) :: {:ok, Lightning.Projects.Project.t()} | {:error, term()}
Merges a sandbox into its target project.
Imports the sandbox's workflow configuration into the target via the provisioner and synchronises collection names. Runs inside a single transaction. Collection data is never copied.
Callers must authorise the merge before calling (e.g. :merge_sandbox).
Parameters
source- The sandbox project being mergedtarget- The project receiving the mergeactor- The user performing the mergeopts- Merge options (:selected_workflow_ids,:deleted_target_workflow_ids)
Returns
{:ok, updated_target}- Merge succeeded{:error, reason}- Workflow merge or collection sync failed
@spec parent_admin?(Lightning.Projects.Project.t(), Lightning.Accounts.User.t()) :: boolean()
Returns true when user has an :admin or :owner role on any ancestor
of project, walking the parent chain.
Used to enforce the parent-admin floor rule: a user who is admin/owner on any ancestor project cannot be removed from, or downgraded within, a sandbox descended from that project.
@spec provision( Lightning.Projects.Project.t(), Lightning.Accounts.User.t(), provision_attrs() ) :: {:ok, Lightning.Projects.Project.t()} | {:error, :unauthorized | :nesting_too_deep | Ecto.Changeset.t() | term()}
Creates a new sandbox project by cloning from a parent project.
The creator becomes the sandbox owner, and all workflow triggers are disabled. Credentials are shared (not duplicated) between parent and sandbox.
Parameters
parent- Project to clone fromactor- User creating the sandbox (needs:editor,:admin, or:ownerrole on parent)attrs- Creation attributes (seeprovision_attrs/0)
Returns
{:ok, sandbox_project}- Successfully created sandbox{:error, :unauthorized}- Actor lacks permission on parent{:error, :nesting_too_deep}- Parent is already atLightning.Config.max_sandbox_nesting_depth/0{:error, changeset}- Validation or database error
Example
{:ok, sandbox} = Sandboxes.provision(parent_project, user, %{
name: "test-environment",
color: "#336699"
})Concurrency note
The nesting-depth check runs inside the same Repo.transaction as the
sandbox insert, but PostgreSQL's default READ COMMITTED isolation does
not lock the parent's ancestry. A concurrent committed reparent of
parent or any of its ancestors between the depth read and the insert
could place the new sandbox one level above the cap. Lightning has no
reparenting code path today, so this is theoretical; if a re-homing
feature ships, this check should be tightened with SELECT FOR UPDATE
on the ancestor chain.
@spec schedule_sandbox_deletion( Lightning.Projects.Project.t() | Ecto.UUID.t(), Lightning.Accounts.User.t() ) :: {:ok, Lightning.Projects.Project.t()} | {:error, :unauthorized | :not_found | term()}
Schedules a sandbox and its entire descendant subtree for deletion.
The sandbox stays in the database for a grace period (controlled by
PURGE_DELETED_AFTER_DAYS) before the Oban purge worker permanently
deletes it. During the grace period the sandbox is hidden from the
parent's sandbox listing but remains recoverable via
cancel_scheduled_sandbox_deletion/2.
All triggers in the subtree are disabled so that scheduled work stops immediately. The scheduled timestamp is applied to the target and every descendant in a single transaction so the entire subtree shares a grace period and gets purged together.
Cascade semantics
Scheduling cascades through every descendant unconditionally. If a child sandbox was already scheduled separately (with an earlier timestamp), that earlier timestamp is overwritten with the new one. The intent is that scheduling a parent always synchronises the whole subtree's grace window; if you need a child to be purged on its original earlier timestamp, do not schedule the parent.
Parameters
sandbox- Sandbox project to schedule (or sandbox ID as string)actor- User performing the action (needs:delete_sandboxpermission)
Returns
{:ok, scheduled_sandbox}- Sandbox subtree scheduled for deletion{:error, :unauthorized}- Actor lacks permission on the sandbox{:error, :not_found}- Sandbox ID not found (when using a string ID){:error, reason}- Database or other failure
@spec sync_collections(Lightning.Projects.Project.t(), Lightning.Projects.Project.t()) :: {:ok, %{created: non_neg_integer(), deleted: non_neg_integer()}} | {:error, term()}
Synchronises collection names from a sandbox to its merge target.
Names only in the source are created empty in the target; names only in
the target are deleted along with their items. Collection data is never
copied. The combined byte-size of deleted collections is reported via
CollectionHook.handle_delete/2 for usage accounting.
Runs inside a single transaction.
@spec update_sandbox( Lightning.Projects.Project.t() | Ecto.UUID.t(), Lightning.Accounts.User.t(), map() ) :: {:ok, Lightning.Projects.Project.t()} | {:error, :unauthorized | :not_found | Ecto.Changeset.t()}
Updates a sandbox project's basic attributes.
Parameters
sandbox- Sandbox project to update (or sandbox ID as string)actor- User performing the update (needs:owneror:adminrole on sandbox)attrs- Map with:name,:color, and/or:envkeys
Returns
{:ok, updated_sandbox}- Successfully updated sandbox{:error, :unauthorized}- Actor lacks permission on sandbox{:error, :not_found}- Sandbox ID not found (when using string ID){:error, changeset}- Validation error
Example
= Sandboxes.update_sandbox(sandbox, user, %{
name: "new-name",
color: "#ff6b35"})