Plan: Persisted Undo/Redo and Workflow CHANGELOG by Bridging Frontend Actions to Backend Refactor API
Leverages tests in #21113 - is a more concrete plan for implementing #9166 in lieu of all the infrastructure added in https://github.com/galaxyproject/galaxy/pull/17774. I spent a few hours doing research with Claude to come up with this plan but as with any plan I think it is a little front-end heavy in quality. After Iteration 3 - I’ve got some real questions but I do think if we implemented iteration 2 and 3 we could keep the editor and backend capabilities in sync and then work to come up with a more detailed plan for the integration of the two. The E2E tests outlined in #21113 would be really helpful also in terms of ensuring there are no regressions as we implement the integration.
Goal
Serialize client-side atomic actions to the refactor API, apply them incrementally, and record a durable action journal that:
- Enables undo/redo across browser sessions.
- Drives a user-visible workflow CHANGELOG.
- Aligns TypeScript actions with Python refactor schema safely and incrementally.
Important Notes
- Frontend and backend actions are not a bijection: The frontend actions and backend refactor actions were designed by different people at different times. Some frontend actions will map to backend actions, some won’t, and some backend actions won’t have frontend equivalents. This is expected and acceptable.
- Connections don’t need explicit actions: Connections are properties of steps, not separate entities. They will be handled through step update actions on the backend.
Scope anchors
- Frontend actions live in:
client/src/components/Workflow/Editor/Actions/(stepActions, workflowActions, commentActions; tests inactions.test.ts)- Undo/redo infra in
client/src/stores/undoRedoStore/
- Backend refactor API lives in:
lib/galaxy/workflow/refactor/(schema.py,execute.py)- API router in
lib/galaxy/webapps/galaxy/api/workflows.py(PUT /api/workflows/{id}/refactor) - Service layer in
lib/galaxy/webapps/galaxy/services/workflows.py(delegates to managers which call theexecute.pylayer)
Step 0 — Comprehensive Undo/Redo Selenium Test Suite
GitHub Issue: https://github.com/galaxyproject/galaxy/issues/21113
Before implementing any backend changes or serialization, establish comprehensive test coverage for all existing undo/redo functionality in the workflow editor. This serves as both documentation of current behavior and regression prevention.
Summary: Create lib/galaxy_test/selenium/test_workflow_editor_undo_redo.py with ~30 comprehensive tests covering:
- Step operations: label changes, position moves, add/remove/duplicate, annotations, output labels, tool state, auto-layout
- Comment operations: add/delete/modify for all comment types (text, markdown, frame, freehand), color changes, position/size changes
- Selection operations: toggle selection, clear selection, duplicate selection, delete selection
- Workflow metadata: name, annotation, license changes
- Connections: add/remove connections with undo/redo
- Complex scenarios: multiple action sequences, undo/redo after save
Deliverables:
- New test file with ~30 tests
- Helper methods for common undo/redo assertions
- All existing undo/redo behavior documented and tested
- Foundation for regression prevention during backend implementation
Acceptance Criteria:
- All tests pass independently
- High code coverage for undo/redo store and action classes
- Consistent test patterns
- Tests serve as living documentation
See the GitHub issue for detailed test specifications.
Step 1 — Inventory, gaps, and architecture decisions
Before implementing anything, thoroughly research and document the current state, gaps, and required changes.
Step 1.0.1: Create action mapping inventory document
- File: Create
FRONTEND_BACKEND_ACTION_MAPPING.mdin project root - Actions:
- Document all frontend actions from
stepActions.ts,workflowActions.ts,commentActions.ts - Document all backend actions from
schema.py(line 232-253 has the union) - Create mapping table showing frontend → backend relationships
- Mark actions as: ✅ Direct mapping, 🔄 Needs enhancement, ❌ Missing, ⚠️ No backend equivalent needed
- Include action class names, file locations, and line numbers for easy reference
- Document all frontend actions from
Step 1.0.2: Research RemoveStepAction semantics
- Goal: Define exact behavior for removing steps
- Research tasks:
- Check how
RemoveStepAction(frontend) handles step removal instepActions.ts:310-346 - Review how
removeStepin step store handles connections - Document required backend behavior:
- Remove step from
_as_dict["steps"] - Remove all incoming connections to this step
- Remove all outgoing connections from this step
- Check if step has workflow_outputs and emit
workflow_output_drop_forcedmessages - Emit
connection_drop_forcedmessages for each dropped connection
- Remove step from
Write specification document:— consolidated intodocs/REMOVE_STEP_ACTION_SPEC.mdFRONTEND_BACKEND_ACTION_MAPPING.mdSection 4
- Check how
Step 1.0.3: Research absolute position support
- Goal: Determine how to add
position_absolutetoUpdateStepPositionAction - Research tasks:
- Review current
UpdateStepPositionActioninschema.py:103-106(only hasposition_shift) - Review executor method
_apply_update_step_positioninexecute.py:89 - Understand how frontend stores positions (absolute left/top coordinates)
- Define schema change: Add optional
position_absolute: Optional[Position]field - Define validation: Exactly one of
position_shiftorposition_absolutemust be provided - Write specification: Add to mapping document under “Position Handling Strategy”
- Review current
Step 1.0.4: Research comment storage and serialization
- Goal: Understand how comments are stored in workflow JSON
- Research tasks:
- Search codebase for workflow JSON comment structure:
grep -r "comments" lib/galaxy/workflow/modules.py lib/galaxy/managers/workflows.py - Find example workflows with comments in test fixtures
- Document comment schema: id, type, position, size, color, data
- Check if comments are included in workflow export/import
- Identify any gaps in serialization/deserialization
Write specification:— consolidated intodocs/COMMENT_PERSISTENCE_SPEC.mdFRONTEND_BACKEND_ACTION_MAPPING.mdSection 7
- Search codebase for workflow JSON comment structure:
Step 1.0.5: Research step ID references
- Goal: Determine feasibility of adding ID-based step references
- Research tasks:
- Review current step reference types in
schema.py:27-37 - Check how step IDs are assigned and persisted in workflow JSON
- Determine if step IDs are stable across saves/loads
- Research ID stability when steps are reordered
- Define new reference types:
StepReferenceById(id: int)InputReferenceById(id: int, input_name: str)OutputReferenceById(id: int, output_name: str)
- Define validation strategy when both
idandlabelprovided - Document in mapping file under “Step Reference Strategy”
- Review current step reference types in
Step 1.0.6: Locate connection action handling
- Goal: Confirm connections work through step updates, not separate actions
- Research tasks:
- Search for connection add/remove in workflow editor:
grep -n "addConnection\|removeConnection" client/src/stores/workflowStepStore.ts - Verify
addConnectioninworkflowStepStore.tscalls step update callbacks - Confirm connections are properties of steps in backend
- Document that connections handled via
ConnectAction/DisconnectActionin backend - Note in mapping: “Frontend connection changes → Backend ConnectAction/DisconnectAction”
- Search for connection add/remove in workflow editor:
Step 1.0.7: Create comprehensive frontend→backend action mapping
- File: Update
FRONTEND_BACKEND_ACTION_MAPPING.mdwith final mapping - Content:
Direct Mappings (✅ Ready to serialize):
| Frontend Action | Frontend File | Backend Action | Notes |
|---|---|---|---|
LazySetLabelAction | stepActions.ts:83 | UpdateStepLabelAction | Direct mapping |
LazySetOutputLabelAction | stepActions.ts:124 | UpdateOutputLabelAction | Direct mapping |
LazySetValueAction (name) | workflowActions.ts:16 | UpdateNameAction | Direct mapping |
LazySetValueAction (annotation) | workflowActions.ts:16 | UpdateAnnotationAction | Direct mapping |
LazySetValueAction (license) | workflowActions.ts:16 | UpdateLicenseAction | Direct mapping |
InsertStepAction | stepActions.ts:254 | AddStepAction | Direct mapping |
Needs Enhancement (🔄 Backend changes required):
| Frontend Action | Frontend File | Backend Action | Required Changes |
|---|---|---|---|
setPosition / LazyMoveMultipleAction | stepActions.ts:598 | UpdateStepPositionAction | Add position_absolute support |
RemoveStepAction | stepActions.ts:310 | ❌ RemoveStepAction | NEW: Must implement in backend |
Comment Actions (❌ All new backend actions needed):
| Frontend Action | Frontend File | Backend Action Needed | Priority |
|---|---|---|---|
AddCommentAction | commentActions.ts:33 | AddCommentAction | High |
DeleteCommentAction | commentActions.ts:47 | DeleteCommentAction | High |
ChangeColorAction | commentActions.ts:61 | UpdateCommentColorAction | Medium |
LazyChangeDataAction | commentActions.ts:139 | UpdateCommentDataAction | High |
LazyChangePositionAction | commentActions.ts:175 | UpdateCommentPositionAction | Medium |
LazyChangeSizeAction | commentActions.ts:186 | UpdateCommentSizeAction | Medium |
RemoveAllFreehandCommentsAction | commentActions.ts:229 | RemoveAllFreehandCommentsAction | Low |
No Backend Equivalent Needed (⚠️ UI-only actions):
| Frontend Action | Frontend File | Reason |
|---|---|---|
UpdateStepAction | stepActions.ts:167 | Generic step update; serializer inspects changed keys to emit specific backend actions (e.g. position → UpdateStepPositionAction). Connection changes do NOT flow through this action. |
SetDataAction | stepActions.ts:229 | Subclass of UpdateStepAction for tool form diffs. Connection changes do NOT flow through this action. |
CopyStepAction | stepActions.ts:348 | Combination of AddStep + copy data |
ToggleStepSelectedAction | stepActions.ts:383 | UI state only |
AutoLayoutAction | stepActions.ts:424 | Results in position updates |
ClearSelectionAction | workflowActions.ts:287 | UI state only |
AddToSelectionAction | workflowActions.ts:344 | UI state only |
RemoveFromSelectionAction | workflowActions.ts:353 | UI state only |
DuplicateSelectionAction | workflowActions.ts:362 | Combination of multiple adds |
DeleteSelectionAction | workflowActions.ts:390 | Combination of multiple removes |
ToggleCommentSelectedAction | commentActions.ts:197 | UI state only |
CopyIntoWorkflowAction | workflowActions.ts:115 | Combination of multiple adds |
LazyMoveMultipleAction | workflowActions.ts:184 | Results in multiple position updates |
Connection Handling:
- Frontend: Connection changes use anonymous
FactoryActioninstances interminals.ts:connect()(line 96) →FactoryActionnamed"connect steps"→ callsconnectionStore.addConnection(connection)disconnect()(line 108) →FactoryActionnamed"disconnect steps"→ callsconnectionStore.removeConnection(id)- These are NOT
UpdateStepActionorSetDataAction. The store mutation ofinput_connectionshappens underneath but is not captured as a typed undo action. - When steps are removed/undone,
RemoveStepActionsaves and restores connections separately.
- Backend:
ConnectActionandDisconnectActionalready exist and are fully implemented inrefactor/schema.py(lines 125-134) andrefactor/execute.py(lines 167-197). They takeinput(step ref + input_name) andoutput(step ref + output_name) references. - Strategy: Map frontend
FactoryActioninstances to existing backendConnectAction/DisconnectAction:- Serializer identifies connection actions by
action.name === "connect steps"/"disconnect steps" - The
Connectionobject ({input: {stepId, name}, output: {stepId, name}}) maps directly to the backend schema - Recommended: Create typed
ConnectStepAction/DisconnectStepActionclasses in frontend (replacing anonymous FactoryActions interminals.ts) for type-safeinstanceofserialization
- Serializer identifies connection actions by
Step 1.0.8: Define implementation scope
-
File: Add to
FRONTEND_BACKEND_ACTION_MAPPING.md -
Core Scope (All major workflow editing actions):
- Step label changes
- Step position changes (with absolute position support)
- Add step
- Remove step (new backend action)
- Output label changes
- Step annotation changes
- Workflow name/annotation/license/report
- All comment actions (add, delete, move, resize, change color, change data, remove all freehand)
-
Future Enhancements (Not in initial implementation):
- Step ID-based references (can use label/order_index initially)
- Tool state updates (complex, can be added later)
Explicit connection actions— use existingConnectAction/DisconnectActionfrom the start; only frontend work is creating typed action classes interminals.ts(belongs in Core Scope)- Action compaction and optimization
Deliverables for Iteration 1:
FRONTEND_BACKEND_ACTION_MAPPING.md- Complete action inventory, mapping, and all specifications (RemoveStep spec in Section 4, comment persistence in Section 7, position handling in Section 5, step ID references in Section 6)- Clear decision on Phase 1 scope
- Research findings documented for position_absolute and step ID references
Acceptance Criteria for Iteration 1:
- All research questions answered with code references
- Single consolidated mapping document (
FRONTEND_BACKEND_ACTION_MAPPING.md) reviewed and approved - No ambiguity about what needs to be built in each phase
- Phase 1 scope is clear and minimal
- Unresolved questions documented for discussion
Iteration 2 — Backend schema and executor enhancements ✅ COMPLETE
Implement all backend changes needed to support core workflow editing actions. This includes step operations, absolute positions, and all comment operations.
Status: All sub-steps complete. 33 unit tests passing. Integration tests added. API docs updated.
Step 2.1: Implement RemoveStepAction schema
- File:
lib/galaxy/workflow/refactor/schema.py - Actions:
- Add
RemoveStepActionclass afterAddStepAction(around line 126):class RemoveStepAction(BaseAction): action_type: Literal["remove_step"] step: step_reference_union = step_target_field - Add to
union_action_classes(line 232) - Verify action_type automatically registers in
ACTION_CLASSES_BY_TYPE
- Add
Step 2.2: Implement RemoveStepAction executor
- File:
lib/galaxy/workflow/refactor/execute.py - Method:
_apply_remove_step(self, action: RemoveStepAction, execution: RefactorActionExecution) - Logic:
- Resolve step reference to get step dict
- Get step’s order_index
- Find all connections where this step is input or output
- For each dropped connection, emit
connection_drop_forcedmessage - Check if step has workflow_outputs
- For each workflow output, emit
workflow_output_drop_forcedmessage - Remove step from
self._as_dict["steps"] - Tolerate order_index gaps — do NOT re-index remaining steps. Step 2.5 adds ID-based references, so subsequent in-batch operations can use IDs instead of order_indices. Re-indexing would break any in-batch references by order_index.
- Tests (
test/unit/workflow/refactor/test_remove_step.py):- Test removing unconnected step
- Test removing step with one connection
- Test removing step with multiple connections
- Test removing step with workflow outputs
- Test execution messages are correct
- Test referencing by label
- Test referencing by order_index
- Test error: step not found
Step 2.3: Add absolute position support to schema
- File:
lib/galaxy/workflow/refactor/schema.py - Changes to
UpdateStepPositionAction(line 103):class UpdateStepPositionAction(BaseAction): action_type: Literal["update_step_position"] step: step_reference_union = step_target_field position_shift: Optional[Position] = None position_absolute: Optional[Position] = None @model_validator(mode='after') def validate_position_exactly_one(self): if self.position_shift is None and self.position_absolute is None: raise ValueError("Must provide either position_shift or position_absolute") if self.position_shift is not None and self.position_absolute is not None: raise ValueError("Cannot provide both position_shift and position_absolute") return self
Step 2.4: Implement absolute position support in executor
- File:
lib/galaxy/workflow/refactor/execute.py - Update:
_apply_update_step_positionmethod (around line 89) - Logic:
def _apply_update_step_position(self, action: UpdateStepPositionAction, execution: RefactorActionExecution): step = self._resolve_step(action.step) if action.position_absolute: # Set absolute position step["position"] = action.position_absolute.to_dict() elif action.position_shift: # Apply relative shift (existing behavior) current_position = step.get("position") or {"left": 0, "top": 0} step["position"] = { "left": current_position["left"] + action.position_shift.left, "top": current_position["top"] + action.position_shift.top, } - Tests (
test/unit/workflow/refactor/test_position.py):- Test position_shift (existing functionality)
- Test position_absolute
- Test error: neither provided
- Test error: both provided
- Test backward compatibility with existing workflows
Step 2.5: Implement step reference resolution strategy
The frontend references steps by ID — all backend actions that allow referencing steps by order_index or label should be augmented to also allow referencing steps by id.
- File:
lib/galaxy/workflow/refactor/schema.py - Changes:
- Add
StepReferenceById:class StepReferenceById(BaseModel): id: int = Field(description="The database ID of the step being referenced.") - Add corresponding input/output reference types:
class InputReferenceById(StepReferenceById): input_name: str = input_name_field class OutputReferenceById(StepReferenceById): output_name: Optional[str] = output_name_field - Update all three union types:
step_reference_union = Union[StepReferenceByOrderIndex, StepReferenceByLabel, StepReferenceById] input_reference_union = Union[InputReferenceByOrderIndex, InputReferenceByLabel, InputReferenceById] output_reference_union = Union[OutputReferenceByOrderIndex, OutputReferenceByLabel, OutputReferenceById]
- Add
- File:
lib/galaxy/workflow/refactor/execute.py - Changes to
_find_step: The currentelsebranch assumes anything that isn’tStepReferenceByLabelisStepReferenceByOrderIndex. Must change to explicitisinstancechecks for all three types, raisingValueErrorfor unknown reference types.
Step 2.6: Add Size model and comment action schemas
- File:
lib/galaxy/workflow/refactor/schema.py - Actions:
- Add
Sizemodel (do NOT reusePosition—left/topfields are misleading for width/height):class Size(BaseModel): width: float height: float def to_dict(self): return {"width": self.width, "height": self.height} - Add comment actions after workflow metadata actions (around line 197):
class CommentReference(BaseModel): comment_id: int # The comment's "id" field (order_index in DB), NOT an array index. # Matches BaseComment.id in schema/workflow/comments.py. # Looked up by scanning the comments array for matching "id". class AddCommentAction(BaseAction): action_type: Literal["add_comment"] type: str # "text", "markdown", "frame", "freehand" position: Position size: Size color: str data: dict[str, Any] class DeleteCommentAction(BaseAction): action_type: Literal["delete_comment"] comment: CommentReference class UpdateCommentPositionAction(BaseAction): action_type: Literal["update_comment_position"] comment: CommentReference position: Position class UpdateCommentSizeAction(BaseAction): action_type: Literal["update_comment_size"] comment: CommentReference size: Size class UpdateCommentColorAction(BaseAction): action_type: Literal["update_comment_color"] comment: CommentReference color: str class UpdateCommentDataAction(BaseAction): action_type: Literal["update_comment_data"] comment: CommentReference data: dict[str, Any] class RemoveAllFreehandCommentsAction(BaseAction): action_type: Literal["remove_all_freehand_comments"] - Add all to
union_action_classes
- Add
- Note: Frontend comments use
[x,y]tuples for position and[width,height]for size. The executor must convert between{left,top}/{width,height}dicts and these tuple representations. - Note: Comment
idin the workflow JSON (mapped fromWorkflowComment.order_index) is a stable identifier, not an array index.FrameComment.child_commentsreferences other comments by thisid. Deleting a comment removes it from the array but does NOT renumber remaining comments’idvalues, so batch operations are safe without reverse-ordering tricks.
Step 2.7: Implement comment action executors
- File:
lib/galaxy/workflow/refactor/execute.py - Methods: Add executor methods for each comment action
- Helper: Add
_find_comment(self, comment_ref: CommentReference)that scansself._as_dict["comments"]for the dict whose"id"field matchescomment_ref.comment_id. RaisesRequestParameterInvalidExceptionif not found. Analogous to_find_stepbut for comments. - Logic:
_apply_add_comment: Append comment dict to array. Assign"id"asmax(c["id"] for c in existing) + 1(or 0 if empty)._apply_delete_comment: Call_find_comment, remove matching dict. Does NOT shift other comments’"id"values — IDs are stable, only the array shrinks._apply_update_comment_position: Call_find_comment, update"position"_apply_update_comment_size: Call_find_comment, update"size"_apply_update_comment_color: Call_find_comment, update"color"_apply_update_comment_data: Call_find_comment, update"data"_apply_remove_all_freehand_comments: Filter array to remove all with"type" == "freehand"
- Batch safety: Because comments are referenced by their
"id"field (not array index), deleting comment id=2 does NOT invalidate a subsequent reference to comment id=5. No reverse-ordering or snapshot-then-apply needed. - Tests (
test/unit/workflow/refactor/test_comments.py):- Test each comment action independently
- Test comment id assignment on add
- Test error: comment not found (invalid comment_id)
- Test round-trip: add → update → delete
- Test batch stability: delete comment id=1, then update comment id=3 in same batch — verify id=3 is correctly found despite array shifting
- Test that delete does not reassign IDs of remaining comments
Step 2.8: Verify workflow JSON comment persistence
- Research Task: Confirm comments are stored in workflow JSON
- Files to check:
lib/galaxy/managers/workflows.pylib/galaxy/workflow/modules.py
- Verify:
- Comments are included in workflow export
- Comments are restored on workflow import
- Comment schema matches frontend expectations
- Document findings in
FRONTEND_BACKEND_ACTION_MAPPING.mdSection 7
Step 2.9: Add backend integration tests for core actions
- File:
test/integration/test_workflow_refactor_api.py - Tests:
- Test step label change via refactor API
- Test step position change (absolute and relative)
- Test add step via refactor API
- Test remove step via refactor API
- Test step annotation changes
- Test output label changes
- Test workflow name/annotation/license changes
- Test add comment via refactor API
- Test delete comment via refactor API
- Test comment modifications (color, position, size, data)
- Test remove all freehand comments
- Test action batching (multiple actions in one request)
- Test dry_run mode
- Test execution messages are returned
Step 2.10: Update API documentation
- File:
lib/galaxy/webapps/galaxy/api/workflows.py - Actions:
- Update docstring for refactor endpoint
- Document new
RemoveStepAction - Document
position_absoluteonUpdateStepPositionAction - Document all comment actions
- Add examples for all core actions (steps, comments, workflow metadata)
- Document execution messages
Deliverables for Iteration 2:
RemoveStepActionimplemented in schema and executorposition_absolutesupport added toUpdateStepPositionAction- All comment action schemas defined and implemented
- All comment action executors implemented
- All step references can be by ID.
- Comprehensive backend tests for all actions (steps, comments, workflow metadata)
- Integration tests for refactor API
- Updated API documentation
Acceptance Criteria for Iteration 2:
- All backend unit tests pass
- Integration tests pass
- RemoveStepAction works correctly with connections and workflow outputs
- Both position_absolute and position_shift work correctly
- All comment actions work correctly (add, delete, modify)
- Workflow metadata actions work correctly
- Backend is ready for frontend integration in Iteration 4
Iteration 3 — Define cross-layer action contract and serializer in the client
Create a pure serialization layer that converts frontend undo/redo actions into backend refactor API actions. This iteration has NO API calls - just type-safe serialization with comprehensive tests.
Step 3.0: Add what field to all SetValueActionHandler instances
- File:
client/src/components/Workflow/Editor/Index.vue - Problem: 6 of 9
SetValueActionHandlerinstances havewhat = null, making serialization routing onaction.whatimpossible. - Fix: Add
whatparameter to all 6 missing handlers:Handler Current whatAdd whatsetNameActionHandlernull"name"setDoiHandlernull"doi"setReadmeHandlernull"readme"setHelpHandlernull"help"setLogoUrlHandlernull"logoUrl"setTagsHandlernull"tags" - Already set (no change needed):
setLicenseHandler("license"),setCreatorHandler("creator"),setAnnotationHandler("annotation")
Step 3.1: Create serializer module structure and import types
- File:
client/src/components/Workflow/Editor/Actions/refactorSerialization.ts - Actions:
- Create module with clear JSDoc documentation
- Import existing TypeScript types from auto-generated schema:
import type { components } from "@/api/schema"; type UpdateStepLabelAction = components["schemas"]["UpdateStepLabelAction"]; type UpdateStepPositionAction = components["schemas"]["UpdateStepPositionAction"]; type UpdateOutputLabelAction = components["schemas"]["UpdateOutputLabelAction"]; type AddStepAction = components["schemas"]["AddStepAction"]; type RemoveStepAction = components["schemas"]["RemoveStepAction"]; // Will add in Iteration 2 type UpdateNameAction = components["schemas"]["UpdateNameAction"]; type UpdateAnnotationAction = components["schemas"]["UpdateAnnotationAction"]; type UpdateLicenseAction = components["schemas"]["UpdateLicenseAction"]; // ... etc for all action types type RefactorAction = components["schemas"]["RefactorRequest"]["actions"][number]; - Create base serialization interface:
interface SerializationResult { actions: RefactorAction[]; title: string; // For changelog success: boolean; error?: string; } - Create main function signature:
export function serializeAction( action: UndoRedoAction, context: SerializationContext ): SerializationResult - Define
SerializationContexttype with step store, state store references
Step 3.2: Remove - types already exist in generated schema
- Note: We’re using auto-generated types from
client/src/api/schema/schema.ts - These are generated from the backend OpenAPI schema
- When we add new backend actions (like
RemoveStepAction, comment actions), we’ll need to:- Add them to backend
schema.py - Regenerate TypeScript schema
- Import the new types in our serializer
- Add them to backend
Step 3.3: Implement label change serialization
- Function:
serializeLabelChange(action: LazySetLabelAction): RefactorAction[] - Logic:
- Get step from store using
action.stepId - Resolve step reference
- Return
UpdateStepLabelActionwith new label
- Get step from store using
- Tests:
- Test with unique label
- Test with step referenced by order_index
- Test error case: step not found
Step 3.4: Implement output label change serialization
- Function:
serializeOutputLabelChange(action: LazySetOutputLabelAction): RefactorAction[] - Logic:
- Get step from store
- Resolve step reference
- Determine which output changed (compare fromValue/toValue)
- Return
UpdateOutputLabelActionwith output reference
- Tests:
- Test setting output label
- Test clearing output label
- Test multiple outputs
Step 3.5: Implement position change serialization (absolute positions)
- Function:
serializePositionChange(action: LazyMutateStepAction<'position'>): RefactorAction[] - Logic:
- Get step from store
- Use
action.toValueas absolute position - Return
UpdateStepPositionActionwithposition_absolute
- Function:
serializeMoveMultiple(action: LazyMoveMultipleAction): RefactorAction[] - Logic:
LazyMoveMultipleActionmoves BOTH steps AND comments- Iterate action’s step list → emit
UpdateStepPositionAction[]withposition_absolutefor each - Iterate action’s comment list → emit
UpdateCommentPositionAction[]for each - Return combined array of all position actions
- Tests:
- Test single step move
- Test move with position calculation
- Test LazyMoveMultipleAction with steps only
- Test LazyMoveMultipleAction with steps AND comments
- Test LazyMoveMultipleAction with comments only
Step 3.6: Implement add step serialization
- Function:
serializeAddStep(action: InsertStepAction): RefactorAction[] - Logic:
- Extract contentId, name, type, position from
action.stepData - Build
AddStepAction:type: from stepDatalabel: from stepData or undefinedposition: absolute positiontool_state: null for now (comes later)
- Return action
- Extract contentId, name, type, position from
- Tests:
- Test adding data input
- Test adding tool step with label
- Test adding tool step without label
- Test position is included
Step 3.7: Implement workflow metadata serialization
- Function:
serializeWorkflowMetadata(action: LazySetValueAction): RefactorAction[] - Logic:
- Route on
action.what(all 9 handlers now havewhatset per Step 3.0):what === "name"→UpdateNameActionwhat === "annotation"→UpdateAnnotationActionwhat === "license"→UpdateLicenseActionwhat === "creator"→UpdateCreatorActionwhat === "doi"/"readme"/"help"/"logoUrl"/"tags"→ future backend actions or no-op
- For metadata types without a backend action yet, return
{ actions: [], success: false, error: "not yet supported" }
- Route on
- Tests:
- Test name change
- Test annotation change
- Test license change
- Test creator change
- Test unsupported metadata type returns graceful error
Step 3.8: Implement comment action serialization
- Functions: Create serializers for all comment actions
serializeAddComment(action: AddCommentAction): RefactorAction[]serializeDeleteComment(action: DeleteCommentAction): RefactorAction[]serializeChangeCommentColor(action: ChangeColorAction): RefactorAction[]serializeChangeCommentData(action: LazyChangeDataAction): RefactorAction[]serializeChangeCommentPosition(action: LazyChangePositionAction): RefactorAction[]serializeChangeCommentSize(action: LazyChangeSizeAction): RefactorAction[]serializeRemoveAllFreehand(action: RemoveAllFreehandCommentsAction): RefactorAction[]
- Logic: Similar pattern to step actions - extract data and build backend action
- Tests: Comprehensive tests for each comment action type
Step 3.9: Implement remove step serialization
- Function:
serializeRemoveStep(action: RemoveStepAction): RefactorAction[] - Logic:
- Get step from action
- Resolve step reference
- Return
RemoveStepActionwith step reference
- Tests:
- Test removing step by label
- Test removing step by order_index
- Test removing connected step (connections handled by backend)
Step 3.10: Implement main serialization dispatcher
- Function:
serializeAction(action: UndoRedoAction, context: SerializationContext): SerializationResult - Logic:
- Use
instanceofchecks to route to specific serializers:if (action instanceof LazySetLabelAction) { return serializeLabelChange(action); } else if (action instanceof LazySetOutputLabelAction) { return serializeOutputLabelChange(action); } else if (action instanceof AddCommentAction) { return serializeAddComment(action); } else if (action instanceof RemoveStepAction) { return serializeRemoveStep(action); } // ... etc for ALL action types - For unsupported actions, return:
{ actions: [], title: action.name, success: false, error: "Action type not yet supported for serialization" } - Wrap in try/catch for robust error handling
- Use
- Tests:
- Test routing to each serializer (steps, comments, workflow metadata)
- Test unsupported action handling
- Test error case handling
Step 3.11: Create comprehensive serializer test suite
- File:
client/src/components/Workflow/Editor/Actions/refactorSerialization.test.ts - Test Structure:
describe('refactorSerialization', () => { describe('Step Actions', () => { describe('serializeLabelChange', () => { ... }); describe('serializeOutputLabelChange', () => { ... }); describe('serializePositionChange', () => { ... }); describe('serializeAddStep', () => { ... }); describe('serializeRemoveStep', () => { ... }); }); describe('Comment Actions', () => { describe('serializeAddComment', () => { ... }); describe('serializeDeleteComment', () => { ... }); describe('serializeChangeCommentColor', () => { ... }); describe('serializeChangeCommentData', () => { ... }); describe('serializeChangeCommentPosition', () => { ... }); describe('serializeChangeCommentSize', () => { ... }); describe('serializeRemoveAllFreehand', () => { ... }); }); describe('Workflow Metadata Actions', () => { describe('serializeWorkflowMetadata', () => { ... }); }); describe('Dispatcher', () => { describe('serializeAction', () => { ... }); describe('resolveStepReference', () => { ... }); }); }); - Test Coverage Goals:
- All serializer functions >95% branch coverage
- Edge cases: missing data, null values, invalid references
- Schema validation: ensure output matches backend schema
Step 3.12: Add serialization documentation
- File:
client/src/components/Workflow/Editor/Actions/README_SERIALIZATION.md - Content:
- Overview of serialization architecture
- Mapping table: Frontend action → Backend action
- Step reference resolution strategy
- Position handling strategy
- Error handling patterns
- Examples of serialized output
- Future enhancements (tool state, action batching)
Deliverables for Iteration 3:
refactorSerialization.ts- Pure serialization logic (no API calls, imports types from auto-generated schema)refactorSerialization.test.ts- Comprehensive test suiteREADME_SERIALIZATION.md- Documentation- All tests passing with >95% coverage
- Note: TypeScript types come from
client/src/api/schema/schema.ts(auto-generated from backend OpenAPI)
Acceptance Criteria for Iteration 3:
- All core actions can be serialized:
- Step actions: label, position, add, remove, output label, annotation
- Comment actions: add, delete, color, data, position, size, remove all freehand
- Workflow metadata: name, annotation, license
- Serialization produces valid JSON matching backend schema
- Comprehensive test coverage
- Zero API dependencies (pure functions)
- Can be used independently for testing/validation
- Clear error messages for unsupported actions
Iteration 4 — Persisted action journal and CHANGELOG endpoints
Create database persistence for workflow refactoring actions by extending the existing PUT /refactor endpoint, and add new endpoints for changelog and revert.
Key architectural decisions (from review):
- One code path: Extend
PUT /api/workflows/{id}/refactorwith optional journal fields instead of creating a parallelPOST /refactor_actionsendpoint. Journal entries are recorded whentitleis present; otherwise existing behavior is unchanged. - Atomic transaction: Journal entry and workflow version share one DB transaction.
- FK to StoredWorkflow: Journal tracks changes across versions of the logical workflow container.
- Layer split: Thin
WorkflowActionJournalManagerfor CRUD; orchestration stays inWorkflowsService/WorkflowContentsManager.
Step 4.1: Design action journal database schema
- File: New migration in
lib/galaxy/model/migrations/ - Table:
workflow_action_journal_entry - Columns:
id(integer, primary key)stored_workflow_id(integer, FK →stored_workflow.id, indexed) — the logical workflow container, NOTworkflow.iduser_id(integer, FK →galaxy_user.id)create_time(timestamp, indexed) — useUsesCreateAndUpdateTimemixintitle(varchar 255) — human-readable action title for changelogsource_action_type(varchar 255, nullable) — frontend action class name (e.g."LazySetLabelAction")action_payloads(JSONType) — array of refactor action objects as serialized dictsworkflow_id_before(integer, FK →workflow.id) — DB PK ofWorkflowrevision before this actionworkflow_id_after(integer, FK →workflow.id) — DB PK ofWorkflowrevision after this actionexecution_messages(JSONType) — array of execution message dictsis_revert(boolean, default False)reverted_entry_id(integer, FK →workflow_action_journal_entry.id, nullable) — if revert, references original entry
- Note: Use
Workflow.id(stable DB PK) for version references, NOT positional version indices which shift on revert. UseJSONType(notJSONorMutableJSONType) for immutable JSON columns.
Step 4.2: Create SQLAlchemy model
- File:
lib/galaxy/model/__init__.py - Class:
WorkflowActionJournalEntry - Inherits:
Base,UsesCreateAndUpdateTime - Fields: Match database schema above
- Relationships:
stored_workflow— Many-to-one withStoredWorkflowuser— Many-to-one withUserworkflow_before— Many-to-one withWorkflow(viaworkflow_id_before)workflow_after— Many-to-one withWorkflow(viaworkflow_id_after)reverted_entry— self-referential Many-to-one
- Methods:
to_dict()— serialize for API response; usesecurity.encode_id()for all IDs in output
Step 4.3: Create database migration
- File: New alembic migration
- Actions:
- Create
workflow_action_journal_entrytable - Add indices on
stored_workflow_id,create_time - Add foreign key constraints (stored_workflow, user, workflow_before, workflow_after, self)
- Test upgrade/downgrade
- Create
Step 4.4: Implement WorkflowActionJournalManager (thin CRUD)
- File:
lib/galaxy/managers/workflow_action_journal_manager.py(new) - Class:
WorkflowActionJournalManager - Constructor:
__init__(self)— no special deps; methods receivesa_sessionviatrans.sa_session - Methods (all take
sa_sessionas first arg, following Galaxy convention):create_entry(sa_session, stored_workflow, user, title, source_action_type, actions, workflow_before, workflow_after, messages)→WorkflowActionJournalEntry- Creates model instance, adds to session (does NOT commit — caller manages transaction)
list_entries(sa_session, stored_workflow, limit=50, offset=0)→tuple[list[WorkflowActionJournalEntry], int]- Returns
(entries, total_count)for pagination
- Returns
get_entry(sa_session, entry_id)→WorkflowActionJournalEntrycreate_revert_entry(sa_session, stored_workflow, user, workflow_before, workflow_after, target_workflow)→WorkflowActionJournalEntry- Sets
is_revert=True,reverted_entry_id=None,title="Reverted to version N"(N fromstored_workflow.version_of(target_workflow)) action_payloads=[],execution_messages=[]- Does NOT commit — caller manages transaction
- Sets
Step 4.5: Register in DI container
- File:
lib/galaxy/app.py - In
GalaxyManagerApplication.__init__()(near line 637 whereworkflow_manageris registered):self._register_singleton(WorkflowActionJournalManager) - File:
lib/galaxy/webapps/galaxy/services/workflows.py - In
WorkflowsService.__init__(): Addworkflow_action_journal_manager: WorkflowActionJournalManagerparameter (lagom auto-resolves)
Step 4.6: Extend PUT /refactor with journal support
- File:
lib/galaxy/workflow/refactor/schema.py - Changes to
RefactorActions(the request body model):class RefactorActions(BaseModel): actions: list[Annotated[union_action_classes, Field(discriminator="action_type")]] dry_run: bool = False title: Optional[str] = None # When present, creates a journal entry source_action_type: Optional[str] = None # Frontend action class name - No new endpoint — the existing
PUT /api/workflows/{workflow_id}/refactorhandles everything - Response: Unchanged
RefactorResponse(already returns full workflow dict + action_executions + dry_run)
Step 4.7: Wire journal writing into refactor flow (atomic transaction)
- File:
lib/galaxy/managers/workflows.py - Changes to
do_refactor()(around line 2027):- Before calling
update_workflow_from_raw_description(), captureworkflow_before = stored_workflow.latest_workflow - Pass
defer_commit=Truetoupdate_workflow_from_raw_description()so it flushes but does NOT commit - After
update_workflow_from_raw_description()returnsrefactored_workflow:- If
refactor_request.titleis not None and notdry_run:- Call
journal_manager.create_entry(trans.sa_session, stored_workflow, trans.user, ...) - This adds the journal entry to the same session
- Call
- Then
trans.sa_session.commit()— one transaction for both workflow version + journal entry
- If
- If
titleis None, commit immediately (existing behavior)
- Before calling
- Changes to
update_workflow_from_raw_description()(line 773):- Add
defer_commit: bool = Falseparameter - When
defer_commit=True: calltrans.sa_session.flush()instead oftrans.sa_session.commit() - When
defer_commit=False: existing behavior (commit)
- Add
- Auth: Use existing
get_stored_workflow(trans, workflow_id)pattern which validates ownership
Step 4.8: Implement GET /api/workflows/{id}/changelog endpoint
- File:
lib/galaxy/webapps/galaxy/api/workflows.py - Route:
GET /api/workflows/{workflow_id}/changelog - Query Params:
limit(default 50),offset(default 0) - Logic:
stored_workflow = self._workflows_manager.get_stored_workflow(trans, workflow_id)entries, total = journal_manager.list_entries(trans.sa_session, stored_workflow, limit, offset)- Return entries as JSON array; set
total_matchesresponse header (Galaxy pagination convention)
- Response (array, with
total_matchesheader):[ { "id": "encoded_id", "title": "Change step label", "source_action_type": "LazySetLabelAction", "create_time": "2025-01-15T10:30:00Z", "user_id": "encoded_user_id", "workflow_id_before": "encoded_wf_id", "workflow_id_after": "encoded_wf_id", "execution_messages": [], "is_revert": false } ]
Step 4.9: Implement POST /api/workflows/{id}/revert endpoint
-
File:
lib/galaxy/webapps/galaxy/api/workflows.py -
Route:
POST /api/workflows/{workflow_id}/revert -
Request Body:
{ "target_workflow_id": "encoded_workflow_id" }target_workflow_idis theWorkflow.id(DB PK, encoded) of the revision to restore. The caller can pass eitherworkflow_id_beforeorworkflow_id_afterfrom a changelog entry — the endpoint is agnostic; it just needs a validWorkflow.idbelonging to thisStoredWorkflow. The frontend renders “Undo everything from this point forward” usingworkflow_id_before, or “Restore this version” usingworkflow_id_after. -
Semantics: Revert always creates a new
Workflowrow (append-only). It does NOT re-pointlatest_workflowto the old row. Rationale:- Preserves the append-only version history invariant used everywhere in Galaxy.
- Old
Workflowrows may be referenced byWorkflowInvocation.workflow_id. - The revert journal entry needs a distinct
workflow_id_after(new row) that differs fromworkflow_id_before(currentlatest_workflowbefore revert).
-
Logic (in
WorkflowContentsManager, following thedo_refactorpattern):stored_workflow = self.get_stored_workflow(trans, workflow_id)— validates ownershiptarget_workflow = stored_workflow.get_internal_version_by_id(decoded_target_id)— validates target belongs to this StoredWorkflow; raises error otherwise- No-op check: If
target_workflow.id == stored_workflow.latest_workflow.id, raiseRequestParameterInvalidException("Target version is already the current version") workflow_before = stored_workflow.latest_workflow— capture for journal entry- Export target workflow to dict via
_workflow_to_dict_export(trans, stored_workflow, workflow=target_workflow, internal=True) raw_description = self.normalize_workflow_format(trans, as_dict)workflow_update_options = WorkflowUpdateOptions(fill_defaults=False, allow_missing_tools=True)new_workflow, errors = self.update_workflow_from_raw_description(trans, stored_workflow, raw_description, workflow_update_options, defer_commit=True)- Create revert journal entry:
journal_manager.create_revert_entry( sa_session=trans.sa_session, stored_workflow=stored_workflow, user=trans.user, workflow_before=workflow_before, workflow_after=new_workflow, target_workflow=target_workflow, ) trans.sa_session.commit()— one transaction for version + journal entry- Return full
RefactorResponse(includes workflow dict)
-
Edge cases:
- Target is current version: Rejected at step 3 with 400 error.
- Target belongs to different StoredWorkflow:
get_internal_version_by_idraises error at step 2. - Target Workflow.id doesn’t exist: Raises
ObjectNotFoundat step 2. - Missing tools in target version:
allow_missing_tools=Truehandles this; warnings in response.
-
Note: Uses
Workflow.id(stable DB PK), not positional version index
Step 4.10: Add backend tests for journal persistence
- File:
test/unit/managers/test_workflow_action_journal_manager.py - Tests:
- Test create_entry stores all fields correctly
- Test list_entries with pagination (limit/offset)
- Test list_entries returns total_count
- Test get_entry by id
- Test create_revert_entry sets is_revert=True, records correct workflow_before/workflow_after, generates version-based title
- Test ordering (newest first)
- Test entries use encoded IDs in to_dict()
Step 4.11: Add API integration tests
- File:
test/integration/test_workflow_changelog_api.py - Tests:
- Test PUT /refactor with title creates journal entry
- Test PUT /refactor without title does NOT create journal entry (backward compat)
- Test GET /changelog returns list with total_matches header
- Test pagination works correctly
- Test revert endpoint creates new version and journal entry
- Test unauthorized access is blocked
- Test dry_run doesn’t create journal entry
- Test execution messages are persisted in journal
- Test atomic transaction: if refactor fails, no journal entry created
- Test response includes full workflow dict
Step 4.12: Add API documentation
- File: Update
lib/galaxy/webapps/galaxy/api/workflows.pydocstrings - Content:
- Document new optional fields on PUT /refactor (
title,source_action_type) - Document GET /changelog endpoint
- Document POST /revert endpoint
- Add request/response examples
- Document error cases
- Explain revert behavior (creates new version, doesn’t delete history)
- Document new optional fields on PUT /refactor (
Deliverables for Iteration 4:
- Database migration for
workflow_action_journal_entrytable - SQLAlchemy model
WorkflowActionJournalEntry WorkflowActionJournalManagerfor journal CRUD- DI registration in
app.py - Extended
PUT /refactorwith optional journal fields defer_commitsupport inupdate_workflow_from_raw_description()- Two new endpoints:
GET /changelog,POST /revert - Comprehensive backend tests (unit + integration)
- API documentation
Acceptance Criteria for Iteration 4:
- All database tests pass
- All API integration tests pass
- PUT /refactor with
titlecreates atomic journal entry + workflow version - PUT /refactor without
titleis identical to current behavior (backward compat) - GET /changelog returns paginated list with
total_matchesheader - Revert creates new version without deleting history
- All API responses use encoded IDs
- Auth uses
get_stored_workflowownership pattern - Transaction atomicity: refactor failure → no orphaned journal entry
Iteration 5 — Frontend integration (opt-in behind feature flag)
Wire up the serializer to the undo/redo store and create UI for changelog viewing. This is behind a feature flag for gradual rollout.
Step 5.1: Add feature flag configuration
- File:
lib/galaxy/config/__init__.py - Setting:
enable_workflow_action_persistence(default: False) - Frontend Config: Expose via
config.ts - Documentation: Add to admin configuration docs
Step 5.2: Create workflow action persistence service
- File:
client/src/services/workflowActionPersistence.ts - Class:
WorkflowActionPersistenceService - Methods:
submitBatch(workflowId, actions, title, sourceActionType?)→Promise<RefactorResponse>- Calls
PUT /api/workflows/{id}/refactorwithtitlefield set
- Calls
getChangelog(workflowId, limit, offset)→Promise<{entries, totalMatches}>- Calls
GET /api/workflows/{id}/changelog, readstotal_matchesfrom response header
- Calls
revertToVersion(workflowId, targetWorkflowId)→Promise<RefactorResponse>- Calls
POST /api/workflows/{id}/revert
- Calls
- Error Handling:
- Network failures: show error toast, allow manual retry
- Validation errors: show user-friendly message
- Server error (500): show error, allow manual retry
Step 5.3: Integrate serializer with undo/redo store (batch-on-save)
- File:
client/src/stores/undoRedoStore/index.ts - Default mode: Batch on save — actions accumulate locally and are sent as one batch when the user saves.
- Changes:
- Add
persistenceEnabledproperty (reads from feature flag) - Add
pendingActions: SerializationResult[]array — accumulates serialized actions locally - Modify
applyAction:applyAction(action: UndoRedoAction) { // Apply locally (unchanged) this.applyActionLocally(action); // If persistence enabled, serialize and queue (do NOT send yet) if (this.persistenceEnabled) { const result = serializeAction(action, context); if (result.success) { this.pendingActions.push(result); } } } flushLazyActionjust finalizes the lazy action locally, does NOT trigger API call- Add
hasPendingActionscomputed property for UI indicators
- Add
Step 5.4: Implement batch submission on save
- File:
client/src/stores/undoRedoStore/index.ts - Logic:
- On save: flatten
pendingActionsinto a singleRefactorActionspayload - Generate batch
titlesummarizing the actions (e.g. “Changed label, moved 3 steps, added comment”) - Call
persistenceService.submitBatch(workflowId, allActions, title) - On success: clear
pendingActions, clear local undo stack, update changelog - On failure: show error, keep
pendingActionsfor retry - Single API call = single workflow version = clean changelog
- On save: flatten
Step 5.5: Create changelog panel component
- File:
client/src/components/Workflow/Editor/ChangelogPanel.vue - Features:
- Display list of journal entries from server
- Show entry title, timestamp, user
- Show execution messages (warnings, errors)
- Click entry to see details
- Pagination controls
- Revert button per entry
- Design: Side panel similar to history panel
Step 5.6: Add changelog panel to workflow editor
- File:
client/src/components/Workflow/Editor/Index.vue - Changes:
- Add “Changelog” tab to right panel
- Only show if feature flag enabled
- Load changelog on workflow open
- Refresh after save completes
- Show loading states
Step 5.7: Implement revert functionality
- File:
client/src/components/Workflow/Editor/Actions/revertActions.ts - Function:
revertToVersion(workflowId, targetWorkflowId) - Logic:
- Confirm with user (modal dialog)
- Call
POST /api/workflows/{id}/revertwithtarget_workflow_id - Reload workflow from server response (full workflow dict returned)
- Clear local undo/redo stack and pending actions
- Show success toast
- Refresh changelog panel
- UI: Add “Revert to this version” button in changelog
Step 5.8: Handle persistence errors gracefully
- File:
client/src/stores/undoRedoStore/persistenceErrorHandler.ts - Scenarios (simpler than per-action mode since we only send on save):
- Network failure on save: Show error toast, keep pending actions, allow retry
- Validation error: Show error details, allow editing before retry
- Server error (500): Show error, allow manual retry
- No retry queue needed — user explicitly triggers save, so retry is manual
Step 5.9: Add unsaved-changes indicator
- File:
client/src/components/Workflow/Editor/UnsavedIndicator.vue - States:
- No pending actions: nothing shown
- Has pending actions: show count badge near save button (e.g. “3 unsaved changes”)
- Save in progress: show spinner
- Save failed: show error indicator with retry
- Location: Top toolbar near save button
Step 5.10: Add E2E tests for persistence
- File:
lib/galaxy_test/selenium/test_workflow_editor_persistence.py - Tests:
test_batch_persistence_on_save:- Enable feature flag
- Make multiple changes (label, position, add step)
- Verify unsaved indicator shows count
- Click save
- Verify single changelog entry created with all changes
- Reload page, verify all changes persisted
test_persistence_revert:- Make and save changes A, then changes B
- Revert to version after A
- Verify workflow state matches post-A state
- Verify revert appears in changelog
test_persistence_save_failure:- Make changes
- Simulate save failure
- Verify pending actions preserved
- Retry save, verify success
Step 5.11: Add feature flag toggle in admin panel
- File:
client/src/components/admin/Settings.vue - Setting: “Enable Workflow Action Persistence”
- Description: “Persist workflow editing actions to database for changelog and cross-session undo/redo”
- Warning: “Beta feature - creates additional workflow versions on save”
Step 5.12: Update user documentation
- File:
doc/source/admin/workflow_editor.rst - Content:
- Document feature flag
- Explain batch-on-save behavior
- Document changelog panel usage
- Explain revert functionality
- Note: each save creates one changelog entry regardless of number of edits
Deliverables for Iteration 5:
- Feature flag configuration
- Persistence service (batch submission, changelog, revert)
- Batch-on-save integration with undo/redo store
- Changelog panel component
- Revert functionality
- Unsaved-changes indicator
- E2E tests for persistence features
- Admin configuration UI
- User documentation
Acceptance Criteria for Iteration 5:
- Feature flag works (can enable/disable)
- Actions accumulate locally and are batched on save
- Single save = single API call = single changelog entry
- Changelog panel shows action history
- Revert functionality works correctly
- Error handling covers save failure scenarios
- E2E tests pass
- No disruption to existing workflow editor when flag disabled
- Performance is acceptable (save adds minimal overhead)
Iteration 6 — Expand E2E test coverage and add observability
Add comprehensive E2E tests for all persisted actions and debugging/monitoring tools.
Step 6.1: Add E2E tests for step action persistence
- File:
lib/galaxy_test/selenium/test_workflow_editor_persistence.py - New Tests:
test_persist_label_change: Change step label, verify in changelog, reload and verifytest_persist_position_change: Move step, verify in changelog, reload and verifytest_persist_add_step: Add new tool step, verify in changelog, reload and verifytest_persist_remove_step: Remove step with connections, verify connection drop messages, reload, reverttest_persist_output_label: Set output label, verify in changelog, reload and verifytest_persist_step_annotation: Set step annotation, verify in changelog, reload and verify
Step 6.2: Add E2E tests for comment action persistence
- File:
lib/galaxy_test/selenium/test_workflow_editor_persistence.py - New Tests:
test_persist_add_comment: Add text comment, verify in changelog, reload and verifytest_persist_delete_comment: Add and delete comment, verify both in changelog, revert deletetest_persist_comment_modifications: Change color/text/position, verify in changelog, reload and verifytest_persist_remove_all_freehand: Add multiple freehand, remove all, verify in changelog
Step 6.3: Add E2E tests for workflow metadata persistence
- File:
lib/galaxy_test/selenium/test_workflow_editor_persistence.py - New Tests:
test_persist_workflow_name: Change name, verify in changelog, reload and verifytest_persist_workflow_annotation: Change annotation, verify in changelog, reload and verifytest_persist_workflow_license: Change license, verify in changelog, reload and verify
Step 6.4: Add E2E tests for complex scenarios
- File:
lib/galaxy_test/selenium/test_workflow_editor_persistence.py - New Tests:
test_persist_multiple_actions_batch: Make several changes, save, verify single changelog entry, reload and verify alltest_persist_revert_middle_version: Save changes A, save changes B, save changes C, revert to post-A version, verify statetest_persist_auto_layout: Run auto-layout (emits N position updates), save, verify all positions persisted correctly
Step 6.5: Add observability and debugging tools
- File:
client/src/components/Workflow/Editor/Actions/serializationDebugger.ts - Features:
- Log all serialization attempts (dev mode only)
- Show serialization errors in console with context
- Dev tools panel showing: last 10 actions, success rate, pending count
- Export serialization history as JSON for bug reports
Step 6.6: Update all documentation
- Note:
test/unit/workflow/refactor/will need__init__.pyfiles for Python test discovery - Files:
client/src/stores/undoRedoStore/README.md- Add “Persisted Actions and Refactor API” sectionclient/src/components/Workflow/Editor/Actions/README_SERIALIZATION.md- Update with all action typesdoc/source/dev/workflow_persistence_architecture.rst- Architecture documentation
- Content: Full documentation of persistence architecture, serialization, batching, error handling
Deliverables for Iteration 6:
- Comprehensive E2E tests for all action types (steps, comments, workflow metadata)
- E2E tests for complex scenarios (batch save, revert, auto-layout)
- Debugging and observability tools
- Complete documentation
Acceptance Criteria for Iteration 6:
- All E2E tests pass
- Tests cover all core action types
- Debugging tools help troubleshoot issues
- Documentation is complete and accurate
Iteration 7 — Governance, performance optimization, and production rollout
Prepare for production deployment with governance policies, performance monitoring, and gradual rollout strategy.
Step 7.1: Implement journal retention policy
- File:
lib/galaxy/managers/workflow_action_journal_manager.py - Feature: Configurable retention
- Settings:
workflow_action_journal_retention_days(default: 90)workflow_action_journal_max_entries_per_workflow(default: 1000)
- Logic:
- Automatic cleanup job runs daily
- Deletes entries older than retention period
- Keeps last N entries regardless of age
- Admin can manually trigger cleanup
Step 7.2: Add journal cleanup command
- File:
lib/galaxy/scripts/cleanup_workflow_journal.py - Command:
galaxyadm cleanup-workflow-journal - Options:
--days: Override retention period--workflow-id: Clean specific workflow--dry-run: Show what would be deleted--force: Skip confirmation
- Report: Number of entries deleted, disk space freed
Step 7.3: Add telemetry and monitoring
- File:
lib/galaxy/managers/workflow_action_journal_manager.py - Metrics to track:
- Number of actions per day
- Average actions per workflow
- Serialization success rate
- API endpoint latency
- Database table size growth
- Failed persistence attempts
- Integration: Statsd/Prometheus metrics
- Alerting: Alert if error rate > 5%
Step 7.4: Optimize database queries
- File:
lib/galaxy/managers/workflow_action_journal_manager.py - Optimizations:
- Add database indices for common queries
- Use query result caching for changelog
- Implement efficient pagination
- Bulk insert for batched actions
- Connection pooling optimization
- Testing: Load test with 10k actions
Step 7.5: Add per-user access controls
- File:
lib/galaxy/managers/workflow_action_journal_manager.py - Rules:
- Users can view actions on workflows they have access to
- Users can only revert their own actions (configurable)
- Admin can view/revert any actions
- Changelog shows user who made each change
- Settings:
allow_revert_others_actions(default: true) - Note: Concurrent editing and optimistic locking deferred to future iteration.
Step 7.6: Implement action compaction (optional)
- File:
lib/galaxy/managers/workflow_action_journal_manager.py - Feature: Compact old actions to save space
- Logic:
- Combine multiple position updates into one
- Combine multiple label changes into one
- Keep compacted entry with “compacted from N actions” note
- Preserve essential history, reduce storage
- Setting:
enable_action_journal_compaction(default: false) - Command:
galaxyadm compact-workflow-journal
Step 7.7: Add admin dashboard for journal monitoring
- File:
client/src/components/admin/WorkflowJournalDashboard.vue - Features:
- Total actions in database
- Actions per workflow (top 10)
- Database size and growth rate
- Error rate over time
- Most active users
- Cleanup job status and schedule
- Actions: Trigger cleanup, adjust retention policy
Step 7.8: Create migration guide for existing users
- File:
doc/source/admin/workflow_action_persistence.rst - Content:
- Benefits of enabling persistence
- Performance impact assessment
- Database sizing guide
- Enabling the feature flag
- Monitoring and troubleshooting
- Retention policy configuration
- Backup recommendations
- Rollback procedure
Step 7.9: Implement gradual rollout strategy
- File:
lib/galaxy/config/__init__.py - Additional Settings:
workflow_action_persistence_rollout_percentage(default: 0)workflow_action_persistence_user_whitelist(list of user emails)
- Logic:
- Random X% of users get feature enabled
- Whitelist always gets feature
- Admin can adjust percentage gradually (0% → 10% → 50% → 100%)
Step 7.10: Add comprehensive logging
- File:
lib/galaxy/managers/workflow_action_journal_manager.py - Log Events:
- Action created (info level)
- Action failed to persist (warning level)
- Revert performed (info level)
- Cleanup job run (info level)
- Serialization errors (error level)
- Log Format: Structured logging with workflow_id, user_id, action_type
Step 7.11: Create troubleshooting guide
- File:
doc/source/admin/workflow_persistence_troubleshooting.rst - Sections:
- Common errors and solutions
- Performance issues
- Database migration issues
- Feature flag not working
- Actions not appearing in changelog
- Revert failures
- Debugging serialization errors
- Checking logs
Step 7.12: Production readiness checklist
- File:
PRODUCTION_READINESS_CHECKLIST.md - Checklist:
- All tests passing (unit, integration, E2E)
- Performance benchmarks met
- Database migration tested
- Rollback procedure tested
- Monitoring and alerting configured
- Documentation complete
- Security review passed
- Load testing completed
- Retention policy configured
- Admin dashboard functional
- Gradual rollout plan approved
Deliverables for Iteration 7:
- Journal retention and cleanup system
- Telemetry and monitoring
- Database optimizations
- Access controls
- Optional action compaction
- Admin dashboard
- Migration and troubleshooting guides
- Gradual rollout strategy
- Production readiness checklist
Acceptance Criteria for Iteration 7:
- Retention policy works correctly
- Cleanup command deletes old entries
- Monitoring shows accurate metrics
- Database queries are performant under load
- Access controls are enforced
- Admin dashboard is functional
- Documentation is complete
- Gradual rollout can be controlled
- System is production-ready
Action mapping details (initial)
Frontend → Backend
LazySetLabelAction→UpdateStepLabelAction(step: by ID, label, or order_index)LazySetOutputLabelAction→UpdateOutputLabelAction(output: by step ref + output_name)setPosition/LazyMoveMultipleAction→UpdateStepPositionAction(useposition_absolute) +UpdateCommentPositionAction(for comments in multi-move)InsertStepAction→AddStepAction(type, label?, position, tool_state? [TBD])RemoveStepAction→RemoveStepAction(new; tolerates order_index gaps)LazySetValueAction→ routes onaction.what:UpdateNameAction/UpdateAnnotationAction/UpdateLicenseAction/UpdateCreatorAction/ etc.- Comment actions:
AddCommentAction→AddCommentActionDeleteCommentAction→DeleteCommentActionLazyChangePositionAction→UpdateCommentPositionActionLazyChangeSizeAction→UpdateCommentSizeAction(usesSizemodel, notPosition)ChangeColorAction→UpdateCommentColorActionLazyChangeDataAction→UpdateCommentDataActionRemoveAllFreehandCommentsAction→RemoveAllFreehandCommentsAction
FactoryAction(name: “connect steps”) interminals.ts:96→ConnectAction(input: step ref + input_name, output: step ref + output_name)FactoryAction(name: “disconnect steps”) interminals.ts:108→DisconnectAction(same reference format)- Recommended: replace FactoryActions with typed
ConnectStepAction/DisconnectStepActionfor type-safe serialization
- Recommended: replace FactoryActions with typed
Concrete file touchpoints
Client
client/src/components/Workflow/Editor/Index.vue— addwhatto 6SetValueActionHandlerinstances- New serializer:
client/src/components/Workflow/Editor/Actions/refactorSerialization.ts - Batch-on-save in
client/src/stores/undoRedoStore/index.ts - Persistence service:
client/src/services/workflowActionPersistence.ts - Changelog panel:
client/src/components/Workflow/Editor/ChangelogPanel.vue - Tests next to
client/src/components/Workflow/Editor/Actions/actions.test.ts
Server
- Schema additions:
lib/galaxy/workflow/refactor/schema.py(remove step, absolute position,Sizemodel, comment actions,StepReferenceById, optionaltitle/source_action_typeonRefactorActions) - Executor:
lib/galaxy/workflow/refactor/execute.py(remove step, absolute position handling, comment actions, explicitisinstancechecks in_find_step) - Journal model:
lib/galaxy/model/__init__.py(WorkflowActionJournalEntry) - Journal manager:
lib/galaxy/managers/workflow_action_journal_manager.py(thin CRUD) - DI registration:
lib/galaxy/app.py(_register_singleton(WorkflowActionJournalManager)) - Transaction wiring:
lib/galaxy/managers/workflows.py(defer_commitflag inupdate_workflow_from_raw_description, journal write indo_refactor) - Service wiring:
lib/galaxy/webapps/galaxy/services/workflows.py(injectWorkflowActionJournalManager) - API:
lib/galaxy/webapps/galaxy/api/workflows.py(extended PUT /refactor, new GET /changelog, new POST /revert) - Migration:
lib/galaxy/model/migrations/(newworkflow_action_journal_entrytable)
Risk management
- Step reference drift: mitigate by preferring step IDs (stable DB PKs); fall back to labels then order_index.
- Position mismatches: use absolute positions exclusively;
position_shiftretained for backward compat only. - Comment identity:
CommentReference.comment_idmatches the comment’s"id"field (stableorder_index-based identifier). Deletion does not renumber remaining IDs, so batch operations are safe. See Step 2.7 batch safety note. - Redo after divergence: disable or prompt; document behavior.
- Concurrent editing: deferred to future iteration. No optimistic locking in initial implementation.
Test strategy
- Frontend unit tests: serialization of each supported action using fixtures from
actions.test.ts(including comments, absolute positions, multi-move with steps+comments). - Backend unit/integration tests: new actions (remove step, comment actions), refactor application, journal append/list, revert, atomic transaction (journal + version),
defer_commitbehavior. - E2E: batch-on-save flow, changelog verification, revert, auto-layout persistence.
Resolved questions
Extend existing PUT /refactor or create separate endpoint?→ Extend PUT /refactor with optionaltitle/source_action_typefieldsStore Workflow.id (DB PK) or positional version index in journal?→ Workflow.id (DB PK) — stable across revertsShould journal FK point to stored_workflow or workflow?→ stored_workflow — journal tracks the logical workflow containerHow to route LazySetValueAction serialization when what is null?→ Addwhatto all handlers — 6 one-line changes in Index.vueRe-index steps after removal or tolerate gaps?→ Tolerate gaps — ID-based refs (Step 2.5) make gaps safeDefault mode: persist immediately per action, or batch on save?→ Batch on save — fewer versions, simpler error handling- Shared-with users: can they create journal entries, or owner-only? → Open — depends on Galaxy’s access model (currently ownership-only for writes)