feat(tools): added typeform form submission trigger, added 4 new tools to complete CRUD typeform tools (#1818)

* feat(tools): added typeform form submission trigger, added 4 new tools to complete CRUD typeform tools

* resolve envvars in trigger configuration upon save, tested typeform

* updated docs

* ack PR comments
This commit is contained in:
Waleed
2025-11-05 17:47:43 -08:00
committed by GitHub
parent 5c611c6d65
commit 60d53ba14a
21 changed files with 1837 additions and 19 deletions

View File

@@ -63,6 +63,7 @@ Convert TTS using ElevenLabs voices
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `audioUrl` | string | The URL of the generated audio |
| `audioFile` | file | The generated audio file |

View File

@@ -47,7 +47,7 @@ In Sim, the Typeform integration enables your agents to programmatically interac
## Usage Instructions
Integrate Typeform into the workflow. Can retrieve responses, download files, and get form insights. Requires API Key.
Integrate Typeform into the workflow. Can retrieve responses, download files, and get form insights. Can be used in trigger mode to trigger a workflow when a form is submitted. Requires API Key.
@@ -72,9 +72,25 @@ Retrieve form responses from Typeform
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `total_items` | number | Total response count |
| `total_items` | number | Total response/form count |
| `page_count` | number | Total page count |
| `items` | json | Response items |
| `items` | json | Response/form items array |
| `id` | string | Form unique identifier |
| `title` | string | Form title |
| `type` | string | Form type |
| `created_at` | string | ISO timestamp of form creation |
| `last_updated_at` | string | ISO timestamp of last update |
| `settings` | json | Form settings object |
| `theme` | json | Theme configuration object |
| `workspace` | json | Workspace information |
| `fields` | json | Form fields/questions array |
| `thankyou_screens` | json | Thank you screens array |
| `_links` | json | Related resource links |
| `deleted` | boolean | Whether the form was successfully deleted |
| `message` | string | Deletion confirmation message |
| `fileUrl` | string | Downloaded file URL |
| `contentType` | string | File content type |
| `filename` | string | File name |
### `typeform_files`
@@ -116,6 +132,129 @@ Retrieve insights and analytics for Typeform forms
| --------- | ---- | ----------- |
| `fields` | array | Number of users who dropped off at this field |
### `typeform_list_forms`
Retrieve a list of all forms in your Typeform account
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Typeform Personal Access Token |
| `search` | string | No | Search query to filter forms by title |
| `page` | number | No | Page number \(default: 1\) |
| `pageSize` | number | No | Number of forms per page \(default: 10, max: 200\) |
| `workspaceId` | string | No | Filter forms by workspace ID |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `total_items` | number | Total number of forms in the account |
| `page_count` | number | Total number of pages available |
| `items` | array | Array of form objects |
### `typeform_get_form`
Retrieve complete details and structure of a specific form
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Typeform Personal Access Token |
| `formId` | string | Yes | Form unique identifier |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `id` | string | Form unique identifier |
| `title` | string | Form title |
| `type` | string | Form type \(form, quiz, etc.\) |
| `created_at` | string | ISO timestamp of form creation |
| `last_updated_at` | string | ISO timestamp of last update |
| `settings` | object | Form settings including language, progress bar, etc. |
| `theme` | object | Theme configuration with colors, fonts, and design settings |
| `workspace` | object | Workspace information |
### `typeform_create_form`
Create a new form with fields and settings
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Typeform Personal Access Token |
| `title` | string | Yes | Form title |
| `type` | string | No | Form type \(default: "form"\). Options: "form", "quiz" |
| `workspaceId` | string | No | Workspace ID to create the form in |
| `fields` | json | No | Array of field objects defining the form structure. Each field needs: type, title, and optional properties/validations |
| `settings` | json | No | Form settings object \(language, progress_bar, etc.\) |
| `themeId` | string | No | Theme ID to apply to the form |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `id` | string | Created form unique identifier |
| `title` | string | Form title |
| `type` | string | Form type |
| `created_at` | string | ISO timestamp of form creation |
| `last_updated_at` | string | ISO timestamp of last update |
| `settings` | object | Form settings |
| `theme` | object | Applied theme configuration |
| `workspace` | object | Workspace information |
| `fields` | array | Array of created form fields |
| `_links` | object | Related resource links |
### `typeform_update_form`
Update an existing form using JSON Patch operations
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Typeform Personal Access Token |
| `formId` | string | Yes | Form unique identifier to update |
| `operations` | json | Yes | Array of JSON Patch operations \(RFC 6902\). Each operation needs: op \(add/remove/replace\), path, and value \(for add/replace\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `id` | string | Updated form unique identifier |
| `title` | string | Form title |
| `type` | string | Form type |
| `created_at` | string | ISO timestamp of form creation |
| `last_updated_at` | string | ISO timestamp of last update |
| `settings` | object | Form settings |
| `theme` | object | Theme configuration |
| `workspace` | object | Workspace information |
| `fields` | array | Array of form fields |
| `thankyou_screens` | array | Array of thank you screens |
| `_links` | object | Related resource links |
### `typeform_delete_form`
Permanently delete a form and all its responses
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Typeform Personal Access Token |
| `formId` | string | Yes | Form unique identifier to delete |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `deleted` | boolean | Whether the form was successfully deleted |
| `message` | string | Deletion confirmation message |
## Notes

View File

@@ -97,6 +97,27 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise<
const body = await request.json()
const { path, provider, providerConfig, isActive } = body
let resolvedProviderConfig = providerConfig
if (providerConfig) {
const { resolveEnvVarsInObject } = await import('@/lib/webhooks/env-resolver')
const webhookDataForResolve = await db
.select({
workspaceId: workflow.workspaceId,
})
.from(webhook)
.innerJoin(workflow, eq(webhook.workflowId, workflow.id))
.where(eq(webhook.id, id))
.limit(1)
if (webhookDataForResolve.length > 0) {
resolvedProviderConfig = await resolveEnvVarsInObject(
providerConfig,
session.user.id,
webhookDataForResolve[0].workspaceId || undefined
)
}
}
// Find the webhook and check permissions
const webhooks = await db
.select({
@@ -160,7 +181,9 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise<
path: path !== undefined ? path : webhooks[0].webhook.path,
provider: provider !== undefined ? provider : webhooks[0].webhook.provider,
providerConfig:
providerConfig !== undefined ? providerConfig : webhooks[0].webhook.providerConfig,
providerConfig !== undefined
? resolvedProviderConfig
: webhooks[0].webhook.providerConfig,
isActive: isActive !== undefined ? isActive : webhooks[0].webhook.isActive,
updatedAt: new Date(),
})

View File

@@ -25,7 +25,6 @@ export async function GET(request: NextRequest) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
// Get query parameters
const { searchParams } = new URL(request.url)
const workflowId = searchParams.get('workflowId')
const blockId = searchParams.get('blockId')
@@ -256,6 +255,13 @@ export async function POST(request: NextRequest) {
// Use the original provider config - Gmail/Outlook configuration functions will inject userId automatically
const finalProviderConfig = providerConfig || {}
const { resolveEnvVarsInObject } = await import('@/lib/webhooks/env-resolver')
const resolvedProviderConfig = await resolveEnvVarsInObject(
finalProviderConfig,
userId,
workflowRecord.workspaceId || undefined
)
// Create external subscriptions before saving to DB to prevent orphaned records
let externalSubscriptionId: string | undefined
let externalSubscriptionCreated = false
@@ -263,7 +269,7 @@ export async function POST(request: NextRequest) {
const createTempWebhookData = () => ({
id: targetWebhookId || nanoid(),
path: finalPath,
providerConfig: finalProviderConfig,
providerConfig: resolvedProviderConfig,
})
if (provider === 'airtable') {
@@ -276,7 +282,7 @@ export async function POST(request: NextRequest) {
requestId
)
if (externalSubscriptionId) {
finalProviderConfig.externalId = externalSubscriptionId
resolvedProviderConfig.externalId = externalSubscriptionId
externalSubscriptionCreated = true
}
} catch (err) {
@@ -337,7 +343,7 @@ export async function POST(request: NextRequest) {
requestId
)
if (externalSubscriptionId) {
finalProviderConfig.externalId = externalSubscriptionId
resolvedProviderConfig.externalId = externalSubscriptionId
externalSubscriptionCreated = true
}
} catch (err) {
@@ -352,21 +358,45 @@ export async function POST(request: NextRequest) {
}
}
if (provider === 'typeform') {
const { createTypeformWebhook } = await import('@/lib/webhooks/webhook-helpers')
logger.info(`[${requestId}] Creating Typeform webhook before saving to database`)
try {
const usedTag = await createTypeformWebhook(request, createTempWebhookData(), requestId)
if (!resolvedProviderConfig.webhookTag) {
resolvedProviderConfig.webhookTag = usedTag
logger.info(`[${requestId}] Stored auto-generated webhook tag: ${usedTag}`)
}
externalSubscriptionCreated = true
} catch (err) {
logger.error(`[${requestId}] Error creating Typeform webhook`, err)
return NextResponse.json(
{
error: 'Failed to create webhook in Typeform',
details: err instanceof Error ? err.message : 'Unknown error',
},
{ status: 500 }
)
}
}
// Now save to database (only if subscription succeeded or provider doesn't need external subscription)
try {
if (targetWebhookId) {
logger.info(`[${requestId}] Updating existing webhook for path: ${finalPath}`, {
webhookId: targetWebhookId,
provider,
hasCredentialId: !!(finalProviderConfig as any)?.credentialId,
credentialId: (finalProviderConfig as any)?.credentialId,
hasCredentialId: !!(resolvedProviderConfig as any)?.credentialId,
credentialId: (resolvedProviderConfig as any)?.credentialId,
})
const updatedResult = await db
.update(webhook)
.set({
blockId,
provider,
providerConfig: finalProviderConfig,
providerConfig: resolvedProviderConfig,
isActive: true,
updatedAt: new Date(),
})
@@ -389,7 +419,7 @@ export async function POST(request: NextRequest) {
blockId,
path: finalPath,
provider,
providerConfig: finalProviderConfig,
providerConfig: resolvedProviderConfig,
isActive: true,
createdAt: new Date(),
updatedAt: new Date(),

View File

@@ -898,7 +898,9 @@ export const WorkflowBlock = memo(
</TooltipContent>
</Tooltip>
)}
{config.subBlocks.some((block) => block.mode) && (
{config.subBlocks.some(
(block) => block.mode === 'basic' || block.mode === 'advanced'
) && (
<Tooltip>
<TooltipTrigger asChild>
<Button

View File

@@ -2,6 +2,7 @@ import { TypeformIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
import type { TypeformResponse } from '@/tools/typeform/types'
import { getTrigger } from '@/triggers'
export const TypeformBlock: BlockConfig<TypeformResponse> = {
type: 'typeform',
@@ -9,7 +10,7 @@ export const TypeformBlock: BlockConfig<TypeformResponse> = {
description: 'Interact with Typeform',
authMode: AuthMode.ApiKey,
longDescription:
'Integrate Typeform into the workflow. Can retrieve responses, download files, and get form insights. Requires API Key.',
'Integrate Typeform into the workflow. Can retrieve responses, download files, and get form insights. Can be used in trigger mode to trigger a workflow when a form is submitted. Requires API Key.',
docsLink: 'https://docs.sim.ai/tools/typeform',
category: 'tools',
bgColor: '#262627', // Typeform brand color
@@ -24,6 +25,11 @@ export const TypeformBlock: BlockConfig<TypeformResponse> = {
{ label: 'Retrieve Responses', id: 'typeform_responses' },
{ label: 'Download File', id: 'typeform_files' },
{ label: 'Form Insights', id: 'typeform_insights' },
{ label: 'List Forms', id: 'typeform_list_forms' },
{ label: 'Get Form Details', id: 'typeform_get_form' },
{ label: 'Create Form', id: 'typeform_create_form' },
{ label: 'Update Form', id: 'typeform_update_form' },
{ label: 'Delete Form', id: 'typeform_delete_form' },
],
value: () => 'typeform_responses',
},
@@ -34,6 +40,17 @@ export const TypeformBlock: BlockConfig<TypeformResponse> = {
layout: 'full',
placeholder: 'Enter your Typeform form ID',
required: true,
condition: {
field: 'operation',
value: [
'typeform_responses',
'typeform_files',
'typeform_insights',
'typeform_get_form',
'typeform_update_form',
'typeform_delete_form',
],
},
},
{
id: 'apiKey',
@@ -113,9 +130,115 @@ export const TypeformBlock: BlockConfig<TypeformResponse> = {
layout: 'half',
condition: { field: 'operation', value: 'typeform_files' },
},
// List forms operation fields
{
id: 'search',
title: 'Search Query',
type: 'short-input',
layout: 'half',
placeholder: 'Search forms by title',
condition: { field: 'operation', value: 'typeform_list_forms' },
},
{
id: 'workspaceId',
title: 'Workspace ID',
type: 'short-input',
layout: 'half',
placeholder: 'Filter by workspace ID',
condition: { field: 'operation', value: 'typeform_list_forms' },
},
{
id: 'page',
title: 'Page Number',
type: 'short-input',
layout: 'half',
placeholder: 'Page number (default: 1)',
condition: { field: 'operation', value: 'typeform_list_forms' },
},
{
id: 'listPageSize',
title: 'Page Size',
type: 'short-input',
layout: 'half',
placeholder: 'Forms per page (default: 10, max: 200)',
condition: { field: 'operation', value: 'typeform_list_forms' },
},
// Create form operation fields
{
id: 'title',
title: 'Form Title',
type: 'short-input',
layout: 'full',
placeholder: 'Enter form title',
condition: { field: 'operation', value: 'typeform_create_form' },
required: true,
},
{
id: 'type',
title: 'Form Type',
type: 'dropdown',
layout: 'half',
options: [
{ label: 'Form', id: 'form' },
{ label: 'Quiz', id: 'quiz' },
],
condition: { field: 'operation', value: 'typeform_create_form' },
},
{
id: 'workspaceIdCreate',
title: 'Workspace ID',
type: 'short-input',
layout: 'half',
placeholder: 'Workspace to create form in',
condition: { field: 'operation', value: 'typeform_create_form' },
},
{
id: 'fields',
title: 'Fields',
type: 'long-input',
layout: 'full',
placeholder: 'JSON array of field objects',
condition: { field: 'operation', value: 'typeform_create_form' },
},
{
id: 'settings',
title: 'Settings',
type: 'long-input',
layout: 'full',
placeholder: 'JSON object for form settings',
condition: { field: 'operation', value: 'typeform_create_form' },
},
{
id: 'themeId',
title: 'Theme ID',
type: 'short-input',
layout: 'half',
placeholder: 'Theme ID to apply',
condition: { field: 'operation', value: 'typeform_create_form' },
},
// Update form operation fields
{
id: 'operations',
title: 'JSON Patch Operations',
type: 'long-input',
layout: 'full',
placeholder: 'JSON array of patch operations (RFC 6902)',
condition: { field: 'operation', value: 'typeform_update_form' },
required: true,
},
...getTrigger('typeform_webhook').subBlocks,
],
tools: {
access: ['typeform_responses', 'typeform_files', 'typeform_insights'],
access: [
'typeform_responses',
'typeform_files',
'typeform_insights',
'typeform_list_forms',
'typeform_get_form',
'typeform_create_form',
'typeform_update_form',
'typeform_delete_form',
],
config: {
tool: (params) => {
switch (params.operation) {
@@ -125,10 +248,56 @@ export const TypeformBlock: BlockConfig<TypeformResponse> = {
return 'typeform_files'
case 'typeform_insights':
return 'typeform_insights'
case 'typeform_list_forms':
return 'typeform_list_forms'
case 'typeform_get_form':
return 'typeform_get_form'
case 'typeform_create_form':
return 'typeform_create_form'
case 'typeform_update_form':
return 'typeform_update_form'
case 'typeform_delete_form':
return 'typeform_delete_form'
default:
return 'typeform_responses'
}
},
params: (params) => {
const {
operation,
listPageSize,
workspaceIdCreate,
fields,
settings,
operations,
...rest
} = params
let parsedFields: any | undefined
let parsedSettings: any | undefined
let parsedOperations: any | undefined
try {
if (fields) parsedFields = JSON.parse(fields)
if (settings) parsedSettings = JSON.parse(settings)
if (operations) parsedOperations = JSON.parse(operations)
} catch (error: any) {
throw new Error(`Invalid JSON input: ${error.message}`)
}
const pageSize = listPageSize !== undefined ? listPageSize : params.pageSize
const workspaceId = workspaceIdCreate || params.workspaceId
return {
...rest,
...(pageSize && { pageSize }),
...(workspaceId && { workspaceId }),
...(parsedFields && { fields: parsedFields }),
...(parsedSettings && { settings: parsedSettings }),
...(parsedOperations && { operations: parsedOperations }),
}
},
},
},
inputs: {
@@ -145,10 +314,48 @@ export const TypeformBlock: BlockConfig<TypeformResponse> = {
fieldId: { type: 'string', description: 'Field identifier' },
filename: { type: 'string', description: 'File name' },
inline: { type: 'boolean', description: 'Inline display option' },
// List forms operation params
search: { type: 'string', description: 'Search query for form titles' },
workspaceId: { type: 'string', description: 'Workspace ID filter' },
page: { type: 'number', description: 'Page number' },
listPageSize: { type: 'number', description: 'Forms per page' },
// Create form operation params
title: { type: 'string', description: 'Form title' },
type: { type: 'string', description: 'Form type (form or quiz)' },
workspaceIdCreate: { type: 'string', description: 'Workspace ID for creation' },
fields: { type: 'json', description: 'Form fields array' },
settings: { type: 'json', description: 'Form settings object' },
themeId: { type: 'string', description: 'Theme ID' },
// Update form operation params
operations: { type: 'json', description: 'JSON Patch operations array' },
},
outputs: {
total_items: { type: 'number', description: 'Total response count' },
// Common outputs (used by responses, list_forms)
total_items: { type: 'number', description: 'Total response/form count' },
page_count: { type: 'number', description: 'Total page count' },
items: { type: 'json', description: 'Response items' },
items: { type: 'json', description: 'Response/form items array' },
// Form details outputs (get_form, create_form, update_form)
id: { type: 'string', description: 'Form unique identifier' },
title: { type: 'string', description: 'Form title' },
type: { type: 'string', description: 'Form type' },
created_at: { type: 'string', description: 'ISO timestamp of form creation' },
last_updated_at: { type: 'string', description: 'ISO timestamp of last update' },
settings: { type: 'json', description: 'Form settings object' },
theme: { type: 'json', description: 'Theme configuration object' },
workspace: { type: 'json', description: 'Workspace information' },
fields: { type: 'json', description: 'Form fields/questions array' },
thankyou_screens: { type: 'json', description: 'Thank you screens array' },
_links: { type: 'json', description: 'Related resource links' },
// Delete form outputs
deleted: { type: 'boolean', description: 'Whether the form was successfully deleted' },
message: { type: 'string', description: 'Deletion confirmation message' },
// File operation outputs
fileUrl: { type: 'string', description: 'Downloaded file URL' },
contentType: { type: 'string', description: 'File content type' },
filename: { type: 'string', description: 'File name' },
},
triggers: {
enabled: true,
available: ['typeform_webhook'],
},
}

View File

@@ -0,0 +1,72 @@
import { getEffectiveDecryptedEnv } from '@/lib/environment/utils'
import { createLogger } from '@/lib/logs/console/logger'
import { extractEnvVarName, isEnvVarReference } from '@/executor/consts'
const logger = createLogger('EnvResolver')
/**
* Resolves environment variable references in a string value
* Uses the same helper functions as the executor's EnvResolver
*
* @param value - The string that may contain env var references
* @param envVars - Object containing environment variable key-value pairs
* @returns The resolved string with env vars replaced
*/
function resolveEnvVarInString(value: string, envVars: Record<string, string>): string {
if (!isEnvVarReference(value)) {
return value
}
const varName = extractEnvVarName(value)
const resolvedValue = envVars[varName]
if (resolvedValue === undefined) {
logger.warn(`Environment variable not found: ${varName}`)
return value // Return original if not found
}
logger.debug(`Resolved environment variable: ${varName}`)
return resolvedValue
}
/**
* Recursively resolves all environment variable references in a configuration object
* Supports the pattern: {{VAR_NAME}}
*
* @param config - Configuration object that may contain env var references
* @param userId - User ID to fetch environment variables for
* @param workspaceId - Optional workspace ID for workspace-specific env vars
* @returns A new object with all env var references resolved
*/
export async function resolveEnvVarsInObject(
config: Record<string, any>,
userId: string,
workspaceId?: string
): Promise<Record<string, any>> {
const envVars = await getEffectiveDecryptedEnv(userId, workspaceId)
const resolved = { ...config }
function resolveValue(value: any): any {
if (typeof value === 'string') {
return resolveEnvVarInString(value, envVars)
}
if (Array.isArray(value)) {
return value.map(resolveValue)
}
if (value !== null && typeof value === 'object') {
const resolvedObj: Record<string, any> = {}
for (const [key, val] of Object.entries(value)) {
resolvedObj[key] = resolveValue(val)
}
return resolvedObj
}
return value
}
for (const [key, value] of Object.entries(resolved)) {
resolved[key] = resolveValue(value)
}
return resolved
}

View File

@@ -357,6 +357,33 @@ export async function verifyProviderAuth(
}
}
if (foundWebhook.provider === 'typeform') {
const secret = providerConfig.secret as string | undefined
if (secret) {
const signature = request.headers.get('Typeform-Signature')
if (!signature) {
logger.warn(`[${requestId}] Typeform webhook missing signature header`)
return new NextResponse('Unauthorized - Missing Typeform signature', { status: 401 })
}
const { validateTypeformSignature } = await import('@/lib/webhooks/utils.server')
const isValidSignature = validateTypeformSignature(secret, signature, rawBody)
if (!isValidSignature) {
logger.warn(`[${requestId}] Typeform signature verification failed`, {
signatureLength: signature.length,
secretLength: secret.length,
})
return new NextResponse('Unauthorized - Invalid Typeform signature', { status: 401 })
}
logger.debug(`[${requestId}] Typeform signature verified successfully`)
}
}
if (foundWebhook.provider === 'generic') {
if (providerConfig.requireAuth) {
const configToken = providerConfig.token

View File

@@ -1170,6 +1170,69 @@ export async function formatWebhookInput(
}
}
if (foundWebhook.provider === 'typeform') {
const eventId = body?.event_id || ''
const eventType = body?.event_type || 'form_response'
const formResponse = body?.form_response || {}
const formId = formResponse.form_id || ''
const token = formResponse.token || ''
const submittedAt = formResponse.submitted_at || ''
const landedAt = formResponse.landed_at || ''
const calculated = formResponse.calculated || {}
const variables = formResponse.variables || []
const hidden = formResponse.hidden || {}
const answers = formResponse.answers || []
const definition = formResponse.definition || {}
const ending = formResponse.ending || {}
const providerConfig = (foundWebhook.providerConfig as Record<string, any>) || {}
const includeDefinition = providerConfig.includeDefinition === true
return {
event_id: eventId,
event_type: eventType,
form_id: formId,
token,
submitted_at: submittedAt,
landed_at: landedAt,
calculated,
variables,
hidden,
answers,
...(includeDefinition ? { definition } : {}),
ending,
typeform: {
event_id: eventId,
event_type: eventType,
form_id: formId,
token,
submitted_at: submittedAt,
landed_at: landedAt,
calculated,
variables,
hidden,
answers,
...(includeDefinition ? { definition } : {}),
ending,
},
raw: body,
webhook: {
data: {
provider: 'typeform',
path: foundWebhook.path,
providerConfig: foundWebhook.providerConfig,
payload: body,
headers: Object.fromEntries(request.headers.entries()),
method: request.method,
},
},
workflowId: foundWorkflow.id,
}
}
// Generic format for other providers
return {
webhook: {
@@ -1234,6 +1297,48 @@ export function validateMicrosoftTeamsSignature(
}
}
/**
* Validates a Typeform webhook request signature using HMAC SHA-256
* @param secret - Typeform webhook secret (plain text)
* @param signature - Typeform-Signature header value (should be in format 'sha256=<signature>')
* @param body - Raw request body string
* @returns Whether the signature is valid
*/
export function validateTypeformSignature(
secret: string,
signature: string,
body: string
): boolean {
try {
if (!secret || !signature || !body) {
return false
}
if (!signature.startsWith('sha256=')) {
return false
}
const providedSignature = signature.substring(7)
const crypto = require('crypto')
const computedHash = crypto.createHmac('sha256', secret).update(body, 'utf8').digest('base64')
if (computedHash.length !== providedSignature.length) {
return false
}
let result = 0
for (let i = 0; i < computedHash.length; i++) {
result |= computedHash.charCodeAt(i) ^ providedSignature.charCodeAt(i)
}
return result === 0
} catch (error) {
logger.error('Error validating Typeform signature:', error)
return false
}
}
/**
* Process webhook provider-specific verification
*/

View File

@@ -6,6 +6,7 @@ import { getOAuthToken, refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/
const teamsLogger = createLogger('TeamsSubscription')
const telegramLogger = createLogger('TelegramWebhook')
const airtableLogger = createLogger('AirtableWebhook')
const typeformLogger = createLogger('TypeformWebhook')
function getProviderConfig(webhook: any): Record<string, any> {
return (webhook.providerConfig as Record<string, any>) || {}
@@ -459,9 +460,160 @@ export async function deleteAirtableWebhook(
}
}
/**
* Create a Typeform webhook subscription
* Throws errors with friendly messages if webhook creation fails
*/
export async function createTypeformWebhook(
request: NextRequest,
webhook: any,
requestId: string
): Promise<string> {
const config = getProviderConfig(webhook)
const formId = config.formId as string | undefined
const apiKey = config.apiKey as string | undefined
const webhookTag = config.webhookTag as string | undefined
const secret = config.secret as string | undefined
if (!formId) {
typeformLogger.warn(`[${requestId}] Missing formId for Typeform webhook ${webhook.id}`)
throw new Error(
'Form ID is required to create a Typeform webhook. Please provide a valid form ID.'
)
}
if (!apiKey) {
typeformLogger.warn(`[${requestId}] Missing apiKey for Typeform webhook ${webhook.id}`)
throw new Error(
'Personal Access Token is required to create a Typeform webhook. Please provide your Typeform API key.'
)
}
const tag = webhookTag || `sim-${webhook.id.substring(0, 8)}`
const notificationUrl = getNotificationUrl(webhook)
try {
const typeformApiUrl = `https://api.typeform.com/forms/${formId}/webhooks/${tag}`
const requestBody: Record<string, any> = {
url: notificationUrl,
enabled: true,
verify_ssl: true,
event_types: {
form_response: true,
},
}
if (secret) {
requestBody.secret = secret
}
const typeformResponse = await fetch(typeformApiUrl, {
method: 'PUT',
headers: {
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(requestBody),
})
if (!typeformResponse.ok) {
const responseBody = await typeformResponse.json().catch(() => ({}))
const errorMessage = responseBody.description || responseBody.message || 'Unknown error'
typeformLogger.error(`[${requestId}] Typeform API error: ${errorMessage}`, {
status: typeformResponse.status,
response: responseBody,
})
let userFriendlyMessage = 'Failed to create Typeform webhook'
if (typeformResponse.status === 401) {
userFriendlyMessage =
'Invalid Personal Access Token. Please verify your Typeform API key and try again.'
} else if (typeformResponse.status === 403) {
userFriendlyMessage =
'Access denied. Please ensure you have a Typeform PRO or PRO+ account and the API key has webhook permissions.'
} else if (typeformResponse.status === 404) {
userFriendlyMessage = 'Form not found. Please verify the form ID is correct.'
} else if (responseBody.description || responseBody.message) {
userFriendlyMessage = `Typeform error: ${errorMessage}`
}
throw new Error(userFriendlyMessage)
}
const responseBody = await typeformResponse.json()
typeformLogger.info(
`[${requestId}] Successfully created Typeform webhook for webhook ${webhook.id} with tag ${tag}`,
{ webhookId: responseBody.id }
)
return tag
} catch (error: any) {
if (
error instanceof Error &&
(error.message.includes('Form ID') ||
error.message.includes('Personal Access Token') ||
error.message.includes('Typeform error'))
) {
throw error
}
typeformLogger.error(
`[${requestId}] Error creating Typeform webhook for webhook ${webhook.id}`,
error
)
throw new Error(
error instanceof Error
? error.message
: 'Failed to create Typeform webhook. Please try again.'
)
}
}
/**
* Delete a Typeform webhook
* Don't fail webhook deletion if cleanup fails
*/
export async function deleteTypeformWebhook(webhook: any, requestId: string): Promise<void> {
try {
const config = getProviderConfig(webhook)
const formId = config.formId as string | undefined
const apiKey = config.apiKey as string | undefined
const webhookTag = config.webhookTag as string | undefined
if (!formId || !apiKey) {
typeformLogger.warn(
`[${requestId}] Missing formId or apiKey for Typeform webhook deletion ${webhook.id}, skipping cleanup`
)
return
}
const tag = webhookTag || `sim-${webhook.id.substring(0, 8)}`
const typeformApiUrl = `https://api.typeform.com/forms/${formId}/webhooks/${tag}`
const typeformResponse = await fetch(typeformApiUrl, {
method: 'DELETE',
headers: {
Authorization: `Bearer ${apiKey}`,
},
})
if (!typeformResponse.ok && typeformResponse.status !== 404) {
typeformLogger.warn(
`[${requestId}] Failed to delete Typeform webhook (non-fatal): ${typeformResponse.status}`
)
} else {
typeformLogger.info(`[${requestId}] Successfully deleted Typeform webhook with tag ${tag}`)
}
} catch (error) {
typeformLogger.warn(`[${requestId}] Error deleting Typeform webhook (non-fatal)`, error)
}
}
/**
* Clean up external webhook subscriptions for a webhook
* Handles Airtable, Teams, and Telegram cleanup
* Handles Airtable, Teams, Telegram, and Typeform cleanup
* Don't fail deletion if cleanup fails
*/
export async function cleanupExternalWebhook(
@@ -475,5 +627,7 @@ export async function cleanupExternalWebhook(
await deleteTeamsSubscription(webhook, workflow, requestId)
} else if (webhook.provider === 'telegram') {
await deleteTelegramWebhook(webhook, requestId)
} else if (webhook.provider === 'typeform') {
await deleteTypeformWebhook(webhook, requestId)
}
}

View File

@@ -227,7 +227,16 @@ import {
import { thinkingTool } from '@/tools/thinking'
import { sendSMSTool } from '@/tools/twilio'
import { getRecordingTool, listCallsTool, makeCallTool } from '@/tools/twilio_voice'
import { typeformFilesTool, typeformInsightsTool, typeformResponsesTool } from '@/tools/typeform'
import {
typeformCreateFormTool,
typeformDeleteFormTool,
typeformFilesTool,
typeformGetFormTool,
typeformInsightsTool,
typeformListFormsTool,
typeformResponsesTool,
typeformUpdateFormTool,
} from '@/tools/typeform'
import type { ToolConfig } from '@/tools/types'
import { visionTool } from '@/tools/vision'
import {
@@ -323,6 +332,11 @@ export const tools: Record<string, ToolConfig> = {
typeform_responses: typeformResponsesTool,
typeform_files: typeformFilesTool,
typeform_insights: typeformInsightsTool,
typeform_list_forms: typeformListFormsTool,
typeform_get_form: typeformGetFormTool,
typeform_create_form: typeformCreateFormTool,
typeform_update_form: typeformUpdateFormTool,
typeform_delete_form: typeformDeleteFormTool,
youtube_search: youtubeSearchTool,
youtube_video_details: youtubeVideoDetailsTool,
youtube_channel_info: youtubeChannelInfoTool,

View File

@@ -0,0 +1,151 @@
import type { TypeformCreateFormParams, TypeformCreateFormResponse } from '@/tools/typeform/types'
import type { ToolConfig } from '@/tools/types'
export const createFormTool: ToolConfig<TypeformCreateFormParams, TypeformCreateFormResponse> = {
id: 'typeform_create_form',
name: 'Typeform Create Form',
description: 'Create a new form with fields and settings',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Typeform Personal Access Token',
},
title: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Form title',
},
type: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Form type (default: "form"). Options: "form", "quiz"',
},
workspaceId: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Workspace ID to create the form in',
},
fields: {
type: 'json',
required: false,
visibility: 'user-only',
description:
'Array of field objects defining the form structure. Each field needs: type, title, and optional properties/validations',
},
settings: {
type: 'json',
required: false,
visibility: 'user-only',
description: 'Form settings object (language, progress_bar, etc.)',
},
themeId: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Theme ID to apply to the form',
},
},
request: {
url: () => 'https://api.typeform.com/forms',
method: 'POST',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
'Content-Type': 'application/json',
}),
body: (params: TypeformCreateFormParams) => {
const body: any = {
title: params.title,
}
if (params.type) {
body.type = params.type
}
if (params.workspaceId) {
body.workspace = {
href: `https://api.typeform.com/workspaces/${params.workspaceId}`,
}
}
if (params.fields) {
body.fields = params.fields
}
if (params.settings) {
body.settings = params.settings
}
if (params.themeId) {
body.theme = {
href: `https://api.typeform.com/themes/${params.themeId}`,
}
}
return body
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: true,
output: data,
}
},
outputs: {
id: {
type: 'string',
description: 'Created form unique identifier',
},
title: {
type: 'string',
description: 'Form title',
},
type: {
type: 'string',
description: 'Form type',
},
created_at: {
type: 'string',
description: 'ISO timestamp of form creation',
},
last_updated_at: {
type: 'string',
description: 'ISO timestamp of last update',
},
settings: {
type: 'object',
description: 'Form settings',
},
theme: {
type: 'object',
description: 'Applied theme configuration',
},
workspace: {
type: 'object',
description: 'Workspace information',
},
fields: {
type: 'array',
description: 'Array of created form fields',
},
_links: {
type: 'object',
description: 'Related resource links',
properties: {
display: { type: 'string', description: 'Public form URL' },
responses: { type: 'string', description: 'Responses API endpoint' },
},
},
},
}

View File

@@ -0,0 +1,64 @@
import type { TypeformDeleteFormParams, TypeformDeleteFormResponse } from '@/tools/typeform/types'
import type { ToolConfig } from '@/tools/types'
export const deleteFormTool: ToolConfig<TypeformDeleteFormParams, TypeformDeleteFormResponse> = {
id: 'typeform_delete_form',
name: 'Typeform Delete Form',
description: 'Permanently delete a form and all its responses',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Typeform Personal Access Token',
},
formId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Form unique identifier to delete',
},
},
request: {
url: (params: TypeformDeleteFormParams) => {
return `https://api.typeform.com/forms/${params.formId}`
},
method: 'DELETE',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
}),
},
transformResponse: async (response: Response) => {
if (response.status === 204) {
return {
success: true,
output: {
deleted: true,
message: 'Form successfully deleted',
},
}
}
const data = await response.json().catch(() => ({}))
return {
success: true,
output: data,
}
},
outputs: {
deleted: {
type: 'boolean',
description: 'Whether the form was successfully deleted',
},
message: {
type: 'string',
description: 'Deletion confirmation message',
},
},
}

View File

@@ -0,0 +1,120 @@
import type { TypeformGetFormParams, TypeformGetFormResponse } from '@/tools/typeform/types'
import type { ToolConfig } from '@/tools/types'
export const getFormTool: ToolConfig<TypeformGetFormParams, TypeformGetFormResponse> = {
id: 'typeform_get_form',
name: 'Typeform Get Form',
description: 'Retrieve complete details and structure of a specific form',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Typeform Personal Access Token',
},
formId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Form unique identifier',
},
},
request: {
url: (params: TypeformGetFormParams) => {
return `https://api.typeform.com/forms/${params.formId}`
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: true,
output: data,
}
},
outputs: {
id: {
type: 'string',
description: 'Form unique identifier',
},
title: {
type: 'string',
description: 'Form title',
},
type: {
type: 'string',
description: 'Form type (form, quiz, etc.)',
},
created_at: {
type: 'string',
description: 'ISO timestamp of form creation',
},
last_updated_at: {
type: 'string',
description: 'ISO timestamp of last update',
},
settings: {
type: 'object',
description: 'Form settings including language, progress bar, etc.',
},
theme: {
type: 'object',
description: 'Theme configuration with colors, fonts, and design settings',
},
workspace: {
type: 'object',
description: 'Workspace information',
properties: {
href: { type: 'string', description: 'Workspace API URL' },
},
},
fields: {
type: 'array',
description: 'Array of form fields/questions',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Field unique identifier' },
title: { type: 'string', description: 'Question text' },
type: {
type: 'string',
description: 'Field type (short_text, email, multiple_choice, etc.)',
},
ref: { type: 'string', description: 'Field reference for webhooks/API' },
properties: { type: 'object', description: 'Field-specific properties' },
validations: { type: 'object', description: 'Validation rules' },
},
},
},
thankyou_screens: {
type: 'array',
description: 'Array of thank you screens',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Screen unique identifier' },
title: { type: 'string', description: 'Thank you message' },
ref: { type: 'string', description: 'Screen reference' },
properties: { type: 'object', description: 'Screen properties' },
},
},
},
_links: {
type: 'object',
description: 'Related resource links',
properties: {
display: { type: 'string', description: 'Public form URL' },
responses: { type: 'string', description: 'Responses API endpoint' },
},
},
},
}

