Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ bun run dev:sockets
- **Docs**: [Fumadocs](https://fumadocs.vercel.app/)
- **Monorepo**: [Turborepo](https://turborepo.org/)
- **Realtime**: [Socket.io](https://socket.io/)
- **Background Jobs**: [Trigger.dev](https://trigger.dev/)

## Contributing

Expand Down
11 changes: 6 additions & 5 deletions apps/docs/content/docs/blocks/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@
"agent",
"api",
"condition",
"function",
"evaluator",
"router",
"response",
"workflow",
"function",
"loop",
"parallel"
"parallel",
"response",
"router",
"webhook_trigger",
"workflow"
]
}
113 changes: 113 additions & 0 deletions apps/docs/content/docs/blocks/webhook_trigger.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
---
title: Webhook Trigger
description: Trigger workflow execution from external webhooks
---

import { Callout } from 'fumadocs-ui/components/callout'
import { Step, Steps } from 'fumadocs-ui/components/steps'
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
import { Card, Cards } from 'fumadocs-ui/components/card'
import { ThemeImage } from '@/components/ui/theme-image'

The Webhook Trigger block allows external services to trigger your workflow execution through HTTP webhooks. Unlike starter blocks, webhook triggers are pure input sources that start workflows without requiring manual intervention.

<ThemeImage
lightSrc="/static/light/webhooktrigger-light.png"
darkSrc="/static/dark/webhooktrigger-dark.png"
alt="Webhook Trigger Block"
width={350}
height={175}
/>

<Callout>
Webhook triggers cannot receive incoming connections and do not expose webhook data to the workflow. They serve as pure execution triggers.
</Callout>

## Overview

The Webhook Trigger block enables you to:

<Steps>
<Step>
<strong>Receive external triggers</strong>: Accept HTTP requests from external services
</Step>
<Step>
<strong>Support multiple providers</strong>: Handle webhooks from Slack, Gmail, GitHub, and more
</Step>
<Step>
<strong>Start workflows automatically</strong>: Execute workflows without manual intervention
</Step>
<Step>
<strong>Provide secure endpoints</strong>: Generate unique webhook URLs for each trigger
</Step>
</Steps>

## How It Works

The Webhook Trigger block operates as a pure input source:

1. **Generate Endpoint** - Creates a unique webhook URL when configured
2. **Receive Request** - Accepts HTTP POST requests from external services
3. **Trigger Execution** - Starts the workflow when a valid request is received

## Configuration Options

### Webhook Provider

Choose from supported service providers:

<Cards>
<Card title="Slack" href="#">
Receive events from Slack apps and bots
</Card>
<Card title="Gmail" href="#">
Handle email-based triggers and notifications
</Card>
<Card title="Airtable" href="#">
Respond to database changes
</Card>
<Card title="Telegram" href="#">
Process bot messages and updates
</Card>
<Card title="WhatsApp" href="#">
Handle messaging events
</Card>
<Card title="GitHub" href="#">
Process repository events and pull requests
</Card>
<Card title="Discord" href="#">
Respond to Discord server events
</Card>
<Card title="Stripe" href="#">
Handle payment and subscription events
</Card>
</Cards>

### Generic Webhooks

For custom integrations or services not listed above, use the **Generic** provider. This option accepts HTTP POST requests from any client and provides flexible authentication options:

- **Optional Authentication** - Configure Bearer token or custom header authentication
- **IP Restrictions** - Limit access to specific IP addresses
- **Request Deduplication** - Automatic duplicate request detection using content hashing
- **Flexible Headers** - Support for custom authentication header names

The Generic provider is ideal for internal services, custom applications, or third-party tools that need to trigger workflows via standard HTTP requests.

### Webhook Configuration

Configure provider-specific settings:

- **Webhook URL** - Automatically generated unique endpoint
- **Provider Settings** - Authentication and validation options
- **Security** - Built-in rate limiting and provider-specific authentication

## Best Practices

- **Use unique webhook URLs** for each integration to maintain security
- **Configure proper authentication** when supported by the provider
- **Keep workflows independent** of webhook payload structure
- **Test webhook endpoints** before deploying to production
- **Monitor webhook delivery** through provider dashboards


Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions apps/sim/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,5 @@ next-env.d.ts

# Uploads
/uploads

.trigger
32 changes: 32 additions & 0 deletions apps/sim/app/api/chat/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,38 @@ import type { NextResponse } from 'next/server'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { env } from '@/lib/env'

// Mock all the problematic imports that cause timeouts
vi.mock('@/db', () => ({
db: {
select: vi.fn(),
update: vi.fn(),
},
}))

vi.mock('@/lib/utils', () => ({
decryptSecret: vi.fn().mockResolvedValue({ decrypted: 'test-secret' }),
}))

vi.mock('@/lib/logs/enhanced-logging-session', () => ({
EnhancedLoggingSession: vi.fn().mockImplementation(() => ({
safeStart: vi.fn().mockResolvedValue(undefined),
safeComplete: vi.fn().mockResolvedValue(undefined),
safeCompleteWithError: vi.fn().mockResolvedValue(undefined),
})),
}))

vi.mock('@/executor', () => ({
Executor: vi.fn(),
}))

vi.mock('@/serializer', () => ({
Serializer: vi.fn(),
}))

vi.mock('@/stores/workflows/server-utils', () => ({
mergeSubblockState: vi.fn().mockReturnValue({}),
}))

describe('Chat API Utils', () => {
beforeEach(() => {
vi.resetModules()
Expand Down
110 changes: 110 additions & 0 deletions apps/sim/app/api/jobs/[jobId]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { runs } from '@trigger.dev/sdk/v3'
import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console-logger'
import { db } from '@/db'
import { apiKey as apiKeyTable } from '@/db/schema'
import { createErrorResponse } from '../../workflows/utils'

const logger = createLogger('TaskStatusAPI')

export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ jobId: string }> }
) {
const { jobId: taskId } = await params
const requestId = crypto.randomUUID().slice(0, 8)

try {
logger.debug(`[${requestId}] Getting status for task: ${taskId}`)

// Try session auth first (for web UI)
const session = await getSession()
let authenticatedUserId: string | null = session?.user?.id || null

if (!authenticatedUserId) {
const apiKeyHeader = request.headers.get('x-api-key')
if (apiKeyHeader) {
const [apiKeyRecord] = await db
.select({ userId: apiKeyTable.userId })
.from(apiKeyTable)
.where(eq(apiKeyTable.key, apiKeyHeader))
.limit(1)

if (apiKeyRecord) {
authenticatedUserId = apiKeyRecord.userId
}
}
}

if (!authenticatedUserId) {
return createErrorResponse('Authentication required', 401)
}

// Fetch task status from Trigger.dev
const run = await runs.retrieve(taskId)

logger.debug(`[${requestId}] Task ${taskId} status: ${run.status}`)

// Map Trigger.dev status to our format
const statusMap = {
QUEUED: 'queued',
WAITING_FOR_DEPLOY: 'queued',
EXECUTING: 'processing',
RESCHEDULED: 'processing',
FROZEN: 'processing',
COMPLETED: 'completed',
CANCELED: 'cancelled',
FAILED: 'failed',
CRASHED: 'failed',
INTERRUPTED: 'failed',
SYSTEM_FAILURE: 'failed',
EXPIRED: 'failed',
} as const

const mappedStatus = statusMap[run.status as keyof typeof statusMap] || 'unknown'

// Build response based on status
const response: any = {
success: true,
taskId,
status: mappedStatus,
metadata: {
startedAt: run.startedAt,
},
}

// Add completion details if finished
if (mappedStatus === 'completed') {
response.output = run.output // This contains the workflow execution results
response.metadata.completedAt = run.finishedAt
response.metadata.duration = run.durationMs
}

// Add error details if failed
if (mappedStatus === 'failed') {
response.error = run.error
response.metadata.completedAt = run.finishedAt
response.metadata.duration = run.durationMs
}

// Add progress info if still processing
if (mappedStatus === 'processing' || mappedStatus === 'queued') {
response.estimatedDuration = 180000 // 3 minutes max from our config
}

return NextResponse.json(response)
} catch (error: any) {
logger.error(`[${requestId}] Error fetching task status:`, error)

if (error.message?.includes('not found') || error.status === 404) {
return createErrorResponse('Task not found', 404)
}

return createErrorResponse('Failed to fetch task status', 500)
}
}

// TODO: Implement task cancellation via Trigger.dev API if needed
// export async function DELETE() { ... }
23 changes: 23 additions & 0 deletions apps/sim/app/api/schedules/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,29 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
})
}

if (action === 'disable' || (body.status && body.status === 'disabled')) {
if (schedule.status === 'disabled') {
return NextResponse.json({ message: 'Schedule is already disabled' }, { status: 200 })
}

const now = new Date()

await db
.update(workflowSchedule)
.set({
status: 'disabled',
updatedAt: now,
nextRunAt: null, // Clear next run time when disabled
})
.where(eq(workflowSchedule.id, scheduleId))

logger.info(`[${requestId}] Disabled schedule: ${scheduleId}`)

return NextResponse.json({
message: 'Schedule disabled successfully',
})
}

logger.warn(`[${requestId}] Unsupported update action for schedule: ${scheduleId}`)
return NextResponse.json({ error: 'Unsupported update action' }, { status: 400 })
} catch (error) {
Expand Down
Loading