mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-09 23:17:59 -05:00
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:
@@ -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 |
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(),
|
||||
})
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'],
|
||||
},
|
||||
}
|
||||
|
||||
72
apps/sim/lib/webhooks/env-resolver.ts
Normal file
72
apps/sim/lib/webhooks/env-resolver.ts
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
151
apps/sim/tools/typeform/create_form.ts
Normal file
151
apps/sim/tools/typeform/create_form.ts
Normal 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' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
64
apps/sim/tools/typeform/delete_form.ts
Normal file
64
apps/sim/tools/typeform/delete_form.ts
Normal 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',
|
||||
},
|
||||
},
|
||||
}
|
||||
120
apps/sim/tools/typeform/get_form.ts
Normal file
120
apps/sim/tools/typeform/get_form.ts
Normal 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' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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
|
||||
|
||||
123
apps/sim/tools/typeform/list_forms.ts
Normal file
123
apps/sim/tools/typeform/list_forms.ts
Normal 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' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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']
|
||||
|
||||
101
apps/sim/tools/typeform/update_form.ts
Normal file
101
apps/sim/tools/typeform/update_form.ts
Normal 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',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
1
apps/sim/triggers/typeform/index.ts
Normal file
1
apps/sim/triggers/typeform/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { typeformWebhookTrigger } from './webhook'
|
||||
326
apps/sim/triggers/typeform/webhook.ts
Normal file
326
apps/sim/triggers/typeform/webhook.ts
Normal 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>',
|
||||
},
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user