fix(webhooks): fixed all webhook structures (#935)

* fix for variable format + trig

* fixed slack variable

* microsoft teams working

* fixed outlook, plus added other minor documentation changes and fixed subblock

* removed discord webhook logic

* added airtable logic

* bun run lint

* test

* test again

* test again 2

* test again 3

* test again 4

* test again 4

* test again 4

* bun run lint

* test 5

* test 6

* test 7

* test 7

* test 7

* test 7

* test 7

* test 7

* test 8

* test 9

* test 9

* test 9

* test 10

* test 10

* bun run lint, plus github fixed

* removed some debug statements #935

* testing resolver removing

* testing trig

---------

Co-authored-by: Adam Gough <adamgough@Adams-MacBook-Pro.local>
Co-authored-by: Adam Gough <adamgough@Mac.attlocal.net>
This commit is contained in:
Adam Gough
2025-08-11 12:50:55 -07:00
committed by GitHub
parent 70aeb0c298
commit 41cc0cdadc
24 changed files with 1136 additions and 512 deletions

View File

@@ -352,12 +352,15 @@ async function createAirtableWebhookSubscription(
return // Cannot proceed without base/table IDs
}
const accessToken = await getOAuthToken(userId, 'airtable') // Use 'airtable' as the providerId key
const accessToken = await getOAuthToken(userId, 'airtable')
if (!accessToken) {
logger.warn(
`[${requestId}] Could not retrieve Airtable access token for user ${userId}. Cannot create webhook in Airtable.`
)
return
// Instead of silently returning, throw an error with clear user guidance
throw new Error(
'Airtable account connection required. Please connect your Airtable account in the trigger configuration and try again.'
)
}
const requestOrigin = new URL(request.url).origin

View File

@@ -100,20 +100,41 @@ export async function POST(
return new NextResponse('Failed to read request body', { status: 400 })
}
// Parse the body as JSON
// Parse the body - handle both JSON and form-encoded payloads
let body: any
try {
body = JSON.parse(rawBody)
// Check content type to handle both JSON and form-encoded payloads
const contentType = request.headers.get('content-type') || ''
if (contentType.includes('application/x-www-form-urlencoded')) {
// GitHub sends form-encoded data with JSON in the 'payload' field
const formData = new URLSearchParams(rawBody)
const payloadString = formData.get('payload')
if (!payloadString) {
logger.warn(`[${requestId}] No payload field found in form-encoded data`)
return new NextResponse('Missing payload field', { status: 400 })
}
body = JSON.parse(payloadString)
logger.debug(`[${requestId}] Parsed form-encoded GitHub webhook payload`)
} else {
// Default to JSON parsing
body = JSON.parse(rawBody)
logger.debug(`[${requestId}] Parsed JSON webhook payload`)
}
if (Object.keys(body).length === 0) {
logger.warn(`[${requestId}] Rejecting empty JSON object`)
return new NextResponse('Empty JSON payload', { status: 400 })
}
} catch (parseError) {
logger.error(`[${requestId}] Failed to parse JSON body`, {
logger.error(`[${requestId}] Failed to parse webhook body`, {
error: parseError instanceof Error ? parseError.message : String(parseError),
contentType: request.headers.get('content-type'),
bodyPreview: `${rawBody?.slice(0, 100)}...`,
})
return new NextResponse('Invalid JSON payload', { status: 400 })
return new NextResponse('Invalid payload format', { status: 400 })
}
// Handle Slack challenge

View File

@@ -94,6 +94,8 @@ export function TriggerModal({
setSelectedCredentialId(credentialValue)
if (triggerDef.provider === 'gmail') {
loadGmailLabels(credentialValue)
} else if (triggerDef.provider === 'outlook') {
loadOutlookFolders(credentialValue)
}
}
}
@@ -139,6 +141,30 @@ export function TriggerModal({
}
}
// Load Outlook folders for the selected credential
const loadOutlookFolders = async (credentialId: string) => {
try {
const response = await fetch(`/api/tools/outlook/folders?credentialId=${credentialId}`)
if (response.ok) {
const data = await response.json()
if (data.folders && Array.isArray(data.folders)) {
const folderOptions = data.folders.map((folder: any) => ({
id: folder.id,
name: folder.name,
}))
setDynamicOptions((prev) => ({
...prev,
folderIds: folderOptions,
}))
}
} else {
logger.error('Failed to load Outlook folders:', response.statusText)
}
} catch (error) {
logger.error('Error loading Outlook folders:', error)
}
}
// Generate webhook path and URL
useEffect(() => {
// For triggers that don't use webhooks (like Gmail polling), skip URL generation
@@ -152,15 +178,14 @@ export function TriggerModal({
// If no path exists, generate one automatically
if (!finalPath) {
const timestamp = Date.now()
const randomId = Math.random().toString(36).substring(2, 8)
finalPath = `/${triggerDef.provider}/${timestamp}-${randomId}`
// Use UUID format consistent with other webhooks
finalPath = crypto.randomUUID()
setGeneratedPath(finalPath)
}
if (finalPath) {
const baseUrl = window.location.origin
setWebhookUrl(`${baseUrl}/api/webhooks/trigger${finalPath}`)
setWebhookUrl(`${baseUrl}/api/webhooks/trigger/${finalPath}`)
}
}, [triggerPath, triggerDef.provider, triggerDef.requiresCredentials, triggerDef.webhook])

View File

@@ -1,6 +1,5 @@
export {
AirtableConfig,
DiscordConfig,
GenericConfig,
GithubConfig,
GmailConfig,

View File

@@ -1,125 +0,0 @@
import { Terminal } from 'lucide-react'
import { Alert, AlertDescription, AlertTitle, CodeBlock, Input } from '@/components/ui'
import {
ConfigField,
ConfigSection,
InstructionsSection,
TestResultDisplay,
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/webhook/components'
interface DiscordConfigProps {
webhookName: string
setWebhookName: (name: string) => void
avatarUrl: string
setAvatarUrl: (url: string) => void
isLoadingToken: boolean
testResult: {
success: boolean
message?: string
test?: any
} | null
copied: string | null
copyToClipboard: (text: string, type: string) => void
testWebhook: () => Promise<void>
}
const examplePayload = JSON.stringify(
{
content: 'Hello from Sim!',
username: 'Optional Custom Name',
avatar_url: 'https://example.com/avatar.png',
},
null,
2
)
export function DiscordConfig({
webhookName,
setWebhookName,
avatarUrl,
setAvatarUrl,
isLoadingToken,
testResult,
copied,
copyToClipboard,
testWebhook, // Passed to TestResultDisplay
}: DiscordConfigProps) {
return (
<div className='space-y-4'>
<ConfigSection title='Discord Appearance (Optional)'>
<ConfigField
id='discord-webhook-name'
label='Webhook Name'
description='This name will be displayed as the sender of messages in Discord.'
>
<Input
id='discord-webhook-name'
value={webhookName}
onChange={(e) => setWebhookName(e.target.value)}
placeholder='Sim Bot'
disabled={isLoadingToken}
/>
</ConfigField>
<ConfigField
id='discord-avatar-url'
label='Avatar URL'
description="URL to an image that will be used as the webhook's avatar."
>
<Input
id='discord-avatar-url'
value={avatarUrl}
onChange={(e) => setAvatarUrl(e.target.value)}
placeholder='https://example.com/avatar.png'
disabled={isLoadingToken}
type='url'
/>
</ConfigField>
</ConfigSection>
<TestResultDisplay
testResult={testResult}
copied={copied}
copyToClipboard={copyToClipboard}
showCurlCommand={true} // Discord can be tested via curl
/>
<InstructionsSection
title='Receiving Events from Discord (Incoming Webhook)'
tip='Create a webhook in Discord and paste its URL into the Webhook URL field above.'
>
<ol className='list-inside list-decimal space-y-1'>
<li>Go to Discord Server Settings {'>'} Integrations.</li>
<li>Click "Webhooks" then "New Webhook".</li>
<li>Customize the name and channel.</li>
<li>Click "Copy Webhook URL".</li>
<li>
Paste the copied Discord URL into the main <strong>Webhook URL</strong> field above.
</li>
<li>Your workflow triggers when Discord sends an event to that URL.</li>
</ol>
</InstructionsSection>
<InstructionsSection title='Sending Messages to Discord (Outgoing via this URL)'>
<p>
To send messages <i>to</i> Discord using the Sim Webhook URL (above), make a POST request
with a JSON body like this:
</p>
<CodeBlock language='json' code={examplePayload} className='mt-2 text-sm' />
<ul className='mt-3 list-outside list-disc space-y-1 pl-4'>
<li>Customize message appearance with embeds (see Discord docs).</li>
<li>Override the default username/avatar per request if needed.</li>
</ul>
</InstructionsSection>
<Alert>
<Terminal className='h-4 w-4' />
<AlertTitle>Security Note</AlertTitle>
<AlertDescription>
The Sim Webhook URL allows sending messages <i>to</i> Discord. Treat it like a password.
Don't share it publicly.
</AlertDescription>
</Alert>
</div>
)
}

View File

@@ -1,5 +1,4 @@
export { AirtableConfig } from './airtable'
export { DiscordConfig } from './discord'
export { GenericConfig } from './generic'
export { GithubConfig } from './github'
export { GmailConfig } from './gmail'

View File

@@ -137,6 +137,10 @@ export function SlackConfig({
<li>Paste the Webhook URL (from above) into the "Request URL" field</li>
</ol>
</li>
<li>
Go to <strong>Install App</strong> in the left sidebar and install the app into your
desired Slack workspace and channel.
</li>
<li>Save changes in both Slack and here.</li>
</ol>
</InstructionsSection>

View File

@@ -12,7 +12,6 @@ import { createLogger } from '@/lib/logs/console/logger'
import {
AirtableConfig,
DeleteConfirmDialog,
DiscordConfig,
GenericConfig,
GithubConfig,
GmailConfig,
@@ -83,8 +82,7 @@ export function WebhookModal({
// Provider-specific state
const [whatsappVerificationToken, setWhatsappVerificationToken] = useState('')
const [githubContentType, setGithubContentType] = useState('application/json')
const [discordWebhookName, setDiscordWebhookName] = useState('')
const [discordAvatarUrl, setDiscordAvatarUrl] = useState('')
const [slackSigningSecret, setSlackSigningSecret] = useState('')
const [telegramBotToken, setTelegramBotToken] = useState('')
// Microsoft Teams-specific state
@@ -106,8 +104,7 @@ export function WebhookModal({
secretHeaderName: '',
requireAuth: false,
allowedIps: '',
discordWebhookName: '',
discordAvatarUrl: '',
airtableWebhookSecret: '',
airtableBaseId: '',
airtableTableId: '',
@@ -184,18 +181,6 @@ export function WebhookModal({
const contentType = config.contentType || 'application/json'
setGithubContentType(contentType)
setOriginalValues((prev) => ({ ...prev, githubContentType: contentType }))
} else if (webhookProvider === 'discord') {
const webhookName = config.webhookName || ''
const avatarUrl = config.avatarUrl || ''
setDiscordWebhookName(webhookName)
setDiscordAvatarUrl(avatarUrl)
setOriginalValues((prev) => ({
...prev,
discordWebhookName: webhookName,
discordAvatarUrl: avatarUrl,
}))
} else if (webhookProvider === 'generic') {
// Set general webhook configuration
const token = config.token || ''
@@ -328,9 +313,6 @@ export function WebhookModal({
(webhookProvider === 'whatsapp' &&
whatsappVerificationToken !== originalValues.whatsappVerificationToken) ||
(webhookProvider === 'github' && githubContentType !== originalValues.githubContentType) ||
(webhookProvider === 'discord' &&
(discordWebhookName !== originalValues.discordWebhookName ||
discordAvatarUrl !== originalValues.discordAvatarUrl)) ||
(webhookProvider === 'generic' &&
(generalToken !== originalValues.generalToken ||
secretHeaderName !== originalValues.secretHeaderName ||
@@ -357,8 +339,6 @@ export function WebhookModal({
webhookProvider,
whatsappVerificationToken,
githubContentType,
discordWebhookName,
discordAvatarUrl,
generalToken,
secretHeaderName,
requireAuth,
@@ -393,9 +373,7 @@ export function WebhookModal({
case 'github':
isValid = generalToken.trim() !== ''
break
case 'discord':
isValid = discordWebhookName.trim() !== ''
break
case 'telegram':
isValid = telegramBotToken.trim() !== ''
break
@@ -442,11 +420,6 @@ export function WebhookModal({
return { verificationToken: whatsappVerificationToken }
case 'github':
return { contentType: githubContentType }
case 'discord':
return {
webhookName: discordWebhookName || undefined,
avatarUrl: discordAvatarUrl || undefined,
}
case 'stripe':
return {}
case 'gmail':
@@ -539,8 +512,6 @@ export function WebhookModal({
secretHeaderName,
requireAuth,
allowedIps,
discordWebhookName,
discordAvatarUrl,
slackSigningSecret,
airtableWebhookSecret,
airtableBaseId,
@@ -738,20 +709,7 @@ export function WebhookModal({
setIncludeRawEmail={setIncludeRawEmail}
/>
)
case 'discord':
return (
<DiscordConfig
webhookName={discordWebhookName}
setWebhookName={setDiscordWebhookName}
avatarUrl={discordAvatarUrl}
setAvatarUrl={setDiscordAvatarUrl}
isLoadingToken={isLoadingToken}
testResult={testResult}
copied={copied}
copyToClipboard={copyToClipboard}
testWebhook={testWebhook}
/>
)
case 'stripe':
return (
<StripeConfig

View File

@@ -3,7 +3,6 @@ import { ExternalLink } from 'lucide-react'
import { useParams } from 'next/navigation'
import {
AirtableIcon,
DiscordIcon,
GithubIcon,
GmailIcon,
MicrosoftTeamsIcon,
@@ -46,11 +45,6 @@ export interface GitHubConfig {
contentType: string
}
export interface DiscordConfig {
webhookName?: string
avatarUrl?: string
}
export type StripeConfig = Record<string, never>
export interface GeneralWebhookConfig {
@@ -103,7 +97,6 @@ export interface MicrosoftTeamsConfig {
export type ProviderConfig =
| WhatsAppConfig
| GitHubConfig
| DiscordConfig
| StripeConfig
| GeneralWebhookConfig
| SlackConfig
@@ -219,25 +212,7 @@ export const WEBHOOK_PROVIDERS: { [key: string]: WebhookProvider } = {
},
},
},
discord: {
id: 'discord',
name: 'Discord',
icon: (props) => <DiscordIcon {...props} />,
configFields: {
webhookName: {
type: 'string',
label: 'Webhook Name',
placeholder: 'Enter a name for the webhook',
description: 'Custom name that will appear as the message sender in Discord.',
},
avatarUrl: {
type: 'string',
label: 'Avatar URL',
placeholder: 'https://example.com/avatar.png',
description: 'URL to an image that will be used as the webhook avatar.',
},
},
},
stripe: {
id: 'stripe',
name: 'Stripe',

View File

@@ -10,7 +10,7 @@ import { fetchAndProcessAirtablePayloads, formatWebhookInput } from '@/lib/webho
import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/db-helpers'
import { updateWorkflowRunCounts } from '@/lib/workflows/utils'
import { db } from '@/db'
import { environment as environmentTable, userStats } from '@/db/schema'
import { environment as environmentTable, userStats, webhook } from '@/db/schema'
import { Executor } from '@/executor'
import { Serializer } from '@/serializer'
import { mergeSubblockState } from '@/stores/workflows/server-utils'
@@ -141,10 +141,21 @@ export const webhookExecution = task({
`[${requestId}] Processing Airtable webhook via fetchAndProcessAirtablePayloads`
)
// Load the actual webhook record from database to get providerConfig
const [webhookRecord] = await db
.select()
.from(webhook)
.where(eq(webhook.id, payload.webhookId))
.limit(1)
if (!webhookRecord) {
throw new Error(`Webhook record not found: ${payload.webhookId}`)
}
const webhookData = {
id: payload.webhookId,
provider: payload.provider,
providerConfig: {}, // Will be loaded within fetchAndProcessAirtablePayloads
providerConfig: webhookRecord.providerConfig,
}
// Create a mock workflow object for Airtable processing
@@ -153,12 +164,85 @@ export const webhookExecution = task({
userId: payload.userId,
}
await fetchAndProcessAirtablePayloads(webhookData, mockWorkflow, requestId)
// Get the processed Airtable input
const airtableInput = await fetchAndProcessAirtablePayloads(
webhookData,
mockWorkflow,
requestId
)
// If we got input (changes), execute the workflow like other providers
if (airtableInput) {
logger.info(`[${requestId}] Executing workflow with Airtable changes`)
// Create executor and execute (same as standard webhook flow)
const executor = new Executor({
workflow: serializedWorkflow,
currentBlockStates: processedBlockStates,
envVarValues: decryptedEnvVars,
workflowInput: airtableInput,
workflowVariables,
contextExtensions: {
executionId,
workspaceId: '',
},
})
// Set up logging on the executor
loggingSession.setupExecutor(executor)
// Execute the workflow
const result = await executor.execute(payload.workflowId, payload.blockId)
// Check if we got a StreamingExecution result
const executionResult =
'stream' in result && 'execution' in result ? result.execution : result
logger.info(`[${requestId}] Airtable webhook execution completed`, {
success: executionResult.success,
workflowId: payload.workflowId,
})
// Update workflow run counts on success
if (executionResult.success) {
await updateWorkflowRunCounts(payload.workflowId)
// Track execution in user stats
await db
.update(userStats)
.set({
totalWebhookTriggers: sql`total_webhook_triggers + 1`,
lastActive: sql`now()`,
})
.where(eq(userStats.userId, payload.userId))
}
// Build trace spans and complete logging session
const { traceSpans, totalDuration } = buildTraceSpans(executionResult)
await loggingSession.safeComplete({
endedAt: new Date().toISOString(),
totalDurationMs: totalDuration || 0,
finalOutput: executionResult.output || {},
traceSpans: traceSpans as any,
})
return {
success: executionResult.success,
workflowId: payload.workflowId,
executionId,
output: executionResult.output,
executedAt: new Date().toISOString(),
provider: payload.provider,
}
}
// No changes to process
logger.info(`[${requestId}] No Airtable changes to process`)
await loggingSession.safeComplete({
endedAt: new Date().toISOString(),
totalDurationMs: 0,
finalOutput: { message: 'Airtable webhook processed' },
finalOutput: { message: 'No Airtable changes to process' },
traceSpans: [],
})
@@ -166,7 +250,7 @@ export const webhookExecution = task({
success: true,
workflowId: payload.workflowId,
executionId,
output: { message: 'Airtable webhook processed' },
output: { message: 'No Airtable changes to process' },
executedAt: new Date().toISOString(),
}
}

View File

@@ -213,19 +213,5 @@ export const DiscordBlock: BlockConfig<DiscordResponse> = {
outputs: {
message: { type: 'string', description: 'Message content' },
data: { type: 'json', description: 'Response data' },
// Trigger outputs
content: { type: 'string', description: 'Message content from Discord webhook' },
username: { type: 'string', description: 'Username of the sender (if provided)' },
avatar_url: { type: 'string', description: 'Avatar URL of the sender (if provided)' },
timestamp: { type: 'string', description: 'Timestamp when the webhook was triggered' },
webhook_id: { type: 'string', description: 'Discord webhook identifier' },
webhook_token: { type: 'string', description: 'Discord webhook token' },
guild_id: { type: 'string', description: 'Discord server/guild ID' },
channel_id: { type: 'string', description: 'Discord channel ID where the event occurred' },
embeds: { type: 'string', description: 'Embedded content data (if any)' },
},
triggers: {
enabled: true,
available: ['discord_webhook'],
},
}

View File

@@ -125,6 +125,14 @@ export const MicrosoftTeamsBlock: BlockConfig<MicrosoftTeamsResponse> = {
condition: { field: 'operation', value: ['write_chat', 'write_channel'] },
required: true,
},
{
id: 'triggerConfig',
title: 'Trigger Configuration',
type: 'trigger-config',
layout: 'full',
triggerProvider: 'microsoftteams',
availableTriggers: ['microsoftteams_webhook'],
},
],
tools: {
access: [

View File

@@ -46,6 +46,14 @@ export const WhatsAppBlock: BlockConfig<WhatsAppResponse> = {
password: true,
required: true,
},
{
id: 'triggerConfig',
title: 'Trigger Configuration',
type: 'trigger-config',
layout: 'full',
triggerProvider: 'whatsapp',
availableTriggers: ['whatsapp_webhook'],
},
],
tools: {
access: ['whatsapp_send_message'],

View File

@@ -23,14 +23,172 @@ export class TriggerBlockHandler implements BlockHandler {
async execute(
block: SerializedBlock,
inputs: Record<string, any>,
_context: ExecutionContext
context: ExecutionContext
): Promise<any> {
logger.info(`Executing trigger block: ${block.id} (Type: ${block.metadata?.id})`)
// Trigger blocks don't execute anything - they just pass through their input data
// The input data comes from the webhook execution context or initial workflow inputs
// For trigger blocks, return the starter block's output which contains the workflow input
// This ensures webhook data like message, sender, chat, etc. are accessible
const starterBlock = context.workflow?.blocks?.find((b) => b.metadata?.id === 'starter')
if (starterBlock) {
const starterState = context.blockStates.get(starterBlock.id)
if (starterState?.output && Object.keys(starterState.output).length > 0) {
const starterOutput = starterState.output
// For trigger blocks, return the inputs directly - these contain the webhook/trigger data
// Generic handling for webhook triggers - extract provider-specific data
// Check if this is a webhook execution with nested structure
if (starterOutput.webhook?.data) {
const webhookData = starterOutput.webhook.data
const provider = webhookData.provider
logger.debug(`Processing webhook trigger for block ${block.id}`, {
provider,
blockType: block.metadata?.id,
})
// Extract the flattened properties that should be at root level
const result: any = {
// Always keep the input at root level
input: starterOutput.input,
}
// FIRST: Copy all existing top-level properties (like 'event', 'message', etc.)
// This ensures that properties already flattened in webhook utils are preserved
for (const [key, value] of Object.entries(starterOutput)) {
if (key !== 'webhook' && key !== provider) {
result[key] = value
}
}
// SECOND: Generic extraction logic based on common webhook patterns
// Pattern 1: Provider-specific nested object (telegram, microsoftteams, etc.)
if (provider && starterOutput[provider]) {
// Copy all properties from provider object to root level for direct access
const providerData = starterOutput[provider]
for (const [key, value] of Object.entries(providerData)) {
// Special handling for GitHub provider - copy all properties
if (provider === 'github') {
// For GitHub, copy all properties (objects and primitives) to root level
if (!result[key]) {
// Special handling for complex objects that might have enumeration issues
if (typeof value === 'object' && value !== null) {
try {
// Deep clone complex objects to avoid reference issues
result[key] = JSON.parse(JSON.stringify(value))
} catch (error) {
// If JSON serialization fails, try direct assignment
result[key] = value
}
} else {
result[key] = value
}
}
} else {
// For other providers, keep existing logic (only copy objects)
if (typeof value === 'object' && value !== null) {
// Don't overwrite existing top-level properties
if (!result[key]) {
result[key] = value
}
}
}
}
// Keep nested structure for backwards compatibility
result[provider] = providerData
// Special handling for GitHub complex objects that might not be copied by the main loop
if (provider === 'github') {
// Comprehensive GitHub object extraction from multiple possible sources
const githubObjects = ['repository', 'sender', 'pusher', 'commits', 'head_commit']
for (const objName of githubObjects) {
// ALWAYS try to get the object, even if something exists (fix for conflicts)
let objectValue = null
// Source 1: Direct from provider data
if (providerData[objName]) {
objectValue = providerData[objName]
}
// Source 2: From webhook payload (raw GitHub webhook)
else if (starterOutput.webhook?.data?.payload?.[objName]) {
objectValue = starterOutput.webhook.data.payload[objName]
}
// Source 3: For commits, try parsing JSON string version if no object found
else if (objName === 'commits' && typeof result.commits === 'string') {
try {
objectValue = JSON.parse(result.commits)
} catch (e) {
// Keep as string if parsing fails
objectValue = result.commits
}
}
// FORCE the object to root level (removed the !result[objName] condition)
if (objectValue !== null && objectValue !== undefined) {
result[objName] = objectValue
}
}
}
}
// Pattern 2: Provider data directly in webhook.data (based on actual structure)
else if (provider && webhookData[provider]) {
const providerData = webhookData[provider]
// Extract all provider properties to root level
for (const [key, value] of Object.entries(providerData)) {
if (typeof value === 'object' && value !== null) {
// Don't overwrite existing top-level properties
if (!result[key]) {
result[key] = value
}
}
}
// Keep nested structure for backwards compatibility
result[provider] = providerData
}
// Pattern 3: Email providers with data in webhook.data.payload.email (Gmail, Outlook)
else if (
provider &&
(provider === 'gmail' || provider === 'outlook') &&
webhookData.payload?.email
) {
const emailData = webhookData.payload.email
// Flatten email fields to root level for direct access
for (const [key, value] of Object.entries(emailData)) {
if (!result[key]) {
result[key] = value
}
}
// Keep the email object for backwards compatibility
result.email = emailData
// Also keep timestamp if present in payload
if (webhookData.payload.timestamp) {
result.timestamp = webhookData.payload.timestamp
}
}
// Always keep webhook metadata
if (starterOutput.webhook) result.webhook = starterOutput.webhook
return result
}
logger.debug(`Returning starter block output for trigger block ${block.id}`, {
starterOutputKeys: Object.keys(starterOutput),
})
return starterOutput
}
}
// Fallback to resolved inputs if no starter block output
if (inputs && Object.keys(inputs).length > 0) {
logger.debug(`Returning trigger inputs for block ${block.id}`, {
inputKeys: Object.keys(inputs),

View File

@@ -223,55 +223,81 @@ export function formatWebhookInput(
input = 'Message received'
}
// Create the message object for easier access
const messageObj = {
id: message.message_id,
text: message.text,
caption: message.caption,
date: message.date,
messageType: message.photo
? 'photo'
: message.document
? 'document'
: message.audio
? 'audio'
: message.video
? 'video'
: message.voice
? 'voice'
: message.sticker
? 'sticker'
: message.location
? 'location'
: message.contact
? 'contact'
: message.poll
? 'poll'
: 'text',
raw: message,
}
// Create sender object
const senderObj = message.from
? {
id: message.from.id,
firstName: message.from.first_name,
lastName: message.from.last_name,
username: message.from.username,
languageCode: message.from.language_code,
isBot: message.from.is_bot,
}
: null
// Create chat object
const chatObj = message.chat
? {
id: message.chat.id,
type: message.chat.type,
title: message.chat.title,
username: message.chat.username,
firstName: message.chat.first_name,
lastName: message.chat.last_name,
}
: null
return {
input, // Primary workflow input - the message content
// NEW: Top-level properties for backward compatibility with <blockName.message> syntax
message: messageObj,
sender: senderObj,
chat: chatObj,
updateId: body.update_id,
updateType: body.message
? 'message'
: body.edited_message
? 'edited_message'
: body.channel_post
? 'channel_post'
: body.edited_channel_post
? 'edited_channel_post'
: 'unknown',
// Keep the nested structure for the new telegram.message.text syntax
telegram: {
message: {
id: message.message_id,
text: message.text,
caption: message.caption,
date: message.date,
messageType: message.photo
? 'photo'
: message.document
? 'document'
: message.audio
? 'audio'
: message.video
? 'video'
: message.voice
? 'voice'
: message.sticker
? 'sticker'
: message.location
? 'location'
: message.contact
? 'contact'
: message.poll
? 'poll'
: 'text',
raw: message,
},
sender: message.from
? {
id: message.from.id,
firstName: message.from.first_name,
lastName: message.from.last_name,
username: message.from.username,
languageCode: message.from.language_code,
isBot: message.from.is_bot,
}
: null,
chat: message.chat
? {
id: message.chat.id,
type: message.chat.type,
title: message.chat.title,
username: message.chat.username,
firstName: message.chat.first_name,
lastName: message.chat.last_name,
}
: null,
message: messageObj,
sender: senderObj,
chat: chatObj,
updateId: body.update_id,
updateType: body.message
? 'message'
@@ -331,6 +357,13 @@ export function formatWebhookInput(
return body
}
if (foundWebhook.provider === 'outlook') {
if (body && typeof body === 'object' && 'email' in body) {
return body // { email: {...}, timestamp: ... }
}
return body
}
if (foundWebhook.provider === 'microsoftteams') {
// Microsoft Teams outgoing webhook - Teams sending data to us
const messageText = body?.text || ''
@@ -341,6 +374,19 @@ export function formatWebhookInput(
return {
input: messageText, // Primary workflow input - the message text
// Top-level properties for backward compatibility with <blockName.text> syntax
type: body?.type || 'message',
id: messageId,
timestamp,
localTimestamp: body?.localTimestamp || '',
serviceUrl: body?.serviceUrl || '',
channelId: body?.channelId || '',
from_id: from.id || '',
from_name: from.name || '',
conversation_id: conversation.id || '',
text: messageText,
microsoftteams: {
message: {
id: messageId,
@@ -385,7 +431,210 @@ export function formatWebhookInput(
}
}
// Generic format for Slack and other providers
if (foundWebhook.provider === 'slack') {
// Slack input formatting logic - check for valid event
const event = body?.event
if (event && body?.type === 'event_callback') {
// Extract event text with fallbacks for different event types
let input = ''
if (event.text) {
input = event.text
} else if (event.type === 'app_mention') {
input = 'App mention received'
} else {
input = 'Slack event received'
}
// Create the event object for easier access
const eventObj = {
event_type: event.type || '',
channel: event.channel || '',
channel_name: '', // Could be resolved via additional API calls if needed
user: event.user || '',
user_name: '', // Could be resolved via additional API calls if needed
text: event.text || '',
timestamp: event.ts || event.event_ts || '',
team_id: body.team_id || event.team || '',
event_id: body.event_id || '',
}
return {
input, // Primary workflow input - the event content
// // // Top-level properties for backward compatibility with <blockName.event> syntax
event: eventObj,
// Keep the nested structure for the new slack.event.text syntax
slack: {
event: eventObj,
},
webhook: {
data: {
provider: 'slack',
path: foundWebhook.path,
providerConfig: foundWebhook.providerConfig,
payload: body,
headers: Object.fromEntries(request.headers.entries()),
method: request.method,
},
},
workflowId: foundWorkflow.id,
}
}
// Fallback for unknown Slack event types
logger.warn('Unknown Slack event type', {
type: body?.type,
hasEvent: !!body?.event,
bodyKeys: Object.keys(body || {}),
})
return {
input: 'Slack webhook received',
slack: {
event: {
event_type: body?.event?.type || body?.type || 'unknown',
channel: body?.event?.channel || '',
user: body?.event?.user || '',
text: body?.event?.text || '',
timestamp: body?.event?.ts || '',
team_id: body?.team_id || '',
event_id: body?.event_id || '',
},
},
webhook: {
data: {
provider: 'slack',
path: foundWebhook.path,
providerConfig: foundWebhook.providerConfig,
payload: body,
headers: Object.fromEntries(request.headers.entries()),
method: request.method,
},
},
workflowId: foundWorkflow.id,
}
}
if (foundWebhook.provider === 'github') {
// GitHub webhook input formatting logic
const eventType = request.headers.get('x-github-event') || 'unknown'
const delivery = request.headers.get('x-github-delivery') || ''
// Extract common GitHub properties
const repository = body?.repository || {}
const sender = body?.sender || {}
const action = body?.action || ''
// Build GitHub-specific variables based on the trigger config outputs
const githubData = {
// Event metadata
event_type: eventType,
action: action,
delivery_id: delivery,
// Repository information (avoid 'repository' to prevent conflict with the object)
repository_full_name: repository.full_name || '',
repository_name: repository.name || '',
repository_owner: repository.owner?.login || '',
repository_id: repository.id || '',
repository_url: repository.html_url || '',
// Sender information (avoid 'sender' to prevent conflict with the object)
sender_login: sender.login || '',
sender_id: sender.id || '',
sender_type: sender.type || '',
sender_url: sender.html_url || '',
// Event-specific data
...(body?.ref && {
ref: body.ref,
branch: body.ref?.replace('refs/heads/', '') || '',
}),
...(body?.before && { before: body.before }),
...(body?.after && { after: body.after }),
...(body?.commits && {
commits: JSON.stringify(body.commits),
commit_count: body.commits.length || 0,
}),
...(body?.head_commit && {
commit_message: body.head_commit.message || '',
commit_author: body.head_commit.author?.name || '',
commit_sha: body.head_commit.id || '',
commit_url: body.head_commit.url || '',
}),
...(body?.pull_request && {
pull_request: JSON.stringify(body.pull_request),
pr_number: body.pull_request.number || '',
pr_title: body.pull_request.title || '',
pr_state: body.pull_request.state || '',
pr_url: body.pull_request.html_url || '',
}),
...(body?.issue && {
issue: JSON.stringify(body.issue),
issue_number: body.issue.number || '',
issue_title: body.issue.title || '',
issue_state: body.issue.state || '',
issue_url: body.issue.html_url || '',
}),
...(body?.comment && {
comment: JSON.stringify(body.comment),
comment_body: body.comment.body || '',
comment_url: body.comment.html_url || '',
}),
}
// Set input based on event type for workflow processing
let input = ''
switch (eventType) {
case 'push':
input = `Push to ${githubData.branch || githubData.ref}: ${githubData.commit_message || 'No commit message'}`
break
case 'pull_request':
input = `${action} pull request: ${githubData.pr_title || 'No title'}`
break
case 'issues':
input = `${action} issue: ${githubData.issue_title || 'No title'}`
break
case 'issue_comment':
case 'pull_request_review_comment':
input = `Comment ${action}: ${githubData.comment_body?.slice(0, 100) || 'No comment body'}${(githubData.comment_body?.length || 0) > 100 ? '...' : ''}`
break
default:
input = `GitHub ${eventType} event${action ? ` (${action})` : ''}`
}
return {
input, // Primary workflow input
// Top-level properties for backward compatibility
...githubData,
// GitHub data structured for trigger handler to extract
github: {
// Processed convenience variables
...githubData,
// Raw GitHub webhook payload for direct field access
...body,
},
webhook: {
data: {
provider: 'github',
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: {
data: {

View File

@@ -225,6 +225,15 @@ export class Serializer {
// This catches missing API keys, credentials, and other user-provided values early
// Fields that are user-or-llm will be validated later after parameter merging
// Skip validation if the block is in trigger mode
if (block.triggerMode || blockConfig.category === 'triggers') {
logger.info('Skipping validation for block in trigger mode', {
blockId: block.id,
blockType: block.type,
})
return
}
// Get the tool configuration to check parameter visibility
const toolAccess = blockConfig.tools?.access
if (!toolAccess || toolAccess.length === 0) {

View File

@@ -6,10 +6,14 @@ export const airtableWebhookTrigger: TriggerConfig = {
name: 'Airtable Webhook',
provider: 'airtable',
description:
'Trigger workflow from Airtable record changes like create, update, and delete events',
'Trigger workflow from Airtable record changes like create, update, and delete events (requires Airtable credentials)',
version: '1.0.0',
icon: AirtableIcon,
// Airtable requires OAuth credentials to create webhooks
requiresCredentials: true,
credentialProvider: 'airtable',
configFields: {
baseId: {
type: 'string',
@@ -69,12 +73,12 @@ export const airtableWebhookTrigger: TriggerConfig = {
},
instructions: [
'Connect your Airtable account using the "Select Airtable credential" button above.',
'Ensure you have provided the correct Base ID and Table ID above.',
'Sim will automatically configure the webhook in your Airtable account when you save.',
'Any changes made to records in the specified table will trigger this workflow.',
"If 'Include Full Record Data' is enabled, the entire record will be sent; otherwise, only the changed fields are sent.",
'You can find your Base ID in the Airtable URL or API documentation for your base.',
'Table IDs can be found in the Airtable API documentation or by inspecting the table URL.',
'You can find your Base ID in the Airtable URL: https://airtable.com/[baseId]/...',
'You can find your Table ID by clicking on the table name and looking in the URL.',
'The webhook will trigger whenever records are created, updated, or deleted in the specified table.',
'Make sure your Airtable account has appropriate permissions for the specified base.',
],
samplePayload: {

View File

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

View File

@@ -1,95 +0,0 @@
import { DiscordIcon } from '@/components/icons'
import type { TriggerConfig } from '../types'
export const discordWebhookTrigger: TriggerConfig = {
id: 'discord_webhook',
name: 'Discord Webhook',
provider: 'discord',
description: 'Trigger workflow from Discord webhook events and send messages to Discord channels',
version: '1.0.0',
icon: DiscordIcon,
configFields: {
webhookName: {
type: 'string',
label: 'Webhook Name',
placeholder: 'Sim Bot',
description: 'This name will be displayed as the sender of messages in Discord.',
required: false,
},
avatarUrl: {
type: 'string',
label: 'Avatar URL',
placeholder: 'https://example.com/avatar.png',
description: "URL to an image that will be used as the webhook's avatar.",
required: false,
},
},
outputs: {
content: {
type: 'string',
description: 'Message content from Discord webhook',
},
username: {
type: 'string',
description: 'Username of the sender (if provided)',
},
avatar_url: {
type: 'string',
description: 'Avatar URL of the sender (if provided)',
},
timestamp: {
type: 'string',
description: 'Timestamp when the webhook was triggered',
},
webhook_id: {
type: 'string',
description: 'Discord webhook identifier',
},
webhook_token: {
type: 'string',
description: 'Discord webhook token',
},
guild_id: {
type: 'string',
description: 'Discord server/guild ID',
},
channel_id: {
type: 'string',
description: 'Discord channel ID where the event occurred',
},
embeds: {
type: 'string',
description: 'Embedded content data (if any)',
},
},
instructions: [
'Go to Discord Server Settings > Integrations.',
'Click "Webhooks" then "New Webhook".',
'Customize the name and channel.',
'Click "Copy Webhook URL".',
'Paste the copied Discord URL into the main <strong>Webhook URL</strong> field above.',
'Your workflow triggers when Discord sends an event to that URL.',
],
samplePayload: {
content: 'Hello from Sim!',
username: 'Optional Custom Name',
avatar_url: 'https://example.com/avatar.png',
timestamp: new Date().toISOString(),
webhook_id: '1234567890123456789',
webhook_token: 'example-webhook-token',
guild_id: '0987654321098765432',
channel_id: '1122334455667788990',
embeds: [],
},
webhook: {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
},
}

View File

@@ -37,37 +37,10 @@ export const githubWebhookTrigger: TriggerConfig = {
},
outputs: {
action: {
type: 'string',
description: 'The action that was performed (e.g., opened, closed, synchronize)',
},
event_type: {
type: 'string',
description: 'Type of GitHub event (e.g., push, pull_request, issues)',
},
repository: {
type: 'string',
description: 'Repository full name (owner/repo)',
},
repository_name: {
type: 'string',
description: 'Repository name only',
},
repository_owner: {
type: 'string',
description: 'Repository owner username or organization',
},
sender: {
type: 'string',
description: 'Username of the user who triggered the event',
},
sender_id: {
type: 'string',
description: 'User ID of the sender',
},
// GitHub webhook payload structure - maps 1:1 to actual GitHub webhook body
ref: {
type: 'string',
description: 'Git reference (for push events)',
description: 'Git reference (e.g., refs/heads/fix/telegram-wh)',
},
before: {
type: 'string',
@@ -77,34 +50,414 @@ export const githubWebhookTrigger: TriggerConfig = {
type: 'string',
description: 'SHA of the commit after the push',
},
created: {
type: 'boolean',
description: 'Whether the push created the reference',
},
deleted: {
type: 'boolean',
description: 'Whether the push deleted the reference',
},
forced: {
type: 'boolean',
description: 'Whether the push was forced',
},
base_ref: {
type: 'string',
description: 'Base reference for the push',
},
compare: {
type: 'string',
description: 'URL to compare the changes',
},
repository: {
id: {
type: 'number',
description: 'Repository ID',
},
node_id: {
type: 'string',
description: 'Repository node ID',
},
name: {
type: 'string',
description: 'Repository name',
},
full_name: {
type: 'string',
description: 'Repository full name (owner/repo)',
},
private: {
type: 'boolean',
description: 'Whether the repository is private',
},
html_url: {
type: 'string',
description: 'Repository HTML URL',
},
fork: {
type: 'boolean',
description: 'Whether the repository is a fork',
},
url: {
type: 'string',
description: 'Repository API URL',
},
created_at: {
type: 'number',
description: 'Repository creation timestamp',
},
updated_at: {
type: 'string',
description: 'Repository last updated time',
},
pushed_at: {
type: 'number',
description: 'Repository last push timestamp',
},
git_url: {
type: 'string',
description: 'Repository git URL',
},
ssh_url: {
type: 'string',
description: 'Repository SSH URL',
},
clone_url: {
type: 'string',
description: 'Repository clone URL',
},
homepage: {
type: 'string',
description: 'Repository homepage URL',
},
size: {
type: 'number',
description: 'Repository size',
},
stargazers_count: {
type: 'number',
description: 'Number of stars',
},
watchers_count: {
type: 'number',
description: 'Number of watchers',
},
language: {
type: 'string',
description: 'Primary programming language',
},
forks_count: {
type: 'number',
description: 'Number of forks',
},
archived: {
type: 'boolean',
description: 'Whether the repository is archived',
},
disabled: {
type: 'boolean',
description: 'Whether the repository is disabled',
},
open_issues_count: {
type: 'number',
description: 'Number of open issues',
},
topics: {
type: 'array',
description: 'Repository topics',
},
visibility: {
type: 'string',
description: 'Repository visibility (public, private)',
},
forks: {
type: 'number',
description: 'Number of forks',
},
open_issues: {
type: 'number',
description: 'Number of open issues',
},
watchers: {
type: 'number',
description: 'Number of watchers',
},
default_branch: {
type: 'string',
description: 'Default branch name',
},
stargazers: {
type: 'number',
description: 'Number of stargazers',
},
master_branch: {
type: 'string',
description: 'Master branch name',
},
owner: {
name: {
type: 'string',
description: 'Owner name',
},
email: {
type: 'string',
description: 'Owner email',
},
login: {
type: 'string',
description: 'Owner username',
},
id: {
type: 'number',
description: 'Owner ID',
},
node_id: {
type: 'string',
description: 'Owner node ID',
},
avatar_url: {
type: 'string',
description: 'Owner avatar URL',
},
gravatar_id: {
type: 'string',
description: 'Owner gravatar ID',
},
url: {
type: 'string',
description: 'Owner API URL',
},
html_url: {
type: 'string',
description: 'Owner profile URL',
},
user_view_type: {
type: 'string',
description: 'User view type',
},
site_admin: {
type: 'boolean',
description: 'Whether the owner is a site admin',
},
},
license: {
type: 'object',
description: 'Repository license information',
key: {
type: 'string',
description: 'License key (e.g., apache-2.0)',
},
name: {
type: 'string',
description: 'License name',
},
spdx_id: {
type: 'string',
description: 'SPDX license identifier',
},
url: {
type: 'string',
description: 'License URL',
},
node_id: {
type: 'string',
description: 'License node ID',
},
},
},
pusher: {
type: 'object',
description: 'Information about who pushed the changes',
name: {
type: 'string',
description: 'Pusher name',
},
email: {
type: 'string',
description: 'Pusher email',
},
},
sender: {
login: {
type: 'string',
description: 'Sender username',
},
id: {
type: 'number',
description: 'Sender ID',
},
node_id: {
type: 'string',
description: 'Sender node ID',
},
avatar_url: {
type: 'string',
description: 'Sender avatar URL',
},
gravatar_id: {
type: 'string',
description: 'Sender gravatar ID',
},
url: {
type: 'string',
description: 'Sender API URL',
},
html_url: {
type: 'string',
description: 'Sender profile URL',
},
user_view_type: {
type: 'string',
description: 'User view type',
},
site_admin: {
type: 'boolean',
description: 'Whether the sender is a site admin',
},
},
commits: {
type: 'string',
description: 'Array of commit objects (for push events)',
type: 'array',
description: 'Array of commit objects',
id: {
type: 'string',
description: 'Commit SHA',
},
tree_id: {
type: 'string',
description: 'Tree SHA',
},
distinct: {
type: 'boolean',
description: 'Whether the commit is distinct',
},
message: {
type: 'string',
description: 'Commit message',
},
timestamp: {
type: 'string',
description: 'Commit timestamp',
},
url: {
type: 'string',
description: 'Commit URL',
},
author: {
type: 'object',
description: 'Commit author',
name: {
type: 'string',
description: 'Author name',
},
email: {
type: 'string',
description: 'Author email',
},
},
committer: {
type: 'object',
description: 'Commit committer',
name: {
type: 'string',
description: 'Committer name',
},
email: {
type: 'string',
description: 'Committer email',
},
},
added: {
type: 'array',
description: 'Array of added files',
},
removed: {
type: 'array',
description: 'Array of removed files',
},
modified: {
type: 'array',
description: 'Array of modified files',
},
},
pull_request: {
type: 'string',
description: 'Pull request object (for pull_request events)',
head_commit: {
type: 'object',
description: 'Head commit object',
id: {
type: 'string',
description: 'Commit SHA',
},
tree_id: {
type: 'string',
description: 'Tree SHA',
},
distinct: {
type: 'boolean',
description: 'Whether the commit is distinct',
},
message: {
type: 'string',
description: 'Commit message',
},
timestamp: {
type: 'string',
description: 'Commit timestamp',
},
url: {
type: 'string',
description: 'Commit URL',
},
author: {
type: 'object',
description: 'Commit author',
name: {
type: 'string',
description: 'Author name',
},
email: {
type: 'string',
description: 'Author email',
},
},
committer: {
type: 'object',
description: 'Commit committer',
name: {
type: 'string',
description: 'Committer name',
},
email: {
type: 'string',
description: 'Committer email',
},
},
added: {
type: 'array',
description: 'Array of added files',
},
removed: {
type: 'array',
description: 'Array of removed files',
},
modified: {
type: 'array',
description: 'Array of modified files',
},
},
issue: {
// Convenient flat fields for easy access
event_type: {
type: 'string',
description: 'Issue object (for issues events)',
description: 'Type of GitHub event (e.g., push, pull_request, issues)',
},
comment: {
action: {
type: 'string',
description: 'Comment object (for comment events)',
description: 'The action that was performed (e.g., opened, closed, synchronize)',
},
branch: {
type: 'string',
description: 'Branch name extracted from ref',
},
commit_message: {
type: 'string',
description: 'Latest commit message',
},
commit_author: {
type: 'string',
description: 'Author of the latest commit',
},
},
instructions: [

View File

@@ -1,7 +1,6 @@
// Import trigger definitions
import { airtableWebhookTrigger } from './airtable'
import { discordWebhookTrigger } from './discord'
import { genericWebhookTrigger } from './generic'
import { githubWebhookTrigger } from './github'
import { gmailPollingTrigger } from './gmail'
@@ -17,7 +16,6 @@ import { whatsappWebhookTrigger } from './whatsapp'
export const TRIGGER_REGISTRY: TriggerRegistry = {
slack_webhook: slackWebhookTrigger,
airtable_webhook: airtableWebhookTrigger,
discord_webhook: discordWebhookTrigger,
generic_webhook: genericWebhookTrigger,
github_webhook: githubWebhookTrigger,
gmail_poller: gmailPollingTrigger,

View File

@@ -21,41 +21,43 @@ export const slackWebhookTrigger: TriggerConfig = {
},
outputs: {
event_type: {
type: 'string',
description: 'Type of Slack event (e.g., app_mention, message)',
},
channel: {
type: 'string',
description: 'Slack channel ID where the event occurred',
},
channel_name: {
type: 'string',
description: 'Human-readable channel name',
},
user: {
type: 'string',
description: 'User ID who triggered the event',
},
user_name: {
type: 'string',
description: 'Username who triggered the event',
},
text: {
type: 'string',
description: 'Message text content',
},
timestamp: {
type: 'string',
description: 'Event timestamp',
},
team_id: {
type: 'string',
description: 'Slack workspace/team ID',
},
event_id: {
type: 'string',
description: 'Unique event identifier',
event: {
event_type: {
type: 'string',
description: 'Type of Slack event (e.g., app_mention, message)',
},
channel: {
type: 'string',
description: 'Slack channel ID where the event occurred',
},
channel_name: {
type: 'string',
description: 'Human-readable channel name',
},
user: {
type: 'string',
description: 'User ID who triggered the event',
},
user_name: {
type: 'string',
description: 'Username who triggered the event',
},
text: {
type: 'string',
description: 'Message text content',
},
timestamp: {
type: 'string',
description: 'Event timestamp',
},
team_id: {
type: 'string',
description: 'Slack workspace/team ID',
},
event_id: {
type: 'string',
description: 'Unique event identifier',
},
},
},

View File

@@ -21,53 +21,55 @@ export const telegramWebhookTrigger: TriggerConfig = {
},
outputs: {
update_id: {
type: 'number',
description: 'Unique identifier for the update',
},
message_id: {
type: 'number',
description: 'Unique message identifier',
},
from_id: {
type: 'number',
description: 'User ID who sent the message',
},
from_username: {
type: 'string',
description: 'Username of the sender',
},
from_first_name: {
type: 'string',
description: 'First name of the sender',
},
from_last_name: {
type: 'string',
description: 'Last name of the sender',
},
chat_id: {
type: 'number',
description: 'Unique identifier for the chat',
},
chat_type: {
type: 'string',
description: 'Type of chat (private, group, supergroup, channel)',
},
chat_title: {
type: 'string',
description: 'Title of the chat (for groups and channels)',
},
text: {
type: 'string',
description: 'Message text content',
},
date: {
type: 'number',
description: 'Date the message was sent (Unix timestamp)',
},
entities: {
type: 'string',
description: 'Special entities in the message (mentions, hashtags, etc.) as JSON string',
message: {
update_id: {
type: 'number',
description: 'Unique identifier for the update',
},
message_id: {
type: 'number',
description: 'Unique message identifier',
},
from_id: {
type: 'number',
description: 'User ID who sent the message',
},
from_username: {
type: 'string',
description: 'Username of the sender',
},
from_first_name: {
type: 'string',
description: 'First name of the sender',
},
from_last_name: {
type: 'string',
description: 'Last name of the sender',
},
chat_id: {
type: 'number',
description: 'Unique identifier for the chat',
},
chat_type: {
type: 'string',
description: 'Type of chat (private, group, supergroup, channel)',
},
chat_title: {
type: 'string',
description: 'Title of the chat (for groups and channels)',
},
text: {
type: 'string',
description: 'Message text content',
},
date: {
type: 'number',
description: 'Date the message was sent (Unix timestamp)',
},
entities: {
type: 'string',
description: 'Special entities in the message (mentions, hashtags, etc.) as JSON string',
},
},
},

View File

@@ -49,7 +49,7 @@ export const whatsappWebhookTrigger: TriggerConfig = {
},
instructions: [
'Go to your <a href="https://developers.facebook.com/apps/" target="_blank" rel="noopener noreferrer" class="text-primary underline transition-colors hover:text-primary/80">Meta for Developers Apps</a> page.',
'Go to your <a href="https://developers.facebook.com/apps/" target="_blank" rel="noopener noreferrer" class="text-primary underline transition-colors hover:text-primary/80">Meta for Developers Apps</a> page and navigate to the "Build with us" --> "App Events" section.',
'If you don\'t have an app:<br><ul class="mt-1 ml-5 list-disc"><li>Create an app from scratch</li><li>Give it a name and select your workspace</li></ul>',
'Select your App, then navigate to WhatsApp > Configuration.',
'Find the Webhooks section and click "Edit".',