View File

@@ -1,7 +1,17 @@
import { createFormTool } from '@/tools/typeform/create_form'
import { deleteFormTool } from '@/tools/typeform/delete_form'
import { filesTool } from '@/tools/typeform/files'
import { getFormTool } from '@/tools/typeform/get_form'
import { insightsTool } from '@/tools/typeform/insights'
import { listFormsTool } from '@/tools/typeform/list_forms'
import { responsesTool } from '@/tools/typeform/responses'
import { updateFormTool } from '@/tools/typeform/update_form'
export const typeformResponsesTool = responsesTool
export const typeformFilesTool = filesTool
export const typeformInsightsTool = insightsTool
export const typeformListFormsTool = listFormsTool
export const typeformGetFormTool = getFormTool
export const typeformCreateFormTool = createFormTool
export const typeformUpdateFormTool = updateFormTool
export const typeformDeleteFormTool = deleteFormTool

View File

@@ -0,0 +1,123 @@
import type { TypeformListFormsParams, TypeformListFormsResponse } from '@/tools/typeform/types'
import type { ToolConfig } from '@/tools/types'
export const listFormsTool: ToolConfig<TypeformListFormsParams, TypeformListFormsResponse> = {
id: 'typeform_list_forms',
name: 'Typeform List Forms',
description: 'Retrieve a list of all forms in your Typeform account',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Typeform Personal Access Token',
},
search: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Search query to filter forms by title',
},
page: {
type: 'number',
required: false,
visibility: 'user-only',
description: 'Page number (default: 1)',
},
pageSize: {
type: 'number',
required: false,
visibility: 'user-only',
description: 'Number of forms per page (default: 10, max: 200)',
},
workspaceId: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Filter forms by workspace ID',
},
},
request: {
url: (params: TypeformListFormsParams) => {
const url = 'https://api.typeform.com/forms'
const queryParams = []
if (params.search) {
queryParams.push(`search=${encodeURIComponent(params.search)}`)
}
if (params.page) {
queryParams.push(`page=${params.page}`)
}
if (params.pageSize) {
queryParams.push(`page_size=${params.pageSize}`)
}
if (params.workspaceId) {
queryParams.push(`workspace_id=${encodeURIComponent(params.workspaceId)}`)
}
return queryParams.length > 0 ? `${url}?${queryParams.join('&')}` : url
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: true,
output: data,
}
},
outputs: {
total_items: {
type: 'number',
description: 'Total number of forms in the account',
},
page_count: {
type: 'number',
description: 'Total number of pages available',
},
items: {
type: 'array',
description: 'Array of form objects',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Form unique identifier' },
title: { type: 'string', description: 'Form title' },
created_at: { type: 'string', description: 'ISO timestamp of form creation' },
last_updated_at: { type: 'string', description: 'ISO timestamp of last update' },
settings: {
type: 'object',
properties: {
is_public: { type: 'boolean', description: 'Whether form is publicly accessible' },
},
},
theme: {
type: 'object',
properties: {
href: { type: 'string', description: 'Theme API URL reference' },
},
},
_links: {
type: 'object',
properties: {
display: { type: 'string', description: 'Public form URL' },
responses: { type: 'string', description: 'Responses API endpoint' },
},
},
},
},
},
},
}

