diff --git a/skyvern-frontend/src/api/types.ts b/skyvern-frontend/src/api/types.ts
index ede37e97..96f141a9 100644
--- a/skyvern-frontend/src/api/types.ts
+++ b/skyvern-frontend/src/api/types.ts
@@ -304,6 +304,7 @@ export type WorkflowRunApiResponse = {
failure_reason: string | null;
modified_at: string;
proxy_location: ProxyLocation | null;
+ script_run: boolean | null;
status: Status;
title?: string;
webhook_callback_url: string;
diff --git a/skyvern-frontend/src/router.tsx b/skyvern-frontend/src/router.tsx
index 9cf9c2a4..0d8cf07d 100644
--- a/skyvern-frontend/src/router.tsx
+++ b/skyvern-frontend/src/router.tsx
@@ -24,6 +24,7 @@ import { WorkflowPostRunParameters } from "./routes/workflows/workflowRun/Workfl
import { WorkflowRunOutput } from "./routes/workflows/workflowRun/WorkflowRunOutput";
import { WorkflowRunOverview } from "./routes/workflows/workflowRun/WorkflowRunOverview";
import { WorkflowRunRecording } from "./routes/workflows/workflowRun/WorkflowRunRecording";
+import { WorkflowRunCode } from "@/routes/workflows/workflowRun/WorkflowRunCode";
import { DebugStoreProvider } from "@/store/DebugStoreContext";
const router = createBrowserRouter([
@@ -158,6 +159,12 @@ const router = createBrowserRouter([
path: "recording",
element: ,
},
+ {
+ path: "code",
+ element: (
+
+ ),
+ },
],
},
],
diff --git a/skyvern-frontend/src/routes/history/RunHistory.tsx b/skyvern-frontend/src/routes/history/RunHistory.tsx
index b64c3351..5eb1e860 100644
--- a/skyvern-frontend/src/routes/history/RunHistory.tsx
+++ b/skyvern-frontend/src/routes/history/RunHistory.tsx
@@ -1,3 +1,6 @@
+import { LightningBoltIcon } from "@radix-ui/react-icons";
+
+import { Tip } from "@/components/Tip";
import { Status, Task, WorkflowRunApiResponse } from "@/api/types";
import { StatusBadge } from "@/components/StatusBadge";
import { StatusFilterDropdown } from "@/components/StatusFilterDropdown";
@@ -162,6 +165,19 @@ function RunHistory() {
);
}
+
+ const workflowTitle =
+ run.script_run === true ? (
+
+
+
+
+ {run.workflow_title ?? ""}
+
+ ) : (
+ run.workflow_title ?? ""
+ );
+
return (
- {run.workflow_title ?? ""}
+ {workflowTitle}
diff --git a/skyvern-frontend/src/routes/workflows/RunWorkflowForm.tsx b/skyvern-frontend/src/routes/workflows/RunWorkflowForm.tsx
index 23a655e4..993aa9b2 100644
--- a/skyvern-frontend/src/routes/workflows/RunWorkflowForm.tsx
+++ b/skyvern-frontend/src/routes/workflows/RunWorkflowForm.tsx
@@ -2,6 +2,12 @@ import { getClient } from "@/api/AxiosClient";
import { ProxyLocation } from "@/api/types";
import { ProxySelector } from "@/components/ProxySelector";
import { Button } from "@/components/ui/button";
+import {
+ Accordion,
+ AccordionContent,
+ AccordionItem,
+ AccordionTrigger,
+} from "@/components/ui/accordion";
import {
Form,
FormControl,
@@ -311,7 +317,7 @@ function RunWorkflowForm({
- Advanced Settings
+ Settings
-
{
- return (
-
-
-
-
-
- Browser Address
-
-
- The address of the Browser server to use for the
- workflow run.
-
-
-
-
-
-
-
-
-
-
-
- );
- }}
- />
- {
- return (
-
-
-
-
-
- Extra HTTP Headers
-
-
- Specify some self defined HTTP requests headers in
- Dict format
-
-
-
-
-
- field.onChange(val)}
- addButtonText="Add Header"
- />
-
-
-
-
-
- );
- }}
- />
- {
- return (
-
-
-
-
-
- Max Screenshot Scrolls
-
-
- {`The maximum number of scrolls for the post action screenshot. Default is ${MAX_SCREENSHOT_SCROLLS_DEFAULT}. If it's set to 0, it will take the current viewport screenshot.`}
-
-
-
-
-
- {
- const value =
- event.target.value === ""
- ? null
- : Number(event.target.value);
- field.onChange(value);
- }}
- />
-
-
-
-
-
- );
- }}
- />
+
+
+
+
+
+
+
+
+
+
+
{
+ return (
+
+
+
+
+
+ Browser Address
+
+
+ The address of the Browser server to use for
+ the workflow run.
+
+
+
+
+
+
+
+
+
+
+
+ );
+ }}
+ />
+ {
+ return (
+
+
+
+
+
+ Extra HTTP Headers
+
+
+ Specify some self defined HTTP requests
+ headers in Dict format
+
+
+
+
+
+ field.onChange(val)}
+ addButtonText="Add Header"
+ />
+
+
+
+
+
+ );
+ }}
+ />
+ {
+ return (
+
+
+
+
+
+ Max Screenshot Scrolls
+
+
+ {`The maximum number of scrolls for the post action screenshot. Default is ${MAX_SCREENSHOT_SCROLLS_DEFAULT}. If it's set to 0, it will take the current viewport screenshot.`}
+
+
+
+
+
+ {
+ const value =
+ event.target.value === ""
+ ? null
+ : Number(event.target.value);
+ field.onChange(value);
+ }}
+ />
+
+
+
+
+
+ );
+ }}
+ />
+
+
+
+
No workflow runs found
) : (
- workflowRuns?.map((workflowRun) => (
- {
- if (event.ctrlKey || event.metaKey) {
- window.open(
- window.location.origin +
- `/workflows/${workflowPermanentId}/${workflowRun.workflow_run_id}/overview`,
- "_blank",
- "noopener,noreferrer",
+ workflowRuns?.map((workflowRun) => {
+ const workflowRunId =
+ workflowRun.script_run === true ? (
+
+
+
+
+ {workflowRun.workflow_run_id ?? ""}
+
+ ) : (
+ workflowRun.workflow_run_id ?? ""
+ );
+
+ return (
+ {
+ if (event.ctrlKey || event.metaKey) {
+ window.open(
+ window.location.origin +
+ `/workflows/${workflowPermanentId}/${workflowRun.workflow_run_id}/overview`,
+ "_blank",
+ "noopener,noreferrer",
+ );
+ return;
+ }
+ navigate(
+ `/workflows/${workflowPermanentId}/${workflowRun.workflow_run_id}/overview`,
);
- return;
- }
- navigate(
- `/workflows/${workflowPermanentId}/${workflowRun.workflow_run_id}/overview`,
- );
- }}
- className="cursor-pointer"
- >
- {workflowRun.workflow_run_id}
-
-
-
-
- {basicLocalTimeFormat(workflowRun.created_at)}
-
-
- ))
+ }}
+ className="cursor-pointer"
+ >
+ {workflowRunId}
+
+
+
+
+ {basicLocalTimeFormat(workflowRun.created_at)}
+
+
+ );
+ })
)}
diff --git a/skyvern-frontend/src/routes/workflows/WorkflowRun.tsx b/skyvern-frontend/src/routes/workflows/WorkflowRun.tsx
index ceceaf72..d25b62d8 100644
--- a/skyvern-frontend/src/routes/workflows/WorkflowRun.tsx
+++ b/skyvern-frontend/src/routes/workflows/WorkflowRun.tsx
@@ -53,6 +53,8 @@ function WorkflowRun() {
workflowPermanentId,
});
+ const hasScript = false;
+
const {
data: workflowRun,
isLoading: workflowRunIsLoading,
@@ -206,6 +208,32 @@ function WorkflowRun() {
webhookFailureReasonData) &&
workflowRun.status === Status.Completed;
+ const switchBarOptions = [
+ {
+ label: "Overview",
+ to: "overview",
+ },
+ {
+ label: "Output",
+ to: "output",
+ },
+ {
+ label: "Parameters",
+ to: "parameters",
+ },
+ {
+ label: "Recording",
+ to: "recording",
+ },
+ ];
+
+ if (!hasScript) {
+ switchBarOptions.push({
+ label: "Code",
+ to: "code",
+ });
+ }
+
return (
{!isEmbedded && (
@@ -352,28 +380,7 @@ function WorkflowRun() {
)}
{workflowFailureReason}
- {!isEmbedded && (
-
- )}
+ {!isEmbedded && }
diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/ExtractionNode/ExtractionNode.tsx b/skyvern-frontend/src/routes/workflows/editor/nodes/ExtractionNode/ExtractionNode.tsx
index 6330e4cb..7c13da00 100644
--- a/skyvern-frontend/src/routes/workflows/editor/nodes/ExtractionNode/ExtractionNode.tsx
+++ b/skyvern-frontend/src/routes/workflows/editor/nodes/ExtractionNode/ExtractionNode.tsx
@@ -36,7 +36,7 @@ import { ModelSelector } from "@/components/ModelSelector";
import { useBlockScriptStore } from "@/store/BlockScriptStore";
import { cn } from "@/util/utils";
import { NodeHeader } from "../components/NodeHeader";
-import { NodeFooter } from "../components/NodeFooter";
+import { NodeTabs } from "../components/NodeTabs";
import { useParams } from "react-router-dom";
import { statusIsRunningOrQueued } from "@/routes/tasks/types";
import { useWorkflowRunQuery } from "@/routes/workflows/hooks/useWorkflowRunQuery";
@@ -278,7 +278,7 @@ function ExtractionNode({ id, data, type }: NodeProps) {
-
+
diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/Taskv2Node/Taskv2Node.tsx b/skyvern-frontend/src/routes/workflows/editor/nodes/Taskv2Node/Taskv2Node.tsx
index 963d6654..12d0316c 100644
--- a/skyvern-frontend/src/routes/workflows/editor/nodes/Taskv2Node/Taskv2Node.tsx
+++ b/skyvern-frontend/src/routes/workflows/editor/nodes/Taskv2Node/Taskv2Node.tsx
@@ -17,7 +17,7 @@ import { MAX_STEPS_DEFAULT, type Taskv2Node } from "./types";
import { ModelSelector } from "@/components/ModelSelector";
import { cn } from "@/util/utils";
import { NodeHeader } from "../components/NodeHeader";
-import { NodeFooter } from "../components/NodeFooter";
+import { NodeTabs } from "../components/NodeTabs";
import { useParams } from "react-router-dom";
import { statusIsRunningOrQueued } from "@/routes/tasks/types";
import { useWorkflowRunQuery } from "@/routes/workflows/hooks/useWorkflowRunQuery";
@@ -196,7 +196,7 @@ function Taskv2Node({ id, data, type }: NodeProps) {
-
+
);
diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/components/NodeFooter.tsx b/skyvern-frontend/src/routes/workflows/editor/nodes/components/NodeTabs.tsx
similarity index 53%
rename from skyvern-frontend/src/routes/workflows/editor/nodes/components/NodeFooter.tsx
rename to skyvern-frontend/src/routes/workflows/editor/nodes/components/NodeTabs.tsx
index 7d0f4f63..cfd96185 100644
--- a/skyvern-frontend/src/routes/workflows/editor/nodes/components/NodeFooter.tsx
+++ b/skyvern-frontend/src/routes/workflows/editor/nodes/components/NodeTabs.tsx
@@ -19,7 +19,7 @@ interface Props {
blockLabel: string;
}
-function NodeFooter({ blockLabel }: Props) {
+function NodeTabs({ blockLabel }: Props) {
const { blockLabel: urlBlockLabel } = useParams();
const blockOutput = useBlockOutputStore((state) => state.outputs[blockLabel]);
const [isExpanded, setIsExpanded] = useState(false);
@@ -61,46 +61,61 @@ function NodeFooter({ blockLabel }: Props) {
-
-
-
-
-
-
+
+
+
+
+ {
- setIsExpanded(!isExpanded);
- }}
>
- {isExpanded ? (
-
- ) : (
-
- )}
-
-
-
-
- {isExpanded ? "Close Outputs" : "Open Outputs"}
-
-
-
+
{
+ setIsExpanded(!isExpanded);
+ }}
+ >
+ {isExpanded ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ {!blockOutput
+ ? "No outputs. Run block first."
+ : isExpanded
+ ? "Close Outputs"
+ : "Open Outputs"}
+
+
+
+
>
);
}
-export { NodeFooter };
+export { NodeTabs };
diff --git a/skyvern-frontend/src/routes/workflows/editor/utils.ts b/skyvern-frontend/src/routes/workflows/editor/utils.ts
index 3ed1dbfb..25c0e881 100644
--- a/skyvern-frontend/src/routes/workflows/editor/utils.ts
+++ b/skyvern-frontend/src/routes/workflows/editor/utils.ts
@@ -115,8 +115,12 @@ const getInitialParameters = (workflow: WorkflowApiResponse) => {
*/
const constructCacheKeyValue = (
codeKey: string,
- workflow: WorkflowApiResponse,
+ workflow?: WorkflowApiResponse,
) => {
+ if (!workflow) {
+ return "";
+ }
+
const workflowParameters = getInitialParameters(workflow)
.filter((p) => p.parameterType === "workflow")
.reduce(
diff --git a/skyvern-frontend/src/routes/workflows/workflowRun/WorkflowRunCode.tsx b/skyvern-frontend/src/routes/workflows/workflowRun/WorkflowRunCode.tsx
new file mode 100644
index 00000000..87327aad
--- /dev/null
+++ b/skyvern-frontend/src/routes/workflows/workflowRun/WorkflowRunCode.tsx
@@ -0,0 +1,179 @@
+import { useEffect, useState } from "react";
+import { useParams } from "react-router-dom";
+import { useQueryClient } from "@tanstack/react-query";
+
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
+import { Label } from "@/components/ui/label";
+import { HelpTooltip } from "@/components/HelpTooltip";
+import { CodeEditor } from "@/routes/workflows/components/CodeEditor";
+import { useBlockScriptsQuery } from "@/routes/workflows/hooks/useBlockScriptsQuery";
+import { useCacheKeyValuesQuery } from "@/routes/workflows/hooks/useCacheKeyValuesQuery";
+import { useWorkflowQuery } from "@/routes/workflows/hooks/useWorkflowQuery";
+import { constructCacheKeyValue } from "@/routes/workflows/editor/utils";
+import { WorkflowApiResponse } from "@/routes/workflows/types/workflowTypes";
+
+interface Props {
+ showCacheKeyValueSelector?: boolean;
+}
+
+const getOrderedBlockLabels = (workflow?: WorkflowApiResponse) => {
+ if (!workflow) {
+ return [];
+ }
+
+ const blockLabels = workflow.workflow_definition.blocks.map(
+ (block) => block.label,
+ );
+
+ return blockLabels;
+};
+
+const getCommentForBlockWithoutCode = (blockLabel: string) => {
+ return `
+ # If the "Generate Code" option is turned on for this workflow when it runs, AI will execute block '${blockLabel}', and generate code for it.
+`;
+};
+
+const getCode = (
+ orderedBlockLabels: string[],
+ blockScripts?: {
+ [blockName: string]: string;
+ },
+): string[] => {
+ const blockCode: string[] = [];
+ const startBlockCode = blockScripts?.__start_block__;
+
+ if (startBlockCode) {
+ blockCode.push(startBlockCode);
+ }
+
+ for (const blockLabel of orderedBlockLabels) {
+ const code = blockScripts?.[blockLabel];
+
+ if (!code) {
+ blockCode.push(getCommentForBlockWithoutCode(blockLabel));
+ continue;
+ }
+
+ blockCode.push(`${code}
+`);
+ }
+
+ return blockCode;
+};
+
+function WorkflowRunCode(props?: Props) {
+ const showCacheKeyValueSelector = props?.showCacheKeyValueSelector ?? false;
+ const queryClient = useQueryClient();
+ const { workflowPermanentId } = useParams();
+ const { data: workflow } = useWorkflowQuery({
+ workflowPermanentId,
+ });
+ const cacheKey = workflow?.cache_key ?? "";
+ const [cacheKeyValue, setCacheKeyValue] = useState(
+ cacheKey === "" ? "" : constructCacheKeyValue(cacheKey, workflow),
+ );
+ const { data: cacheKeyValues } = useCacheKeyValuesQuery({
+ cacheKey,
+ debounceMs: 100,
+ page: 1,
+ workflowPermanentId,
+ });
+
+ useEffect(() => {
+ setCacheKeyValue(
+ cacheKeyValues?.values[0] ?? constructCacheKeyValue(cacheKey, workflow),
+ );
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [cacheKeyValues, setCacheKeyValue, workflow]);
+
+ useEffect(() => {
+ queryClient.invalidateQueries({
+ queryKey: [
+ "cache-key-values",
+ workflowPermanentId,
+ cacheKey,
+ 1,
+ undefined,
+ ],
+ });
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [workflow]);
+
+ const { data: blockScripts } = useBlockScriptsQuery({
+ cacheKey,
+ cacheKeyValue,
+ workflowPermanentId,
+ });
+
+ const orderedBlockLabels = getOrderedBlockLabels(workflow);
+ const code = getCode(orderedBlockLabels, blockScripts).join("");
+
+ if (code.length === 0) {
+ return (
+
+ No code has been generated yet.
+
+ );
+ }
+
+ if (
+ !showCacheKeyValueSelector ||
+ (cacheKeyValues?.values ?? []).length <= 1
+ ) {
+ return (
+
+ );
+ }
+
+ return (
+
+
+
+ Code Cache Key
+
+
+
setCacheKeyValue(v)}
+ >
+
+
+
+
+ {(cacheKeyValues?.values ?? []).map((value) => {
+ return (
+
+ {value}
+
+ );
+ })}
+
+
+
+
+
+ );
+}
+
+export { WorkflowRunCode };