FRONTEND_BACKEND_ACTION_MAPPING

Frontend-Backend Action Mapping: Workflow Editor Undo/Redo Persistence

1. Frontend Action Inventory

Base Classes

ClassFileTypePurpose
UndoRedoActionundoRedoStore/undoRedoAction.ts:3AbstractBase for all actions
LazyUndoRedoActionundoRedoStore/undoRedoAction.ts:40AbstractBase for batching/deferred actions
FactoryActionundoRedoStore/index.ts:230ImmediateGeneric inline action builder

Step Actions (Actions/stepActions.ts)

ClassLineTypeKey FieldsdataAttributes
LazyMutateStepAction<K>15LazystepId, key, fromValue, toValue{ type: "step-mutate", what: key }
LazySetLabelAction91LazystepId, stateStore, label sync{ type: "set-label" }
LazySetOutputLabelAction136LazystepId, fromLabel, toLabel(inherited)
UpdateStepAction179ImmediatestepId, fromPartial, toPartial{}
SetDataAction241ImmediateExtends UpdateStepAction, diffs two Steps{}
InsertStepAction266ImmediatestepData: InsertStepData{ type: "step-insert", "step-type": ... }
RemoveStepAction322Immediatestep (cloned), connections (cloned){ type: "step-remove" }
CopyStepAction364Immediatestep: NewStep{ type: "step-copy" }
ToggleStepSelectedAction403ImmediatestepId, toggleTo{}
AutoLayoutAction444ImmediateworkflowId, positions map{}

Workflow Actions (Actions/workflowActions.ts)

ClassLineTypeKey FieldsdataAttributes
LazySetValueAction<T>16LazyfromValue, toValue, what{ type: "set-${what}" } if what set
CopyIntoWorkflowAction115Immediatedata: Partial<Workflow>, position{}
LazyMoveMultipleAction184Lazysteps[], comments[], positions{}
ClearSelectionAction287ImmediateselectionState{}
AddToSelectionAction344Immediateselection{}
RemoveFromSelectionAction353Immediateselection{}
DuplicateSelectionAction362Immediateextends CopyIntoWorkflowAction{}
DeleteSelectionAction390Immediatestored sub-actions, connections{}

Comment Actions (Actions/commentActions.ts)

ClassLineTypeKey FieldsdataAttributes
AddCommentAction33Immediatecomment (cloned){ type: "comment-add", "comment-type": ... }
DeleteCommentAction51Immediatecomment (cloned){ type: "comment-delete" }
ChangeColorAction69ImmediatecommentId, toColor, fromColor{ type: "comment-color" }
LazyMutateCommentAction<K>102LazycommentId, key, startData, endData(abstract)
LazyChangeDataAction151Lazyextends LazyMutateCommentAction<“data”>{}
LazyChangePositionAction187Lazyextends LazyMutateCommentAction<“position”>{}
LazyChangeSizeAction198Lazyextends LazyMutateCommentAction<“size”>{}
ToggleCommentSelectedAction209ImmediatecommentId, toggleTo{}
RemoveAllFreehandCommentsAction241Immediatecomments[] (cloned){}

Connection Actions (via FactoryAction in modules/terminals.ts)

Action NameLineTypedataAttributes
"connect steps"96Immediate{ type: "connect" }
"disconnect steps"109Immediate{ type: "disconnect" }

SetValueActionHandler Instances (Index.vue)

HandlerLinewhatNeeds Fix?
setNameActionHandler370"name"No
setLicenseHandler387"license"No
setCreatorHandler402"creator"No
setDoiHandler415nullYes -> "doi"
setAnnotationHandler426"annotation"No
setReadmeHandler442nullYes -> "readme"
setHelpHandler468nullYes -> "help"
setLogoUrlHandler481nullYes -> "logoUrl"
setTagsHandler495nullYes -> "tags"

2. Backend Refactor Action Inventory

Action Classes (refactor/schema.py)

