(
+ null,
+ );
+
+ const credentialGetter = useCredentialGetter();
+
+ const previewQuery = useQuery<
+ WebhookPreview,
+ AxiosError<{ detail?: string }>
+ >({
+ queryKey: ["webhookReplayPreview", runId],
+ enabled: open && runId.length > 0,
+ queryFn: async () => {
+ const client = await getClient(credentialGetter);
+ const response = await client.get(`/internal/runs/${runId}/test-webhook`);
+ return response.data as WebhookPreview;
+ },
+ });
+
+ useEffect(() => {
+ if (previewQuery.data) {
+ setTargetUrl(previewQuery.data.default_webhook_url ?? "");
+ setFormError(null);
+ setReplayResult(null);
+ }
+ }, [previewQuery.data]);
+
+ const replayMutation = useMutation<
+ WebhookReplayResult,
+ AxiosError<{ detail?: string }>,
+ string | null
+ >({
+ mutationFn: async (overrideUrl: string | null) => {
+ const client = await getClient(credentialGetter);
+ const response = await client.post(
+ `/internal/runs/${runId}/test-webhook`,
+ {
+ override_webhook_url: overrideUrl,
+ },
+ );
+ return response.data as WebhookReplayResult;
+ },
+ onSuccess: (data) => {
+ setReplayResult(data);
+ setFormError(null);
+ const isSuccessful =
+ data.error === null &&
+ data.status_code !== null &&
+ data.status_code >= 200 &&
+ data.status_code < 300;
+
+ toast({
+ variant: isSuccessful ? "success" : "destructive",
+ title: isSuccessful
+ ? "Webhook replay sent"
+ : "Replay completed with issues",
+ description:
+ data.status_code !== null
+ ? `Received status ${data.status_code}${
+ data.latency_ms !== null ? ` in ${data.latency_ms} ms` : ""
+ }.`
+ : "Replay request dispatched.",
+ });
+ },
+ onError: (error) => {
+ const detail = error.response?.data?.detail ?? error.message;
+ setFormError(detail);
+ toast({
+ variant: "destructive",
+ title: "Replay failed",
+ description: detail,
+ });
+ },
+ });
+
+ const previewErrorMessage =
+ previewQuery.error?.response?.data?.detail ??
+ previewQuery.error?.message ??
+ null;
+
+ const payloadText = useMemo(() => {
+ const payload =
+ replayResult?.payload ?? previewQuery.data?.payload ?? undefined;
+ return formatJson(payload);
+ }, [replayResult?.payload, previewQuery.data?.payload]);
+ const defaultUrl = previewQuery.data?.default_webhook_url?.trim() ?? "";
+
+ const handleSend = () => {
+ if (!previewQuery.data || replayMutation.isPending) {
+ return;
+ }
+ const trimmed = targetUrl.trim();
+
+ if (!trimmed && !defaultUrl) {
+ setFormError("Provide a webhook URL before sending.");
+ return;
+ }
+
+ const override =
+ trimmed.length > 0 && trimmed !== defaultUrl ? trimmed : null;
+
+ setFormError(null);
+ replayMutation.mutate(override);
+ };
+
+ const handleOpenChange = (nextOpen: boolean) => {
+ if (isControlled) {
+ onOpenChange?.(nextOpen);
+ } else {
+ setUncontrolledOpen(nextOpen);
+ }
+ if (!nextOpen) {
+ setReplayResult(null);
+ setFormError(null);
+ setTargetUrl("");
+ replayMutation.reset();
+ }
+ };
+
+ return (
+
+ );
+}
diff --git a/skyvern-frontend/src/routes/tasks/detail/TaskDetails.tsx b/skyvern-frontend/src/routes/tasks/detail/TaskDetails.tsx
index 31d99eef..f8667175 100644
--- a/skyvern-frontend/src/routes/tasks/detail/TaskDetails.tsx
+++ b/skyvern-frontend/src/routes/tasks/detail/TaskDetails.tsx
@@ -1,4 +1,5 @@
import { getClient } from "@/api/AxiosClient";
+import { useState } from "react";
import {
Status,
TaskApiResponse,
@@ -25,7 +26,8 @@ import { useCredentialGetter } from "@/hooks/useCredentialGetter";
import { CodeEditor } from "@/routes/workflows/components/CodeEditor";
import { WorkflowApiResponse } from "@/routes/workflows/types/workflowTypes";
import { apiBaseUrl } from "@/util/env";
-import { CopyApiCommandDropdown } from "@/components/CopyApiCommandDropdown";
+import { ApiWebhookActionsMenu } from "@/components/ApiWebhookActionsMenu";
+import { WebhookReplayDialog } from "@/components/WebhookReplayDialog";
import { type ApiCommandOptions } from "@/util/apiCommands";
import { PlayIcon, ReloadIcon } from "@radix-ui/react-icons";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
@@ -124,6 +126,8 @@ function TaskDetails() {
},
});
+ const [replayOpen, setReplayOpen] = useState(false);
+
if (taskIsError) {
return Error: {taskError?.message}
;
}
@@ -189,7 +193,8 @@ function TaskDetails() {
)}
- {
if (!task) {
return {
@@ -211,6 +216,15 @@ function TaskDetails() {
},
} satisfies ApiCommandOptions;
}}
+ webhookDisabled={taskIsLoading || !taskHasTerminalState}
+ onTestWebhook={() => setReplayOpen(true)}
+ />
+
{taskIsRunningOrQueued && (
-
({
method: "POST",
@@ -319,6 +321,15 @@ function WorkflowRun() {
},
}) satisfies ApiCommandOptions
}
+ webhookDisabled={workflowRunIsLoading || !workflowRunIsFinalized}
+ onTestWebhook={() => setReplayOpen(true)}
+ />
+