Skip to content

Sandbox DevX: draft, live, and promote-via-sandbox #4852

Description

@elias-ba

A workflow is either a draft (editable, not processing data) or live (running in production, read-only on main). To change a live workflow you edit it in a sandbox and promote the change back to the parent (the existing sandbox merge for now, a PR-style review later). Where sandboxes are not available, the fallback is switch back to draft, which takes the workflow offline while you edit.

It is built on the sandboxes we already have. "Edit in sandbox" creates or joins an ordinary project sandbox via Lightning.Projects.Sandboxes; "Promote" runs the existing Sandboxes.merge/4 back to the parent. There is no new sandbox primitive, and a sandbox is not narrowed to one workflow at creation (that would break the portable-project.yaml invariant). What is new is the lifecycle state, restore, in-editor switching, and the editor UX. Promotion for now is just that merge with today's authorization; a PR-style review is a later phase.

Concept: the "Sandbox Developer Experience" walkthrough. Builds on V1 sandboxes and the Sandboxes Vision. Reviewed with product and design on 2026-06-11.

Delivery plan and sequencing

Two phases. Phase 1 is everything we can deliver now. Phase 2 is everything that should wait, because it needs a design decision, a prerequisite investigation, or belongs to the later branch-protection step. Each sub-issue carries a phase:1-now or phase:2-later label.