View File

@@ -104,6 +104,152 @@ export interface TypeformResponsesResponse extends ToolResponse {
}
}
export interface TypeformListFormsParams {
apiKey: string
search?: string
page?: number
pageSize?: number
workspaceId?: string
}
export interface TypeformListFormsResponse extends ToolResponse {
output: {
total_items: number
page_count: number
items: Array<{
id: string
title: string
created_at: string
last_updated_at: string
settings: {
is_public: boolean
[key: string]: any
}
theme: {
href: string
}
_links: {
display: string
responses: string
}
[key: string]: any
}>
}
}
export interface TypeformGetFormParams {
apiKey: string
formId: string
}
export interface TypeformGetFormResponse extends ToolResponse {
output: {
id: string
title: string
type: string
created_at: string
last_updated_at: string
settings: Record<string, any>
theme: Record<string, any>
workspace: {
href: string
}
fields: Array<{
id: string
title: string
type: string
ref: string
properties?: Record<string, any>
validations?: Record<string, any>
[key: string]: any
}>
thankyou_screens?: Array<{
id: string
title: string
ref: string
properties?: Record<string, any>
[key: string]: any
}>
_links: {
display: string
responses: string
}
[key: string]: any
}
}
export interface TypeformCreateFormParams {
apiKey: string
title: string
type?: string
workspaceId?: string
fields?: Array<Record<string, any>>
settings?: Record<string, any>
themeId?: string
}
export interface TypeformCreateFormResponse extends ToolResponse {
output: {
id: string
title: string
type: string
created_at: string
last_updated_at: string
settings: Record<string, any>
theme: Record<string, any>
workspace?: {
href: string
}
fields: Array<Record<string, any>>
_links: {
display: string
responses: string
}
[key: string]: any
}
}
export interface TypeformUpdateFormParams {
apiKey: string
formId: string
operations: Array<{
op: 'add' | 'remove' | 'replace'
path: string
value?: any
}>
}
export interface TypeformUpdateFormResponse extends ToolResponse {
output: {
id: string
title: string
type: string
created_at: string
last_updated_at: string
settings: Record<string, any>
theme: Record<string, any>
workspace?: {
href: string
}
fields: Array<Record<string, any>>
thankyou_screens?: Array<Record<string, any>>
_links: Record<string, any>
[key: string]: any
}
}
export interface TypeformDeleteFormParams {
apiKey: string
formId: string
}
export interface TypeformDeleteFormResponse extends ToolResponse {
output: {
deleted: boolean
message: string
}
}
export interface TypeformResponse extends ToolResponse {
output:
| TypeformResponsesResponse['output']

View File

@@ -0,0 +1,101 @@
import type { TypeformUpdateFormParams, TypeformUpdateFormResponse } from '@/tools/typeform/types'
import type { ToolConfig } from '@/tools/types'
export const updateFormTool: ToolConfig<TypeformUpdateFormParams, TypeformUpdateFormResponse> = {
id: 'typeform_update_form',
name: 'Typeform Update Form',
description: 'Update an existing form using JSON Patch operations',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Typeform Personal Access Token',
},
formId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Form unique identifier to update',
},
operations: {
type: 'json',
required: true,
visibility: 'user-only',
description:
'Array of JSON Patch operations (RFC 6902). Each operation needs: op (add/remove/replace), path, and value (for add/replace)',
},
},
request: {
url: (params: TypeformUpdateFormParams) => {
return `https://api.typeform.com/forms/${params.formId}`
},
method: 'PATCH',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
'Content-Type': 'application/json',
}),
body: (params: TypeformUpdateFormParams) => {
return params.operations
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: true,
output: data,
}
},
outputs: {
id: {
type: 'string',
description: 'Updated form unique identifier',
},
title: {
type: 'string',
description: 'Form title',
},
type: {
type: 'string',
description: 'Form type',
},
created_at: {
type: 'string',
description: 'ISO timestamp of form creation',
},
last_updated_at: {
type: 'string',
description: 'ISO timestamp of last update',
},
settings: {
type: 'object',
description: 'Form settings',
},
theme: {
type: 'object',
description: 'Theme configuration',
},
workspace: {
type: 'object',
description: 'Workspace information',
},
fields: {
type: 'array',
description: 'Array of form fields',
},
thankyou_screens: {
type: 'array',
description: 'Array of thank you screens',
},
_links: {
type: 'object',
description: 'Related resource links',
},
},
}