ClassLineaction_typeFieldsHas Executor?
UpdateStepLabelAction95"update_step_label"step, label: strYes (L85)
UpdateStepPositionAction101"update_step_position"step, position_shift: PositionYes (L89)
AddStepAction107"add_step"type, tool_state?, label?, position?Yes (L118)
ConnectAction125"connect"input: input_ref, output: output_refYes (L188)
DisconnectAction131"disconnect"input: input_ref, output: output_refYes (L167)
AddInputAction137"add_input"type, label?, position?, etc.Yes (L134)
ExtractInputAction150"extract_input"input: input_ref, label?, position?Yes (L212)
ExtractUntypedParameter157"extract_untyped_parameter"name, label?, position?Yes (L255)
RemoveUnlabeledWorkflowOutputs164"remove_unlabeled_workflow_outputs"(none)Yes (L359)
UpdateNameAction168"update_name"name: strYes (L103)
UpdateAnnotationAction173"update_annotation"annotation: strYes (L106)
UpdateLicenseAction178"update_license"license: strYes (L109)
UpdateCreatorAction183"update_creator"creator: AnyYes (L112)
UpdateReportAction192"update_report"report: ReportYes (L115)
UpdateOutputLabelAction197"update_output_label"output: output_ref, output_labelYes (L95)
FillStepDefaultsAction203"fill_step_defaults"stepYes (L208)
FileDefaultsAction208"fill_defaults"(none)Yes (L201)
UpgradeSubworkflowAction212"upgrade_subworkflow"step, content_id?Yes (L370)
UpgradeToolAction220"upgrade_tool"step, tool_version?Yes (L387)
UpgradeAllStepsAction226"upgrade_all_steps"(none)Yes (L408)

Step Reference Types (schema.py:25-57)

TypeFieldsUsed In
StepReferenceByOrderIndexorder_index: intstep_reference_union
StepReferenceByLabellabel: strstep_reference_union
InputReferenceByOrderIndexorder_index, input_nameinput_reference_union
InputReferenceByLabellabel, input_nameinput_reference_union
OutputReferenceByOrderIndexorder_index, output_name? (default “output”)output_reference_union
OutputReferenceByLabellabel, output_name?output_reference_union

No StepReferenceById exists yet. Frontend uses numeric step IDs; backend only supports order_index and label.

Position Model (schema.py:60-69)

class Position(BaseModel):
    left: float
    top: float

Only supports relative shift currently. No absolute positioning.

Request Model (schema.py:267-269)

class RefactorActions(BaseModel):
    actions: list[union_action_classes]
    dry_run: bool = False

No title or source_action_type fields yet.

Step Resolution (execute.py:419-435)

_find_step does:

  1. StepReferenceByLabel → linear scan of steps matching label
  2. Else assumes StepReferenceByOrderIndex → direct dict lookup
  3. Validates order_index < len(steps)

Problem: The else branch assumes anything non-label is order_index. Adding StepReferenceById requires explicit isinstance checks.


3. Frontend → Backend Action Mapping

Direct Mappings (Ready to Serialize)

Frontend ActionBackend ActionNotes
LazySetLabelActionUpdateStepLabelActionDirect: step ref + label
LazySetOutputLabelActionUpdateOutputLabelActionDirect: output ref + output_label
InsertStepActionAddStepActionDirect: type, label?, position?
LazySetValueAction (what=“name”)UpdateNameActionDirect
LazySetValueAction (what=“annotation”)UpdateAnnotationActionDirect
LazySetValueAction (what=“license”)UpdateLicenseActionDirect
LazySetValueAction (what=“creator”)UpdateCreatorActionDirect
FactoryAction “connect steps”ConnectActionDirect: input ref + output ref
FactoryAction “disconnect steps”DisconnectActionDirect: input ref + output ref

Needs Backend Enhancement

Frontend ActionBackend Action NeededRequired Changes
LazyMutateStepAction<"position">UpdateStepPositionActionAdd position_absolute field
LazyMoveMultipleActionUpdateStepPositionAction[] + UpdateCommentPositionAction[]Absolute position + new comment actions
RemoveStepActionNEW RemoveStepActionNew schema + executor

Needs New Backend Actions (Comments)

Frontend ActionBackend Action Needed
AddCommentActionAddCommentAction
DeleteCommentActionDeleteCommentAction
ChangeColorActionUpdateCommentColorAction
LazyChangeDataActionUpdateCommentDataAction
LazyChangePositionActionUpdateCommentPositionAction
LazyChangeSizeActionUpdateCommentSizeAction
RemoveAllFreehandCommentsActionRemoveAllFreehandCommentsAction

