From 709f4cf946a399317a68f39d976d5a23817b2e7c Mon Sep 17 00:00:00 2001
From: Nate McGrady
Date: Thu, 27 Nov 2025 01:14:45 -0600
Subject: [PATCH 1/2] add schema inference feature to trigger config
---
components/workflow/config/trigger-config.tsx | 32 +++-
components/workflow/utils/json-parser.ts | 144 ++++++++++++++++++
2 files changed, 173 insertions(+), 3 deletions(-)
create mode 100644 components/workflow/utils/json-parser.ts
diff --git a/components/workflow/config/trigger-config.tsx b/components/workflow/config/trigger-config.tsx
index ca8e6740..64c5a5b6 100644
--- a/components/workflow/config/trigger-config.tsx
+++ b/components/workflow/config/trigger-config.tsx
@@ -14,6 +14,7 @@ import {
SelectValue,
} from "@/components/ui/select";
import { TimezoneSelect } from "@/components/ui/timezone-select";
+import { inferSchemaFromJSON } from "../utils/json-parser";
import { SchemaBuilder, type SchemaField } from "./schema-builder";
type TriggerConfigProps = {
@@ -40,6 +41,12 @@ export function TriggerConfig({
}
};
+ const handleInferSchema = (mockRequest: string) => {
+ const inferredSchema = inferSchemaFromJSON(mockRequest);
+ onUpdateConfig("webhookSchema", JSON.stringify(inferredSchema));
+ toast.success("Schema inferred from mock payload");
+ };
+
return (
<>
@@ -137,9 +144,28 @@ export function TriggerConfig({
value={(config?.webhookMockRequest as string) || ""}
/>
-
- Enter a sample JSON payload to test the webhook trigger.
-
+
+
+ Enter a sample JSON payload to test the webhook trigger.
+
+
+
>
)}
diff --git a/components/workflow/utils/json-parser.ts b/components/workflow/utils/json-parser.ts
new file mode 100644
index 00000000..a6a9e4de
--- /dev/null
+++ b/components/workflow/utils/json-parser.ts
@@ -0,0 +1,144 @@
+import { nanoid } from "nanoid";
+import type { SchemaField } from "@/components/workflow/config/schema-builder";
+
+type ValidFieldType = SchemaField["type"];
+type ValidItemType = NonNullable;
+
+type ArrayStructure = {
+ type: "array";
+ itemType: ValidFieldType | FieldsStructure;
+};
+
+type FieldsStructure = {
+ [key: string]: ValidFieldType | FieldsStructure | ArrayStructure;
+};
+
+const detectType = (value: unknown): ValidFieldType => {
+ if (value === null) {
+ return "string";
+ }
+ if (Array.isArray(value)) {
+ return "array";
+ }
+ const t = typeof value;
+ if (t === "object") {
+ return "object";
+ }
+ if (t === "string") {
+ return "string";
+ }
+ if (t === "number") {
+ return "number";
+ }
+ if (t === "boolean") {
+ return "boolean";
+ }
+ return "string";
+};
+
+const processArray = (arr: unknown[]): ArrayStructure => {
+ if (arr.length === 0) {
+ return { type: "array", itemType: "string" };
+ }
+
+ const firstElement = arr[0];
+ if (
+ typeof firstElement === "object" &&
+ firstElement !== null &&
+ !Array.isArray(firstElement)
+ ) {
+ return { type: "array", itemType: extractFields(firstElement) };
+ }
+
+ // For primitive arrays, detect the type of the first element
+ return { type: "array", itemType: detectType(firstElement) };
+};
+
+const extractFields = (obj: unknown): FieldsStructure => {
+ if (obj === null || typeof obj !== "object" || Array.isArray(obj)) {
+ return {};
+ }
+
+ const result: FieldsStructure = {};
+
+ for (const key in obj as Record) {
+ if (Object.hasOwn(obj, key)) {
+ const value = (obj as Record)[key];
+ const valueType = detectType(value);
+
+ if (valueType === "object") {
+ result[key] = extractFields(value);
+ } else if (valueType === "array") {
+ result[key] = processArray(value as unknown[]);
+ } else {
+ result[key] = valueType;
+ }
+ }
+ }
+ return result;
+};
+
+const createPrimitiveField = (
+ key: string,
+ type: ValidFieldType
+): SchemaField => ({
+ id: nanoid(),
+ name: key,
+ type,
+});
+
+const createArrayField = (
+ key: string,
+ arrayStructure: ArrayStructure
+): SchemaField => {
+ const field: SchemaField = {
+ id: nanoid(),
+ name: key,
+ type: "array",
+ };
+
+ if (typeof arrayStructure.itemType === "string") {
+ field.itemType = arrayStructure.itemType as ValidItemType;
+ } else if (typeof arrayStructure.itemType === "object") {
+ field.itemType = "object";
+ field.fields = convertToSchemaFields(arrayStructure.itemType);
+ }
+
+ return field;
+};
+
+const createObjectField = (
+ key: string,
+ fieldsStructure: FieldsStructure
+): SchemaField => ({
+ id: nanoid(),
+ name: key,
+ type: "object",
+ fields: convertToSchemaFields(fieldsStructure),
+});
+
+const convertToSchemaFields = (
+ fieldsStructure: FieldsStructure
+): SchemaField[] => {
+ const result: SchemaField[] = [];
+
+ for (const [key, value] of Object.entries(fieldsStructure)) {
+ if (typeof value === "string") {
+ result.push(createPrimitiveField(key, value));
+ } else if (typeof value === "object" && value !== null) {
+ if ("type" in value && value.type === "array") {
+ result.push(createArrayField(key, value as ArrayStructure));
+ } else {
+ result.push(createObjectField(key, value as FieldsStructure));
+ }
+ }
+ }
+
+ return result;
+};
+
+export const inferSchemaFromJSON = (jsonString: string): SchemaField[] => {
+ const parsed = JSON.parse(jsonString);
+ const fieldsStructure = extractFields(parsed);
+ return convertToSchemaFields(fieldsStructure);
+};
From 7f79fd2946f456f7b9370c006933f223614cb96e Mon Sep 17 00:00:00 2001
From: Nate McGrady
Date: Mon, 1 Dec 2025 23:26:24 -0600
Subject: [PATCH 2/2] feat(workflow): add schema inference
---
components/workflow/config/trigger-config.tsx | 56 +++++++++++--------
components/workflow/utils/json-parser.ts | 19 ++++++-
2 files changed, 48 insertions(+), 27 deletions(-)
diff --git a/components/workflow/config/trigger-config.tsx b/components/workflow/config/trigger-config.tsx
index 64c5a5b6..c1df4b7f 100644
--- a/components/workflow/config/trigger-config.tsx
+++ b/components/workflow/config/trigger-config.tsx
@@ -1,9 +1,15 @@
"use client";
-import { Clock, Copy, Play, Webhook } from "lucide-react";
+import { Clock, Copy, MoreVertical, Play, Webhook } from "lucide-react";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import { CodeEditor } from "@/components/ui/code-editor";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
@@ -125,7 +131,28 @@ export function TriggerConfig({
-
+
+
+
+
+
+
+
+ {
+ handleInferSchema(config.webhookMockRequest as string);
+ }}
+ >
+ Infer Schema
+
+
+
+
-
-
- Enter a sample JSON payload to test the webhook trigger.
-
-
-
+
+ Enter a sample JSON payload to test the webhook trigger.
+
>
)}
diff --git a/components/workflow/utils/json-parser.ts b/components/workflow/utils/json-parser.ts
index a6a9e4de..1b3a1f27 100644
--- a/components/workflow/utils/json-parser.ts
+++ b/components/workflow/utils/json-parser.ts
@@ -42,6 +42,11 @@ const processArray = (arr: unknown[]): ArrayStructure => {
}
const firstElement = arr[0];
+
+ if (Array.isArray(firstElement)) {
+ return { type: "array", itemType: "object" };
+ }
+
if (
typeof firstElement === "object" &&
firstElement !== null &&
@@ -50,8 +55,11 @@ const processArray = (arr: unknown[]): ArrayStructure => {
return { type: "array", itemType: extractFields(firstElement) };
}
- // For primitive arrays, detect the type of the first element
- return { type: "array", itemType: detectType(firstElement) };
+ const detectedType = detectType(firstElement);
+ if (detectedType === "array") {
+ return { type: "array", itemType: "object" };
+ }
+ return { type: "array", itemType: detectedType };
};
const extractFields = (obj: unknown): FieldsStructure => {
@@ -98,7 +106,12 @@ const createArrayField = (
};
if (typeof arrayStructure.itemType === "string") {
- field.itemType = arrayStructure.itemType as ValidItemType;
+ if (arrayStructure.itemType === "array") {
+ field.itemType = "object";
+ field.fields = [];
+ } else {
+ field.itemType = arrayStructure.itemType as ValidItemType;
+ }
} else if (typeof arrayStructure.itemType === "object") {
field.itemType = "object";
field.fields = convertToSchemaFields(arrayStructure.itemType);