+
+
+ Add {type === "workflow" ? "Workflow" : "Credential"} Parameter
+
+
+
+
+ Key
+ setKey(e.target.value)} />
+
+
+ Description
+ setDescription(e.target.value)}
+ />
+
+ {type === "workflow" && (
+
+ Value Type
+
+ setParameterType(value as WorkflowParameterValueType)
+ }
+ >
+
+
+
+
+
+ {workflowParameterTypeOptions.map((option) => (
+
+ {option.label}
+
+ ))}
+
+
+
+
+ )}
+ {type === "credential" && (
+ <>
+
+ URL Parameter Key
+ setUrlParameterKey(e.target.value)}
+ />
+
+
+ Collection ID
+ setCollectionId(e.target.value)}
+ />
+
+ >
+ )}
+
+ {
+ if (type === "workflow") {
+ onSave({
+ key,
+ parameterType: "workflow",
+ dataType: parameterType,
+ description,
+ });
+ }
+ if (type === "credential") {
+ onSave({
+ key,
+ parameterType: "credential",
+ collectionId,
+ urlParameterKey,
+ description,
+ });
+ }
+ }}
+ >
+ Save
+
+
+
+ );
+}
+
+export { WorkflowParameterAddPanel };
diff --git a/skyvern-frontend/src/routes/workflows/editor/panels/WorkflowParameterEditPanel.tsx b/skyvern-frontend/src/routes/workflows/editor/panels/WorkflowParameterEditPanel.tsx
new file mode 100644
index 0000000000..33c515d465
--- /dev/null
+++ b/skyvern-frontend/src/routes/workflows/editor/panels/WorkflowParameterEditPanel.tsx
@@ -0,0 +1,149 @@
+import { Cross2Icon } from "@radix-ui/react-icons";
+import { Label } from "@/components/ui/label";
+import { Input } from "@/components/ui/input";
+import { useState } from "react";
+import { WorkflowParameterValueType } from "../../types/workflowTypes";
+import {
+ Select,
+ SelectContent,
+ SelectGroup,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
+import { Button } from "@/components/ui/button";
+import { ParametersState } from "../FlowRenderer";
+
+type Props = {
+ type: "workflow" | "credential";
+ onClose: () => void;
+ onSave: (value: ParametersState[number]) => void;
+ initialValues: ParametersState[number];
+};
+
+const workflowParameterTypeOptions = [
+ { label: "string", value: WorkflowParameterValueType.String },
+ { label: "number", value: WorkflowParameterValueType.Float },
+ { label: "boolean", value: WorkflowParameterValueType.Boolean },
+ { label: "file", value: WorkflowParameterValueType.FileURL },
+ { label: "JSON", value: WorkflowParameterValueType.JSON },
+];
+
+function WorkflowParameterEditPanel({
+ type,
+ onClose,
+ onSave,
+ initialValues,
+}: Props) {
+ const [key, setKey] = useState(initialValues.key);
+ const [urlParameterKey, setUrlParameterKey] = useState(
+ initialValues.parameterType === "credential"
+ ? initialValues.urlParameterKey
+ : "",
+ );
+ const [description, setDescription] = useState(
+ initialValues.description || "",
+ );
+ const [collectionId, setCollectionId] = useState(
+ initialValues.parameterType === "credential"
+ ? initialValues.collectionId
+ : "",
+ );
+ const [parameterType, setParameterType] =
+ useState(
+ initialValues.parameterType === "workflow"
+ ? initialValues.dataType
+ : "string",
+ );
+
+ return (
+
+
+
+ Edit {type === "workflow" ? "Workflow" : "Credential"} Parameter
+
+
+
+
+ Key
+ setKey(e.target.value)} />
+
+
+ Description
+ setDescription(e.target.value)}
+ />
+
+ {type === "workflow" && (
+
+ Value Type
+
+ setParameterType(value as WorkflowParameterValueType)
+ }
+ >
+
+
+
+
+
+ {workflowParameterTypeOptions.map((option) => (
+
+ {option.label}
+
+ ))}
+
+
+
+
+ )}
+ {type === "credential" && (
+ <>
+
+ URL Parameter Key
+ setUrlParameterKey(e.target.value)}
+ />
+
+
+ Collection ID
+ setCollectionId(e.target.value)}
+ />
+
+ >
+ )}
+
+ {
+ if (type === "workflow") {
+ onSave({
+ key,
+ parameterType: "workflow",
+ dataType: parameterType,
+ description,
+ });
+ }
+ if (type === "credential") {
+ onSave({
+ key,
+ parameterType: "credential",
+ urlParameterKey,
+ collectionId,
+ description,
+ });
+ }
+ }}
+ >
+ Save
+
+
+
+ );
+}
+
+export { WorkflowParameterEditPanel };
diff --git a/skyvern-frontend/src/routes/workflows/editor/panels/WorkflowParametersPanel.tsx b/skyvern-frontend/src/routes/workflows/editor/panels/WorkflowParametersPanel.tsx
index e265c249b6..c377639157 100644
--- a/skyvern-frontend/src/routes/workflows/editor/panels/WorkflowParametersPanel.tsx
+++ b/skyvern-frontend/src/routes/workflows/editor/panels/WorkflowParametersPanel.tsx
@@ -1,45 +1,227 @@
-import { useParams } from "react-router-dom";
-import { useWorkflowQuery } from "../../hooks/useWorkflowQuery";
+import { useState } from "react";
+import { useWorkflowParametersState } from "../useWorkflowParametersState";
+import { WorkflowParameterAddPanel } from "./WorkflowParameterAddPanel";
+import { ParametersState } from "../FlowRenderer";
+import { WorkflowParameterEditPanel } from "./WorkflowParameterEditPanel";
+import { MixerVerticalIcon, PlusIcon } from "@radix-ui/react-icons";
+import { Button } from "@/components/ui/button";
+import { GarbageIcon } from "@/components/icons/GarbageIcon";
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from "@/components/ui/dialog";
+import { DialogClose } from "@radix-ui/react-dialog";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu";
-function WorkflowParametersPanel() {
- const { workflowPermanentId } = useParams();
+const WORKFLOW_EDIT_PANEL_WIDTH = 20 * 16;
+const WORKFLOW_EDIT_PANEL_GAP = 1 * 16;
- const { data: workflow, isLoading } = useWorkflowQuery({
- workflowPermanentId,
+function WorkflowParametersPanel() {
+ const [workflowParameters, setWorkflowParameters] =
+ useWorkflowParametersState();
+ const [operationPanelState, setOperationPanelState] = useState<{
+ active: boolean;
+ operation: "add" | "edit";
+ parameter?: ParametersState[number] | null;
+ type: "workflow" | "credential";
+ }>({
+ active: false,
+ operation: "add",
+ parameter: null,
+ type: "workflow",
});
- if (isLoading || !workflow) {
- return null;
- }
-
- const workflowParameters = workflow.workflow_definition.parameters.filter(
- (parameter) => parameter.parameter_type === "workflow",
- );
-
return (
-
-
- Workflow Parameters
-
- Create placeholder values that you can link in nodes. You will be
- prompted to fill them in before running your workflow.
-
-
-
- {workflowParameters.map((parameter) => {
- return (
-
+
+
+ Workflow Parameters
+
+ Create placeholder values that you can link in nodes. You will be
+ prompted to fill them in before running your workflow.
+
+
+
+
+
+
+ Add Parameter
+
+
+
+ Add Parameter
+
+ {
+ setOperationPanelState({
+ active: true,
+ operation: "add",
+ type: "workflow",
+ });
+ }}
+ >
+ Workflow Parameter
+
+ {
+ setOperationPanelState({
+ active: true,
+ operation: "add",
+ type: "credential",
+ });
+ }}
>
- {parameter.key}
-
- {parameter.workflow_parameter_type}
-
+ Credential Parameter
+
+
+
+
+
+ {workflowParameters.map((parameter) => {
+ return (
+
+
+ {parameter.key}
+ {parameter.parameterType === "workflow" ? (
+
+ {parameter.dataType}
+
+ ) : (
+
+ {parameter.parameterType}
+
+ )}
+
+
+ {
+ setOperationPanelState({
+ active: true,
+ operation: "edit",
+ parameter: parameter,
+ type: parameter.parameterType,
+ });
+ }}
+ />
+
+
+
+
+
+
+ Are you sure?
+
+ This parameter will be deleted.
+
+
+
+
+ Cancel
+
+ {
+ setWorkflowParameters(
+ workflowParameters.filter(
+ (p) => p.key !== parameter.key,
+ ),
+ );
+ }}
+ >
+ Delete
+
+
+
+
+
+
+ );
+ })}
+
+
+ {operationPanelState.active && (
+
+ {operationPanelState.operation === "add" && (
+
+ {
+ setWorkflowParameters([...workflowParameters, parameter]);
+ setOperationPanelState({
+ active: false,
+ operation: "add",
+ type: "workflow",
+ });
+ }}
+ onClose={() => {
+ setOperationPanelState({
+ active: false,
+ operation: "add",
+ type: "workflow",
+ });
+ }}
+ />
- );
- })}
-
+ )}
+ {operationPanelState.operation === "edit" &&
+ operationPanelState.parameter && (
+
+ {
+ setWorkflowParameters(
+ workflowParameters.map((parameter) => {
+ if (
+ parameter.key === operationPanelState.parameter?.key
+ ) {
+ return editedParameter;
+ }
+ return parameter;
+ }),
+ );
+ setOperationPanelState({
+ active: false,
+ operation: "edit",
+ parameter: null,
+ type: "workflow",
+ });
+ }}
+ onClose={() => {
+ setOperationPanelState({
+ active: false,
+ operation: "edit",
+ parameter: null,
+ type: "workflow",
+ });
+ }}
+ />
+
+ )}
+
+ )}
);
}
diff --git a/skyvern-frontend/src/routes/workflows/editor/useWorkflowParametersState.ts b/skyvern-frontend/src/routes/workflows/editor/useWorkflowParametersState.ts
new file mode 100644
index 0000000000..3ca98d5a03
--- /dev/null
+++ b/skyvern-frontend/src/routes/workflows/editor/useWorkflowParametersState.ts
@@ -0,0 +1,14 @@
+import { useContext } from "react";
+import { WorkflowParametersStateContext } from "./WorkflowParametersStateContext";
+
+function useWorkflowParametersState() {
+ const value = useContext(WorkflowParametersStateContext);
+ if (value === undefined) {
+ throw new Error(
+ "useWorkflowParametersState must be used within a WorkflowParametersStateProvider",
+ );
+ }
+ return value;
+}
+
+export { useWorkflowParametersState };
diff --git a/skyvern-frontend/src/routes/workflows/editor/workflowEditorUtils.ts b/skyvern-frontend/src/routes/workflows/editor/workflowEditorUtils.ts
index 76a5dd15dc..b811197159 100644
--- a/skyvern-frontend/src/routes/workflows/editor/workflowEditorUtils.ts
+++ b/skyvern-frontend/src/routes/workflows/editor/workflowEditorUtils.ts
@@ -2,6 +2,18 @@ import { Edge } from "@xyflow/react";
import { AppNode } from "./nodes";
import Dagre from "@dagrejs/dagre";
import type { WorkflowBlock } from "../types/workflowTypes";
+import { nodeTypes } from "./nodes";
+import { taskNodeDefaultData } from "./nodes/TaskNode/types";
+import { LoopNode, loopNodeDefaultData } from "./nodes/LoopNode/types";
+import { codeBlockNodeDefaultData } from "./nodes/CodeBlockNode/types";
+import { downloadNodeDefaultData } from "./nodes/DownloadNode/types";
+import { uploadNodeDefaultData } from "./nodes/UploadNode/types";
+import { sendEmailNodeDefaultData } from "./nodes/SendEmailNode/types";
+import { textPromptNodeDefaultData } from "./nodes/TextPromptNode/types";
+import { fileParserNodeDefaultData } from "./nodes/FileParserNode/types";
+import { BlockYAML } from "../types/workflowYamlTypes";
+import { NodeAdderNode } from "./nodes/NodeAdderNode/types";
+import { REACT_FLOW_EDGE_Z_INDEX } from "./constants";
function layoutUtil(
nodes: Array,
@@ -52,8 +64,12 @@ function layout(
(node) => node.id === edge.source || node.id === edge.target,
),
);
+ const maxChildWidth = Math.max(
+ ...childNodes.map((node) => node.measured?.width ?? 0),
+ );
+ const loopNodeWidth = 60 * 16; // 60 rem
const layouted = layoutUtil(childNodes, childEdges, {
- marginx: 240,
+ marginx: (loopNodeWidth - maxChildWidth) / 2,
marginy: 200,
});
loopNodeChildren[index] = layouted.nodes;
@@ -75,6 +91,8 @@ function convertToNode(
): AppNode {
const common = {
draggable: false,
+ position: { x: 0, y: 0 },
+ connectable: false,
};
switch (block.block_type) {
case "task": {
@@ -84,17 +102,17 @@ function convertToNode(
type: "task",
data: {
label: block.label,
- editable: false,
+ editable: true,
url: block.url ?? "",
navigationGoal: block.navigation_goal ?? "",
dataExtractionGoal: block.data_extraction_goal ?? "",
- dataSchema: block.data_schema ?? null,
- errorCodeMapping: block.error_code_mapping ?? null,
+ dataSchema: JSON.stringify(block.data_schema, null, 2),
+ errorCodeMapping: JSON.stringify(block.error_code_mapping, null, 2),
allowDownloads: block.complete_on_download ?? false,
maxRetries: block.max_retries ?? null,
maxStepsOverride: block.max_steps_per_run ?? null,
+ parameterKeys: block.parameters.map((p) => p.key),
},
- position: { x: 0, y: 0 },
};
}
case "code": {
@@ -104,10 +122,9 @@ function convertToNode(
type: "codeBlock",
data: {
label: block.label,
- editable: false,
+ editable: true,
code: block.code,
},
- position: { x: 0, y: 0 },
};
}
case "send_email": {
@@ -117,13 +134,13 @@ function convertToNode(
type: "sendEmail",
data: {
label: block.label,
- editable: false,
+ editable: true,
body: block.body,
- fileAttachments: block.file_attachments,
- recipients: block.recipients,
+ fileAttachments: block.file_attachments.join(", "),
+ recipients: block.recipients.join(", "),
subject: block.subject,
+ sender: block.sender,
},
- position: { x: 0, y: 0 },
};
}
case "text_prompt": {
@@ -133,11 +150,10 @@ function convertToNode(
type: "textPrompt",
data: {
label: block.label,
- editable: false,
+ editable: true,
prompt: block.prompt,
- jsonSchema: block.json_schema ?? null,
+ jsonSchema: JSON.stringify(block.json_schema, null, 2),
},
- position: { x: 0, y: 0 },
};
}
case "for_loop": {
@@ -147,10 +163,9 @@ function convertToNode(
type: "loop",
data: {
label: block.label,
- editable: false,
+ editable: true,
loopValue: block.loop_over.key,
},
- position: { x: 0, y: 0 },
};
}
case "file_url_parser": {
@@ -160,10 +175,9 @@ function convertToNode(
type: "fileParser",
data: {
label: block.label,
- editable: false,
+ editable: true,
fileUrl: block.file_url,
},
- position: { x: 0, y: 0 },
};
}
@@ -174,10 +188,9 @@ function convertToNode(
type: "download",
data: {
label: block.label,
- editable: false,
+ editable: true,
url: block.url,
},
- position: { x: 0, y: 0 },
};
}
@@ -188,10 +201,9 @@ function convertToNode(
type: "upload",
data: {
label: block.label,
- editable: false,
+ editable: true,
path: block.path,
},
- position: { x: 0, y: 0 },
};
}
}
@@ -210,22 +222,274 @@ function getElements(
nodes.push(convertToNode({ id, parentId }, block));
if (block.block_type === "for_loop") {
const subElements = getElements(block.loop_blocks, id);
+ if (subElements.nodes.length === 0) {
+ nodes.push({
+ id: `${id}-nodeAdder`,
+ type: "nodeAdder",
+ position: { x: 0, y: 0 },
+ data: {},
+ draggable: false,
+ connectable: false,
+ });
+ }
nodes.push(...subElements.nodes);
edges.push(...subElements.edges);
}
if (index !== blocks.length - 1) {
edges.push({
id: `edge-${id}-${nextId}`,
+ type: "edgeWithAddButton",
source: id,
target: nextId,
style: {
strokeWidth: 2,
},
+ zIndex: REACT_FLOW_EDGE_Z_INDEX,
});
}
});
+ if (nodes.length > 0) {
+ edges.push({
+ id: "edge-nodeAdder",
+ type: "default",
+ source: nodes[nodes.length - 1]!.id,
+ target: "nodeAdder",
+ style: {
+ strokeWidth: 2,
+ },
+ });
+ nodes.push({
+ id: "nodeAdder",
+ type: "nodeAdder",
+ position: { x: 0, y: 0 },
+ data: {},
+ draggable: false,
+ connectable: false,
+ });
+ }
+
return { nodes, edges };
}
-export { getElements, layout };
+function createNode(
+ identifiers: { id: string; parentId?: string },
+ nodeType: Exclude,
+ labelPostfix: string, // unique label requirement
+): AppNode {
+ const label = "Block " + labelPostfix;
+ const common = {
+ draggable: false,
+ position: { x: 0, y: 0 },
+ };
+ switch (nodeType) {
+ case "task": {
+ return {
+ ...identifiers,
+ ...common,
+ type: "task",
+ data: {
+ ...taskNodeDefaultData,
+ label,
+ },
+ };
+ }
+ case "loop": {
+ return {
+ ...identifiers,
+ ...common,
+ type: "loop",
+ data: {
+ ...loopNodeDefaultData,
+ label,
+ },
+ };
+ }
+ case "codeBlock": {
+ return {
+ ...identifiers,
+ ...common,
+ type: "codeBlock",
+ data: {
+ ...codeBlockNodeDefaultData,
+ label,
+ },
+ };
+ }
+ case "download": {
+ return {
+ ...identifiers,
+ ...common,
+ type: "download",
+ data: {
+ ...downloadNodeDefaultData,
+ label,
+ },
+ };
+ }
+ case "upload": {
+ return {
+ ...identifiers,
+ ...common,
+ type: "upload",
+ data: {
+ ...uploadNodeDefaultData,
+ label,
+ },
+ };
+ }
+ case "sendEmail": {
+ return {
+ ...identifiers,
+ ...common,
+ type: "sendEmail",
+ data: {
+ ...sendEmailNodeDefaultData,
+ label,
+ },
+ };
+ }
+ case "textPrompt": {
+ return {
+ ...identifiers,
+ ...common,
+ type: "textPrompt",
+ data: {
+ ...textPromptNodeDefaultData,
+ label,
+ },
+ };
+ }
+ case "fileParser": {
+ return {
+ ...identifiers,
+ ...common,
+ type: "fileParser",
+ data: {
+ ...fileParserNodeDefaultData,
+ label,
+ },
+ };
+ }
+ }
+}
+
+function JSONParseSafe(json: string): Record | null {
+ try {
+ return JSON.parse(json);
+ } catch {
+ return null;
+ }
+}
+
+function getWorkflowBlock(
+ node: Exclude,
+): BlockYAML {
+ switch (node.type) {
+ case "task": {
+ return {
+ block_type: "task",
+ label: node.data.label,
+ url: node.data.url,
+ navigation_goal: node.data.navigationGoal,
+ data_extraction_goal: node.data.dataExtractionGoal,
+ data_schema: JSONParseSafe(node.data.dataSchema),
+ error_code_mapping: JSONParseSafe(node.data.errorCodeMapping) as Record<
+ string,
+ string
+ > | null,
+ max_retries: node.data.maxRetries ?? undefined,
+ max_steps_per_run: node.data.maxStepsOverride,
+ complete_on_download: node.data.allowDownloads,
+ parameter_keys: node.data.parameterKeys,
+ };
+ }
+ case "sendEmail": {
+ return {
+ block_type: "send_email",
+ label: node.data.label,
+ body: node.data.body,
+ file_attachments: node.data.fileAttachments.split(","),
+ recipients: node.data.recipients.split(","),
+ subject: node.data.subject,
+ sender: node.data.sender,
+ };
+ }
+ case "codeBlock": {
+ return {
+ block_type: "code",
+ label: node.data.label,
+ code: node.data.code,
+ };
+ }
+ case "download": {
+ return {
+ block_type: "download_to_s3",
+ label: node.data.label,
+ url: node.data.url,
+ };
+ }
+ case "upload": {
+ return {
+ block_type: "upload_to_s3",
+ label: node.data.label,
+ path: node.data.path,
+ };
+ }
+ case "fileParser": {
+ return {
+ block_type: "file_url_parser",
+ label: node.data.label,
+ file_url: node.data.fileUrl,
+ file_type: "csv",
+ };
+ }
+ case "textPrompt": {
+ return {
+ block_type: "text_prompt",
+ label: node.data.label,
+ llm_key: "",
+ prompt: node.data.prompt,
+ json_schema: JSONParseSafe(node.data.jsonSchema),
+ };
+ }
+ default: {
+ throw new Error("Invalid node type for getWorkflowBlock");
+ }
+ }
+}
+
+function getWorkflowBlocksUtil(nodes: Array): Array {
+ return nodes.flatMap((node) => {
+ if (node.parentId) {
+ return [];
+ }
+ if (node.type === "loop") {
+ return [
+ {
+ block_type: "for_loop",
+ label: node.data.label,
+ loop_over_parameter_key: node.data.loopValue,
+ loop_blocks: nodes
+ .filter((n) => n.parentId === node.id)
+ .map((n) => {
+ return getWorkflowBlock(
+ n as Exclude,
+ );
+ }),
+ },
+ ];
+ }
+ return [
+ getWorkflowBlock(node as Exclude),
+ ];
+ });
+}
+
+function getWorkflowBlocks(nodes: Array): Array {
+ return getWorkflowBlocksUtil(
+ nodes.filter((node) => node.type !== "nodeAdder"),
+ );
+}
+
+export { getElements, layout, createNode, getWorkflowBlocks };
diff --git a/skyvern-frontend/src/routes/workflows/types/workflowTypes.ts b/skyvern-frontend/src/routes/workflows/types/workflowTypes.ts
index a43065f9cc..1afb803f9c 100644
--- a/skyvern-frontend/src/routes/workflows/types/workflowTypes.ts
+++ b/skyvern-frontend/src/routes/workflows/types/workflowTypes.ts
@@ -196,7 +196,7 @@ export type WorkflowBlock =
| FileURLParserBlock;
export type WorkflowDefinition = {
- parameters: Array;
+ parameters: Array;
blocks: Array;
};
@@ -211,6 +211,7 @@ export type WorkflowApiResponse = {
workflow_definition: WorkflowDefinition;
proxy_location: string;
webhook_callback_url: string;
+ totp_verification_url: string;
created_at: string;
modified_at: string;
deleted_at: string | null;
diff --git a/skyvern-frontend/src/routes/workflows/types/workflowYamlTypes.ts b/skyvern-frontend/src/routes/workflows/types/workflowYamlTypes.ts
new file mode 100644
index 0000000000..b93c0f5fc3
--- /dev/null
+++ b/skyvern-frontend/src/routes/workflows/types/workflowYamlTypes.ts
@@ -0,0 +1,133 @@
+export type WorkflowCreateYAMLRequest = {
+ title: string;
+ description?: string | null;
+ proxy_location?: string | null;
+ webhook_callback_url?: string | null;
+ totp_verification_url?: string | null;
+ workflow_definition: WorkflowDefinitionYAML;
+ is_saved_task?: boolean;
+};
+
+export type WorkflowDefinitionYAML = {
+ parameters: Array;
+ blocks: Array;
+};
+
+export type ParameterYAML =
+ | WorkflowParameterYAML
+ | BitwardenLoginCredentialParameterYAML;
+
+export type ParameterYAMLBase = {
+ parameter_type: string;
+ key: string;
+ description?: string | null;
+};
+
+export type WorkflowParameterYAML = ParameterYAMLBase & {
+ parameter_type: "workflow";
+ workflow_parameter_type: string;
+ default_value: string | null;
+};
+
+export type BitwardenLoginCredentialParameterYAML = ParameterYAMLBase & {
+ parameter_type: "bitwarden_login_credential";
+ bitwarden_collection_id: string;
+ url_parameter_key: string;
+ bitwarden_client_id_aws_secret_key: "SKYVERN_BITWARDEN_CLIENT_ID";
+ bitwarden_client_secret_aws_secret_key: "SKYVERN_BITWARDEN_CLIENT_SECRET";
+ bitwarden_master_password_aws_secret_key: "SKYVERN_BITWARDEN_MASTER_PASSWORD";
+};
+
+const BlockTypes = {
+ TASK: "task",
+ FOR_LOOP: "for_loop",
+ CODE: "code",
+ TEXT_PROMPT: "text_prompt",
+ DOWNLOAD_TO_S3: "download_to_s3",
+ UPLOAD_TO_S3: "upload_to_s3",
+ SEND_EMAIL: "send_email",
+ FILE_URL_PARSER: "file_url_parser",
+} as const;
+
+export type BlockType = (typeof BlockTypes)[keyof typeof BlockTypes];
+
+export type BlockYAML =
+ | TaskBlockYAML
+ | CodeBlockYAML
+ | TextPromptBlockYAML
+ | DownloadToS3BlockYAML
+ | UploadToS3BlockYAML
+ | SendEmailBlockYAML
+ | FileUrlParserBlockYAML
+ | ForLoopBlockYAML;
+
+export type BlockYAMLBase = {
+ block_type: BlockType;
+ label: string;
+ continue_on_failure?: boolean;
+};
+
+export type TaskBlockYAML = BlockYAMLBase & {
+ block_type: "task";
+ url: string | null;
+ title?: string;
+ navigation_goal: string | null;
+ data_extraction_goal: string | null;
+ data_schema: Record | null;
+ error_code_mapping: Record | null;
+ max_retries?: number;
+ max_steps_per_run?: number | null;
+ parameter_keys?: Array | null;
+ complete_on_download?: boolean;
+};
+
+export type CodeBlockYAML = BlockYAMLBase & {
+ block_type: "code";
+ code: string;
+ parameter_keys?: Array | null;
+};
+
+export type TextPromptBlockYAML = BlockYAMLBase & {
+ block_type: "text_prompt";
+ llm_key: string;
+ prompt: string;
+ json_schema?: Record | null;
+ parameter_keys?: Array | null;
+};
+
+export type DownloadToS3BlockYAML = BlockYAMLBase & {
+ block_type: "download_to_s3";
+ url: string;
+};
+
+export type UploadToS3BlockYAML = BlockYAMLBase & {
+ block_type: "upload_to_s3";
+ path?: string | null;
+};
+
+export type SendEmailBlockYAML = BlockYAMLBase & {
+ block_type: "send_email";
+
+ smtp_host_secret_parameter_key?: string;
+ smtp_port_secret_parameter_key?: string;
+ smtp_username_secret_parameter_key?: string;
+ smtp_password_secret_parameter_key?: string;
+
+ sender: string;
+ recipients: Array;
+ subject: string;
+ body: string;
+ file_attachments?: Array | null;
+};
+
+export type FileUrlParserBlockYAML = BlockYAMLBase & {
+ block_type: "file_url_parser";
+ file_url: string;
+ file_type: "csv";
+};
+
+export type ForLoopBlockYAML = BlockYAMLBase & {
+ block_type: "for_loop";
+ loop_over_parameter_key: string;
+ loop_blocks: Array;
+};
diff --git a/skyvern-frontend/src/store/WorkflowPanelStore.ts b/skyvern-frontend/src/store/WorkflowPanelStore.ts
new file mode 100644
index 0000000000..554e27c906
--- /dev/null
+++ b/skyvern-frontend/src/store/WorkflowPanelStore.ts
@@ -0,0 +1,49 @@
+import { create } from "zustand";
+
+type WorkflowPanelState = {
+ active: boolean;
+ content: "parameters" | "nodeLibrary";
+ data?: {
+ previous?: string | null;
+ next?: string | null;
+ parent?: string;
+ connectingEdgeType?: string;
+ };
+};
+
+type WorkflowPanelStore = {
+ workflowPanelState: WorkflowPanelState;
+ closeWorkflowPanel: () => void;
+ setWorkflowPanelState: (state: WorkflowPanelState) => void;
+ toggleWorkflowPanel: () => void;
+};
+
+const useWorkflowPanelStore = create((set, get) => {
+ return {
+ workflowPanelState: {
+ active: false,
+ content: "parameters",
+ },
+ setWorkflowPanelState: (workflowPanelState: WorkflowPanelState) => {
+ set({ workflowPanelState });
+ },
+ closeWorkflowPanel: () => {
+ set({
+ workflowPanelState: {
+ ...get().workflowPanelState,
+ active: false,
+ },
+ });
+ },
+ toggleWorkflowPanel: () => {
+ set({
+ workflowPanelState: {
+ ...get().workflowPanelState,
+ active: !get().workflowPanelState.active,
+ },
+ });
+ },
+ };
+});
+
+export { useWorkflowPanelStore };