No Backend Equivalent Needed (UI-Only)

Frontend ActionReason
UpdateStepActionGeneric partial update; serializer inspects changed keys to emit specific backend actions
SetDataActionSubclass of UpdateStepAction; captures tool form diffs
CopyStepActionComposite: AddStep + copy data
ToggleStepSelectedActionUI selection state only
AutoLayoutActionDecomposes to N position updates
ClearSelectionActionUI selection state only
AddToSelectionActionUI selection state only
RemoveFromSelectionActionUI selection state only
DuplicateSelectionActionComposite: multiple AddStep + AddComment
DeleteSelectionActionComposite: multiple RemoveStep + DeleteComment
CopyIntoWorkflowActionComposite: multiple adds
ToggleCommentSelectedActionUI selection state only
LazyMoveMultipleActionDecomposes to N position updates (steps + comments)

4. RemoveStepAction Specification

Frontend Behavior (stepActions.ts:322-362)

On run:

  1. stepStore.removeStep(stepId) which:
    • Removes all connections for step (both incoming/outgoing) via connectionStore
    • Deletes step from steps dict
    • Cleans up extra inputs, multi-select state, mapOver state, positions, terminals
  2. Sets activeNodeId = null
  3. Marks hasChanges = true

On undo:

  1. stepStore.addStep(step) — restores cloned step
  2. Re-adds all saved connections via connectionStore.addConnection()
  3. Marks hasChanges = true

Backend Requirements

class RemoveStepAction(BaseAction):
    action_type: Literal["remove_step"]
    step: step_reference_union = step_target_field

Executor _apply_remove_step must:

  1. Resolve step reference via _find_step()
  2. Get step’s order_index
  3. Find all connections where this step is referenced:
    • As output: scan all steps’ input_connections for entries where connection["id"] == removed_step["id"]
    • As input: remove input_connections from the step itself
  4. For each dropped connection → emit connection_drop_forced message
  5. For each workflow_output on removed step → emit workflow_output_drop_forced message
  6. Remove step from self._as_dict["steps"]
  7. Tolerate order_index gaps — do NOT re-index remaining steps

Connection Cleanup Algorithm

removed_step_id = step["id"]
for other_order_index, other_step in self._as_dict["steps"].items():
    if other_order_index == order_index:
        continue
    input_connections = other_step.get("input_connections", {})
    for input_name, connections in input_connections.items():
        connections = _listify_connections(connections)
        for conn in connections:
            if conn["id"] == removed_step_id:
                # emit connection_drop_forced message
        input_connections[input_name] = [c for c in connections if c["id"] != removed_step_id]

5. Position Handling Strategy

Current State

Required Changes

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

Executor change:

def _apply_update_step_position(self, action, execution):
    step = self._find_step_for_action(action)
    if action.position_absolute:
        step["position"] = action.position_absolute.to_dict()
    elif action.position_shift:
        # existing relative logic
        step["position"]["left"] += action.position_shift.left
        step["position"]["top"] += action.position_shift.top

Serialization Strategy


6. Step Reference Strategy

Current State

ID Semantics

Context”id” meansStable?
WorkflowStep.id (DB model)Auto-increment PKYes across saves
step_dict["id"] in workflow JSONorder_index valuePositional, changes if reordered
Frontend Step.idLoaded from step_dict[“id”]Matches workflow dict “id”
Connection {"id": N}Output step’s dict “id”Same as above

Required Changes

class StepReferenceById(BaseModel):
    id: int  # Database PK of WorkflowStep

class InputReferenceById(StepReferenceById):
    input_name: str

class OutputReferenceById(StepReferenceById):
    output_name: Optional[str] = output_name_field

Update unions:

step_reference_union = Union[StepReferenceByOrderIndex, StepReferenceByLabel, StepReferenceById]
input_reference_union = Union[InputReferenceByOrderIndex, InputReferenceByLabel, InputReferenceById]
output_reference_union = Union[OutputReferenceByOrderIndex, OutputReferenceByLabel, OutputReferenceById]

Update _find_step to use explicit isinstance checks for all three types instead of the current if/else that assumes non-label = order_index.

Open Question

The frontend’s Step.id is loaded from step_dict["id"] which is order_index. The database WorkflowStep.id (auto-increment PK) is a different value. Which “id” should StepReferenceById use? The plan says database PK, but the frontend doesn’t currently have access to it during editing. Resolution needed.


7. Comment Persistence

Storage Architecture

Database model (model/__init__.py:9188-9269):

Pydantic schema (schema/workflow/comments.py):

ID Management

Export/Import

Position/Size Format Difference

FieldFrontendBackend JSONBackend Model
position[x, y] tuple[x, y]MutableJSONType
size[width, height] tuple[width, height]JSONType

Comment actions use tuples, NOT Position model’s {left, top} dict. The executor must handle this format.

New Size Model Needed

class Size(BaseModel):
    width: float
    height: float

    def to_dict(self):
        return {"width": self.width, "height": self.height}

Do NOT reuse Positionleft/top fields are misleading for width/height. Note: comment size in workflow JSON is [width, height] tuple, not {width, height} dict. Executor converts between formats.


8. Connection Handling

Architecture

Frontend Connection Flow

Terminal.connect(other)
  → FactoryAction("connect steps")
    → onRun: connectionStore.addConnection(connection)
      → stepStore.addConnection(connection)  // sync to input_connections
    → onUndo: connectionStore.removeConnection(connectionId)

Backend Connection Format

# In step dict:
step["input_connections"]["input_name"] = [
    {"id": output_step_id, "output_name": "output"},
    ...
]

Frontend → Backend Mapping

Frontend Connection:
  input: {stepId: 1, name: "input_file"}
  output: {stepId: 0, name: "output"}

→ Backend ConnectAction:
  input: {order_index: 1, input_name: "input_file"}  # or by label
  output: {order_index: 0, output_name: "output"}      # or by label

Recommendation: Typed Frontend Action Classes

Replace anonymous FactoryActions with typed classes for type-safe serialization:

class ConnectStepAction extends UndoRedoAction {
    constructor(
        private input: InputTerminal,
        private output: OutputTerminal,
        private connectionStore: WorkflowConnectionStore
    ) { ... }
}

class DisconnectStepAction extends UndoRedoAction { ... }

This enables instanceof routing in the serializer instead of matching on action.name === "connect steps".


9. Implementation Scope

Core Scope (Iteration 2-3)

Backend (Iteration 2):

  1. RemoveStepAction schema + executor
  2. position_absolute on UpdateStepPositionAction
  3. StepReferenceById + union updates + _find_step fix
  4. Size model
  5. All 7 comment action schemas + executors
  6. CommentReference model with _find_comment helper
  7. Unit tests + integration tests

Frontend Serializer (Iteration 3):

  1. Fix 5 null what values in SetValueActionHandler instances
  2. Serializer for each mappable action type
  3. Dispatcher routing by instanceof
  4. Comprehensive serializer test suite

Deferred (Future)

  1. Tool state serialization (complex, needs more research)
  2. Action compaction/optimization
  3. Concurrent editing / optimistic locking
  4. Typed ConnectStepAction/DisconnectStepAction classes (recommended but can serialize FactoryAction by name initially)

10. Unresolved Questions

  1. Step ID semantics: Frontend Step.id comes from step_dict["id"] which is order_index. StepReferenceById in plan says “database ID” but frontend doesn’t have DB PKs during editing. Should we use order_index as the “id” for now, or expose DB PKs to frontend?
  2. Order_index gaps after removal: Backend _apply_add_step uses len(steps) for new order_index. If steps are sparse (gaps from removal), does this create collisions? May need max(keys) + 1 instead.
  3. Comment position format: Comments use [x, y] tuples in JSON. Should AddCommentAction schema use Position model (with left/top) and have executor convert, or use a separate tuple-based model?
  4. Workflow outputs on removed steps: Backend just removes the step dict (which contains workflow_outputs). Is that sufficient, or do we need to clean references from other steps/report markdown?
  5. Connection FactoryAction serialization: Serialize by action.name === "connect steps" initially, or require typed classes first? Name-based is fragile but faster to implement.
  6. LazySetValueAction for doi/readme/help/logoUrl/tags: These have no backend action equivalents. Should they return no-op from serializer, or do we need new backend actions?
  7. Shared workflow access: Can shared-with users create journal entries, or owner-only? Depends on Galaxy’s access model.