diff --git a/skyvern-frontend/src/routes/workflows/ImportWorkflowButton.tsx b/skyvern-frontend/src/routes/workflows/ImportWorkflowButton.tsx index 3e542da2..d0b44d54 100644 --- a/skyvern-frontend/src/routes/workflows/ImportWorkflowButton.tsx +++ b/skyvern-frontend/src/routes/workflows/ImportWorkflowButton.tsx @@ -35,9 +35,13 @@ function getErrorMessage(error: unknown, fallback: string): string { interface ImportWorkflowButtonProps { onImportStart?: () => void; + selectedFolderId?: string | null; } -function ImportWorkflowButton({ onImportStart }: ImportWorkflowButtonProps) { +function ImportWorkflowButton({ + onImportStart, + selectedFolderId, +}: ImportWorkflowButtonProps) { const inputId = useId(); const credentialGetter = useCredentialGetter(); const queryClient = useQueryClient(); @@ -45,6 +49,10 @@ function ImportWorkflowButton({ onImportStart }: ImportWorkflowButtonProps) { const createWorkflowFromYamlMutation = async (yaml: string) => { try { const client = await getClient(credentialGetter); + const params: Record = {}; + if (selectedFolderId) { + params.folder_id = selectedFolderId; + } await client.post( "/workflows", yaml, @@ -52,12 +60,16 @@ function ImportWorkflowButton({ onImportStart }: ImportWorkflowButtonProps) { headers: { "Content-Type": "text/plain", }, + params, }, ); queryClient.invalidateQueries({ queryKey: ["workflows"], }); + queryClient.invalidateQueries({ + queryKey: ["folders"], + }); toast({ variant: "success", title: "Workflow imported", @@ -78,10 +90,15 @@ function ImportWorkflowButton({ onImportStart }: ImportWorkflowButtonProps) { formData.append("file", file); const client = await getClient(credentialGetter); + const params: Record = {}; + if (selectedFolderId) { + params.folder_id = selectedFolderId; + } await client.post("/workflows/import-pdf", formData, { headers: { "Content-Type": "multipart/form-data", }, + params, }); // Notify parent to start polling diff --git a/skyvern-frontend/src/routes/workflows/Workflows.tsx b/skyvern-frontend/src/routes/workflows/Workflows.tsx index 77c9c0cc..143288e1 100644 --- a/skyvern-frontend/src/routes/workflows/Workflows.tsx +++ b/skyvern-frontend/src/routes/workflows/Workflows.tsx @@ -32,6 +32,7 @@ import { LightningBoltIcon, MagnifyingGlassIcon, MixerHorizontalIcon, + Pencil2Icon, PlayIcon, PlusIcon, ReloadIcon, @@ -415,11 +416,17 @@ function Workflows() { />
- + + + + Open in Editor + + + @@ -676,7 +715,7 @@ function Workflows() {
- + e.preventDefault()} + >

Move to folder

diff --git a/skyvern-frontend/src/routes/workflows/hooks/useActiveImportsPolling.ts b/skyvern-frontend/src/routes/workflows/hooks/useActiveImportsPolling.ts index b39b0ab7..48a9f5a5 100644 --- a/skyvern-frontend/src/routes/workflows/hooks/useActiveImportsPolling.ts +++ b/skyvern-frontend/src/routes/workflows/hooks/useActiveImportsPolling.ts @@ -57,8 +57,9 @@ export function useActiveImportsPolling() { description: `Successfully imported ${prevImport.title || "workflow"}`, }); - // Refresh workflows to show new workflow + // Refresh workflows and folders to show new workflow and update folder counts queryClient.invalidateQueries({ queryKey: ["workflows"] }); + queryClient.invalidateQueries({ queryKey: ["folders"] }); } }); diff --git a/skyvern-frontend/src/routes/workflows/hooks/useCreateWorkflowMutation.ts b/skyvern-frontend/src/routes/workflows/hooks/useCreateWorkflowMutation.ts index 4165453d..edab55b2 100644 --- a/skyvern-frontend/src/routes/workflows/hooks/useCreateWorkflowMutation.ts +++ b/skyvern-frontend/src/routes/workflows/hooks/useCreateWorkflowMutation.ts @@ -29,6 +29,9 @@ function useCreateWorkflowMutation() { queryClient.invalidateQueries({ queryKey: ["workflows"], }); + queryClient.invalidateQueries({ + queryKey: ["folders"], + }); navigate(`/workflows/${response.data.workflow_permanent_id}/debug`); }, }); diff --git a/skyvern-frontend/src/routes/workflows/types/workflowYamlTypes.ts b/skyvern-frontend/src/routes/workflows/types/workflowYamlTypes.ts index f4674882..461fb110 100644 --- a/skyvern-frontend/src/routes/workflows/types/workflowYamlTypes.ts +++ b/skyvern-frontend/src/routes/workflows/types/workflowYamlTypes.ts @@ -20,6 +20,7 @@ export type WorkflowCreateYAMLRequest = { ai_fallback?: boolean; run_sequentially?: boolean; sequential_key?: string | null; + folder_id?: string | null; }; export type WorkflowDefinitionYAML = { diff --git a/skyvern/forge/sdk/db/client.py b/skyvern/forge/sdk/db/client.py index 21311815..57fd236e 100644 --- a/skyvern/forge/sdk/db/client.py +++ b/skyvern/forge/sdk/db/client.py @@ -1436,6 +1436,23 @@ class AgentDB: if version: workflow.version = version session.add(workflow) + + # Update folder's modified_at if folder_id is provided + if folder_id: + # Validate folder exists and belongs to the same organization + folder_stmt = ( + select(FolderModel) + .where(FolderModel.folder_id == folder_id) + .where(FolderModel.organization_id == organization_id) + .where(FolderModel.deleted_at.is_(None)) + ) + folder_model = await session.scalar(folder_stmt) + if not folder_model: + raise ValueError( + f"Folder {folder_id} not found or does not belong to organization {organization_id}" + ) + folder_model.modified_at = datetime.utcnow() + await session.commit() await session.refresh(workflow) return convert_to_workflow(workflow, self.debug_enabled) diff --git a/skyvern/forge/sdk/routes/agent_protocol.py b/skyvern/forge/sdk/routes/agent_protocol.py index 6c230c10..6765e3a5 100644 --- a/skyvern/forge/sdk/routes/agent_protocol.py +++ b/skyvern/forge/sdk/routes/agent_protocol.py @@ -484,6 +484,7 @@ async def cancel_run( ) async def create_workflow_legacy( request: Request, + folder_id: str | None = Query(None, description="Optional folder ID to assign the workflow to"), current_org: Organization = Depends(org_auth_service.get_current_org), ) -> Workflow: analytics.capture("skyvern-oss-agent-workflow-create-legacy") @@ -495,6 +496,9 @@ async def create_workflow_legacy( try: workflow_create_request = WorkflowCreateYAMLRequest.model_validate(workflow_yaml) + # Override folder_id if provided as query parameter + if folder_id is not None: + workflow_create_request.folder_id = folder_id return await app.WORKFLOW_SERVICE.create_workflow_from_request( organization=current_org, request=workflow_create_request ) @@ -535,6 +539,7 @@ async def create_workflow_legacy( ) async def create_workflow( data: WorkflowRequest, + folder_id: str | None = Query(None, description="Optional folder ID to assign the workflow to"), current_org: Organization = Depends(org_auth_service.get_current_org), ) -> Workflow: analytics.capture("skyvern-oss-agent-workflow-create") @@ -549,6 +554,9 @@ async def create_workflow( status_code=422, detail="Invalid workflow definition. Workflow should be provided in either yaml or json format.", ) + # Override folder_id if provided as query parameter + if folder_id is not None: + workflow_definition.folder_id = folder_id return await app.WORKFLOW_SERVICE.create_workflow_from_request( organization=current_org, request=workflow_definition, @@ -669,6 +677,7 @@ async def _validate_file_size(file: UploadFile) -> UploadFile: async def import_workflow_from_pdf( background_tasks: BackgroundTasks, file: UploadFile = Depends(_validate_file_size), + folder_id: str | None = Query(None, description="Optional folder ID to assign the imported workflow to"), current_org: Organization = Depends(org_auth_service.get_current_org), ) -> dict[str, Any]: """Import a workflow from a PDF file containing Standard Operating Procedures.""" @@ -702,6 +711,7 @@ async def import_workflow_from_pdf( workflow_definition={"parameters": [], "blocks": []}, organization_id=current_org.organization_id, status=WorkflowStatus.importing, + folder_id=folder_id, ) # Process PDF import in background (LLM call is the slow part) diff --git a/skyvern/forge/sdk/workflow/service.py b/skyvern/forge/sdk/workflow/service.py index eb4da84e..4321e1a4 100644 --- a/skyvern/forge/sdk/workflow/service.py +++ b/skyvern/forge/sdk/workflow/service.py @@ -2771,6 +2771,7 @@ class WorkflowService: ai_fallback=request.ai_fallback, run_sequentially=request.run_sequentially, sequential_key=request.sequential_key, + folder_id=request.folder_id, ) # Keeping track of the new workflow id to delete it if an error occurs during the creation process new_workflow_id = potential_workflow.workflow_id diff --git a/skyvern/schemas/workflows.py b/skyvern/schemas/workflows.py index 19e8c256..5f17b6cb 100644 --- a/skyvern/schemas/workflows.py +++ b/skyvern/schemas/workflows.py @@ -562,6 +562,7 @@ class WorkflowCreateYAMLRequest(BaseModel): cache_key: str | None = "default" run_sequentially: bool = False sequential_key: str | None = None + folder_id: str | None = None class WorkflowRequest(BaseModel):