Phase 1: deliver now. The draft/live lifecycle is already merged (#4857 and #4858, via #4867, #4869, #4872, #4873, and coherence in #4900). Finishing it means making the read-only lock write-authoritative (#4898, since today it is UI-only and advisory) and the Apollo prompt cleanup (#4899), both to main. On top of that, the sandbox round trip for paid plans: edit in sandbox (#4859), promote as the existing sandbox merge with today's authorization (#4929, merge_sandbox, owner and admin), diverged surfacing (#4863), and the production-credentials warning (#4860). The parent-added-workflow deletion is a pre-existing live bug in the sandbox merge, tracked separately as #4919 (not epic scope); since promote reuses that merge, land #4919 before or alongside the round trip. Also in Phase 1: recording a version at each go-live and promote (#4866), restore from a version (#4864, the safety net that rolls a broken live workflow back), in-editor version and sandbox switching (#4865, using the interim in-memory project-id map), and the locked Edit-in-sandbox upsell for plans without sandboxes (#4920). Sensible internal order: land the lifecycle finish to main, then the round trip, then these. The interim wire-format inference holds while #4897 waits.

Phase 2: wait. Promotion requests and approval are the branch-protection step (#4861 and #4862): they build on the Phase 1 merge once we invest in review and a required-reviewer policy (deliberately deferred). The protected-project flag and CLI push rules (#4921) wait on the push-rule decisions, and the open decisions to close are tracked in #4922. Carrying workflow state in the wire format plus CLI (#4897) is held here, cross-repo and dependent on the push-rule decision. The change-size indicator on versions (#4931) is a Phase 2 enhancement on top of Phase 1 version recording.

Decisions needed before parts of the later work can build are collected in the open-decisions tracker: the draft-necessity and live-vs-enabled question, the CLI and provisioner push rules plus a protected-project flag, the in-editor switching approach (interim in-memory project-id map vs project slugs), and endpoint naming on promote. Agreed path to closure: a states and branches flowchart plus a compiled comparison of the prior models, then reconvene.

Definition of done (Phase 1)

Each Phase 1 item ships with test coverage for its new surface: backend for release recording, restore, and the promote path; frontend for the version list, the restore action, and the in-sandbox header. The already-merged lifecycle UI has thin frontend test coverage today, so closing that gap is part of finishing Phase 1 well. Authorization is explicit per action: go live and switch to draft reuse :edit_workflow; edit in sandbox uses :provision_sandbox; promote and restore use :merge_sandbox.

Why

Today a workflow has no notion of "published": its only state is whether a trigger is enabled. Editing one that is processing production data means editing the live thing in place, with no separation between what is running and what you are changing, no review before a change reaches production, and no record of what was released and by whom.

V1 sandboxes gave us the isolation primitive (a full project clone you can change without touching the parent, with merge/4 to bring workflows back), but they are surfaced as a project-management feature, disconnected from editing a workflow. This epic makes the editor itself lifecycle-aware: a published workflow is locked on main, and changing it branches into a sandbox, keeps production on the old version, and comes back through a review. That is the safety story without new isolation or merge machinery, because both already exist.

What

A workflow gains an explicit lifecycle state.

State (badge) Where it lives Editable? Processing data? Primary action Secondary
draft the workflow on its own project yes no Go live Run (test)
live the workflow on its own project (main) no (read-only) yes Edit in sandbox (where available) or Switch to draft (otherwise) none
sandbox a cloned workflow inside a project sandbox yes yes (own trigger endpoint) Promote Run (test)
sandbox + promote pending (later) the sandbox copy with an open promote yes (edits update the request) yes View request Withdraw
archived sandbox a sandbox scheduled for deletion after promote no (read-only) no (trigger disabled) Delete none

The badge reflects context: on its own project a workflow shows draft or live; opened inside a sandbox it shows sandbox (plus promote pending, diverged, or archived sandbox). It renders in the collaborative editor header, next to the workflow name.

Lifecycle where sandboxes are available

  1. A new workflow starts as draft: editable, auto-saving, test-runnable, not processing data.
  2. Go live publishes it (live, v1), enables its triggers, and makes main read-only.
  3. Edit in sandbox opens a picker: create a new sandbox from the live version, or join an active one (sorted by last-edited, freshest first). The workflow opens in the sandbox editor, fully editable, with its own live trigger endpoint.
  4. Promote merges the change back to the parent using the existing sandbox merge with today's authorization, the version increments (v2), and the sandbox is archived. A PR-style review with pending, withdraw, and approval is the later branch-protection phase.
  5. The version history is the list of go-lives and promotes: who published each, from which sandbox, and when. See Record a version at each go-live and promote #4866.

Lifecycle where sandboxes are not available

  1. New workflow starts as draft, same as above.
  2. Go live makes it live and read-only. Edit in sandbox is visible but locked; the available action is Switch to draft.
  3. Switch to draft takes the workflow offline (disables triggers), behind a confirmation dialog, so it can be edited. Edit, test, Go live again.

Two user segments: organizational users route every change through the sandbox-and-approval path; individuals on smaller plans can edit directly on main via switch-to-draft.

Restore

Restore is the git-revert of workflows: it republishes a chosen earlier version as a new version and never overwrites the live workflow in place. It is an exact state replacement (the old version's content becomes the new content), published through the plan's normal path, promote on sandbox plans, switch-to-draft-then-go-live without. So a restore reads as a new forward version with a clean trail, and it is the safety net for a bad go-live or promote. See #4864.

Edge cases

  • Diverged sandbox. Main changed since the branch. Informational, not blocking; the promote/merge summary shows both sides, reusing MergeProjects.diverged_workflows/2. See Promote safety: surface diverged workflows in the merge #4863.
  • Parent gained a workflow after the fork. A workflow added to the parent after the sandbox was created looks like "deleted in the sandbox" to the merge and is pre-selected for deletion. Confirmed bug; fixed as Sandbox merge deletes a parent workflow added after the sandbox was forked #4919.
  • Archived sandbox. After promote: read-only, trigger disabled, deletable or kept for audit (existing scheduled-deletion). Stale sandboxes are acceptable and sort to the bottom by last-edited.

Design decisions

Lifecycle state is an explicit field, not derived from triggers.

Today "live" is implicit (a workflow is active when a trigger is enabled), which is why there is no read-only concept. Add state to Lightning.Workflows.Workflow:

field :state, Ecto.Enum, values: [:draft, :live], default: :draft

Go live sets :live and enables triggers in one transaction; switch to draft does the reverse. Triggers stay the execution mechanism; state is the user-facing contract that gates editing. It lives on the workflow, not the project, since the lifecycle is per-workflow.

Read-only on main reuses the existing editor permission.

useWorkflowReadOnly() (assets/js/collaborative-editor/hooks/useWorkflow.tsx) reads can_edit_workflow, computed in LightningWeb.Channels.WorkflowChannel, and already disables Save, the trigger toggle, and the editors. We extend that computation: read-only when the workflow is :live and the project is not a sandbox. Inside a sandbox the clone stays editable. One source of truth, so every surface that respects read-only gets the live lock for free.

Edit in sandbox reuses full-clone sandboxes; the one change is the edited workflow's trigger goes live.

"Create new sandbox" calls Sandboxes.provision/3 as today: a full clone of the parent's configuration. We do not let users pick a subset of workflows at creation, because a sandbox must stay a faithful, portable project.yaml, which is what makes promote, CLI deploy, GitHub sync, and migration one operation. The one-workflow experience comes from scoping the editor; selectivity belongs at promotion. The one divergence from V1: cloning disables triggers (clone_triggers_with_disabled_state), but here the edited workflow's trigger is enabled so the sandbox is live with its own /i/... endpoint.

Promote is the existing merge now; the review object is a later phase.

For this release, Promote is exactly the existing sandbox merge (Sandboxes.merge/4 to MergeProjects.merge_project/3 with selected_workflow_ids) with today's authorization (merge_sandbox, owner and admin). There is no request object and no approval step (#4929). The branch-protection phase (#4861, #4862) later wraps that merge in a PR-style review:

WorkflowPromotion
- id, workflow_id, source_project_id, target_project_id
- status (:pending | :withdrawn | :merged)
- requested_by_id, reviewed_by_id, note
- inserted_at, updated_at

In that phase Promote creates a :pending record, the sandbox stays editable while pending, withdraw cancels, and approval runs the merge for that workflow, records a new version, and archives the sandbox, with a required-reviewer policy as the following step. Independent of phase, the merge must not delete a workflow the parent gained after the fork: that row is currently flagged "Deleted in sandbox" and pre-selected, so a select-all merge soft-deletes it. Default those rows to unchecked (keep) with a neutral label (#4919); deliberate deletion still works.

A "version" is recorded at go-live and promote, not per save.

workflow_snapshots captures state per save (the recovery and content layer) and workflow_versions is an internal hash log for divergence and CLI export. Neither is the user-facing history. A new thin workflow_releases table records a version only at go-live and promote, with a sequential number, author, kind, and source sandbox, pointing at the snapshot for content; the version list reads from that. There is no per-keystroke database autosave, so no session-collapse mechanism is needed. Change-size metadata is a Phase 2 enhancement (#4931). See #4866.

Switching versions and sandboxes happens inside the editor.

This needs project-id mapping, since the editor URL carries the project UID and a sandbox is a different project. Long-term: project slugs; interim: an in-memory sandbox-to-project map. A proper cross-entity diff is out of scope.

Sandbox credential safety: fail loudly now, isolate later.

  • Target (Vision): connections are env-bound, so a sandbox uses test credentials.
  • Today: clone_credentials_from_parent shares the parent's real credentials.
  • This epic: a sandbox using production credentials shows a prominent red warning; nothing is disabled.

True isolation (env-bound connections, mocked endpoints) is out of scope, as is disabling cron triggers in a sandbox.

Feature gating reuses the usage-limiting extension.

UsageLimiting.limit_action/2 already gates :activate_workflow and :new_sandbox. Edit in sandbox is gated the same way and Go live through :activate_workflow; when disallowed, Edit in sandbox locks with the limiter's tooltip and Switch to draft is the path. Upsell copy comes from the extension, not hardcoded.

Still open

Estimate

Estimates live on the individual sub-issues, split across Phase 1 and Phase 2. The original single 61-point figure predates the phase split and the scope changes (promote reduced to the existing merge, versioning reframed onto go-live and promote, two bugs pulled out), so it is superseded by the per-issue points.

Metadata

Metadata

Assignees

Type

No type

Fields

No fields configured for issues without a type.

Projects

Status
Epics In Progress

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions