diff --git a/apps/docs/content/docs/en/tools/google_forms.mdx b/apps/docs/content/docs/en/tools/google_forms.mdx new file mode 100644 index 0000000000..d095484049 --- /dev/null +++ b/apps/docs/content/docs/en/tools/google_forms.mdx @@ -0,0 +1,86 @@ +--- +title: Google Forms +description: Read responses from a Google Form +--- + +import { BlockInfoCard } from "@/components/ui/block-info-card" + + + + + + + + + + + + `} +/> + +{/* MANUAL-CONTENT-START:intro */} +[Google Forms](https://forms.google.com) is Google's online survey and form tool that allows users to create forms, collect responses, and analyze results. As part of Google's productivity suite, Google Forms makes it easy to gather information, feedback, and data from users. + +Learn how to integrate the Google Forms tool in Sim to automatically read and process form responses in your workflows. This tutorial walks you through connecting Google Forms, retrieving responses, and using collected data to power automation. Perfect for syncing survey results, registrations, or feedback with your agents in real-time. + +With Google Forms, you can: + +- **Create surveys and forms**: Design custom forms for feedback, registration, quizzes, and more +- **Collect responses automatically**: Gather data from users in real-time +- **Analyze results**: View responses in Google Forms or export to Google Sheets for further analysis +- **Collaborate easily**: Share forms and work with others to build and review questions +- **Integrate with other Google services**: Connect with Google Sheets, Drive, and more + +In Sim, the Google Forms integration enables your agents to programmatically access form responses. This allows for powerful automation scenarios such as processing survey data, triggering workflows based on new submissions, and syncing form results with other tools. Your agents can fetch all responses for a form, retrieve a specific response, and use the data to drive intelligent automation. By connecting Sim with Google Forms, you can automate data collection, streamline feedback processing, and incorporate form responses into your agent's capabilities. +{/* MANUAL-CONTENT-END */} + +## Usage Instructions + +Integrate Google Forms into your workflow. Provide a Form ID to list responses, or specify a Response ID to fetch a single response. Requires OAuth. + + + +## Tools + +### `google_forms_get_responses` + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| formId | string | Yes | The ID of the Google Form | +| responseId | string | No | If provided, returns this specific response | +| pageSize | number | No | Max responses to return (service may return fewer). Defaults to 5000 | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `data` | json | Response or list of responses | + + + +## Notes + +- Category: `tools` +- Type: `google_forms` diff --git a/apps/sim/app/api/webhooks/trigger/[path]/route.ts b/apps/sim/app/api/webhooks/trigger/[path]/route.ts index 06482be20d..76ca29e834 100644 --- a/apps/sim/app/api/webhooks/trigger/[path]/route.ts +++ b/apps/sim/app/api/webhooks/trigger/[path]/route.ts @@ -201,6 +201,37 @@ export async function POST( } } + // Handle Google Forms shared-secret authentication (Apps Script forwarder) + if (foundWebhook.provider === 'google_forms') { + const providerConfig = (foundWebhook.providerConfig as Record) || {} + const expectedToken = providerConfig.token as string | undefined + const secretHeaderName = providerConfig.secretHeaderName as string | undefined + + if (expectedToken) { + let isTokenValid = false + + if (secretHeaderName) { + const headerValue = request.headers.get(secretHeaderName.toLowerCase()) + if (headerValue === expectedToken) { + isTokenValid = true + } + } else { + const authHeader = request.headers.get('authorization') + if (authHeader?.toLowerCase().startsWith('bearer ')) { + const token = authHeader.substring(7) + if (token === expectedToken) { + isTokenValid = true + } + } + } + + if (!isTokenValid) { + logger.warn(`[${requestId}] Google Forms webhook authentication failed for path: ${path}`) + return new NextResponse('Unauthorized - Invalid secret', { status: 401 }) + } + } + } + // Handle generic webhook authentication if enabled if (foundWebhook.provider === 'generic') { const providerConfig = (foundWebhook.providerConfig as Record) || {} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deploy-modal/components/chat-deploy/components/auth-selector.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deploy-modal/components/chat-deploy/components/auth-selector.tsx index d4584b691c..d377036659 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deploy-modal/components/chat-deploy/components/auth-selector.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deploy-modal/components/chat-deploy/components/auth-selector.tsx @@ -144,8 +144,7 @@ export function AuthSelector({ className={cn( 'group h-7 w-7 rounded-md p-0', 'text-muted-foreground/60 transition-all duration-200', - 'hover:scale-105 hover:bg-muted/50 hover:text-foreground', - 'active:scale-95', + 'hover:bg-muted/50 hover:text-foreground', 'disabled:cursor-not-allowed disabled:opacity-50', 'focus-visible:ring-2 focus-visible:ring-muted-foreground/20 focus-visible:ring-offset-1' )} @@ -162,8 +161,7 @@ export function AuthSelector({ className={cn( 'group h-7 w-7 rounded-md p-0', 'text-muted-foreground/60 transition-all duration-200', - 'hover:scale-105 hover:bg-muted/50 hover:text-foreground', - 'active:scale-95', + 'hover:bg-muted/50 hover:text-foreground', 'disabled:cursor-not-allowed disabled:opacity-30', 'focus-visible:ring-2 focus-visible:ring-muted-foreground/20 focus-visible:ring-offset-1' )} @@ -171,7 +169,7 @@ export function AuthSelector({ {copySuccess ? ( ) : ( - + )} Copy password @@ -184,15 +182,14 @@ export function AuthSelector({ className={cn( 'group h-7 w-7 rounded-md p-0', 'text-muted-foreground/60 transition-all duration-200', - 'hover:scale-105 hover:bg-muted/50 hover:text-foreground', - 'active:scale-95', + 'hover:bg-muted/50 hover:text-foreground', 'focus-visible:ring-2 focus-visible:ring-muted-foreground/20 focus-visible:ring-offset-1' )} > {showPassword ? ( - + ) : ( - + )} {showPassword ? 'Hide password' : 'Show password'} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/webhook-settings/webhook-settings.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/webhook-settings/webhook-settings.tsx index 2dd4b2be99..a4ce370cad 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/webhook-settings/webhook-settings.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/webhook-settings/webhook-settings.tsx @@ -605,7 +605,7 @@ export function WebhookSettings({ workflowId, open, onOpenChange }: WebhookSetti {copySuccess[webhook.id] ? ( ) : ( - + )} Copy webhook URL @@ -643,7 +643,7 @@ export function WebhookSettings({ workflowId, open, onOpenChange }: WebhookSetti 'focus-visible:ring-2 focus-visible:ring-muted-foreground/20 focus-visible:ring-offset-1' )} > - + Test webhook @@ -665,7 +665,7 @@ export function WebhookSettings({ workflowId, open, onOpenChange }: WebhookSetti 'focus-visible:ring-2 focus-visible:ring-muted-foreground/20 focus-visible:ring-offset-1' )} > - + Edit webhook @@ -687,7 +687,7 @@ export function WebhookSettings({ workflowId, open, onOpenChange }: WebhookSetti 'focus-visible:ring-2 focus-visible:ring-muted-foreground/20 focus-visible:ring-offset-1' )} > - + Delete webhook @@ -874,8 +874,7 @@ export function WebhookSettings({ workflowId, open, onOpenChange }: WebhookSetti className={cn( 'group h-7 w-7 rounded-md p-0', 'text-muted-foreground/60 transition-all duration-200', - 'hover:scale-105 hover:bg-muted/50 hover:text-foreground', - 'active:scale-95', + 'hover:bg-muted/50 hover:text-foreground', 'disabled:cursor-not-allowed disabled:opacity-30', 'focus-visible:ring-2 focus-visible:ring-muted-foreground/20 focus-visible:ring-offset-1' )} @@ -883,7 +882,7 @@ export function WebhookSettings({ workflowId, open, onOpenChange }: WebhookSetti {copySuccess.form ? ( ) : ( - + )} Copy secret @@ -901,16 +900,15 @@ export function WebhookSettings({ workflowId, open, onOpenChange }: WebhookSetti className={cn( 'group h-7 w-7 rounded-md p-0', 'text-muted-foreground/60 transition-all duration-200', - 'hover:scale-105 hover:bg-muted/50 hover:text-foreground', - 'active:scale-95', + 'hover:bg-muted/50 hover:text-foreground', 'focus-visible:ring-2 focus-visible:ring-muted-foreground/20 focus-visible:ring-offset-1' )} onClick={() => setShowSecret(!showSecret)} > {showSecret ? ( - + ) : ( - + )} {showSecret ? 'Hide secret' : 'Show secret'} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/short-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/short-input.tsx index 87e22b5dcb..bb95ae7504 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/short-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/short-input.tsx @@ -366,7 +366,7 @@ export function ShortInput({
{selectedValues.length > 0 ? ( @@ -209,6 +209,7 @@ export function TriggerConfigSection({ placeholder={fieldDef.placeholder} value={value} onChange={(e) => onChange(fieldId, Number(e.target.value))} + className='h-9 rounded-[8px]' /> {fieldDef.description && (

{fieldDef.description}

@@ -247,52 +248,61 @@ export function TriggerConfigSection({ )}
-
-
- onChange(fieldId, e.target.value)} - className={cn( - 'h-10 flex-1', - isSecret ? 'pr-10' : '', - 'focus-visible:ring-2 focus-visible:ring-primary/20' - )} - /> - {isSecret && ( +
+ onChange(fieldId, e.target.value)} + className={cn( + 'h-9 rounded-[8px]', + isSecret ? 'pr-32' : '', + 'focus-visible:ring-2 focus-visible:ring-primary/20' + )} + /> + {isSecret && ( +
- )} -
- {isSecret && ( - + +
)}
@@ -329,31 +339,37 @@ export function TriggerConfigSection({
-
-
- (e.target as HTMLInputElement).select()} - /> -
- +
)} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/trigger-config/components/trigger-instructions.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/trigger-config/components/trigger-instructions.tsx index 966bfd6f87..115e228524 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/trigger-config/components/trigger-instructions.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/trigger-config/components/trigger-instructions.tsx @@ -1,4 +1,6 @@ -import { Notice } from '@/components/ui' +import { useState } from 'react' +import { Check, ChevronDown, ChevronRight, Copy } from 'lucide-react' +import { Button, Notice } from '@/components/ui' import { cn } from '@/lib/utils' import { JSONView } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/console/components' import type { TriggerConfig } from '@/triggers/types' @@ -8,6 +10,7 @@ interface TriggerInstructionsProps { webhookUrl: string samplePayload: any triggerDef: TriggerConfig + config?: Record } export function TriggerInstructions({ @@ -15,7 +18,30 @@ export function TriggerInstructions({ webhookUrl, samplePayload, triggerDef, + config = {}, }: TriggerInstructionsProps) { + const [copied, setCopied] = useState(false) + const [isExpanded, setIsExpanded] = useState(false) + + const token = (config as any)?.token as string | undefined + const secretHeaderName = (config as any)?.secretHeaderName as string | undefined + const formId = (config as any)?.formId || '' + const headerLine = secretHeaderName + ? `{ '${secretHeaderName}': TOKEN }` + : "{ Authorization: 'Bearer ' + TOKEN }" + + const googleFormsSnippet = token + ? `const WEBHOOK_URL = '${webhookUrl || ''}';\nconst TOKEN = '${token}'; // from Sim Trigger Configuration\nconst FORM_ID = '${formId}'; // optional but recommended\n\nfunction onFormSubmit(e) {\n var answers = {};\n var formResponse = e && e.response;\n if (formResponse && typeof formResponse.getItemResponses === 'function') {\n var itemResponses = formResponse.getItemResponses() || [];\n for (var i = 0; i < itemResponses.length; i++) {\n var ir = itemResponses[i];\n var question = ir.getItem().getTitle();\n var value = ir.getResponse();\n if (Array.isArray(value)) {\n value = value.length === 1 ? value[0] : value;\n }\n answers[question] = value;\n }\n } else if (e && e.namedValues) {\n var namedValues = e.namedValues || {};\n for (var k in namedValues) {\n var v = namedValues[k];\n answers[k] = Array.isArray(v) ? (v.length === 1 ? v[0] : v) : v;\n }\n }\n\n var payload = {\n provider: 'googleforms',\n formId: FORM_ID || undefined,\n responseId: Utilities.getUuid(),\n createTime: new Date().toISOString(),\n lastSubmittedTime: new Date().toISOString(),\n answers: answers,\n raw: e || {}\n };\n\n UrlFetchApp.fetch(WEBHOOK_URL, {\n method: 'post',\n contentType: 'application/json',\n payload: JSON.stringify(payload),\n headers: ${headerLine},\n muteHttpExceptions: true\n });\n}` + : `const WEBHOOK_URL = '${webhookUrl || ''}';\nconst FORM_ID = '${formId}'; // optional but recommended\n\nfunction onFormSubmit(e) {\n var answers = {};\n var formResponse = e && e.response;\n if (formResponse && typeof formResponse.getItemResponses === 'function') {\n var itemResponses = formResponse.getItemResponses() || [];\n for (var i = 0; i < itemResponses.length; i++) {\n var ir = itemResponses[i];\n var question = ir.getItem().getTitle();\n var value = ir.getResponse();\n if (Array.isArray(value)) {\n value = value.length === 1 ? value[0] : value;\n }\n answers[question] = value;\n }\n } else if (e && e.namedValues) {\n var namedValues = e.namedValues || {};\n for (var k in namedValues) {\n var v = namedValues[k];\n answers[k] = Array.isArray(v) ? (v.length === 1 ? v[0] : v) : v;\n }\n }\n\n var payload = {\n provider: 'googleforms',\n formId: FORM_ID || undefined,\n responseId: Utilities.getUuid(),\n createTime: new Date().toISOString(),\n lastSubmittedTime: new Date().toISOString(),\n answers: answers,\n raw: e || {}\n };\n\n UrlFetchApp.fetch(WEBHOOK_URL, {\n method: 'post',\n contentType: 'application/json',\n payload: JSON.stringify(payload),\n muteHttpExceptions: true\n });\n}` + + const copySnippet = async () => { + try { + await navigator.clipboard.writeText(googleFormsSnippet) + setCopied(true) + setTimeout(() => setCopied(false), 1500) + } catch {} + } + return (
@@ -27,6 +53,59 @@ export function TriggerInstructions({ ))}
+ + {triggerDef.provider === 'google_forms' && ( +
+
+
setIsExpanded(!isExpanded)} + > +
+ {isExpanded ? ( + + ) : ( + + )} + {triggerDef.icon && ( + + )} +
Apps Script snippet
+
+ {isExpanded && ( + + )} +
+ {isExpanded && ( +
+
+                    {googleFormsSnippet}
+                  
+
+ )} +
+
+ )}
) : null } - title={`${triggerDef.provider.charAt(0).toUpperCase() + triggerDef.provider.slice(1)} Event Payload Example`} + title={`${triggerDef.provider.charAt(0).toUpperCase() + triggerDef.provider.slice(1).replace(/_/g, ' ')} Event Payload Example`} > Your workflow will receive a payload similar to this when a subscribed event occurs.
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/trigger-config/components/trigger-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/trigger-config/components/trigger-modal.tsx index da632152aa..3f446833a2 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/trigger-config/components/trigger-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/trigger-config/components/trigger-modal.tsx @@ -336,7 +336,7 @@ export function TriggerModal({ return ( e.preventDefault()} > @@ -419,7 +419,7 @@ export function TriggerModal({ onClick={handleDelete} disabled={isDeleting || isSaving} size='default' - className='h-10' + className='h-9 rounded-[8px]' > {isDeleting ? (
@@ -431,7 +431,12 @@ export function TriggerModal({ )}
-