diff --git a/AGENTS.md b/AGENTS.md index bbe28478..c00d0b58 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -83,3 +83,18 @@ If any of the above commands fail or show errors: - **No dependencies field**: Do not use the `dependencies` field in plugin `index.ts` files. All API calls should use native `fetch`. - **Why**: Using `fetch` instead of SDKs reduces supply chain attack surface. SDKs have transitive dependencies that could be compromised. +## Step Output Format +All plugin steps must return a standardized output format: + +```typescript +// Success +return { success: true, data: { id: "...", name: "..." } }; + +// Error +return { success: false, error: { message: "Error description" } }; +``` + +- **outputFields** in plugin `index.ts` should reference fields without `data.` prefix (e.g., `{ field: "id" }` not `{ field: "data.id" }`) +- Template variables automatically unwrap: `{{GetUser.firstName}}` resolves to `data.firstName` +- Logs display only the inner `data` or `error` object, not the full wrapper + diff --git a/README.md b/README.md index fe4dccd0..6dd549e7 100644 --- a/README.md +++ b/README.md @@ -92,6 +92,7 @@ Visit [http://localhost:3000](http://localhost:3000) to get started. - **Stripe**: Create Customer, Get Customer, Create Invoice - **Superagent**: Guard, Redact - **v0**: Create Chat, Send Message +- **Webflow**: List Sites, Get Site, Publish Site ## Code Generation diff --git a/components/workflow/workflow-runs.tsx b/components/workflow/workflow-runs.tsx index 4c4624ad..ec93e167 100644 --- a/components/workflow/workflow-runs.tsx +++ b/components/workflow/workflow-runs.tsx @@ -27,6 +27,7 @@ import { executionLogsAtom, selectedExecutionIdAtom, } from "@/lib/workflow-store"; +import { findActionById } from "@/plugins"; import { Button } from "../ui/button"; import { Spinner } from "../ui/spinner"; @@ -68,7 +69,7 @@ function getOutputConfig(nodeType: string): OutputDisplayConfig | undefined { // Helper to extract the displayable value from output based on config function getOutputDisplayValue( output: unknown, - config: OutputDisplayConfig + config: { type: "image" | "video" | "url"; field: string } ): string | undefined { if (typeof output !== "object" || output === null) { return; @@ -265,31 +266,51 @@ function CollapsibleSection({ function OutputDisplay({ output, input, + actionType, }: { output: unknown; input?: unknown; + actionType?: string; }) { - // Get actionType from input to look up the output config - const actionType = - typeof input === "object" && input !== null - ? (input as Record).actionType - : undefined; - const config = - typeof actionType === "string" ? getOutputConfig(actionType) : undefined; - const displayValue = config - ? getOutputDisplayValue(output, config) + // Look up action from plugin registry to get outputConfig (including custom components) + const action = actionType ? findActionById(actionType) : undefined; + const pluginConfig = action?.outputConfig; + + // Fall back to auto-generated config for legacy support (only built-in types) + const builtInConfig = actionType ? getOutputConfig(actionType) : undefined; + + // Get the effective built-in config (plugin config if not component, else auto-generated) + const effectiveBuiltInConfig = + pluginConfig?.type !== "component" ? pluginConfig : builtInConfig; + + // Get display value for built-in types (image/video/url) + const displayValue = effectiveBuiltInConfig + ? getOutputDisplayValue(output, effectiveBuiltInConfig) : undefined; // Check for legacy base64 image - const isLegacyBase64 = !config && isBase64ImageOutput(output); + const isLegacyBase64 = + !(pluginConfig || builtInConfig) && isBase64ImageOutput(output); const renderRichResult = () => { - if (config && displayValue) { - switch (config.type) { + // Priority 1: Custom component from plugin outputConfig + if (pluginConfig?.type === "component") { + const CustomComponent = pluginConfig.component; + return ( +
+ +
+ ); + } + + // Priority 2: Built-in output config (image/video/url) + if (effectiveBuiltInConfig && displayValue) { + switch (effectiveBuiltInConfig.type) { case "image": { // Handle base64 images by adding data URI prefix if needed const imageSrc = - config.field === "base64" && !displayValue.startsWith("data:") + effectiveBuiltInConfig.field === "base64" && + !displayValue.startsWith("data:") ? `data:image/png;base64,${displayValue}` : displayValue; return ( @@ -355,6 +376,12 @@ function OutputDisplay({ const richResult = renderRichResult(); const hasRichResult = richResult !== null; + // Determine external link for URL type configs + const externalLink = + effectiveBuiltInConfig?.type === "url" && displayValue + ? displayValue + : undefined; + return ( <> {/* Always show JSON output */} @@ -368,7 +395,7 @@ function OutputDisplay({ {hasRichResult && ( {richResult} @@ -458,7 +485,11 @@ function ExecutionLogEntry({ )} {log.output !== null && log.output !== undefined && ( - + )} {log.error && ( (input: T): Omit { - const { _context, ...rest } = input; - return rest as Omit; +const INTERNAL_FIELDS = ["_context", "actionType", "integrationId"] as const; + +/** + * Strip internal fields from input for logging (we don't want to log internal metadata) + */ +function stripInternalFields( + input: T +): Omit { + const result = { ...input }; + for (const field of INTERNAL_FIELDS) { + delete (result as Record)[field]; + } + return result as Omit; } /** @@ -160,30 +170,45 @@ export async function withStepLogging( input: TInput, stepLogic: () => Promise ): Promise { - // Extract context and log input without _context + // Extract context and log input without internal fields const context = input._context as StepContextWithWorkflow | undefined; - const loggedInput = stripContext(input); + const loggedInput = stripInternalFields(input); const logInfo = await logStepStart(context, loggedInput); try { const result = await stepLogic(); - // Check if result indicates an error - const isErrorResult = + // Check if result has standardized format { success, data } or { success, error } + const isStandardizedResult = result && typeof result === "object" && "success" in result && + typeof (result as { success: unknown }).success === "boolean"; + + // Check if result indicates an error + const isErrorResult = + isStandardizedResult && (result as { success: boolean }).success === false; if (isErrorResult) { - const errorResult = result as { success: false; error?: string }; - await logStepComplete( - logInfo, - "error", - result, - errorResult.error || "Step execution failed" - ); + const errorResult = result as { + success: false; + error?: string | { message: string }; + }; + // Support both old format (error: string) and new format (error: { message: string }) + const errorMessage = + typeof errorResult.error === "string" + ? errorResult.error + : errorResult.error?.message || "Step execution failed"; + // Log just the error object, not the full result + const loggedOutput = errorResult.error ?? { message: errorMessage }; + await logStepComplete(logInfo, "error", loggedOutput, errorMessage); + } else if (isStandardizedResult) { + // For standardized success results, log just the data + const successResult = result as { success: true; data?: unknown }; + await logStepComplete(logInfo, "success", successResult.data ?? result); } else { + // For non-standardized results, log as-is await logStepComplete(logInfo, "success", result); } diff --git a/lib/utils/template.ts b/lib/utils/template.ts index 0a4c56f7..553b1345 100644 --- a/lib/utils/template.ts +++ b/lib/utils/template.ts @@ -160,6 +160,33 @@ export function processConfigTemplates( /** * Resolve a field path in data like "field.nested" or "items[0]" */ +/** + * Check if data has the standardized step output format: { success: boolean, data: {...} } + */ +function isStandardizedOutput( + data: unknown +): data is { success: boolean; data: unknown } { + return ( + data !== null && + typeof data === "object" && + "success" in data && + "data" in data && + typeof (data as Record).success === "boolean" + ); +} + +/** + * Unwrap standardized output to get the inner data + * For { success: true, data: {...} } returns the inner data object + */ +function unwrapStandardizedOutput(data: unknown): unknown { + if (isStandardizedOutput(data)) { + return data.data; + } + return data; +} + +// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Field path resolution requires nested logic for arrays and standardized outputs function resolveFieldPath(data: unknown, fieldPath: string): unknown { if (!data) { return; @@ -168,6 +195,18 @@ function resolveFieldPath(data: unknown, fieldPath: string): unknown { const parts = fieldPath.split("."); let current: unknown = data; + // For standardized outputs, automatically look inside data.data + // unless explicitly accessing 'success', 'data', or 'error' + const firstPart = parts[0]?.trim(); + if ( + isStandardizedOutput(current) && + firstPart !== "success" && + firstPart !== "data" && + firstPart !== "error" + ) { + current = current.data; + } + for (const part of parts) { const trimmedPart = part.trim(); @@ -449,9 +488,13 @@ export function getAvailableFields(nodeOutputs: NodeOutputs): Array<{ sample: output.data, }); + // For standardized outputs, extract fields from inside data + // so autocomplete shows firstName instead of data.firstName + const dataToExtract = unwrapStandardizedOutput(output.data); + // Add individual fields if data is an object - if (output.data && typeof output.data === "object") { - extractFields(output.data, output.label, fields, { + if (dataToExtract && typeof dataToExtract === "object") { + extractFields(dataToExtract, output.label, fields, { currentPath: `{{${output.label}`, }); } diff --git a/lib/workflow-executor.workflow.ts b/lib/workflow-executor.workflow.ts index afcf0157..d7b59176 100644 --- a/lib/workflow-executor.workflow.ts +++ b/lib/workflow-executor.workflow.ts @@ -56,6 +56,7 @@ export type WorkflowExecutionInput = { * Helper to replace template variables in conditions */ // biome-ignore lint/nursery/useMaxParams: Helper function needs all parameters for template replacement +// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Template variable replacement requires nested logic for standardized outputs function replaceTemplateVariable( match: string, nodeId: string, @@ -85,6 +86,21 @@ function replaceTemplateVariable( // biome-ignore lint/suspicious/noExplicitAny: Dynamic data traversal let current: any = output.data; + // For standardized outputs { success, data, error }, automatically look inside data + // unless explicitly accessing success/data/error + const firstField = fields[0]; + if ( + current && + typeof current === "object" && + "success" in current && + "data" in current && + firstField !== "success" && + firstField !== "data" && + firstField !== "error" + ) { + current = current.data; + } + for (const field of fields) { if (current && typeof current === "object") { current = current[field]; @@ -310,6 +326,21 @@ function processTemplates( // biome-ignore lint/suspicious/noExplicitAny: Dynamic output data traversal let current: any = output.data; + // For standardized outputs { success, data, error }, automatically look inside data + // unless explicitly accessing success/data/error + const firstField = fields[0]; + if ( + current && + typeof current === "object" && + "success" in current && + "data" in current && + firstField !== "success" && + firstField !== "data" && + firstField !== "error" + ) { + current = current.data; + } + for (const field of fields) { if (current && typeof current === "object") { current = current[field]; @@ -555,12 +586,19 @@ export async function executeWorkflow(input: WorkflowExecutionInput) { (stepResult as { success: boolean }).success === false; if (isErrorResult) { - const errorResult = stepResult as { success: false; error?: string }; + const errorResult = stepResult as { + success: false; + error?: string | { message: string }; + }; + // Support both old format (error: string) and new format (error: { message: string }) + const errorMessage = + typeof errorResult.error === "string" + ? errorResult.error + : errorResult.error?.message || + `Step "${actionType}" in node "${node.data.label || node.id}" failed without a specific error message.`; result = { success: false, - error: - errorResult.error || - `Step "${actionType}" in node "${node.data.label || node.id}" failed without a specific error message.`, + error: errorMessage, }; } else { result = { diff --git a/plugins/clerk/components/user-card.tsx b/plugins/clerk/components/user-card.tsx new file mode 100644 index 00000000..5722a9bf --- /dev/null +++ b/plugins/clerk/components/user-card.tsx @@ -0,0 +1,53 @@ +import type { ResultComponentProps } from "@/plugins/registry"; + +// The logging layer unwraps standardized outputs, so we receive just the data +type ClerkUserData = { + id: string; + firstName: string | null; + lastName: string | null; + primaryEmailAddress: string | null; + createdAt: number; +}; + +export function UserCard({ output }: ResultComponentProps) { + const data = output as ClerkUserData; + + // Validate we have the expected data shape + if (!data || typeof data !== "object" || !("id" in data)) { + return null; + } + + const initials = [data.firstName?.[0], data.lastName?.[0]] + .filter(Boolean) + .join("") + .toUpperCase(); + + const fullName = [data.firstName, data.lastName].filter(Boolean).join(" "); + const createdDate = data.createdAt + ? new Date(data.createdAt).toLocaleDateString() + : "Unknown"; + + return ( +
+
+ {initials || "?"} +
+
+
+ {fullName || "Unknown User"} +
+ {data.primaryEmailAddress && ( +
+ {data.primaryEmailAddress} +
+ )} +
+ Created {createdDate} +
+
+
+ ); +} + + + diff --git a/plugins/clerk/index.ts b/plugins/clerk/index.ts index 490bc5eb..fa943669 100644 --- a/plugins/clerk/index.ts +++ b/plugins/clerk/index.ts @@ -1,5 +1,6 @@ import type { IntegrationPlugin } from "../registry"; import { registerIntegration } from "../registry"; +import { UserCard } from "./components/user-card"; import { ClerkIcon } from "./icon"; const clerkPlugin: IntegrationPlugin = { @@ -41,10 +42,15 @@ const clerkPlugin: IntegrationPlugin = { stepFunction: "clerkGetUserStep", stepImportPath: "get-user", outputFields: [ - { field: "user.id", description: "User ID" }, - { field: "user.first_name", description: "First name" }, - { field: "user.last_name", description: "Last name" }, + { field: "id", description: "User ID" }, + { field: "firstName", description: "First name" }, + { field: "lastName", description: "Last name" }, + { field: "primaryEmailAddress", description: "Primary email address" }, ], + outputConfig: { + type: "component", + component: UserCard, + }, configFields: [ { key: "userId", @@ -64,10 +70,15 @@ const clerkPlugin: IntegrationPlugin = { stepFunction: "clerkCreateUserStep", stepImportPath: "create-user", outputFields: [ - { field: "user.id", description: "User ID" }, - { field: "user.first_name", description: "First name" }, - { field: "user.last_name", description: "Last name" }, + { field: "id", description: "User ID" }, + { field: "firstName", description: "First name" }, + { field: "lastName", description: "Last name" }, + { field: "primaryEmailAddress", description: "Primary email address" }, ], + outputConfig: { + type: "component", + component: UserCard, + }, configFields: [ { key: "emailAddress", @@ -129,10 +140,15 @@ const clerkPlugin: IntegrationPlugin = { stepFunction: "clerkUpdateUserStep", stepImportPath: "update-user", outputFields: [ - { field: "user.id", description: "User ID" }, - { field: "user.first_name", description: "First name" }, - { field: "user.last_name", description: "Last name" }, + { field: "id", description: "User ID" }, + { field: "firstName", description: "First name" }, + { field: "lastName", description: "Last name" }, + { field: "primaryEmailAddress", description: "Primary email address" }, ], + outputConfig: { + type: "component", + component: UserCard, + }, configFields: [ { key: "userId", diff --git a/plugins/clerk/steps/create-user.ts b/plugins/clerk/steps/create-user.ts index 53b0368a..735ed40a 100644 --- a/plugins/clerk/steps/create-user.ts +++ b/plugins/clerk/steps/create-user.ts @@ -4,11 +4,7 @@ import { fetchCredentials } from "@/lib/credential-fetcher"; import { type StepInput, withStepLogging } from "@/lib/steps/step-handler"; import { getErrorMessage } from "@/lib/utils"; import type { ClerkCredentials } from "../credentials"; -import type { ClerkUser } from "../types"; - -type CreateUserResult = - | { success: true; user: ClerkUser } - | { success: false; error: string }; +import { type ClerkUserResult, toClerkUserData } from "../types"; export type ClerkCreateUserCoreInput = { emailAddress: string; @@ -30,21 +26,23 @@ export type ClerkCreateUserInput = StepInput & async function stepHandler( input: ClerkCreateUserCoreInput, credentials: ClerkCredentials -): Promise { +): Promise { const secretKey = credentials.CLERK_SECRET_KEY; if (!secretKey) { return { success: false, - error: - "CLERK_SECRET_KEY is not configured. Please add it in Project Integrations.", + error: { + message: + "CLERK_SECRET_KEY is not configured. Please add it in Project Integrations.", + }, }; } if (!input.emailAddress) { return { success: false, - error: "Email address is required.", + error: { message: "Email address is required." }, }; } @@ -69,7 +67,7 @@ async function stepHandler( } catch { return { success: false, - error: "Invalid JSON format for publicMetadata", + error: { message: "Invalid JSON format for publicMetadata" }, }; } } @@ -79,7 +77,7 @@ async function stepHandler( } catch { return { success: false, - error: "Invalid JSON format for privateMetadata", + error: { message: "Invalid JSON format for privateMetadata" }, }; } } @@ -95,21 +93,23 @@ async function stepHandler( }); if (!response.ok) { - const error = await response.json().catch(() => ({})); + const errorBody = await response.json().catch(() => ({})); return { success: false, - error: - error.errors?.[0]?.message || - `Failed to create user: ${response.status}`, + error: { + message: + errorBody.errors?.[0]?.message || + `Failed to create user: ${response.status}`, + }, }; } - const user = await response.json(); - return { success: true, user }; - } catch (error) { + const apiUser = await response.json(); + return { success: true, data: toClerkUserData(apiUser) }; + } catch (err) { return { success: false, - error: `Failed to create user: ${getErrorMessage(error)}`, + error: { message: `Failed to create user: ${getErrorMessage(err)}` }, }; } } @@ -119,7 +119,7 @@ async function stepHandler( */ export async function clerkCreateUserStep( input: ClerkCreateUserInput -): Promise { +): Promise { "use step"; const credentials = input.integrationId diff --git a/plugins/clerk/steps/delete-user.ts b/plugins/clerk/steps/delete-user.ts index b3ff451b..1f5bc0c3 100644 --- a/plugins/clerk/steps/delete-user.ts +++ b/plugins/clerk/steps/delete-user.ts @@ -6,8 +6,8 @@ import { getErrorMessage } from "@/lib/utils"; import type { ClerkCredentials } from "../credentials"; type DeleteUserResult = - | { success: true; deleted: boolean } - | { success: false; error: string }; + | { success: true; data: { deleted: true } } + | { success: false; error: { message: string } }; export type ClerkDeleteUserCoreInput = { userId: string; @@ -30,15 +30,17 @@ async function stepHandler( if (!secretKey) { return { success: false, - error: - "CLERK_SECRET_KEY is not configured. Please add it in Project Integrations.", + error: { + message: + "CLERK_SECRET_KEY is not configured. Please add it in Project Integrations.", + }, }; } if (!input.userId) { return { success: false, - error: "User ID is required.", + error: { message: "User ID is required." }, }; } @@ -56,20 +58,22 @@ async function stepHandler( ); if (!response.ok) { - const error = await response.json().catch(() => ({})); + const errorBody = await response.json().catch(() => ({})); return { success: false, - error: - error.errors?.[0]?.message || - `Failed to delete user: ${response.status}`, + error: { + message: + errorBody.errors?.[0]?.message || + `Failed to delete user: ${response.status}`, + }, }; } - return { success: true, deleted: true }; - } catch (error) { + return { success: true, data: { deleted: true } }; + } catch (err) { return { success: false, - error: `Failed to delete user: ${getErrorMessage(error)}`, + error: { message: `Failed to delete user: ${getErrorMessage(err)}` }, }; } } diff --git a/plugins/clerk/steps/get-user.ts b/plugins/clerk/steps/get-user.ts index 83864e3a..0a68eb3d 100644 --- a/plugins/clerk/steps/get-user.ts +++ b/plugins/clerk/steps/get-user.ts @@ -4,11 +4,7 @@ import { fetchCredentials } from "@/lib/credential-fetcher"; import { type StepInput, withStepLogging } from "@/lib/steps/step-handler"; import { getErrorMessage } from "@/lib/utils"; import type { ClerkCredentials } from "../credentials"; -import type { ClerkUser } from "../types"; - -type GetUserResult = - | { success: true; user: ClerkUser } - | { success: false; error: string }; +import { type ClerkUserResult, toClerkUserData } from "../types"; export type ClerkGetUserCoreInput = { userId: string; @@ -25,21 +21,23 @@ export type ClerkGetUserInput = StepInput & async function stepHandler( input: ClerkGetUserCoreInput, credentials: ClerkCredentials -): Promise { +): Promise { const secretKey = credentials.CLERK_SECRET_KEY; if (!secretKey) { return { success: false, - error: - "CLERK_SECRET_KEY is not configured. Please add it in Project Integrations.", + error: { + message: + "CLERK_SECRET_KEY is not configured. Please add it in Project Integrations.", + }, }; } if (!input.userId) { return { success: false, - error: "User ID is required.", + error: { message: "User ID is required." }, }; } @@ -56,20 +54,23 @@ async function stepHandler( ); if (!response.ok) { - const error = await response.json().catch(() => ({})); + const errorBody = await response.json().catch(() => ({})); return { success: false, - error: - error.errors?.[0]?.message || `Failed to get user: ${response.status}`, + error: { + message: + errorBody.errors?.[0]?.message || + `Failed to get user: ${response.status}`, + }, }; } - const user = await response.json(); - return { success: true, user }; - } catch (error) { + const apiUser = await response.json(); + return { success: true, data: toClerkUserData(apiUser) }; + } catch (err) { return { success: false, - error: `Failed to get user: ${getErrorMessage(error)}`, + error: { message: `Failed to get user: ${getErrorMessage(err)}` }, }; } } @@ -79,7 +80,7 @@ async function stepHandler( */ export async function clerkGetUserStep( input: ClerkGetUserInput -): Promise { +): Promise { "use step"; const credentials = input.integrationId diff --git a/plugins/clerk/steps/update-user.ts b/plugins/clerk/steps/update-user.ts index 6083270a..682ece71 100644 --- a/plugins/clerk/steps/update-user.ts +++ b/plugins/clerk/steps/update-user.ts @@ -4,11 +4,7 @@ import { fetchCredentials } from "@/lib/credential-fetcher"; import { type StepInput, withStepLogging } from "@/lib/steps/step-handler"; import { getErrorMessage } from "@/lib/utils"; import type { ClerkCredentials } from "../credentials"; -import type { ClerkUser } from "../types"; - -type UpdateUserResult = - | { success: true; user: ClerkUser } - | { success: false; error: string }; +import { type ClerkUserResult, toClerkUserData } from "../types"; export type ClerkUpdateUserCoreInput = { userId: string; @@ -29,21 +25,23 @@ export type ClerkUpdateUserInput = StepInput & async function stepHandler( input: ClerkUpdateUserCoreInput, credentials: ClerkCredentials -): Promise { +): Promise { const secretKey = credentials.CLERK_SECRET_KEY; if (!secretKey) { return { success: false, - error: - "CLERK_SECRET_KEY is not configured. Please add it in Project Integrations.", + error: { + message: + "CLERK_SECRET_KEY is not configured. Please add it in Project Integrations.", + }, }; } if (!input.userId) { return { success: false, - error: "User ID is required.", + error: { message: "User ID is required." }, }; } @@ -63,7 +61,7 @@ async function stepHandler( } catch { return { success: false, - error: "Invalid JSON format for publicMetadata", + error: { message: "Invalid JSON format for publicMetadata" }, }; } } @@ -73,7 +71,7 @@ async function stepHandler( } catch { return { success: false, - error: "Invalid JSON format for privateMetadata", + error: { message: "Invalid JSON format for privateMetadata" }, }; } } @@ -92,21 +90,23 @@ async function stepHandler( ); if (!response.ok) { - const error = await response.json().catch(() => ({})); + const errorBody = await response.json().catch(() => ({})); return { success: false, - error: - error.errors?.[0]?.message || - `Failed to update user: ${response.status}`, + error: { + message: + errorBody.errors?.[0]?.message || + `Failed to update user: ${response.status}`, + }, }; } - const user = await response.json(); - return { success: true, user }; - } catch (error) { + const apiUser = await response.json(); + return { success: true, data: toClerkUserData(apiUser) }; + } catch (err) { return { success: false, - error: `Failed to update user: ${getErrorMessage(error)}`, + error: { message: `Failed to update user: ${getErrorMessage(err)}` }, }; } } @@ -116,7 +116,7 @@ async function stepHandler( */ export async function clerkUpdateUserStep( input: ClerkUpdateUserInput -): Promise { +): Promise { "use step"; const credentials = input.integrationId diff --git a/plugins/clerk/types.ts b/plugins/clerk/types.ts index 269bb669..016f433a 100644 --- a/plugins/clerk/types.ts +++ b/plugins/clerk/types.ts @@ -1,4 +1,4 @@ -export type ClerkUser = { +export type ClerkApiUser = { id: string; first_name: string | null; last_name: string | null; @@ -12,3 +12,36 @@ export type ClerkUser = { created_at: number; updated_at: number; }; + +/** + * Flat user data for workflow steps + */ +export type ClerkUserData = { + id: string; + firstName: string | null; + lastName: string | null; + primaryEmailAddress: string | null; + createdAt: number; + updatedAt: number; +}; + +/** + * Standard step output format + */ +export type ClerkUserResult = + | { success: true; data: ClerkUserData } + | { success: false; error: { message: string } }; + +export function toClerkUserData(apiUser: ClerkApiUser): ClerkUserData { + const primaryEmail = apiUser.email_addresses.find( + (e) => e.id === apiUser.primary_email_address_id + ); + return { + id: apiUser.id, + firstName: apiUser.first_name, + lastName: apiUser.last_name, + primaryEmailAddress: primaryEmail?.email_address ?? null, + createdAt: apiUser.created_at, + updatedAt: apiUser.updated_at, + }; +} diff --git a/plugins/fal/steps/generate-image.ts b/plugins/fal/steps/generate-image.ts index 78627ea5..e878f8d2 100644 --- a/plugins/fal/steps/generate-image.ts +++ b/plugins/fal/steps/generate-image.ts @@ -31,11 +31,9 @@ type FalImageResponse = { error?: string; }; -type GenerateImageResult = { - imageUrl: string; - width?: number; - height?: number; -}; +type GenerateImageResult = + | { success: true; data: { imageUrl: string; width?: number; height?: number } } + | { success: false; error: { message: string } }; export type FalGenerateImageCoreInput = { model: string; @@ -104,7 +102,13 @@ async function stepHandler( const apiKey = credentials.FAL_API_KEY; if (!apiKey) { - throw new Error("FAL_API_KEY is not configured. Please add it in Project Integrations."); + return { + success: false, + error: { + message: + "FAL_API_KEY is not configured. Please add it in Project Integrations.", + }, + }; } try { @@ -124,14 +128,20 @@ async function stepHandler( if (!response.ok) { const errorText = await response.text(); - throw new Error(`HTTP ${response.status}: ${errorText}`); + return { + success: false, + error: { message: `HTTP ${response.status}: ${errorText}` }, + }; } const queueResponse = (await response.json()) as FalQueueResponse; // If the response is queued, poll for the result let result: FalImageResponse; - if (queueResponse.status === "IN_QUEUE" || queueResponse.status === "IN_PROGRESS") { + if ( + queueResponse.status === "IN_QUEUE" || + queueResponse.status === "IN_PROGRESS" + ) { result = await pollForResult( queueResponse.status_url, queueResponse.response_url, @@ -143,21 +153,26 @@ async function stepHandler( } if (result.error) { - throw new Error(result.error); + return { success: false, error: { message: result.error } }; } if (!result.images || result.images.length === 0) { - throw new Error("No images returned from fal.ai"); + return { + success: false, + error: { message: "No images returned from fal.ai" }, + }; } const image = result.images[0]; return { - imageUrl: image.url, - width: image.width, - height: image.height, + success: true, + data: { imageUrl: image.url, width: image.width, height: image.height }, }; } catch (error) { - throw new Error(`Failed to generate image: ${getErrorMessage(error)}`); + return { + success: false, + error: { message: `Failed to generate image: ${getErrorMessage(error)}` }, + }; } } diff --git a/plugins/fal/steps/generate-video.ts b/plugins/fal/steps/generate-video.ts index 47dcb283..721337fc 100644 --- a/plugins/fal/steps/generate-video.ts +++ b/plugins/fal/steps/generate-video.ts @@ -28,9 +28,9 @@ type FalVideoResponse = { error?: string; }; -type GenerateVideoResult = { - videoUrl: string; -}; +type GenerateVideoResult = + | { success: true; data: { videoUrl: string } } + | { success: false; error: { message: string } }; export type FalGenerateVideoCoreInput = { model: string; @@ -96,7 +96,13 @@ async function stepHandler( const apiKey = credentials.FAL_API_KEY; if (!apiKey) { - throw new Error("FAL_API_KEY is not configured. Please add it in Project Integrations."); + return { + success: false, + error: { + message: + "FAL_API_KEY is not configured. Please add it in Project Integrations.", + }, + }; } try { @@ -121,13 +127,19 @@ async function stepHandler( if (!response.ok) { const errorText = await response.text(); - throw new Error(`HTTP ${response.status}: ${errorText}`); + return { + success: false, + error: { message: `HTTP ${response.status}: ${errorText}` }, + }; } const queueResponse = (await response.json()) as FalQueueResponse; let result: FalVideoResponse; - if (queueResponse.status === "IN_QUEUE" || queueResponse.status === "IN_PROGRESS") { + if ( + queueResponse.status === "IN_QUEUE" || + queueResponse.status === "IN_PROGRESS" + ) { result = await pollForResult( queueResponse.status_url, queueResponse.response_url, @@ -138,18 +150,22 @@ async function stepHandler( } if (result.error) { - throw new Error(result.error); + return { success: false, error: { message: result.error } }; } if (!result.video?.url) { - throw new Error("No video returned from fal.ai"); + return { + success: false, + error: { message: "No video returned from fal.ai" }, + }; } + return { success: true, data: { videoUrl: result.video.url } }; + } catch (error) { return { - videoUrl: result.video.url, + success: false, + error: { message: `Failed to generate video: ${getErrorMessage(error)}` }, }; - } catch (error) { - throw new Error(`Failed to generate video: ${getErrorMessage(error)}`); } } diff --git a/plugins/fal/steps/image-to-image.ts b/plugins/fal/steps/image-to-image.ts index 28efc460..de8eb53d 100644 --- a/plugins/fal/steps/image-to-image.ts +++ b/plugins/fal/steps/image-to-image.ts @@ -35,11 +35,9 @@ type FalImageToImageResponse = { error?: string; }; -type ImageToImageResult = { - imageUrl: string; - width?: number; - height?: number; -}; +type ImageToImageResult = + | { success: true; data: { imageUrl: string; width?: number; height?: number } } + | { success: false; error: { message: string } }; export type FalImageToImageCoreInput = { model: string; @@ -99,7 +97,13 @@ async function stepHandler( const apiKey = credentials.FAL_API_KEY; if (!apiKey) { - throw new Error("FAL_API_KEY is not configured. Please add it in Project Integrations."); + return { + success: false, + error: { + message: + "FAL_API_KEY is not configured. Please add it in Project Integrations.", + }, + }; } try { @@ -121,13 +125,19 @@ async function stepHandler( if (!response.ok) { const errorText = await response.text(); - throw new Error(`HTTP ${response.status}: ${errorText}`); + return { + success: false, + error: { message: `HTTP ${response.status}: ${errorText}` }, + }; } const queueResponse = (await response.json()) as FalQueueResponse; let result: FalImageToImageResponse; - if (queueResponse.status === "IN_QUEUE" || queueResponse.status === "IN_PROGRESS") { + if ( + queueResponse.status === "IN_QUEUE" || + queueResponse.status === "IN_PROGRESS" + ) { result = await pollForResult( queueResponse.status_url, queueResponse.response_url, @@ -138,22 +148,29 @@ async function stepHandler( } if (result.error) { - throw new Error(result.error); + return { success: false, error: { message: result.error } }; } // Handle both array format (images) and single image format const image = result.images?.[0] || result.image; if (!image?.url) { - throw new Error("No image returned from fal.ai"); + return { + success: false, + error: { message: "No image returned from fal.ai" }, + }; } return { - imageUrl: image.url, - width: image.width, - height: image.height, + success: true, + data: { imageUrl: image.url, width: image.width, height: image.height }, }; } catch (error) { - throw new Error(`Failed to transform image: ${getErrorMessage(error)}`); + return { + success: false, + error: { + message: `Failed to transform image: ${getErrorMessage(error)}`, + }, + }; } } diff --git a/plugins/fal/steps/remove-background.ts b/plugins/fal/steps/remove-background.ts index f04b07bd..b087ef4e 100644 --- a/plugins/fal/steps/remove-background.ts +++ b/plugins/fal/steps/remove-background.ts @@ -28,9 +28,9 @@ type FalRemoveBackgroundResponse = { error?: string; }; -type RemoveBackgroundResult = { - imageUrl: string; -}; +type RemoveBackgroundResult = + | { success: true; data: { imageUrl: string } } + | { success: false; error: { message: string } }; export type FalRemoveBackgroundCoreInput = { imageUrl: string; @@ -87,7 +87,13 @@ async function stepHandler( const apiKey = credentials.FAL_API_KEY; if (!apiKey) { - throw new Error("FAL_API_KEY is not configured. Please add it in Project Integrations."); + return { + success: false, + error: { + message: + "FAL_API_KEY is not configured. Please add it in Project Integrations.", + }, + }; } try { @@ -104,13 +110,19 @@ async function stepHandler( if (!response.ok) { const errorText = await response.text(); - throw new Error(`HTTP ${response.status}: ${errorText}`); + return { + success: false, + error: { message: `HTTP ${response.status}: ${errorText}` }, + }; } const queueResponse = (await response.json()) as FalQueueResponse; let result: FalRemoveBackgroundResponse; - if (queueResponse.status === "IN_QUEUE" || queueResponse.status === "IN_PROGRESS") { + if ( + queueResponse.status === "IN_QUEUE" || + queueResponse.status === "IN_PROGRESS" + ) { result = await pollForResult( queueResponse.status_url, queueResponse.response_url, @@ -121,18 +133,24 @@ async function stepHandler( } if (result.error) { - throw new Error(result.error); + return { success: false, error: { message: result.error } }; } if (!result.image?.url) { - throw new Error("No image returned from fal.ai"); + return { + success: false, + error: { message: "No image returned from fal.ai" }, + }; } + return { success: true, data: { imageUrl: result.image.url } }; + } catch (error) { return { - imageUrl: result.image.url, + success: false, + error: { + message: `Failed to remove background: ${getErrorMessage(error)}`, + }, }; - } catch (error) { - throw new Error(`Failed to remove background: ${getErrorMessage(error)}`); } } diff --git a/plugins/fal/steps/upscale-image.ts b/plugins/fal/steps/upscale-image.ts index 2f87ce18..71d1b78a 100644 --- a/plugins/fal/steps/upscale-image.ts +++ b/plugins/fal/steps/upscale-image.ts @@ -30,11 +30,9 @@ type FalUpscaleResponse = { error?: string; }; -type UpscaleImageResult = { - imageUrl: string; - width?: number; - height?: number; -}; +type UpscaleImageResult = + | { success: true; data: { imageUrl: string; width?: number; height?: number } } + | { success: false; error: { message: string } }; export type FalUpscaleImageCoreInput = { model: string; @@ -93,7 +91,13 @@ async function stepHandler( const apiKey = credentials.FAL_API_KEY; if (!apiKey) { - throw new Error("FAL_API_KEY is not configured. Please add it in Project Integrations."); + return { + success: false, + error: { + message: + "FAL_API_KEY is not configured. Please add it in Project Integrations.", + }, + }; } try { @@ -114,13 +118,19 @@ async function stepHandler( if (!response.ok) { const errorText = await response.text(); - throw new Error(`HTTP ${response.status}: ${errorText}`); + return { + success: false, + error: { message: `HTTP ${response.status}: ${errorText}` }, + }; } const queueResponse = (await response.json()) as FalQueueResponse; let result: FalUpscaleResponse; - if (queueResponse.status === "IN_QUEUE" || queueResponse.status === "IN_PROGRESS") { + if ( + queueResponse.status === "IN_QUEUE" || + queueResponse.status === "IN_PROGRESS" + ) { result = await pollForResult( queueResponse.status_url, queueResponse.response_url, @@ -131,20 +141,29 @@ async function stepHandler( } if (result.error) { - throw new Error(result.error); + return { success: false, error: { message: result.error } }; } if (!result.image?.url) { - throw new Error("No image returned from fal.ai"); + return { + success: false, + error: { message: "No image returned from fal.ai" }, + }; } return { - imageUrl: result.image.url, - width: result.image.width, - height: result.image.height, + success: true, + data: { + imageUrl: result.image.url, + width: result.image.width, + height: result.image.height, + }, }; } catch (error) { - throw new Error(`Failed to upscale image: ${getErrorMessage(error)}`); + return { + success: false, + error: { message: `Failed to upscale image: ${getErrorMessage(error)}` }, + }; } } diff --git a/plugins/linear/steps/create-ticket.ts b/plugins/linear/steps/create-ticket.ts index bf89d487..a6e63a29 100644 --- a/plugins/linear/steps/create-ticket.ts +++ b/plugins/linear/steps/create-ticket.ts @@ -30,8 +30,8 @@ type CreateIssueMutationResponse = { }; type CreateTicketResult = - | { success: true; id: string; url: string; title: string } - | { success: false; error: string }; + | { success: true; data: { id: string; url: string; title: string } } + | { success: false; error: { message: string } }; export type CreateTicketCoreInput = { ticketTitle: string; @@ -77,8 +77,10 @@ async function stepHandler( if (!apiKey) { return { success: false, - error: - "LINEAR_API_KEY is not configured. Please add it in Project Integrations.", + error: { + message: + "LINEAR_API_KEY is not configured. Please add it in Project Integrations.", + }, }; } @@ -94,7 +96,7 @@ async function stepHandler( if (teamsResult.errors?.length) { return { success: false, - error: teamsResult.errors[0].message, + error: { message: teamsResult.errors[0].message }, }; } @@ -102,7 +104,7 @@ async function stepHandler( if (!firstTeam) { return { success: false, - error: "No teams found in Linear workspace", + error: { message: "No teams found in Linear workspace" }, }; } targetTeamId = firstTeam.id; @@ -130,7 +132,7 @@ async function stepHandler( if (createResult.errors?.length) { return { success: false, - error: createResult.errors[0].message, + error: { message: createResult.errors[0].message }, }; } @@ -138,20 +140,22 @@ async function stepHandler( if (!issue) { return { success: false, - error: "Failed to create issue", + error: { message: "Failed to create issue" }, }; } return { success: true, - id: issue.id, - url: issue.url, - title: issue.title, + data: { + id: issue.id, + url: issue.url, + title: issue.title, + }, }; } catch (error) { return { success: false, - error: `Failed to create ticket: ${getErrorMessage(error)}`, + error: { message: `Failed to create ticket: ${getErrorMessage(error)}` }, }; } } diff --git a/plugins/linear/steps/find-issues.ts b/plugins/linear/steps/find-issues.ts index 55b5ee56..0be08ef4 100644 --- a/plugins/linear/steps/find-issues.ts +++ b/plugins/linear/steps/find-issues.ts @@ -37,8 +37,8 @@ type LinearIssue = { }; type FindIssuesResult = - | { success: true; issues: LinearIssue[]; count: number } - | { success: false; error: string }; + | { success: true; data: { issues: LinearIssue[]; count: number } } + | { success: false; error: { message: string } }; export type FindIssuesCoreInput = { linearAssigneeId?: string; @@ -85,8 +85,10 @@ async function stepHandler( if (!apiKey) { return { success: false, - error: - "LINEAR_API_KEY is not configured. Please add it in Project Integrations.", + error: { + message: + "LINEAR_API_KEY is not configured. Please add it in Project Integrations.", + }, }; } @@ -132,7 +134,7 @@ async function stepHandler( if (result.errors?.length) { return { success: false, - error: result.errors[0].message, + error: { message: result.errors[0].message }, }; } @@ -149,13 +151,15 @@ async function stepHandler( return { success: true, - issues: mappedIssues, - count: mappedIssues.length, + data: { + issues: mappedIssues, + count: mappedIssues.length, + }, }; } catch (error) { return { success: false, - error: `Failed to find issues: ${getErrorMessage(error)}`, + error: { message: `Failed to find issues: ${getErrorMessage(error)}` }, }; } } diff --git a/plugins/perplexity/steps/ask.ts b/plugins/perplexity/steps/ask.ts index 4d1ce1c7..b1869652 100644 --- a/plugins/perplexity/steps/ask.ts +++ b/plugins/perplexity/steps/ask.ts @@ -33,11 +33,9 @@ type PerplexityResponse = { }; }; -type AskResult = { - answer: string; - citations: string[]; - model: string; -}; +type AskResult = + | { success: true; data: { answer: string; citations: string[]; model: string } } + | { success: false; error: { message: string } }; export type PerplexityAskCoreInput = { question: string; @@ -60,7 +58,10 @@ async function stepHandler( const apiKey = credentials.PERPLEXITY_API_KEY; if (!apiKey) { - throw new Error("Perplexity API Key is not configured."); + return { + success: false, + error: { message: "Perplexity API Key is not configured." }, + }; } try { @@ -99,7 +100,10 @@ async function stepHandler( if (!response.ok) { const errorText = await response.text(); - throw new Error(`HTTP ${response.status}: ${errorText}`); + return { + success: false, + error: { message: `HTTP ${response.status}: ${errorText}` }, + }; } const result = (await response.json()) as PerplexityResponse; @@ -110,12 +114,14 @@ async function stepHandler( ); return { - answer, - citations, - model: result.model, + success: true, + data: { answer, citations, model: result.model }, }; } catch (error) { - throw new Error(`Failed to ask: ${getErrorMessage(error)}`); + return { + success: false, + error: { message: `Failed to ask: ${getErrorMessage(error)}` }, + }; } } diff --git a/plugins/perplexity/steps/research.ts b/plugins/perplexity/steps/research.ts index 1c62694d..b911f4ba 100644 --- a/plugins/perplexity/steps/research.ts +++ b/plugins/perplexity/steps/research.ts @@ -33,11 +33,9 @@ type PerplexityResponse = { }; }; -type ResearchResult = { - report: string; - citations: string[]; - model: string; -}; +type ResearchResult = + | { success: true; data: { report: string; citations: string[]; model: string } } + | { success: false; error: { message: string } }; export type PerplexityResearchCoreInput = { topic: string; @@ -59,7 +57,10 @@ async function stepHandler( const apiKey = credentials.PERPLEXITY_API_KEY; if (!apiKey) { - throw new Error("Perplexity API Key is not configured."); + return { + success: false, + error: { message: "Perplexity API Key is not configured." }, + }; } const depthInstructions = getDepthInstructions(input.depth); @@ -89,7 +90,10 @@ async function stepHandler( if (!response.ok) { const errorText = await response.text(); - throw new Error(`HTTP ${response.status}: ${errorText}`); + return { + success: false, + error: { message: `HTTP ${response.status}: ${errorText}` }, + }; } const result = (await response.json()) as PerplexityResponse; @@ -100,12 +104,14 @@ async function stepHandler( ); return { - report, - citations, - model: result.model, + success: true, + data: { report, citations, model: result.model }, }; } catch (error) { - throw new Error(`Failed to research: ${getErrorMessage(error)}`); + return { + success: false, + error: { message: `Failed to research: ${getErrorMessage(error)}` }, + }; } } diff --git a/plugins/perplexity/steps/search.ts b/plugins/perplexity/steps/search.ts index 9c6d51e1..5ee312f5 100644 --- a/plugins/perplexity/steps/search.ts +++ b/plugins/perplexity/steps/search.ts @@ -33,11 +33,9 @@ type PerplexityResponse = { }; }; -type SearchResult = { - answer: string; - citations: string[]; - model: string; -}; +type SearchResult = + | { success: true; data: { answer: string; citations: string[]; model: string } } + | { success: false; error: { message: string } }; export type PerplexitySearchCoreInput = { query: string; @@ -59,7 +57,10 @@ async function stepHandler( const apiKey = credentials.PERPLEXITY_API_KEY; if (!apiKey) { - throw new Error("Perplexity API Key is not configured."); + return { + success: false, + error: { message: "Perplexity API Key is not configured." }, + }; } try { @@ -89,7 +90,10 @@ async function stepHandler( if (!response.ok) { const errorText = await response.text(); - throw new Error(`HTTP ${response.status}: ${errorText}`); + return { + success: false, + error: { message: `HTTP ${response.status}: ${errorText}` }, + }; } const result = (await response.json()) as PerplexityResponse; @@ -100,12 +104,14 @@ async function stepHandler( ); return { - answer, - citations, - model: result.model, + success: true, + data: { answer, citations, model: result.model }, }; } catch (error) { - throw new Error(`Failed to search: ${getErrorMessage(error)}`); + return { + success: false, + error: { message: `Failed to search: ${getErrorMessage(error)}` }, + }; } } diff --git a/plugins/registry.ts b/plugins/registry.ts index 8a31d1c7..9e73c407 100644 --- a/plugins/registry.ts +++ b/plugins/registry.ts @@ -91,15 +91,31 @@ export type OutputField = { description: string; }; +/** + * Result Component Props + * Props passed to custom result components + */ +export type ResultComponentProps = { + output: unknown; + input?: unknown; +}; + /** * Output Display Config * Specifies how to render step output in the workflow runs panel */ -export type OutputDisplayConfig = { - // Type of display: image renders as img, video renders as video element, url renders in iframe +export type OutputDisplayConfig = + | { + // Built-in display types type: "image" | "video" | "url"; // Field name in the step output that contains the displayable value field: string; + } + | { + // Custom component display + type: "component"; + // React component to render the output + component: React.ComponentType; }; /** diff --git a/plugins/resend/steps/send-email.ts b/plugins/resend/steps/send-email.ts index 79552f8c..0e4aaea7 100644 --- a/plugins/resend/steps/send-email.ts +++ b/plugins/resend/steps/send-email.ts @@ -17,8 +17,8 @@ type ResendErrorResponse = { }; type SendEmailResult = - | { success: true; id: string } - | { success: false; error: string }; + | { success: true; data: { id: string } } + | { success: false; error: { message: string } }; export type SendEmailCoreInput = { emailFrom?: string; @@ -51,8 +51,10 @@ async function stepHandler( if (!apiKey) { return { success: false, - error: - "RESEND_API_KEY is not configured. Please add it in Project Integrations.", + error: { + message: + "RESEND_API_KEY is not configured. Please add it in Project Integrations.", + }, }; } @@ -61,8 +63,10 @@ async function stepHandler( if (!senderEmail) { return { success: false, - error: - "No sender is configured. Please add it in the action or in Project Integrations.", + error: { + message: + "No sender is configured. Please add it in the action or in Project Integrations.", + }, }; } @@ -95,17 +99,20 @@ async function stepHandler( const errorData = (await response.json()) as ResendErrorResponse; return { success: false, - error: errorData.message || `HTTP ${response.status}: Failed to send email`, + error: { + message: + errorData.message || `HTTP ${response.status}: Failed to send email`, + }, }; } const data = (await response.json()) as ResendEmailResponse; - return { success: true, id: data.id }; + return { success: true, data: { id: data.id } }; } catch (error) { - const message = error instanceof Error ? error.message : String(error); + const errorMessage = error instanceof Error ? error.message : String(error); return { success: false, - error: `Failed to send email: ${message}`, + error: { message: `Failed to send email: ${errorMessage}` }, }; } } diff --git a/plugins/webflow/steps/get-site.ts b/plugins/webflow/steps/get-site.ts index ba8aa6cc..5a9e4257 100644 --- a/plugins/webflow/steps/get-site.ts +++ b/plugins/webflow/steps/get-site.ts @@ -24,23 +24,24 @@ type WebflowSiteResponse = { }>; }; +type GetSiteData = { + id: string; + displayName: string; + shortName: string; + previewUrl: string; + lastPublished?: string; + lastUpdated: string; + timeZone: string; + customDomains: Array<{ + id: string; + url: string; + lastPublished?: string; + }>; +}; + type GetSiteResult = - | { - success: true; - id: string; - displayName: string; - shortName: string; - previewUrl: string; - lastPublished?: string; - lastUpdated: string; - timeZone: string; - customDomains: Array<{ - id: string; - url: string; - lastPublished?: string; - }>; - } - | { success: false; error: string }; + | { success: true; data: GetSiteData } + | { success: false; error: { message: string } }; export type GetSiteCoreInput = { siteId: string; @@ -60,15 +61,17 @@ async function stepHandler( if (!apiKey) { return { success: false, - error: - "WEBFLOW_API_KEY is not configured. Please add it in Project Integrations.", + error: { + message: + "WEBFLOW_API_KEY is not configured. Please add it in Project Integrations.", + }, }; } if (!input.siteId) { return { success: false, - error: "Site ID is required", + error: { message: "Site ID is required" }, }; } @@ -76,18 +79,19 @@ async function stepHandler( const response = await fetch( `${WEBFLOW_API_URL}/sites/${encodeURIComponent(input.siteId)}`, { - method: "GET", - headers: { - Accept: "application/json", - Authorization: `Bearer ${apiKey}`, - }, - }); + method: "GET", + headers: { + Accept: "application/json", + Authorization: `Bearer ${apiKey}`, + }, + } + ); if (!response.ok) { const errorData = (await response.json()) as { message?: string }; return { success: false, - error: errorData.message || `HTTP ${response.status}`, + error: { message: errorData.message || `HTTP ${response.status}` }, }; } @@ -95,19 +99,21 @@ async function stepHandler( return { success: true, - id: site.id, - displayName: site.displayName, - shortName: site.shortName, - previewUrl: site.previewUrl, - lastPublished: site.lastPublished, - lastUpdated: site.lastUpdated, - timeZone: site.timeZone, - customDomains: site.customDomains || [], + data: { + id: site.id, + displayName: site.displayName, + shortName: site.shortName, + previewUrl: site.previewUrl, + lastPublished: site.lastPublished, + lastUpdated: site.lastUpdated, + timeZone: site.timeZone, + customDomains: site.customDomains || [], + }, }; } catch (error) { return { success: false, - error: `Failed to get site: ${getErrorMessage(error)}`, + error: { message: `Failed to get site: ${getErrorMessage(error)}` }, }; } } diff --git a/plugins/webflow/steps/list-sites.ts b/plugins/webflow/steps/list-sites.ts index 5d9682eb..f9970664 100644 --- a/plugins/webflow/steps/list-sites.ts +++ b/plugins/webflow/steps/list-sites.ts @@ -24,21 +24,19 @@ type WebflowSite = { }>; }; +type SiteData = { + id: string; + displayName: string; + shortName: string; + previewUrl: string; + lastPublished?: string; + lastUpdated: string; + customDomains: string[]; +}; + type ListSitesResult = - | { - success: true; - sites: Array<{ - id: string; - displayName: string; - shortName: string; - previewUrl: string; - lastPublished?: string; - lastUpdated: string; - customDomains: string[]; - }>; - count: number; - } - | { success: false; error: string }; + | { success: true; data: { sites: SiteData[]; count: number } } + | { success: false; error: { message: string } }; export type ListSitesCoreInput = Record; @@ -56,8 +54,10 @@ async function stepHandler( if (!apiKey) { return { success: false, - error: - "WEBFLOW_API_KEY is not configured. Please add it in Project Integrations.", + error: { + message: + "WEBFLOW_API_KEY is not configured. Please add it in Project Integrations.", + }, }; } @@ -74,7 +74,7 @@ async function stepHandler( const errorData = (await response.json()) as { message?: string }; return { success: false, - error: errorData.message || `HTTP ${response.status}`, + error: { message: errorData.message || `HTTP ${response.status}` }, }; } @@ -92,13 +92,12 @@ async function stepHandler( return { success: true, - sites, - count: sites.length, + data: { sites, count: sites.length }, }; } catch (error) { return { success: false, - error: `Failed to list sites: ${getErrorMessage(error)}`, + error: { message: `Failed to list sites: ${getErrorMessage(error)}` }, }; } } diff --git a/plugins/webflow/steps/publish-site.ts b/plugins/webflow/steps/publish-site.ts index 8a9d2468..34fadb2e 100644 --- a/plugins/webflow/steps/publish-site.ts +++ b/plugins/webflow/steps/publish-site.ts @@ -19,10 +19,9 @@ type PublishResponse = { type PublishSiteResult = | { success: true; - publishedDomains: string[]; - publishedToSubdomain: boolean; + data: { publishedDomains: string[]; publishedToSubdomain: boolean }; } - | { success: false; error: string }; + | { success: false; error: { message: string } }; export type PublishSiteCoreInput = { siteId: string; @@ -44,15 +43,17 @@ async function stepHandler( if (!apiKey) { return { success: false, - error: - "WEBFLOW_API_KEY is not configured. Please add it in Project Integrations.", + error: { + message: + "WEBFLOW_API_KEY is not configured. Please add it in Project Integrations.", + }, }; } if (!input.siteId) { return { success: false, - error: "Site ID is required", + error: { message: "Site ID is required" }, }; } @@ -102,7 +103,7 @@ async function stepHandler( const errorData = (await response.json()) as { message?: string }; return { success: false, - error: errorData.message || `HTTP ${response.status}`, + error: { message: errorData.message || `HTTP ${response.status}` }, }; } @@ -110,13 +111,15 @@ async function stepHandler( return { success: true, - publishedDomains: result.customDomains?.map((d) => d.url) || [], - publishedToSubdomain: result.publishToWebflowSubdomain ?? false, + data: { + publishedDomains: result.customDomains?.map((d) => d.url) || [], + publishedToSubdomain: result.publishToWebflowSubdomain ?? false, + }, }; } catch (error) { return { success: false, - error: `Failed to publish site: ${getErrorMessage(error)}`, + error: { message: `Failed to publish site: ${getErrorMessage(error)}` }, }; } } diff --git a/scripts/discover-plugins.ts b/scripts/discover-plugins.ts index 0b1e2fef..604d3afc 100644 --- a/scripts/discover-plugins.ts +++ b/scripts/discover-plugins.ts @@ -608,7 +608,6 @@ async function generateStepRegistry(): Promise { integration: string; stepImportPath: string; stepFunction: string; - outputConfig?: { type: string; field: string }; }> = []; for (const integration of integrations) { @@ -620,7 +619,6 @@ async function generateStepRegistry(): Promise { integration: integration.type, stepImportPath: action.stepImportPath, stepFunction: action.stepFunction, - outputConfig: action.outputConfig, }); } } @@ -752,7 +750,7 @@ async function generateOutputDisplayConfigs(): Promise { ); const integrations = getAllIntegrations(); - // Collect output configs + // Collect output configs (only built-in types, not component types) const outputConfigs: Array<{ actionId: string; type: string; @@ -761,7 +759,8 @@ async function generateOutputDisplayConfigs(): Promise { for (const integration of integrations) { for (const action of integration.actions) { - if (action.outputConfig) { + // Only include built-in config types (image/video/url), not component types + if (action.outputConfig && action.outputConfig.type !== "component") { outputConfigs.push({ actionId: computeActionId(integration.type, action.slug), type: action.outputConfig.type,