[Backend] Saving Workflows as Templates (#4278)
This commit is contained in:
@@ -51,6 +51,7 @@ from skyvern.forge.sdk.db.models import (
|
||||
WorkflowRunOutputParameterModel,
|
||||
WorkflowRunParameterModel,
|
||||
WorkflowScriptModel,
|
||||
WorkflowTemplateModel,
|
||||
)
|
||||
from skyvern.forge.sdk.db.utils import (
|
||||
_custom_json_serializer,
|
||||
@@ -1525,7 +1526,19 @@ class AgentDB:
|
||||
if organization_id:
|
||||
get_workflow_query = get_workflow_query.filter_by(organization_id=organization_id)
|
||||
if workflow := (await session.scalars(get_workflow_query)).first():
|
||||
return convert_to_workflow(workflow, self.debug_enabled)
|
||||
is_template = (
|
||||
await self.is_workflow_template(
|
||||
workflow_permanent_id=workflow.workflow_permanent_id,
|
||||
organization_id=workflow.organization_id,
|
||||
)
|
||||
if organization_id
|
||||
else False
|
||||
)
|
||||
return convert_to_workflow(
|
||||
workflow,
|
||||
self.debug_enabled,
|
||||
is_template=is_template,
|
||||
)
|
||||
return None
|
||||
except SQLAlchemyError:
|
||||
LOG.error("SQLAlchemyError", exc_info=True)
|
||||
@@ -1552,7 +1565,19 @@ class AgentDB:
|
||||
get_workflow_query = get_workflow_query.order_by(WorkflowModel.version.desc())
|
||||
async with self.Session() as session:
|
||||
if workflow := (await session.scalars(get_workflow_query)).first():
|
||||
return convert_to_workflow(workflow, self.debug_enabled)
|
||||
is_template = (
|
||||
await self.is_workflow_template(
|
||||
workflow_permanent_id=workflow.workflow_permanent_id,
|
||||
organization_id=workflow.organization_id,
|
||||
)
|
||||
if organization_id
|
||||
else False
|
||||
)
|
||||
return convert_to_workflow(
|
||||
workflow,
|
||||
self.debug_enabled,
|
||||
is_template=is_template,
|
||||
)
|
||||
return None
|
||||
except SQLAlchemyError:
|
||||
LOG.error("SQLAlchemyError", exc_info=True)
|
||||
@@ -1581,7 +1606,19 @@ class AgentDB:
|
||||
get_workflow_query = get_workflow_query.filter(WorkflowRunModel.workflow_run_id == workflow_run_id)
|
||||
async with self.Session() as session:
|
||||
if workflow := (await session.scalars(get_workflow_query)).first():
|
||||
return convert_to_workflow(workflow, self.debug_enabled)
|
||||
is_template = (
|
||||
await self.is_workflow_template(
|
||||
workflow_permanent_id=workflow.workflow_permanent_id,
|
||||
organization_id=workflow.organization_id,
|
||||
)
|
||||
if organization_id
|
||||
else False
|
||||
)
|
||||
return convert_to_workflow(
|
||||
workflow,
|
||||
self.debug_enabled,
|
||||
is_template=is_template,
|
||||
)
|
||||
return None
|
||||
except SQLAlchemyError:
|
||||
LOG.error("SQLAlchemyError", exc_info=True)
|
||||
@@ -1606,7 +1643,18 @@ class AgentDB:
|
||||
|
||||
async with self.Session() as session:
|
||||
workflows = (await session.scalars(get_workflows_query)).all()
|
||||
return [convert_to_workflow(workflow, self.debug_enabled) for workflow in workflows]
|
||||
template_permanent_ids: set[str] = set()
|
||||
if workflows and organization_id:
|
||||
template_permanent_ids = await self.get_org_template_permanent_ids(organization_id)
|
||||
|
||||
return [
|
||||
convert_to_workflow(
|
||||
workflow,
|
||||
self.debug_enabled,
|
||||
is_template=workflow.workflow_permanent_id in template_permanent_ids,
|
||||
)
|
||||
for workflow in workflows
|
||||
]
|
||||
except SQLAlchemyError:
|
||||
LOG.error("SQLAlchemyError", exc_info=True)
|
||||
raise
|
||||
@@ -1655,7 +1703,20 @@ class AgentDB:
|
||||
main_query.order_by(WorkflowModel.created_at.desc()).limit(page_size).offset(db_page * page_size)
|
||||
)
|
||||
workflows = (await session.scalars(main_query)).all()
|
||||
return [convert_to_workflow(workflow, self.debug_enabled) for workflow in workflows]
|
||||
|
||||
# Map template status by permanent_id so API responses surface is_template
|
||||
template_permanent_ids: set[str] = set()
|
||||
if workflows and organization_id:
|
||||
template_permanent_ids = await self.get_org_template_permanent_ids(organization_id)
|
||||
|
||||
return [
|
||||
convert_to_workflow(
|
||||
workflow,
|
||||
self.debug_enabled,
|
||||
is_template=workflow.workflow_permanent_id in template_permanent_ids,
|
||||
)
|
||||
for workflow in workflows
|
||||
]
|
||||
except SQLAlchemyError:
|
||||
LOG.error("SQLAlchemyError", exc_info=True)
|
||||
raise
|
||||
@@ -1667,6 +1728,7 @@ class AgentDB:
|
||||
page_size: int = 10,
|
||||
only_saved_tasks: bool = False,
|
||||
only_workflows: bool = False,
|
||||
only_templates: bool = False,
|
||||
search_key: str | None = None,
|
||||
folder_id: str | None = None,
|
||||
statuses: list[WorkflowStatus] | None = None,
|
||||
@@ -1717,6 +1779,13 @@ class AgentDB:
|
||||
main_query = main_query.where(WorkflowModel.is_saved_task.is_(True))
|
||||
elif only_workflows:
|
||||
main_query = main_query.where(WorkflowModel.is_saved_task.is_(False))
|
||||
if only_templates:
|
||||
# Filter by workflow_templates table (templates at permanent_id level)
|
||||
template_subquery = select(WorkflowTemplateModel.workflow_permanent_id).where(
|
||||
WorkflowTemplateModel.organization_id == organization_id,
|
||||
WorkflowTemplateModel.deleted_at.is_(None),
|
||||
)
|
||||
main_query = main_query.where(WorkflowModel.workflow_permanent_id.in_(template_subquery))
|
||||
if statuses:
|
||||
main_query = main_query.where(WorkflowModel.status.in_(statuses))
|
||||
if folder_id:
|
||||
@@ -1851,7 +1920,18 @@ class AgentDB:
|
||||
main_query.order_by(WorkflowModel.created_at.desc()).limit(page_size).offset(db_page * page_size)
|
||||
)
|
||||
workflows = (await session.scalars(main_query)).all()
|
||||
return [convert_to_workflow(workflow, self.debug_enabled) for workflow in workflows]
|
||||
template_permanent_ids: set[str] = set()
|
||||
if workflows and organization_id:
|
||||
template_permanent_ids = await self.get_org_template_permanent_ids(organization_id)
|
||||
|
||||
return [
|
||||
convert_to_workflow(
|
||||
workflow,
|
||||
self.debug_enabled,
|
||||
is_template=workflow.workflow_permanent_id in template_permanent_ids,
|
||||
)
|
||||
for workflow in workflows
|
||||
]
|
||||
except SQLAlchemyError:
|
||||
LOG.error("SQLAlchemyError", exc_info=True)
|
||||
raise
|
||||
@@ -1895,7 +1975,19 @@ class AgentDB:
|
||||
workflow.import_error = import_error
|
||||
await session.commit()
|
||||
await session.refresh(workflow)
|
||||
return convert_to_workflow(workflow, self.debug_enabled)
|
||||
is_template = (
|
||||
await self.is_workflow_template(
|
||||
workflow_permanent_id=workflow.workflow_permanent_id,
|
||||
organization_id=workflow.organization_id,
|
||||
)
|
||||
if organization_id
|
||||
else False
|
||||
)
|
||||
return convert_to_workflow(
|
||||
workflow,
|
||||
self.debug_enabled,
|
||||
is_template=is_template,
|
||||
)
|
||||
else:
|
||||
raise NotFoundError("Workflow not found")
|
||||
except SQLAlchemyError:
|
||||
@@ -1927,6 +2019,95 @@ class AgentDB:
|
||||
await session.execute(update_deleted_at_query)
|
||||
await session.commit()
|
||||
|
||||
async def add_workflow_template(
|
||||
self,
|
||||
workflow_permanent_id: str,
|
||||
organization_id: str,
|
||||
) -> None:
|
||||
"""Add a workflow to the templates table."""
|
||||
try:
|
||||
async with self.Session() as session:
|
||||
existing = (
|
||||
await session.scalars(
|
||||
select(WorkflowTemplateModel)
|
||||
.where(WorkflowTemplateModel.workflow_permanent_id == workflow_permanent_id)
|
||||
.where(WorkflowTemplateModel.organization_id == organization_id)
|
||||
)
|
||||
).first()
|
||||
if existing:
|
||||
if existing.deleted_at is not None:
|
||||
existing.deleted_at = None
|
||||
await session.commit()
|
||||
return
|
||||
template = WorkflowTemplateModel(
|
||||
workflow_permanent_id=workflow_permanent_id,
|
||||
organization_id=organization_id,
|
||||
)
|
||||
session.add(template)
|
||||
await session.commit()
|
||||
except SQLAlchemyError:
|
||||
LOG.error("SQLAlchemyError in add_workflow_template", exc_info=True)
|
||||
raise
|
||||
|
||||
async def remove_workflow_template(
|
||||
self,
|
||||
workflow_permanent_id: str,
|
||||
organization_id: str,
|
||||
) -> None:
|
||||
"""Soft delete a workflow from the templates table."""
|
||||
try:
|
||||
async with self.Session() as session:
|
||||
update_deleted_at_query = (
|
||||
update(WorkflowTemplateModel)
|
||||
.where(WorkflowTemplateModel.workflow_permanent_id == workflow_permanent_id)
|
||||
.where(WorkflowTemplateModel.organization_id == organization_id)
|
||||
.where(WorkflowTemplateModel.deleted_at.is_(None))
|
||||
.values(deleted_at=datetime.utcnow())
|
||||
)
|
||||
await session.execute(update_deleted_at_query)
|
||||
await session.commit()
|
||||
except SQLAlchemyError:
|
||||
LOG.error("SQLAlchemyError in remove_workflow_template", exc_info=True)
|
||||
raise
|
||||
|
||||
async def get_org_template_permanent_ids(
|
||||
self,
|
||||
organization_id: str,
|
||||
) -> set[str]:
|
||||
"""Get all workflow_permanent_ids that are templates for an organization."""
|
||||
try:
|
||||
async with self.Session() as session:
|
||||
result = await session.scalars(
|
||||
select(WorkflowTemplateModel.workflow_permanent_id)
|
||||
.where(WorkflowTemplateModel.organization_id == organization_id)
|
||||
.where(WorkflowTemplateModel.deleted_at.is_(None))
|
||||
)
|
||||
return set(result.all())
|
||||
except SQLAlchemyError:
|
||||
LOG.error("SQLAlchemyError in get_org_template_permanent_ids", exc_info=True)
|
||||
raise
|
||||
|
||||
async def is_workflow_template(
|
||||
self,
|
||||
workflow_permanent_id: str,
|
||||
organization_id: str,
|
||||
) -> bool:
|
||||
"""Check if a workflow is marked as a template."""
|
||||
try:
|
||||
async with self.Session() as session:
|
||||
result = (
|
||||
await session.scalars(
|
||||
select(WorkflowTemplateModel)
|
||||
.where(WorkflowTemplateModel.workflow_permanent_id == workflow_permanent_id)
|
||||
.where(WorkflowTemplateModel.organization_id == organization_id)
|
||||
.where(WorkflowTemplateModel.deleted_at.is_(None))
|
||||
)
|
||||
).first()
|
||||
return result is not None
|
||||
except SQLAlchemyError:
|
||||
LOG.error("SQLAlchemyError in is_workflow_template", exc_info=True)
|
||||
raise
|
||||
|
||||
async def create_folder(
|
||||
self,
|
||||
organization_id: str,
|
||||
|
||||
Reference in New Issue
Block a user