diff --git a/skyvern-frontend/src/api/types.ts b/skyvern-frontend/src/api/types.ts index ed4a72df..078dd851 100644 --- a/skyvern-frontend/src/api/types.ts +++ b/skyvern-frontend/src/api/types.ts @@ -135,6 +135,7 @@ export type CreateTaskRequest = { data_extraction_goal?: string | null; navigation_payload?: Record | string | null; extracted_information_schema?: Record | string | null; + extra_http_headers?: Record | null; error_code_mapping?: Record | null; proxy_location?: ProxyLocation | null; totp_verification_url?: string | null; @@ -282,6 +283,7 @@ export type WorkflowRunStatusApiResponse = { status: Status; proxy_location: ProxyLocation | null; webhook_callback_url: string | null; + extra_http_headers: Record | null; created_at: string; modified_at: string; parameters: Record; @@ -338,6 +340,7 @@ export type TaskV2 = { totp_verification_url: string | null; totp_identifier: string | null; proxy_location: ProxyLocation | null; + extra_http_headers: Record | null; }; export type Createv2TaskRequest = { diff --git a/skyvern-frontend/src/components/KeyValueInput.tsx b/skyvern-frontend/src/components/KeyValueInput.tsx new file mode 100644 index 00000000..b4dd59ac --- /dev/null +++ b/skyvern-frontend/src/components/KeyValueInput.tsx @@ -0,0 +1,236 @@ +import { PlusIcon, Cross2Icon } from "@radix-ui/react-icons"; +import { useEffect, useState } from "react"; +import { nanoid } from "nanoid"; +import { Input } from "./ui/input"; +import { Button } from "./ui/button"; +import { toast } from "./ui/use-toast"; + +export type KeyValueInputProps = { + value: Record | string | null; + onChange: (value: Record | string | null) => void; + addButtonText?: string; + readOnly?: boolean; +}; + +type Pair = { + id: string; + key: string; + value: string; +}; + +type KV = { + key: string; + value: string; +}; + +function parsePairs(value: Record | string | null): KV[] { + if (!value) { + return []; + } + try { + const obj = typeof value === "string" ? JSON.parse(value) : value; + if (obj && typeof obj === "object" && !Array.isArray(obj)) { + return Object.entries(obj).map(([k, v]) => ({ + key: k, + value: String(v), + })); + } + } catch { + // ignore + } + return []; +} + +function KeyValueInput({ + value, + onChange, + addButtonText = "Add", + readOnly = false, +}: KeyValueInputProps) { + const [focusLast, setFocusLast] = useState(false); + const [pairs, setPairs] = useState(() => + parsePairs(value).map((p) => ({ id: nanoid(), ...p })), + ); + + useEffect(() => { + const obj: Record = {}; + let hasDuplicateKey = false; + + for (const { key, value } of pairs) { + if (!key) { + continue; + } + if (key in obj) { + hasDuplicateKey = true; + continue; + } + obj[key] = value; + } + + if (!hasDuplicateKey) { + const output = + typeof value === "string" + ? Object.keys(obj).length + ? JSON.stringify(obj) + : "" + : Object.keys(obj).length + ? obj + : null; + onChange(output); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [pairs]); + + // reset focusLast on next render cycle + useEffect(() => { + if (focusLast) { + setFocusLast(false); + } + }, [focusLast]); + + const handleRemove = (id: string) => { + setPairs((prev) => prev.filter((p) => p.id !== id)); + }; + + const handleAdd = () => { + const newId = nanoid(); + setPairs((prev) => [...prev, { id: newId, key: "", value: "" }]); + setFocusLast(true); + }; + + /** + * Fires when the user shifts focus outside the component. Handles: + * - duplicate keys + * - removing empty entries + * + * In the case of duplicates: + * - last in wins + * - former k/v is removed + * - toast is shown, indicating the old value vs the new value for that key + */ + const handleBlurCapture = (e: React.FocusEvent) => { + if ( + e.relatedTarget === null || + (e.currentTarget && + e.relatedTarget && + !e.currentTarget.contains(e.relatedTarget as Node)) + ) { + const obj: Record = {}; + const reversedPairs = [...pairs].reverse(); + for (const { key, value } of reversedPairs) { + if (!key && !value) { + continue; + } + if (key) { + if (key in obj) { + const oldValue = value; + const newValue = obj[key]; + toast({ + variant: "warning", + title: `Duplicate Header ('${key}')`, + description: `Header '${key}' already existed. It was changed from '${oldValue}' to '${newValue}'.`, + }); + continue; + } + obj[key] = value; + } + } + + const reversedObj = Object.fromEntries(Object.entries(obj).reverse()); + + const output = + typeof value === "string" + ? Object.keys(reversedObj).length + ? JSON.stringify(reversedObj) + : "" + : Object.keys(reversedObj).length + ? reversedObj + : null; + + onChange(output); + + setPairs( + Object.entries(reversedObj).map(([key, value]) => ({ + id: nanoid(), + key, + value, + })), + ); + } + }; + + return ( +
+ {pairs.map((pair, idx) => ( +
+ { + setPairs((prev) => + prev.map((p) => + p.id === pair.id ? { ...p, key: e.target.value } : p, + ), + ); + }} + /> + { + setPairs((prev) => + prev.map((p) => + p.id === pair.id ? { ...p, value: e.target.value } : p, + ), + ); + }} + /> + {!readOnly && ( + + )} +
+ ))} + {!readOnly && ( + + )} +
+ ); +} + +export { KeyValueInput }; diff --git a/skyvern-frontend/src/routes/tasks/create/CreateNewTaskForm.tsx b/skyvern-frontend/src/routes/tasks/create/CreateNewTaskForm.tsx index 565bbbdc..8ed87eb0 100644 --- a/skyvern-frontend/src/routes/tasks/create/CreateNewTaskForm.tsx +++ b/skyvern-frontend/src/routes/tasks/create/CreateNewTaskForm.tsx @@ -17,6 +17,7 @@ import { import { Input } from "@/components/ui/input"; import { Separator } from "@/components/ui/separator"; import { toast } from "@/components/ui/use-toast"; +import { KeyValueInput } from "@/components/KeyValueInput"; import { useApiCredential } from "@/hooks/useApiCredential"; import { useCredentialGetter } from "@/hooks/useCredentialGetter"; import { CodeEditor } from "@/routes/workflows/components/CodeEditor"; @@ -61,6 +62,14 @@ function createTaskRequestObject( extractedInformationSchema = formValues.extractedInformationSchema; } } + let extraHttpHeaders = null; + if (formValues.extraHttpHeaders) { + try { + extraHttpHeaders = JSON.parse(formValues.extraHttpHeaders); + } catch (e) { + extraHttpHeaders = formValues.extraHttpHeaders; + } + } let errorCodeMapping = null; if (formValues.errorCodeMapping) { try { @@ -79,6 +88,7 @@ function createTaskRequestObject( proxy_location: formValues.proxyLocation ?? ProxyLocation.Residential, navigation_payload: transform(formValues.navigationPayload), extracted_information_schema: extractedInformationSchema, + extra_http_headers: extraHttpHeaders, totp_identifier: transform(formValues.totpIdentifier), error_code_mapping: errorCodeMapping, max_screenshot_scrolling_times: formValues.maxScreenshotScrollingTimes, @@ -601,6 +611,35 @@ function CreateNewTaskForm({ initialValues }: Props) { )} /> + ( + +
+ +
+

Extra HTTP Headers

+

+ Specify some self defined HTTP requests headers in + Dict format +

+
+
+
+ + field.onChange(val)} + addButtonText="Add Header" + /> + + +
+
+
+ )} + /> @@ -133,6 +134,9 @@ function CreateNewTaskFormPage() { data.workflow_definition.blocks[0] .include_action_history_in_verification, maxScreenshotScrollingTimes: data.max_screenshot_scrolling_times, + extraHttpHeaders: data.extra_http_headers + ? JSON.stringify(data.extra_http_headers) + : null, }} /> diff --git a/skyvern-frontend/src/routes/tasks/create/PromptBox.tsx b/skyvern-frontend/src/routes/tasks/create/PromptBox.tsx index 2d6cfa3b..8e93e605 100644 --- a/skyvern-frontend/src/routes/tasks/create/PromptBox.tsx +++ b/skyvern-frontend/src/routes/tasks/create/PromptBox.tsx @@ -14,6 +14,7 @@ import { MessageIcon } from "@/components/icons/MessageIcon"; import { TrophyIcon } from "@/components/icons/TrophyIcon"; import { ProxySelector } from "@/components/ProxySelector"; import { Input } from "@/components/ui/input"; +import { KeyValueInput } from "@/components/KeyValueInput"; import { CustomSelectItem, Select, @@ -161,6 +162,7 @@ function PromptBox() { useState(null); const [showAdvancedSettings, setShowAdvancedSettings] = useState(false); const [dataSchema, setDataSchema] = useState(null); + const [extraHttpHeaders, setExtraHttpHeaders] = useState(null); const startObserverCruiseMutation = useMutation({ mutationFn: async (prompt: string) => { @@ -184,6 +186,15 @@ function PromptBox() { } })() : null, + extra_http_headers: extraHttpHeaders + ? (() => { + try { + return JSON.parse(extraHttpHeaders); + } catch (e) { + return extraHttpHeaders; + } + })() + : null, }, { headers: { @@ -414,6 +425,30 @@ function PromptBox() { }} /> +
+
+
Extra HTTP Headers
+
+ Specify some self defined HTTP requests headers in Dict + format +
+
+
+ + setExtraHttpHeaders( + val === null + ? null + : typeof val === "string" + ? val || null + : JSON.stringify(val), + ) + } + addButtonText="Add Header" + /> +
+
Publish Workflow
diff --git a/skyvern-frontend/src/routes/tasks/create/retry/RetryTask.tsx b/skyvern-frontend/src/routes/tasks/create/retry/RetryTask.tsx index 74bcdd30..4aef9d76 100644 --- a/skyvern-frontend/src/routes/tasks/create/retry/RetryTask.tsx +++ b/skyvern-frontend/src/routes/tasks/create/retry/RetryTask.tsx @@ -46,6 +46,9 @@ function RetryTask() { task.request.include_action_history_in_verification ?? false, maxScreenshotScrollingTimes: task.request.max_screenshot_scrolling_times ?? null, + extraHttpHeaders: task.request.extra_http_headers + ? JSON.stringify(task.request.extra_http_headers) + : null, }} />
diff --git a/skyvern-frontend/src/routes/tasks/create/taskFormTypes.ts b/skyvern-frontend/src/routes/tasks/create/taskFormTypes.ts index e58afc69..e65631ed 100644 --- a/skyvern-frontend/src/routes/tasks/create/taskFormTypes.ts +++ b/skyvern-frontend/src/routes/tasks/create/taskFormTypes.ts @@ -10,6 +10,7 @@ const createNewTaskFormSchemaBase = z.object({ dataExtractionGoal: z.string().or(z.null()), navigationPayload: z.string().or(z.null()), extractedInformationSchema: z.string().or(z.null()), + extraHttpHeaders: z.string().or(z.null()), maxStepsOverride: z.number().or(z.null()).optional(), totpIdentifier: z.string().or(z.null()), errorCodeMapping: z.string().or(z.null()), diff --git a/skyvern-frontend/src/routes/tasks/detail/TaskParameters.tsx b/skyvern-frontend/src/routes/tasks/detail/TaskParameters.tsx index 49300b06..902a5bcd 100644 --- a/skyvern-frontend/src/routes/tasks/detail/TaskParameters.tsx +++ b/skyvern-frontend/src/routes/tasks/detail/TaskParameters.tsx @@ -1,11 +1,13 @@ import { getClient } from "@/api/AxiosClient"; import { TaskApiResponse } from "@/api/types"; import { AutoResizingTextarea } from "@/components/AutoResizingTextarea/AutoResizingTextarea"; +import { KeyValueInput } from "@/components/KeyValueInput"; import { Input } from "@/components/ui/input"; import { Skeleton } from "@/components/ui/skeleton"; import { Switch } from "@/components/ui/switch"; import { useCredentialGetter } from "@/hooks/useCredentialGetter"; import { CodeEditor } from "@/routes/workflows/components/CodeEditor"; +import { MAX_SCREENSHOT_SCROLLING_TIMES_DEFAULT } from "@/routes/workflows/editor/nodes/Taskv2Node/types"; import { useQuery } from "@tanstack/react-query"; import { useParams } from "react-router-dom"; @@ -115,6 +117,25 @@ function TaskParameters() { maxHeight="500px" />
+
+
+

Extra HTTP Headers

+

+ Specify some self defined HTTP requests headers in Dict format +

+
+
+ {}} + /> +
+

Webhook Callback URL

@@ -124,6 +145,19 @@ function TaskParameters() {
+
+
+

Max Scrolling Screenshots

+

+ The maximum number of times to scroll the page +

+
+ +

Include Action History

diff --git a/skyvern-frontend/src/routes/workflows/RunWorkflowForm.tsx b/skyvern-frontend/src/routes/workflows/RunWorkflowForm.tsx index 99648871..854774d9 100644 --- a/skyvern-frontend/src/routes/workflows/RunWorkflowForm.tsx +++ b/skyvern-frontend/src/routes/workflows/RunWorkflowForm.tsx @@ -11,6 +11,7 @@ import { FormMessage, } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; +import { KeyValueInput } from "@/components/KeyValueInput"; import { toast } from "@/components/ui/use-toast"; import { useApiCredential } from "@/hooks/useApiCredential"; import { useCredentialGetter } from "@/hooks/useCredentialGetter"; @@ -36,6 +37,7 @@ type Props = { proxyLocation: ProxyLocation; webhookCallbackUrl: string; maxScreenshotScrollingTimes: number | null; + extraHttpHeaders: Record | null; }; }; @@ -76,6 +78,7 @@ type RunWorkflowRequestBody = { webhook_callback_url?: string | null; browser_session_id: string | null; max_screenshot_scrolling_times?: number | null; + extra_http_headers?: Record | null; }; function getRunWorkflowRequestBody( @@ -87,6 +90,7 @@ function getRunWorkflowRequestBody( proxyLocation, browserSessionId, maxScreenshotScrollingTimes, + extraHttpHeaders, ...parameters } = values; @@ -111,6 +115,15 @@ function getRunWorkflowRequestBody( body.webhook_callback_url = webhookCallbackUrl; } + if (extraHttpHeaders) { + try { + body.extra_http_headers = JSON.parse(extraHttpHeaders); + } catch (e) { + console.error("Invalid extra Header JSON"); + body.extra_http_headers = null; + } + } + return body; } @@ -119,6 +132,7 @@ type RunWorkflowFormType = Record & { proxyLocation: ProxyLocation; browserSessionId: string | null; maxScreenshotScrollingTimes: number | null; + extraHttpHeaders: string | null; }; function RunWorkflowForm({ @@ -141,6 +155,9 @@ function RunWorkflowForm({ proxyLocation: initialSettings.proxyLocation, browserSessionId: browserSessionIdDefault, maxScreenshotScrollingTimes: initialSettings.maxScreenshotScrollingTimes, + extraHttpHeaders: initialSettings.extraHttpHeaders + ? JSON.stringify(initialSettings.extraHttpHeaders) + : null, }, }); const apiCredential = useApiCredential(); @@ -192,6 +209,7 @@ function RunWorkflowForm({ proxyLocation, browserSessionId, maxScreenshotScrollingTimes, + extraHttpHeaders, ...parameters } = values; @@ -205,6 +223,7 @@ function RunWorkflowForm({ proxyLocation, browserSessionId, maxScreenshotScrollingTimes, + extraHttpHeaders, }); } @@ -408,6 +427,40 @@ function RunWorkflowForm({ ); }} /> + { + return ( + +
+ +
+
+ Extra HTTP Headers +
+

+ Specify some self defined HTTP requests headers in + Dict format +

+
+
+
+ + field.onChange(val)} + addButtonText="Add Header" + /> + + +
+
+
+ ); + }} + /> ) + : null; + const initialValues = location.state?.data ? location.state.data : workflowParameters?.reduce( @@ -115,6 +119,8 @@ function WorkflowRunParameters() { maxScreenshotScrollingTimes ?? workflow.max_screenshot_scrolling_times ?? null, + extraHttpHeaders: + extraHttpHeaders ?? workflow.extra_http_headers ?? null, }} />
diff --git a/skyvern-frontend/src/routes/workflows/editor/FlowRenderer.tsx b/skyvern-frontend/src/routes/workflows/editor/FlowRenderer.tsx index a67653ef..d5486c51 100644 --- a/skyvern-frontend/src/routes/workflows/editor/FlowRenderer.tsx +++ b/skyvern-frontend/src/routes/workflows/editor/FlowRenderer.tsx @@ -282,6 +282,39 @@ function FlowRenderer({ return; } const client = await getClient(credentialGetter); + const extraHttpHeaders: Record = {}; + if (data.settings.extraHttpHeaders) { + try { + const parsedHeaders = JSON.parse(data.settings.extraHttpHeaders); + if ( + parsedHeaders && + typeof parsedHeaders === "object" && + !Array.isArray(parsedHeaders) + ) { + for (const [key, value] of Object.entries(parsedHeaders)) { + if (key && typeof key === "string") { + if (key in extraHttpHeaders) { + toast({ + title: "Error", + description: `Duplicate key '${key}' in extra http headers`, + variant: "destructive", + }); + continue; + } + extraHttpHeaders[key] = String(value); + } + } + } + } catch (error) { + toast({ + title: "Error", + description: "Invalid JSON format in extra http headers", + variant: "destructive", + }); + return; + } + } + const requestBody: WorkflowCreateYAMLRequest = { title: data.title, description: workflow.description, @@ -292,6 +325,7 @@ function FlowRenderer({ max_screenshot_scrolling_times: data.settings.maxScreenshotScrollingTimes, totp_verification_url: workflow.totp_verification_url, + extra_http_headers: extraHttpHeaders, workflow_definition: { parameters: data.parameters, blocks: data.blocks, diff --git a/skyvern-frontend/src/routes/workflows/editor/WorkflowEditor.tsx b/skyvern-frontend/src/routes/workflows/editor/WorkflowEditor.tsx index 322a3ef2..7cbf84de 100644 --- a/skyvern-frontend/src/routes/workflows/editor/WorkflowEditor.tsx +++ b/skyvern-frontend/src/routes/workflows/editor/WorkflowEditor.tsx @@ -61,6 +61,9 @@ function WorkflowEditor() { webhookCallbackUrl: workflow.webhook_callback_url, model: workflow.model, maxScreenshotScrollingTimes: workflow.max_screenshot_scrolling_times, + extraHttpHeaders: workflow.extra_http_headers + ? JSON.stringify(workflow.extra_http_headers) + : null, }; const elements = getElements( diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/StartNode/StartNode.tsx b/skyvern-frontend/src/routes/workflows/editor/nodes/StartNode/StartNode.tsx index 1e0a97f5..8b30732c 100644 --- a/skyvern-frontend/src/routes/workflows/editor/nodes/StartNode/StartNode.tsx +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/StartNode/StartNode.tsx @@ -21,6 +21,7 @@ import { ModelsResponse } from "@/api/types"; import { ModelSelector } from "@/components/ModelSelector"; import { WorkflowModel } from "@/routes/workflows/types/workflowTypes"; import { MAX_SCREENSHOT_SCROLLING_TIMES_DEFAULT } from "../Taskv2Node/types"; +import { KeyValueInput } from "@/components/KeyValueInput"; function StartNode({ id, data }: NodeProps) { const credentialGetter = useCredentialGetter(); @@ -55,6 +56,7 @@ function StartNode({ id, data }: NodeProps) { maxScreenshotScrollingTimes: data.withWorkflowSettings ? data.maxScreenshotScrollingTimes : null, + extraHttpHeaders: data.withWorkflowSettings ? data.extraHttpHeaders : null, }); function handleChange(key: string, value: unknown) { @@ -134,6 +136,19 @@ function StartNode({ id, data }: NodeProps) { />
+
+
+ + +
+ + handleChange("extraHttpHeaders", val) + } + addButtonText="Add Header" + /> +
diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/StartNode/types.ts b/skyvern-frontend/src/routes/workflows/editor/nodes/StartNode/types.ts index 6fa5ae31..44e65d85 100644 --- a/skyvern-frontend/src/routes/workflows/editor/nodes/StartNode/types.ts +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/StartNode/types.ts @@ -10,6 +10,7 @@ export type WorkflowStartNodeData = { persistBrowserSession: boolean; model: WorkflowModel | null; maxScreenshotScrollingTimes: number | null; + extraHttpHeaders: string | null; editable: boolean; }; diff --git a/skyvern-frontend/src/routes/workflows/editor/workflowEditorUtils.ts b/skyvern-frontend/src/routes/workflows/editor/workflowEditorUtils.ts index ee2c0e2a..801efaf2 100644 --- a/skyvern-frontend/src/routes/workflows/editor/workflowEditorUtils.ts +++ b/skyvern-frontend/src/routes/workflows/editor/workflowEditorUtils.ts @@ -664,6 +664,7 @@ function getElements( webhookCallbackUrl: settings.webhookCallbackUrl ?? "", model: settings.model, maxScreenshotScrollingTimes: settings.maxScreenshotScrollingTimes, + extraHttpHeaders: settings.extraHttpHeaders, editable, }), ); @@ -1325,6 +1326,7 @@ function getWorkflowSettings(nodes: Array): WorkflowSettings { webhookCallbackUrl: null, model: null, maxScreenshotScrollingTimes: null, + extraHttpHeaders: null, }; const startNodes = nodes.filter(isStartNode); const startNodeWithWorkflowSettings = startNodes.find( @@ -1341,6 +1343,7 @@ function getWorkflowSettings(nodes: Array): WorkflowSettings { webhookCallbackUrl: data.webhookCallbackUrl, model: data.model, maxScreenshotScrollingTimes: data.maxScreenshotScrollingTimes, + extraHttpHeaders: data.extraHttpHeaders, }; } return defaultSettings; @@ -1997,6 +2000,7 @@ function convert(workflow: WorkflowApiResponse): WorkflowCreateYAMLRequest { model: workflow.model, totp_verification_url: workflow.totp_verification_url, max_screenshot_scrolling_times: workflow.max_screenshot_scrolling_times, + extra_http_headers: workflow.extra_http_headers, workflow_definition: { parameters: convertParametersToParameterYAML(userParameters), blocks: convertBlocksToBlockYAML(workflow.workflow_definition.blocks), diff --git a/skyvern-frontend/src/routes/workflows/types/workflowTypes.ts b/skyvern-frontend/src/routes/workflows/types/workflowTypes.ts index feef5c59..2cd2e39b 100644 --- a/skyvern-frontend/src/routes/workflows/types/workflowTypes.ts +++ b/skyvern-frontend/src/routes/workflows/types/workflowTypes.ts @@ -466,6 +466,7 @@ export type WorkflowApiResponse = { workflow_definition: WorkflowDefinition; proxy_location: ProxyLocation | null; webhook_callback_url: string | null; + extra_http_headers: Record | null; persist_browser_session: boolean; model: WorkflowModel | null; totp_verification_url: string | null; @@ -482,6 +483,7 @@ export type WorkflowSettings = { persistBrowserSession: boolean; model: WorkflowModel | null; maxScreenshotScrollingTimes: number | null; + extraHttpHeaders: string | null; }; export type WorkflowModel = JsonObjectExtendable<{ model_name: string }>; diff --git a/skyvern-frontend/src/routes/workflows/types/workflowYamlTypes.ts b/skyvern-frontend/src/routes/workflows/types/workflowYamlTypes.ts index e7a40553..ddd04038 100644 --- a/skyvern-frontend/src/routes/workflows/types/workflowYamlTypes.ts +++ b/skyvern-frontend/src/routes/workflows/types/workflowYamlTypes.ts @@ -13,6 +13,7 @@ export type WorkflowCreateYAMLRequest = { workflow_definition: WorkflowDefinitionYAML; is_saved_task?: boolean; max_screenshot_scrolling_times?: number | null; + extra_http_headers?: Record | null; }; export type WorkflowDefinitionYAML = { diff --git a/skyvern-frontend/src/routes/workflows/workflowRun/WorkflowPostRunParameters.tsx b/skyvern-frontend/src/routes/workflows/workflowRun/WorkflowPostRunParameters.tsx index 6bd45414..7aa74177 100644 --- a/skyvern-frontend/src/routes/workflows/workflowRun/WorkflowPostRunParameters.tsx +++ b/skyvern-frontend/src/routes/workflows/workflowRun/WorkflowPostRunParameters.tsx @@ -11,6 +11,7 @@ import { Input } from "@/components/ui/input"; import { ProxySelector } from "@/components/ProxySelector"; import { SendEmailBlockParameters } from "./blockInfo/SendEmailBlockInfo"; import { ProxyLocation } from "@/api/types"; +import { KeyValueInput } from "@/components/KeyValueInput"; function WorkflowPostRunParameters() { const { data: workflowRunTimeline, isLoading: workflowRunTimelineIsLoading } = @@ -54,6 +55,10 @@ function WorkflowPostRunParameters() { ? workflowRun.task_v2?.proxy_location : workflowRun.proxy_location; + const extraHttpHeaders = isTaskV2 + ? workflowRun.task_v2?.extra_http_headers + : workflowRun.extra_http_headers; + return (
{activeBlock && isTaskVariantBlock(activeBlock) ? ( @@ -147,6 +152,20 @@ function WorkflowPostRunParameters() { }} />
+
+
+

Extra HTTP Headers

+
+
+ {}} + /> +
+
{workflowRun.task_v2 ? ( diff --git a/skyvern/forge/sdk/workflow/models/workflow.py b/skyvern/forge/sdk/workflow/models/workflow.py index d344d6d8..ff285b25 100644 --- a/skyvern/forge/sdk/workflow/models/workflow.py +++ b/skyvern/forge/sdk/workflow/models/workflow.py @@ -152,6 +152,7 @@ class WorkflowRunResponseBase(BaseModel): webhook_callback_url: str | None = None totp_verification_url: str | None = None totp_identifier: str | None = None + extra_http_headers: dict[str, str] | None = None queued_at: datetime | None = None started_at: datetime | None = None finished_at: datetime | None = None diff --git a/skyvern/forge/sdk/workflow/service.py b/skyvern/forge/sdk/workflow/service.py index c74326c7..b630a261 100644 --- a/skyvern/forge/sdk/workflow/service.py +++ b/skyvern/forge/sdk/workflow/service.py @@ -1184,6 +1184,7 @@ class WorkflowService: webhook_callback_url=workflow_run.webhook_callback_url, totp_verification_url=workflow_run.totp_verification_url, totp_identifier=workflow_run.totp_identifier, + extra_http_headers=workflow_run.extra_http_headers, queued_at=workflow_run.queued_at, started_at=workflow_run.started_at, finished_at=workflow_run.finished_at,