View File

@@ -12,6 +12,7 @@ import { slackWebhookTrigger } from '@/triggers/slack'
import { stripeWebhookTrigger } from '@/triggers/stripe'
import { telegramWebhookTrigger } from '@/triggers/telegram'
import { twilioVoiceWebhookTrigger } from '@/triggers/twilio_voice'
import { typeformWebhookTrigger } from '@/triggers/typeform'
import type { TriggerRegistry } from '@/triggers/types'
import {
webflowCollectionItemChangedTrigger,
@@ -32,6 +33,7 @@ export const TRIGGER_REGISTRY: TriggerRegistry = {
outlook_poller: outlookPollingTrigger,
stripe_webhook: stripeWebhookTrigger,
telegram_webhook: telegramWebhookTrigger,
typeform_webhook: typeformWebhookTrigger,
whatsapp_webhook: whatsappWebhookTrigger,
google_forms_webhook: googleFormsWebhookTrigger,
twilio_voice_webhook: twilioVoiceWebhookTrigger,

View File

@@ -0,0 +1 @@
export { typeformWebhookTrigger } from './webhook'

View File

@@ -0,0 +1,326 @@
import { TypeformIcon } from '@/components/icons'
import type { TriggerConfig } from '@/triggers/types'
export const typeformWebhookTrigger: TriggerConfig = {
id: 'typeform_webhook',
name: 'Typeform Webhook',
provider: 'typeform',
description: 'Trigger workflow when a Typeform submission is received',
version: '1.0.0',
icon: TypeformIcon,
subBlocks: [
{
id: 'webhookUrlDisplay',
title: 'Webhook URL',
type: 'short-input',
readOnly: true,
showCopyButton: true,
useWebhookUrl: true,
placeholder: 'Webhook URL will be generated',
mode: 'trigger',
},
{
id: 'formId',
title: 'Form ID',
type: 'short-input',
placeholder: 'Enter your Typeform form ID',
description:
'The unique identifier for your Typeform. Find it in the form URL or form settings.',
required: true,
mode: 'trigger',
},
{
id: 'apiKey',
title: 'Personal Access Token',
type: 'short-input',
placeholder: 'Enter your Typeform personal access token',
description:
'Required to automatically register the webhook with Typeform. Get yours at https://admin.typeform.com/account#/section/tokens',
password: true,
required: true,
mode: 'trigger',
},
{
id: 'secret',
title: 'Webhook Secret',
type: 'short-input',
placeholder: 'Enter a secret for webhook signature verification (optional)',
description:
'A secret string used to verify webhook authenticity. Highly recommended for security. Generate a secure random string (min 20 characters recommended).',
password: true,
required: false,
mode: 'trigger',
},
{
id: 'includeDefinition',
title: 'Include Form Definition',
type: 'switch',
description:
'Include the complete form structure (questions, fields, endings) in your workflow variables. Note: Typeform always sends this data, but enabling this makes it accessible in your workflow.',
defaultValue: false,
mode: 'trigger',
},
{
id: 'triggerInstructions',
title: 'Setup Instructions',
type: 'text',
defaultValue: [
'Get your Typeform Personal Access Token from <a href="https://admin.typeform.com/account#/section/tokens" target="_blank" rel="noopener noreferrer">https://admin.typeform.com/account#/section/tokens</a>',
'Find your Form ID in the URL when editing your form (e.g., <code>https://admin.typeform.com/form/ABC123/create</code> → Form ID is <code>ABC123</code>)',
'Fill in the form above with your Form ID and Personal Access Token',
'Optionally add a Webhook Secret for enhanced security - Sim will verify all incoming webhooks match this secret',
'Click "Save" below - Sim will automatically register the webhook with Typeform',
'<strong>Note:</strong> Requires a Typeform PRO or PRO+ account to use webhooks',
]
.map(
(instruction, index) =>
`<div class="mb-3"><strong>${index + 1}.</strong> ${instruction}</div>`
)
.join(''),
mode: 'trigger',
},
{
id: 'triggerSave',
title: '',
type: 'trigger-save',
mode: 'trigger',
triggerId: 'typeform_webhook',
},
{
id: 'samplePayload',
title: 'Event Payload Example',
type: 'code',
language: 'json',
defaultValue: JSON.stringify(
{
event_id: '01HQZYX5K2F4G8H9J0K1L2M3N4',
event_type: 'form_response',
form_response: {
form_id: 'ABC123',
token: 'def456ghi789jkl012',
submitted_at: '2025-01-15T10:30:00Z',
landed_at: '2025-01-15T10:28:45Z',
calculated: {
score: 85,
},
variables: [
{
key: 'score',
type: 'number',
number: 4,
},
{
key: 'name',
type: 'text',
text: 'typeform',
},
],
hidden: {
utm_source: 'newsletter',
utm_campaign: 'spring_2025',
},
answers: [
{
type: 'text',
text: 'John Doe',
field: {
id: 'abc123',
type: 'short_text',
ref: 'name_field',
},
},
{
type: 'email',
email: 'john@example.com',
field: {
id: 'def456',
type: 'email',
ref: 'email_field',
},
},
{
type: 'choice',
choice: {
id: 'meFVw3iGRxZB',
label: 'Very Satisfied',
ref: 'ed7f4756-c28f-4374-bb65-bfe5e3235c0c',
},
field: {
id: 'ghi789',
type: 'multiple_choice',
ref: 'satisfaction_field',
},
},
{
type: 'choices',
choices: {
ids: ['eXnU3oA141Cg', 'aTZmZGYV6liX', 'bCdEfGhIjKlM'],
labels: ['TypeScript', 'Python', 'Go'],
refs: [
'238d1802-9921-4687-a37b-5e50f56ece8e',
'd867c542-1e72-4619-908f-aaae38cabb61',
'f123g456-h789-i012-j345-k678l901m234',
],
},
field: {
id: 'jkl012',
type: 'multiple_choice',
ref: 'languages_field',
},
},
{
type: 'number',
number: 5,
field: {
id: 'mno345',
type: 'number',
ref: 'rating_field',
},
},
{
type: 'boolean',
boolean: true,
field: {
id: 'pqr678',
type: 'yes_no',
ref: 'subscribe_field',
},
},
{
type: 'date',
date: '2025-01-20',
field: {
id: 'stu901',
type: 'date',
ref: 'appointment_field',
},
},
],
definition: {
id: 'ABC123',
title: 'Customer Feedback Survey',
fields: [
{
id: 'abc123',
title: 'What is your name?',
type: 'short_text',
ref: 'name_field',
},
{
id: 'def456',
title: 'What is your email?',
type: 'email',
ref: 'email_field',
},
],
endings: [
{
id: 'end123',
title: 'Thank you!',
type: 'thankyou_screen',
},
],
},
ending: {
id: 'end123',
ref: '01GRC8GR2017M6WW347T86VV39',
},
},
},
null,
2
),
readOnly: true,
collapsible: true,
defaultCollapsed: true,
mode: 'trigger',
},
],
outputs: {
event_id: {
type: 'string',
description: 'Unique identifier for this webhook event',
},
event_type: {
type: 'string',
description: 'Type of event (always "form_response" for form submissions)',
},
form_id: {
type: 'string',
description: 'Typeform form identifier',
},
token: {
type: 'string',
description: 'Unique response/submission identifier',
},
submitted_at: {
type: 'string',
description: 'ISO timestamp when the form was submitted',
},
landed_at: {
type: 'string',
description: 'ISO timestamp when the user first landed on the form',
},
calculated: {
score: {
type: 'number',
description: 'Calculated score value',
},
},
variables: {
type: 'array',
description: 'Array of dynamic variables with key, type, and value',
},
hidden: {
type: 'json',
description: 'Hidden fields passed to the form (e.g., UTM parameters)',
},
answers: {
type: 'array',
description:
'Array of respondent answers (only includes answered questions). Each answer contains type, value, and field reference.',
},
definition: {
id: {
type: 'string',
description: 'Form ID',
},
title: {
type: 'string',
description: 'Form title',
},
fields: {
type: 'array',
description: 'Array of form fields',
},
endings: {
type: 'array',
description: 'Array of form endings',
},
},
ending: {
id: {
type: 'string',
description: 'Ending screen ID',
},
ref: {
type: 'string',
description: 'Ending screen reference',
},
},
raw: {
type: 'json',
description: 'Complete original webhook payload from Typeform',
},
},
webhook: {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Typeform-Signature': 'sha256=<signature>',
},
},
}