mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-14 01:18:15 -05:00
Compare commits
6 Commits
improvemen
...
fix/add-de
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
42fa5b2198 | ||
|
|
8ccf4548a6 | ||
|
|
ebbe67aae3 | ||
|
|
2b49d15ec8 | ||
|
|
3d037c9b74 | ||
|
|
eb52f69efd |
@@ -552,6 +552,53 @@ All fields automatically have:
|
||||
- `mode: 'trigger'` - Only shown in trigger mode
|
||||
- `condition: { field: 'selectedTriggerId', value: triggerId }` - Only shown when this trigger is selected
|
||||
|
||||
## Trigger Outputs & Webhook Input Formatting
|
||||
|
||||
### Important: Two Sources of Truth
|
||||
|
||||
There are two related but separate concerns:
|
||||
|
||||
1. **Trigger `outputs`** - Schema/contract defining what fields SHOULD be available. Used by UI for tag dropdown.
|
||||
2. **`formatWebhookInput`** - Implementation that transforms raw webhook payload into actual data. Located in `apps/sim/lib/webhooks/utils.server.ts`.
|
||||
|
||||
**These MUST be aligned.** The fields returned by `formatWebhookInput` should match what's defined in trigger `outputs`. If they differ:
|
||||
- Tag dropdown shows fields that don't exist (broken variable resolution)
|
||||
- Or actual data has fields not shown in dropdown (users can't discover them)
|
||||
|
||||
### When to Add a formatWebhookInput Handler
|
||||
|
||||
- **Simple providers**: If the raw webhook payload structure already matches your outputs, you don't need a handler. The generic fallback returns `body` directly.
|
||||
- **Complex providers**: If you need to transform, flatten, extract nested data, compute fields, or handle conditional logic, add a handler.
|
||||
|
||||
### Adding a Handler
|
||||
|
||||
In `apps/sim/lib/webhooks/utils.server.ts`, add a handler block:
|
||||
|
||||
```typescript
|
||||
if (foundWebhook.provider === '{service}') {
|
||||
// Transform raw webhook body to match trigger outputs
|
||||
return {
|
||||
eventType: body.type,
|
||||
resourceId: body.data?.id || '',
|
||||
timestamp: body.created_at,
|
||||
resource: body.data,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Key rules:**
|
||||
- Return fields that match your trigger `outputs` definition exactly
|
||||
- No wrapper objects like `webhook: { data: ... }` or `{service}: { ... }`
|
||||
- No duplication (don't spread body AND add individual fields)
|
||||
- Use `null` for missing optional data, not empty objects with empty strings
|
||||
|
||||
### Verify Alignment
|
||||
|
||||
Run the alignment checker:
|
||||
```bash
|
||||
bunx scripts/check-trigger-alignment.ts {service}
|
||||
```
|
||||
|
||||
## Trigger Outputs
|
||||
|
||||
Trigger outputs use the same schema as block outputs (NOT tool outputs).
|
||||
@@ -649,6 +696,11 @@ export const {service}WebhookTrigger: TriggerConfig = {
|
||||
- [ ] Added `delete{Service}Webhook` function to `provider-subscriptions.ts`
|
||||
- [ ] Added provider to `cleanupExternalWebhook` function
|
||||
|
||||
### Webhook Input Formatting
|
||||
- [ ] Added handler in `apps/sim/lib/webhooks/utils.server.ts` (if custom formatting needed)
|
||||
- [ ] Handler returns fields matching trigger `outputs` exactly
|
||||
- [ ] Run `bunx scripts/check-trigger-alignment.ts {service}` to verify alignment
|
||||
|
||||
### Testing
|
||||
- [ ] Run `bun run type-check` to verify no TypeScript errors
|
||||
- [ ] Restart dev server to pick up new triggers
|
||||
|
||||
@@ -356,6 +356,9 @@ const WorkflowContent = React.memo(() => {
|
||||
/** Stores source node/handle info when a connection drag starts for drop-on-block detection. */
|
||||
const connectionSourceRef = useRef<{ nodeId: string; handleId: string } | null>(null)
|
||||
|
||||
/** Tracks whether onConnect successfully handled the connection (ReactFlow pattern). */
|
||||
const connectionCompletedRef = useRef(false)
|
||||
|
||||
/** Stores start positions for multi-node drag undo/redo recording. */
|
||||
const multiNodeDragStartRef = useRef<Map<string, { x: number; y: number; parentId?: string }>>(
|
||||
new Map()
|
||||
@@ -2214,7 +2217,8 @@ const WorkflowContent = React.memo(() => {
|
||||
)
|
||||
|
||||
/**
|
||||
* Captures the source handle when a connection drag starts
|
||||
* Captures the source handle when a connection drag starts.
|
||||
* Resets connectionCompletedRef to track if onConnect handles this connection.
|
||||
*/
|
||||
const onConnectStart = useCallback((_event: any, params: any) => {
|
||||
const handleId: string | undefined = params?.handleId
|
||||
@@ -2223,6 +2227,7 @@ const WorkflowContent = React.memo(() => {
|
||||
nodeId: params?.nodeId,
|
||||
handleId: params?.handleId,
|
||||
}
|
||||
connectionCompletedRef.current = false
|
||||
}, [])
|
||||
|
||||
/** Handles new edge connections with container boundary validation. */
|
||||
@@ -2283,6 +2288,7 @@ const WorkflowContent = React.memo(() => {
|
||||
isInsideContainer: true,
|
||||
},
|
||||
})
|
||||
connectionCompletedRef.current = true
|
||||
return
|
||||
}
|
||||
|
||||
@@ -2311,6 +2317,7 @@ const WorkflowContent = React.memo(() => {
|
||||
}
|
||||
: undefined,
|
||||
})
|
||||
connectionCompletedRef.current = true
|
||||
}
|
||||
},
|
||||
[addEdge, getNodes, blocks]
|
||||
@@ -2319,8 +2326,9 @@ const WorkflowContent = React.memo(() => {
|
||||
/**
|
||||
* Handles connection drag end. Detects if the edge was dropped over a block
|
||||
* and automatically creates a connection to that block's target handle.
|
||||
* Only creates a connection if ReactFlow didn't already handle it (e.g., when
|
||||
* dropping on the block body instead of a handle).
|
||||
*
|
||||
* Uses connectionCompletedRef to check if onConnect already handled this connection
|
||||
* (ReactFlow pattern for distinguishing handle-to-handle vs handle-to-body drops).
|
||||
*/
|
||||
const onConnectEnd = useCallback(
|
||||
(event: MouseEvent | TouchEvent) => {
|
||||
@@ -2332,6 +2340,12 @@ const WorkflowContent = React.memo(() => {
|
||||
return
|
||||
}
|
||||
|
||||
// If onConnect already handled this connection, skip (handle-to-handle case)
|
||||
if (connectionCompletedRef.current) {
|
||||
connectionSourceRef.current = null
|
||||
return
|
||||
}
|
||||
|
||||
// Get cursor position in flow coordinates
|
||||
const clientPos = 'changedTouches' in event ? event.changedTouches[0] : event
|
||||
const flowPosition = screenToFlowPosition({
|
||||
@@ -2342,25 +2356,14 @@ const WorkflowContent = React.memo(() => {
|
||||
// Find node under cursor
|
||||
const targetNode = findNodeAtPosition(flowPosition)
|
||||
|
||||
// Create connection if valid target found AND edge doesn't already exist
|
||||
// ReactFlow's onConnect fires first when dropping on a handle, so we check
|
||||
// if that connection already exists to avoid creating duplicates.
|
||||
// IMPORTANT: We must read directly from the store (not React state) because
|
||||
// the store update from ReactFlow's onConnect may not have triggered a
|
||||
// React re-render yet when this callback runs (typically 1-2ms later).
|
||||
// Create connection if valid target found (handle-to-body case)
|
||||
if (targetNode && targetNode.id !== source.nodeId) {
|
||||
const currentEdges = useWorkflowStore.getState().edges
|
||||
const edgeAlreadyExists = currentEdges.some(
|
||||
(e) => e.source === source.nodeId && e.target === targetNode.id
|
||||
)
|
||||
if (!edgeAlreadyExists) {
|
||||
onConnect({
|
||||
source: source.nodeId,
|
||||
sourceHandle: source.handleId,
|
||||
target: targetNode.id,
|
||||
targetHandle: 'target',
|
||||
})
|
||||
}
|
||||
onConnect({
|
||||
source: source.nodeId,
|
||||
sourceHandle: source.handleId,
|
||||
target: targetNode.id,
|
||||
targetHandle: 'target',
|
||||
})
|
||||
}
|
||||
|
||||
connectionSourceRef.current = null
|
||||
|
||||
@@ -172,7 +172,7 @@ export const ScheduleBlock: BlockConfig = {
|
||||
{ label: 'Melbourne (UTC+10)', id: 'Australia/Melbourne' },
|
||||
{ label: 'Auckland (UTC+12)', id: 'Pacific/Auckland' },
|
||||
],
|
||||
value: () => 'UTC',
|
||||
value: () => Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
required: false,
|
||||
mode: 'trigger',
|
||||
condition: { field: 'scheduleType', value: ['minutes', 'hourly'], not: true },
|
||||
|
||||
@@ -313,6 +313,26 @@ export const getBlock = (type: string): BlockConfig | undefined => {
|
||||
return registry[normalized]
|
||||
}
|
||||
|
||||
export const getLatestBlock = (baseType: string): BlockConfig | undefined => {
|
||||
const normalized = baseType.replace(/-/g, '_')
|
||||
|
||||
const versionedKeys = Object.keys(registry).filter((key) => {
|
||||
const match = key.match(new RegExp(`^${normalized}_v(\\d+)$`))
|
||||
return match !== null
|
||||
})
|
||||
|
||||
if (versionedKeys.length > 0) {
|
||||
const sorted = versionedKeys.sort((a, b) => {
|
||||
const versionA = Number.parseInt(a.match(/_v(\d+)$/)?.[1] || '0', 10)
|
||||
const versionB = Number.parseInt(b.match(/_v(\d+)$/)?.[1] || '0', 10)
|
||||
return versionB - versionA
|
||||
})
|
||||
return registry[sorted[0]]
|
||||
}
|
||||
|
||||
return registry[normalized]
|
||||
}
|
||||
|
||||
export const getBlockByToolName = (toolName: string): BlockConfig | undefined => {
|
||||
return Object.values(registry).find((block) => block.tools?.access?.includes(toolName))
|
||||
}
|
||||
|
||||
@@ -378,21 +378,10 @@ function buildManualTriggerOutput(
|
||||
}
|
||||
|
||||
function buildIntegrationTriggerOutput(
|
||||
finalInput: unknown,
|
||||
_finalInput: unknown,
|
||||
workflowInput: unknown
|
||||
): NormalizedBlockOutput {
|
||||
const base: NormalizedBlockOutput = isPlainObject(workflowInput)
|
||||
? ({ ...(workflowInput as Record<string, unknown>) } as NormalizedBlockOutput)
|
||||
: {}
|
||||
|
||||
if (isPlainObject(finalInput)) {
|
||||
Object.assign(base, finalInput as Record<string, unknown>)
|
||||
base.input = { ...(finalInput as Record<string, unknown>) }
|
||||
} else {
|
||||
base.input = finalInput
|
||||
}
|
||||
|
||||
return mergeFilesIntoOutput(base, workflowInput)
|
||||
return isPlainObject(workflowInput) ? (workflowInput as NormalizedBlockOutput) : {}
|
||||
}
|
||||
|
||||
function extractSubBlocks(block: SerializedBlock): Record<string, unknown> | undefined {
|
||||
|
||||
@@ -22,7 +22,7 @@ import { useUndoRedoStore } from '@/stores/undo-redo'
|
||||
import { useWorkflowDiffStore } from '@/stores/workflow-diff/store'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
|
||||
import { mergeSubblockState, normalizeName } from '@/stores/workflows/utils'
|
||||
import { filterNewEdges, mergeSubblockState, normalizeName } from '@/stores/workflows/utils'
|
||||
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
|
||||
import type { BlockState, Loop, Parallel, Position } from '@/stores/workflows/workflow/types'
|
||||
|
||||
@@ -242,7 +242,10 @@ export function useCollaborativeWorkflow() {
|
||||
case EDGES_OPERATIONS.BATCH_ADD_EDGES: {
|
||||
const { edges } = payload
|
||||
if (Array.isArray(edges) && edges.length > 0) {
|
||||
workflowStore.batchAddEdges(edges)
|
||||
const newEdges = filterNewEdges(edges, workflowStore.edges)
|
||||
if (newEdges.length > 0) {
|
||||
workflowStore.batchAddEdges(newEdges)
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
@@ -976,6 +979,9 @@ export function useCollaborativeWorkflow() {
|
||||
|
||||
if (edges.length === 0) return false
|
||||
|
||||
const newEdges = filterNewEdges(edges, workflowStore.edges)
|
||||
if (newEdges.length === 0) return false
|
||||
|
||||
const operationId = crypto.randomUUID()
|
||||
|
||||
addToQueue({
|
||||
@@ -983,16 +989,16 @@ export function useCollaborativeWorkflow() {
|
||||
operation: {
|
||||
operation: EDGES_OPERATIONS.BATCH_ADD_EDGES,
|
||||
target: OPERATION_TARGETS.EDGES,
|
||||
payload: { edges },
|
||||
payload: { edges: newEdges },
|
||||
},
|
||||
workflowId: activeWorkflowId || '',
|
||||
userId: session?.user?.id || 'unknown',
|
||||
})
|
||||
|
||||
workflowStore.batchAddEdges(edges)
|
||||
workflowStore.batchAddEdges(newEdges)
|
||||
|
||||
if (!options?.skipUndoRedo) {
|
||||
edges.forEach((edge) => undoRedo.recordAddEdge(edge.id))
|
||||
newEdges.forEach((edge) => undoRedo.recordAddEdge(edge.id))
|
||||
}
|
||||
|
||||
return true
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getBlock } from '@/blocks/registry'
|
||||
import { getLatestBlock } from '@/blocks/registry'
|
||||
import { getAllTriggers } from '@/triggers'
|
||||
|
||||
export interface TriggerOption {
|
||||
@@ -49,22 +49,13 @@ export function getTriggerOptions(): TriggerOption[] {
|
||||
continue
|
||||
}
|
||||
|
||||
const block = getBlock(provider)
|
||||
const block = getLatestBlock(provider)
|
||||
|
||||
if (block) {
|
||||
providerMap.set(provider, {
|
||||
value: provider,
|
||||
label: block.name, // Use block's display name (e.g., "Slack", "GitHub")
|
||||
color: block.bgColor || '#6b7280', // Use block's hex color, fallback to gray
|
||||
})
|
||||
} else {
|
||||
const label = formatProviderName(provider)
|
||||
providerMap.set(provider, {
|
||||
value: provider,
|
||||
label,
|
||||
color: '#6b7280', // gray fallback
|
||||
})
|
||||
}
|
||||
providerMap.set(provider, {
|
||||
value: provider,
|
||||
label: block?.name || formatProviderName(provider),
|
||||
color: block?.bgColor || '#6b7280',
|
||||
})
|
||||
}
|
||||
|
||||
const integrationOptions = Array.from(providerMap.values()).sort((a, b) =>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2290,7 +2290,7 @@ describe('hasWorkflowChanged', () => {
|
||||
block1: createBlock('block1', {
|
||||
type: 'starter',
|
||||
subBlocks: {
|
||||
triggerConfig: { value: { event: 'push' } },
|
||||
model: { value: 'gpt-4' },
|
||||
webhookId: { value: null },
|
||||
},
|
||||
}),
|
||||
@@ -2302,7 +2302,7 @@ describe('hasWorkflowChanged', () => {
|
||||
block1: createBlock('block1', {
|
||||
type: 'starter',
|
||||
subBlocks: {
|
||||
triggerConfig: { value: { event: 'push' } },
|
||||
model: { value: 'gpt-4' },
|
||||
webhookId: { value: 'wh_123456' },
|
||||
},
|
||||
}),
|
||||
@@ -2318,7 +2318,7 @@ describe('hasWorkflowChanged', () => {
|
||||
block1: createBlock('block1', {
|
||||
type: 'starter',
|
||||
subBlocks: {
|
||||
triggerConfig: { value: { event: 'push' } },
|
||||
model: { value: 'gpt-4' },
|
||||
triggerPath: { value: '' },
|
||||
},
|
||||
}),
|
||||
@@ -2330,7 +2330,7 @@ describe('hasWorkflowChanged', () => {
|
||||
block1: createBlock('block1', {
|
||||
type: 'starter',
|
||||
subBlocks: {
|
||||
triggerConfig: { value: { event: 'push' } },
|
||||
model: { value: 'gpt-4' },
|
||||
triggerPath: { value: '/api/webhooks/abc123' },
|
||||
},
|
||||
}),
|
||||
@@ -2346,7 +2346,7 @@ describe('hasWorkflowChanged', () => {
|
||||
block1: createBlock('block1', {
|
||||
type: 'starter',
|
||||
subBlocks: {
|
||||
triggerConfig: { value: { event: 'push' } },
|
||||
model: { value: 'gpt-4' },
|
||||
webhookId: { value: null },
|
||||
triggerPath: { value: '' },
|
||||
},
|
||||
@@ -2359,7 +2359,7 @@ describe('hasWorkflowChanged', () => {
|
||||
block1: createBlock('block1', {
|
||||
type: 'starter',
|
||||
subBlocks: {
|
||||
triggerConfig: { value: { event: 'push' } },
|
||||
model: { value: 'gpt-4' },
|
||||
webhookId: { value: 'wh_123456' },
|
||||
triggerPath: { value: '/api/webhooks/abc123' },
|
||||
},
|
||||
@@ -2371,14 +2371,18 @@ describe('hasWorkflowChanged', () => {
|
||||
})
|
||||
|
||||
it.concurrent(
|
||||
'should detect change when triggerConfig differs but runtime metadata also differs',
|
||||
'should detect change when actual config differs but runtime metadata also differs',
|
||||
() => {
|
||||
// Test that when a real config field changes along with runtime metadata,
|
||||
// the change is still detected. Using 'model' as the config field since
|
||||
// triggerConfig is now excluded from comparison (individual trigger fields
|
||||
// are compared separately).
|
||||
const deployedState = createWorkflowState({
|
||||
blocks: {
|
||||
block1: createBlock('block1', {
|
||||
type: 'starter',
|
||||
subBlocks: {
|
||||
triggerConfig: { value: { event: 'push' } },
|
||||
model: { value: 'gpt-4' },
|
||||
webhookId: { value: null },
|
||||
},
|
||||
}),
|
||||
@@ -2390,7 +2394,7 @@ describe('hasWorkflowChanged', () => {
|
||||
block1: createBlock('block1', {
|
||||
type: 'starter',
|
||||
subBlocks: {
|
||||
triggerConfig: { value: { event: 'pull_request' } },
|
||||
model: { value: 'gpt-4o' },
|
||||
webhookId: { value: 'wh_123456' },
|
||||
},
|
||||
}),
|
||||
@@ -2402,8 +2406,12 @@ describe('hasWorkflowChanged', () => {
|
||||
)
|
||||
|
||||
it.concurrent(
|
||||
'should not detect change when runtime metadata is added to current state',
|
||||
'should not detect change when triggerConfig differs (individual fields compared separately)',
|
||||
() => {
|
||||
// triggerConfig is excluded from comparison because:
|
||||
// 1. Individual trigger fields are stored as separate subblocks and compared individually
|
||||
// 2. The client populates triggerConfig with default values from trigger definitions,
|
||||
// which aren't present in the deployed state, causing false positive change detection
|
||||
const deployedState = createWorkflowState({
|
||||
blocks: {
|
||||
block1: createBlock('block1', {
|
||||
@@ -2420,7 +2428,36 @@ describe('hasWorkflowChanged', () => {
|
||||
block1: createBlock('block1', {
|
||||
type: 'starter',
|
||||
subBlocks: {
|
||||
triggerConfig: { value: { event: 'push' } },
|
||||
triggerConfig: { value: { event: 'pull_request', extraField: true } },
|
||||
},
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
expect(hasWorkflowChanged(currentState, deployedState)).toBe(false)
|
||||
}
|
||||
)
|
||||
|
||||
it.concurrent(
|
||||
'should not detect change when runtime metadata is added to current state',
|
||||
() => {
|
||||
const deployedState = createWorkflowState({
|
||||
blocks: {
|
||||
block1: createBlock('block1', {
|
||||
type: 'starter',
|
||||
subBlocks: {
|
||||
model: { value: 'gpt-4' },
|
||||
},
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
const currentState = createWorkflowState({
|
||||
blocks: {
|
||||
block1: createBlock('block1', {
|
||||
type: 'starter',
|
||||
subBlocks: {
|
||||
model: { value: 'gpt-4' },
|
||||
webhookId: { value: 'wh_123456' },
|
||||
triggerPath: { value: '/api/webhooks/abc123' },
|
||||
},
|
||||
@@ -2440,7 +2477,7 @@ describe('hasWorkflowChanged', () => {
|
||||
block1: createBlock('block1', {
|
||||
type: 'starter',
|
||||
subBlocks: {
|
||||
triggerConfig: { value: { event: 'push' } },
|
||||
model: { value: 'gpt-4' },
|
||||
webhookId: { value: 'wh_old123' },
|
||||
triggerPath: { value: '/api/webhooks/old' },
|
||||
},
|
||||
@@ -2453,7 +2490,7 @@ describe('hasWorkflowChanged', () => {
|
||||
block1: createBlock('block1', {
|
||||
type: 'starter',
|
||||
subBlocks: {
|
||||
triggerConfig: { value: { event: 'push' } },
|
||||
model: { value: 'gpt-4' },
|
||||
},
|
||||
}),
|
||||
},
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
import type { Edge } from 'reactflow'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
|
||||
export function filterNewEdges(edgesToAdd: Edge[], currentEdges: Edge[]): Edge[] {
|
||||
return edgesToAdd.filter((edge) => {
|
||||
if (edge.source === edge.target) return false
|
||||
return !currentEdges.some(
|
||||
(e) =>
|
||||
e.source === edge.source &&
|
||||
e.sourceHandle === edge.sourceHandle &&
|
||||
e.target === edge.target &&
|
||||
e.targetHandle === edge.targetHandle
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
import { getBlockOutputs } from '@/lib/workflows/blocks/block-outputs'
|
||||
import { getBlock } from '@/blocks'
|
||||
import { normalizeName } from '@/executor/constants'
|
||||
|
||||
@@ -297,7 +297,7 @@ describe('workflow store', () => {
|
||||
expectEdgeConnects(edges, 'block-1', 'block-2')
|
||||
})
|
||||
|
||||
it('should not add duplicate edges', () => {
|
||||
it('should not add duplicate connections', () => {
|
||||
const { addBlock, batchAddEdges } = useWorkflowStore.getState()
|
||||
|
||||
addBlock('block-1', 'starter', 'Start', { x: 0, y: 0 })
|
||||
@@ -309,17 +309,6 @@ describe('workflow store', () => {
|
||||
const state = useWorkflowStore.getState()
|
||||
expectEdgeCount(state, 1)
|
||||
})
|
||||
|
||||
it('should prevent self-referencing edges', () => {
|
||||
const { addBlock, batchAddEdges } = useWorkflowStore.getState()
|
||||
|
||||
addBlock('block-1', 'function', 'Self', { x: 0, y: 0 })
|
||||
|
||||
batchAddEdges([{ id: 'e1', source: 'block-1', target: 'block-1' }])
|
||||
|
||||
const state = useWorkflowStore.getState()
|
||||
expectEdgeCount(state, 0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('batchRemoveEdges', () => {
|
||||
|
||||
@@ -9,7 +9,12 @@ import { getBlock } from '@/blocks'
|
||||
import type { SubBlockConfig } from '@/blocks/types'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
|
||||
import { getUniqueBlockName, mergeSubblockState, normalizeName } from '@/stores/workflows/utils'
|
||||
import {
|
||||
filterNewEdges,
|
||||
getUniqueBlockName,
|
||||
mergeSubblockState,
|
||||
normalizeName,
|
||||
} from '@/stores/workflows/utils'
|
||||
import type {
|
||||
Position,
|
||||
SubBlockState,
|
||||
@@ -496,25 +501,11 @@ export const useWorkflowStore = create<WorkflowStore>()(
|
||||
|
||||
batchAddEdges: (edges: Edge[]) => {
|
||||
const currentEdges = get().edges
|
||||
const filtered = filterNewEdges(edges, currentEdges)
|
||||
const newEdges = [...currentEdges]
|
||||
const existingEdgeIds = new Set(currentEdges.map((e) => e.id))
|
||||
// Track existing connections to prevent duplicates (same source->target)
|
||||
const existingConnections = new Set(currentEdges.map((e) => `${e.source}->${e.target}`))
|
||||
|
||||
for (const edge of edges) {
|
||||
// Skip if edge ID already exists
|
||||
if (existingEdgeIds.has(edge.id)) continue
|
||||
|
||||
// Skip self-referencing edges
|
||||
if (edge.source === edge.target) continue
|
||||
|
||||
// Skip if connection already exists (same source and target)
|
||||
const connectionKey = `${edge.source}->${edge.target}`
|
||||
if (existingConnections.has(connectionKey)) continue
|
||||
|
||||
// Skip if would create a cycle
|
||||
for (const edge of filtered) {
|
||||
if (wouldCreateCycle([...newEdges], edge.source, edge.target)) continue
|
||||
|
||||
newEdges.push({
|
||||
id: edge.id || crypto.randomUUID(),
|
||||
source: edge.source,
|
||||
@@ -524,8 +515,6 @@ export const useWorkflowStore = create<WorkflowStore>()(
|
||||
type: edge.type || 'default',
|
||||
data: edge.data || {},
|
||||
})
|
||||
existingEdgeIds.add(edge.id)
|
||||
existingConnections.add(connectionKey)
|
||||
}
|
||||
|
||||
const blocks = get().blocks
|
||||
|
||||
@@ -3,6 +3,7 @@ import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||
import { AGENT, isCustomTool } from '@/executor/constants'
|
||||
import { useCustomToolsStore } from '@/stores/custom-tools'
|
||||
import { useEnvironmentStore } from '@/stores/settings/environment'
|
||||
import { extractErrorMessage } from '@/tools/error-extractors'
|
||||
import { tools } from '@/tools/registry'
|
||||
import type { TableRow, ToolConfig, ToolResponse } from '@/tools/types'
|
||||
|
||||
@@ -162,14 +163,22 @@ export async function executeRequest(
|
||||
const externalResponse = await fetch(url, { method, headers, body })
|
||||
|
||||
if (!externalResponse.ok) {
|
||||
let errorContent
|
||||
let errorData: any
|
||||
try {
|
||||
errorContent = await externalResponse.json()
|
||||
errorData = await externalResponse.json()
|
||||
} catch (_e) {
|
||||
errorContent = { message: externalResponse.statusText }
|
||||
try {
|
||||
errorData = await externalResponse.text()
|
||||
} catch (_e2) {
|
||||
errorData = null
|
||||
}
|
||||
}
|
||||
|
||||
const error = errorContent.message || `${toolId} API error: ${externalResponse.statusText}`
|
||||
const error = extractErrorMessage({
|
||||
status: externalResponse.status,
|
||||
statusText: externalResponse.statusText,
|
||||
data: errorData,
|
||||
})
|
||||
logger.error(`${toolId} error:`, { error })
|
||||
throw new Error(error)
|
||||
}
|
||||
|
||||
@@ -96,23 +96,3 @@ export function buildMeetingOutputs(): Record<string, TriggerOutput> {
|
||||
},
|
||||
} as Record<string, TriggerOutput>
|
||||
}
|
||||
|
||||
/**
|
||||
* Build output schema for generic webhook events
|
||||
*/
|
||||
export function buildGenericOutputs(): Record<string, TriggerOutput> {
|
||||
return {
|
||||
payload: {
|
||||
type: 'object',
|
||||
description: 'Raw webhook payload',
|
||||
},
|
||||
headers: {
|
||||
type: 'object',
|
||||
description: 'Request headers',
|
||||
},
|
||||
timestamp: {
|
||||
type: 'string',
|
||||
description: 'ISO8601 received timestamp',
|
||||
},
|
||||
} as Record<string, TriggerOutput>
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { CirclebackIcon } from '@/components/icons'
|
||||
import type { TriggerConfig } from '@/triggers/types'
|
||||
import { buildGenericOutputs, circlebackSetupInstructions, circlebackTriggerOptions } from './utils'
|
||||
import { buildMeetingOutputs, circlebackSetupInstructions, circlebackTriggerOptions } from './utils'
|
||||
|
||||
export const circlebackWebhookTrigger: TriggerConfig = {
|
||||
id: 'circleback_webhook',
|
||||
@@ -74,7 +74,7 @@ export const circlebackWebhookTrigger: TriggerConfig = {
|
||||
},
|
||||
],
|
||||
|
||||
outputs: buildGenericOutputs(),
|
||||
outputs: buildMeetingOutputs(),
|
||||
|
||||
webhook: {
|
||||
method: 'POST',
|
||||
|
||||
@@ -31,8 +31,14 @@ export const TRIGGER_PERSISTED_SUBBLOCK_IDS: string[] = [
|
||||
/**
|
||||
* Trigger-related subblock IDs that represent runtime metadata. They should remain
|
||||
* in the workflow state but must not be modified or cleared by diff operations.
|
||||
*
|
||||
* Note: 'triggerConfig' is included because it's an aggregate of individual trigger
|
||||
* field subblocks. Those individual fields are compared separately, so comparing
|
||||
* triggerConfig would be redundant. Additionally, the client populates triggerConfig
|
||||
* with default values from the trigger definition on load, which aren't present in
|
||||
* the deployed state, causing false positive change detection.
|
||||
*/
|
||||
export const TRIGGER_RUNTIME_SUBBLOCK_IDS: string[] = ['webhookId', 'triggerPath']
|
||||
export const TRIGGER_RUNTIME_SUBBLOCK_IDS: string[] = ['webhookId', 'triggerPath', 'triggerConfig']
|
||||
|
||||
/**
|
||||
* Maximum number of consecutive failures before a trigger (schedule/webhook) is auto-disabled.
|
||||
|
||||
@@ -116,6 +116,11 @@ export const githubIssueClosedTrigger: TriggerConfig = {
|
||||
],
|
||||
|
||||
outputs: {
|
||||
event_type: {
|
||||
type: 'string',
|
||||
description:
|
||||
'GitHub event type from X-GitHub-Event header (e.g., issues, pull_request, push)',
|
||||
},
|
||||
action: {
|
||||
type: 'string',
|
||||
description: 'Action performed (opened, closed, reopened, edited, etc.)',
|
||||
|
||||
@@ -117,6 +117,10 @@ export const githubIssueCommentTrigger: TriggerConfig = {
|
||||
],
|
||||
|
||||
outputs: {
|
||||
event_type: {
|
||||
type: 'string',
|
||||
description: 'GitHub event type from X-GitHub-Event header (e.g., issue_comment)',
|
||||
},
|
||||
action: {
|
||||
type: 'string',
|
||||
description: 'Action performed (created, edited, deleted)',
|
||||
|
||||
@@ -137,6 +137,11 @@ export const githubIssueOpenedTrigger: TriggerConfig = {
|
||||
],
|
||||
|
||||
outputs: {
|
||||
event_type: {
|
||||
type: 'string',
|
||||
description:
|
||||
'GitHub event type from X-GitHub-Event header (e.g., issues, pull_request, push)',
|
||||
},
|
||||
action: {
|
||||
type: 'string',
|
||||
description: 'Action performed (opened, closed, reopened, edited, etc.)',
|
||||
|
||||
@@ -117,6 +117,10 @@ export const githubPRClosedTrigger: TriggerConfig = {
|
||||
],
|
||||
|
||||
outputs: {
|
||||
event_type: {
|
||||
type: 'string',
|
||||
description: 'GitHub event type from X-GitHub-Event header (e.g., pull_request)',
|
||||
},
|
||||
action: {
|
||||
type: 'string',
|
||||
description: 'Action performed (opened, closed, synchronize, reopened, edited, etc.)',
|
||||
|
||||
@@ -117,6 +117,10 @@ export const githubPRCommentTrigger: TriggerConfig = {
|
||||
],
|
||||
|
||||
outputs: {
|
||||
event_type: {
|
||||
type: 'string',
|
||||
description: 'GitHub event type from X-GitHub-Event header (e.g., issue_comment)',
|
||||
},
|
||||
action: {
|
||||
type: 'string',
|
||||
description: 'Action performed (created, edited, deleted)',
|
||||
|
||||
@@ -116,6 +116,10 @@ export const githubPRMergedTrigger: TriggerConfig = {
|
||||
],
|
||||
|
||||
outputs: {
|
||||
event_type: {
|
||||
type: 'string',
|
||||
description: 'GitHub event type from X-GitHub-Event header (e.g., pull_request)',
|
||||
},
|
||||
action: {
|
||||
type: 'string',
|
||||
description: 'Action performed (opened, closed, synchronize, reopened, edited, etc.)',
|
||||
|
||||
@@ -116,6 +116,10 @@ export const githubPROpenedTrigger: TriggerConfig = {
|
||||
],
|
||||
|
||||
outputs: {
|
||||
event_type: {
|
||||
type: 'string',
|
||||
description: 'GitHub event type from X-GitHub-Event header (e.g., pull_request)',
|
||||
},
|
||||
action: {
|
||||
type: 'string',
|
||||
description: 'Action performed (opened, closed, synchronize, reopened, edited, etc.)',
|
||||
|
||||
@@ -117,6 +117,10 @@ export const githubPRReviewedTrigger: TriggerConfig = {
|
||||
],
|
||||
|
||||
outputs: {
|
||||
event_type: {
|
||||
type: 'string',
|
||||
description: 'GitHub event type from X-GitHub-Event header (e.g., pull_request_review)',
|
||||
},
|
||||
action: {
|
||||
type: 'string',
|
||||
description: 'Action performed (submitted, edited, dismissed)',
|
||||
|
||||
@@ -116,6 +116,14 @@ export const githubPushTrigger: TriggerConfig = {
|
||||
],
|
||||
|
||||
outputs: {
|
||||
event_type: {
|
||||
type: 'string',
|
||||
description: 'GitHub event type from X-GitHub-Event header (e.g., push)',
|
||||
},
|
||||
branch: {
|
||||
type: 'string',
|
||||
description: 'Branch name derived from ref (e.g., main from refs/heads/main)',
|
||||
},
|
||||
ref: {
|
||||
type: 'string',
|
||||
description: 'Git reference that was pushed (e.g., refs/heads/main)',
|
||||
|
||||
@@ -116,6 +116,10 @@ export const githubReleasePublishedTrigger: TriggerConfig = {
|
||||
],
|
||||
|
||||
outputs: {
|
||||
event_type: {
|
||||
type: 'string',
|
||||
description: 'GitHub event type from X-GitHub-Event header (e.g., release)',
|
||||
},
|
||||
action: {
|
||||
type: 'string',
|
||||
description:
|
||||
|
||||
@@ -117,6 +117,10 @@ export const githubWorkflowRunTrigger: TriggerConfig = {
|
||||
],
|
||||
|
||||
outputs: {
|
||||
event_type: {
|
||||
type: 'string',
|
||||
description: 'GitHub event type from X-GitHub-Event header (e.g., workflow_run)',
|
||||
},
|
||||
action: {
|
||||
type: 'string',
|
||||
description: 'Action performed (requested, in_progress, completed)',
|
||||
|
||||
@@ -265,11 +265,6 @@ function buildBaseWebhookOutputs(): Record<string, TriggerOutput> {
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
webhook: {
|
||||
type: 'json',
|
||||
description: 'Webhook metadata including provider, path, and raw payload',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { LemlistIcon } from '@/components/icons'
|
||||
import { buildTriggerSubBlocks } from '@/triggers'
|
||||
import {
|
||||
buildActivityOutputs,
|
||||
buildEmailBouncedOutputs,
|
||||
buildLemlistExtraFields,
|
||||
lemlistSetupInstructions,
|
||||
lemlistTriggerOptions,
|
||||
@@ -27,7 +27,7 @@ export const lemlistEmailBouncedTrigger: TriggerConfig = {
|
||||
extraFields: buildLemlistExtraFields('lemlist_email_bounced'),
|
||||
}),
|
||||
|
||||
outputs: buildActivityOutputs(),
|
||||
outputs: buildEmailBouncedOutputs(),
|
||||
|
||||
webhook: {
|
||||
method: 'POST',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { LemlistIcon } from '@/components/icons'
|
||||
import { buildTriggerSubBlocks } from '@/triggers'
|
||||
import {
|
||||
buildActivityOutputs,
|
||||
buildEmailClickedOutputs,
|
||||
buildLemlistExtraFields,
|
||||
lemlistSetupInstructions,
|
||||
lemlistTriggerOptions,
|
||||
@@ -27,7 +27,7 @@ export const lemlistEmailClickedTrigger: TriggerConfig = {
|
||||
extraFields: buildLemlistExtraFields('lemlist_email_clicked'),
|
||||
}),
|
||||
|
||||
outputs: buildActivityOutputs(),
|
||||
outputs: buildEmailClickedOutputs(),
|
||||
|
||||
webhook: {
|
||||
method: 'POST',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { LemlistIcon } from '@/components/icons'
|
||||
import { buildTriggerSubBlocks } from '@/triggers'
|
||||
import {
|
||||
buildActivityOutputs,
|
||||
buildEmailOpenedOutputs,
|
||||
buildLemlistExtraFields,
|
||||
lemlistSetupInstructions,
|
||||
lemlistTriggerOptions,
|
||||
@@ -27,7 +27,7 @@ export const lemlistEmailOpenedTrigger: TriggerConfig = {
|
||||
extraFields: buildLemlistExtraFields('lemlist_email_opened'),
|
||||
}),
|
||||
|
||||
outputs: buildActivityOutputs(),
|
||||
outputs: buildEmailOpenedOutputs(),
|
||||
|
||||
webhook: {
|
||||
method: 'POST',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { LemlistIcon } from '@/components/icons'
|
||||
import { buildTriggerSubBlocks } from '@/triggers'
|
||||
import {
|
||||
buildEmailReplyOutputs,
|
||||
buildEmailRepliedOutputs,
|
||||
buildLemlistExtraFields,
|
||||
lemlistSetupInstructions,
|
||||
lemlistTriggerOptions,
|
||||
@@ -30,7 +30,7 @@ export const lemlistEmailRepliedTrigger: TriggerConfig = {
|
||||
extraFields: buildLemlistExtraFields('lemlist_email_replied'),
|
||||
}),
|
||||
|
||||
outputs: buildEmailReplyOutputs(),
|
||||
outputs: buildEmailRepliedOutputs(),
|
||||
|
||||
webhook: {
|
||||
method: 'POST',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { LemlistIcon } from '@/components/icons'
|
||||
import { buildTriggerSubBlocks } from '@/triggers'
|
||||
import {
|
||||
buildActivityOutputs,
|
||||
buildEmailSentOutputs,
|
||||
buildLemlistExtraFields,
|
||||
lemlistSetupInstructions,
|
||||
lemlistTriggerOptions,
|
||||
@@ -27,7 +27,7 @@ export const lemlistEmailSentTrigger: TriggerConfig = {
|
||||
extraFields: buildLemlistExtraFields('lemlist_email_sent'),
|
||||
}),
|
||||
|
||||
outputs: buildActivityOutputs(),
|
||||
outputs: buildEmailSentOutputs(),
|
||||
|
||||
webhook: {
|
||||
method: 'POST',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { LemlistIcon } from '@/components/icons'
|
||||
import { buildTriggerSubBlocks } from '@/triggers'
|
||||
import {
|
||||
buildActivityOutputs,
|
||||
buildInterestOutputs,
|
||||
buildLemlistExtraFields,
|
||||
lemlistSetupInstructions,
|
||||
lemlistTriggerOptions,
|
||||
@@ -27,7 +27,7 @@ export const lemlistInterestedTrigger: TriggerConfig = {
|
||||
extraFields: buildLemlistExtraFields('lemlist_interested'),
|
||||
}),
|
||||
|
||||
outputs: buildActivityOutputs(),
|
||||
outputs: buildInterestOutputs(),
|
||||
|
||||
webhook: {
|
||||
method: 'POST',
|
||||
|
||||
@@ -2,7 +2,7 @@ import { LemlistIcon } from '@/components/icons'
|
||||
import { buildTriggerSubBlocks } from '@/triggers'
|
||||
import {
|
||||
buildLemlistExtraFields,
|
||||
buildLinkedInReplyOutputs,
|
||||
buildLinkedInRepliedOutputs,
|
||||
lemlistSetupInstructions,
|
||||
lemlistTriggerOptions,
|
||||
} from '@/triggers/lemlist/utils'
|
||||
@@ -27,7 +27,7 @@ export const lemlistLinkedInRepliedTrigger: TriggerConfig = {
|
||||
extraFields: buildLemlistExtraFields('lemlist_linkedin_replied'),
|
||||
}),
|
||||
|
||||
outputs: buildLinkedInReplyOutputs(),
|
||||
outputs: buildLinkedInRepliedOutputs(),
|
||||
|
||||
webhook: {
|
||||
method: 'POST',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { LemlistIcon } from '@/components/icons'
|
||||
import { buildTriggerSubBlocks } from '@/triggers'
|
||||
import {
|
||||
buildActivityOutputs,
|
||||
buildInterestOutputs,
|
||||
buildLemlistExtraFields,
|
||||
lemlistSetupInstructions,
|
||||
lemlistTriggerOptions,
|
||||
@@ -27,7 +27,7 @@ export const lemlistNotInterestedTrigger: TriggerConfig = {
|
||||
extraFields: buildLemlistExtraFields('lemlist_not_interested'),
|
||||
}),
|
||||
|
||||
outputs: buildActivityOutputs(),
|
||||
outputs: buildInterestOutputs(),
|
||||
|
||||
webhook: {
|
||||
method: 'POST',
|
||||
|
||||
@@ -66,203 +66,254 @@ export function buildLemlistExtraFields(triggerId: string) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Base activity outputs shared across all Lemlist triggers
|
||||
* Core fields present in ALL Lemlist webhook payloads
|
||||
* See: https://help.lemlist.com/en/articles/9423940-use-the-api-to-list-activity-types
|
||||
*/
|
||||
function buildBaseActivityOutputs(): Record<string, TriggerOutput> {
|
||||
const coreOutputs = {
|
||||
_id: {
|
||||
type: 'string',
|
||||
description: 'Unique activity identifier',
|
||||
},
|
||||
type: {
|
||||
type: 'string',
|
||||
description: 'Activity type (e.g., emailsSent, emailsReplied)',
|
||||
},
|
||||
createdAt: {
|
||||
type: 'string',
|
||||
description: 'Activity creation timestamp (ISO 8601)',
|
||||
},
|
||||
teamId: {
|
||||
type: 'string',
|
||||
description: 'Lemlist team identifier',
|
||||
},
|
||||
leadId: {
|
||||
type: 'string',
|
||||
description: 'Lead identifier',
|
||||
},
|
||||
campaignId: {
|
||||
type: 'string',
|
||||
description: 'Campaign identifier',
|
||||
},
|
||||
campaignName: {
|
||||
type: 'string',
|
||||
description: 'Campaign name',
|
||||
},
|
||||
} as const
|
||||
|
||||
/**
|
||||
* Lead fields present in webhook payloads
|
||||
*/
|
||||
const leadOutputs = {
|
||||
email: {
|
||||
type: 'string',
|
||||
description: 'Lead email address',
|
||||
},
|
||||
firstName: {
|
||||
type: 'string',
|
||||
description: 'Lead first name',
|
||||
},
|
||||
lastName: {
|
||||
type: 'string',
|
||||
description: 'Lead last name',
|
||||
},
|
||||
companyName: {
|
||||
type: 'string',
|
||||
description: 'Lead company name',
|
||||
},
|
||||
linkedinUrl: {
|
||||
type: 'string',
|
||||
description: 'Lead LinkedIn profile URL',
|
||||
},
|
||||
} as const
|
||||
|
||||
/**
|
||||
* Sequence/campaign tracking fields for email activities
|
||||
*/
|
||||
const sequenceOutputs = {
|
||||
sequenceId: {
|
||||
type: 'string',
|
||||
description: 'Sequence identifier',
|
||||
},
|
||||
sequenceStep: {
|
||||
type: 'number',
|
||||
description: 'Current step in the sequence (0-indexed)',
|
||||
},
|
||||
totalSequenceStep: {
|
||||
type: 'number',
|
||||
description: 'Total number of steps in the sequence',
|
||||
},
|
||||
isFirst: {
|
||||
type: 'boolean',
|
||||
description: 'Whether this is the first activity of this type for this step',
|
||||
},
|
||||
} as const
|
||||
|
||||
/**
|
||||
* Sender information fields
|
||||
*/
|
||||
const senderOutputs = {
|
||||
sendUserId: {
|
||||
type: 'string',
|
||||
description: 'Sender user identifier',
|
||||
},
|
||||
sendUserEmail: {
|
||||
type: 'string',
|
||||
description: 'Sender email address',
|
||||
},
|
||||
sendUserName: {
|
||||
type: 'string',
|
||||
description: 'Sender display name',
|
||||
},
|
||||
} as const
|
||||
|
||||
/**
|
||||
* Email content fields
|
||||
*/
|
||||
const emailContentOutputs = {
|
||||
subject: {
|
||||
type: 'string',
|
||||
description: 'Email subject line',
|
||||
},
|
||||
text: {
|
||||
type: 'string',
|
||||
description: 'Email body content (HTML)',
|
||||
},
|
||||
messageId: {
|
||||
type: 'string',
|
||||
description: 'Email message ID (RFC 2822 format)',
|
||||
},
|
||||
emailId: {
|
||||
type: 'string',
|
||||
description: 'Lemlist email identifier',
|
||||
},
|
||||
} as const
|
||||
|
||||
/**
|
||||
* Build outputs for email sent events
|
||||
*/
|
||||
export function buildEmailSentOutputs(): Record<string, TriggerOutput> {
|
||||
return {
|
||||
type: {
|
||||
type: 'string',
|
||||
description: 'Activity type (emailsReplied, linkedinReplied, interested, emailsOpened, etc.)',
|
||||
},
|
||||
_id: {
|
||||
type: 'string',
|
||||
description: 'Unique activity identifier',
|
||||
},
|
||||
leadId: {
|
||||
type: 'string',
|
||||
description: 'Associated lead ID',
|
||||
},
|
||||
campaignId: {
|
||||
type: 'string',
|
||||
description: 'Campaign ID',
|
||||
},
|
||||
campaignName: {
|
||||
type: 'string',
|
||||
description: 'Campaign name',
|
||||
},
|
||||
sequenceId: {
|
||||
type: 'string',
|
||||
description: 'Sequence ID within the campaign',
|
||||
},
|
||||
stepId: {
|
||||
type: 'string',
|
||||
description: 'Step ID that triggered this activity',
|
||||
},
|
||||
createdAt: {
|
||||
type: 'string',
|
||||
description: 'When the activity occurred (ISO 8601)',
|
||||
},
|
||||
}
|
||||
...coreOutputs,
|
||||
...leadOutputs,
|
||||
...sequenceOutputs,
|
||||
...senderOutputs,
|
||||
...emailContentOutputs,
|
||||
} as Record<string, TriggerOutput>
|
||||
}
|
||||
|
||||
/**
|
||||
* Lead outputs - information about the lead
|
||||
* Build outputs for email replied events
|
||||
*/
|
||||
function buildLeadOutputs(): Record<string, TriggerOutput> {
|
||||
export function buildEmailRepliedOutputs(): Record<string, TriggerOutput> {
|
||||
return {
|
||||
lead: {
|
||||
_id: {
|
||||
type: 'string',
|
||||
description: 'Lead unique identifier',
|
||||
},
|
||||
email: {
|
||||
type: 'string',
|
||||
description: 'Lead email address',
|
||||
},
|
||||
firstName: {
|
||||
type: 'string',
|
||||
description: 'Lead first name',
|
||||
},
|
||||
lastName: {
|
||||
type: 'string',
|
||||
description: 'Lead last name',
|
||||
},
|
||||
companyName: {
|
||||
type: 'string',
|
||||
description: 'Lead company name',
|
||||
},
|
||||
phone: {
|
||||
type: 'string',
|
||||
description: 'Lead phone number',
|
||||
},
|
||||
linkedinUrl: {
|
||||
type: 'string',
|
||||
description: 'Lead LinkedIn profile URL',
|
||||
},
|
||||
picture: {
|
||||
type: 'string',
|
||||
description: 'Lead profile picture URL',
|
||||
},
|
||||
icebreaker: {
|
||||
type: 'string',
|
||||
description: 'Personalized icebreaker text',
|
||||
},
|
||||
timezone: {
|
||||
type: 'string',
|
||||
description: 'Lead timezone (e.g., America/New_York)',
|
||||
},
|
||||
isUnsubscribed: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the lead is unsubscribed',
|
||||
},
|
||||
},
|
||||
}
|
||||
...coreOutputs,
|
||||
...leadOutputs,
|
||||
...sequenceOutputs,
|
||||
...senderOutputs,
|
||||
...emailContentOutputs,
|
||||
} as Record<string, TriggerOutput>
|
||||
}
|
||||
|
||||
/**
|
||||
* Standard activity outputs (activity + lead data)
|
||||
* Build outputs for email opened events
|
||||
*/
|
||||
export function buildActivityOutputs(): Record<string, TriggerOutput> {
|
||||
export function buildEmailOpenedOutputs(): Record<string, TriggerOutput> {
|
||||
return {
|
||||
...buildBaseActivityOutputs(),
|
||||
...buildLeadOutputs(),
|
||||
webhook: {
|
||||
type: 'json',
|
||||
description: 'Full webhook payload with all activity-specific data',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Email-specific outputs (includes message content for replies)
|
||||
*/
|
||||
export function buildEmailReplyOutputs(): Record<string, TriggerOutput> {
|
||||
return {
|
||||
...buildBaseActivityOutputs(),
|
||||
...buildLeadOutputs(),
|
||||
...coreOutputs,
|
||||
...leadOutputs,
|
||||
...sequenceOutputs,
|
||||
...senderOutputs,
|
||||
messageId: {
|
||||
type: 'string',
|
||||
description: 'Email message ID',
|
||||
description: 'Email message ID that was opened',
|
||||
},
|
||||
subject: {
|
||||
type: 'string',
|
||||
description: 'Email subject line',
|
||||
},
|
||||
text: {
|
||||
type: 'string',
|
||||
description: 'Email reply text content',
|
||||
},
|
||||
html: {
|
||||
type: 'string',
|
||||
description: 'Email reply HTML content',
|
||||
},
|
||||
sentAt: {
|
||||
type: 'string',
|
||||
description: 'When the reply was sent',
|
||||
},
|
||||
webhook: {
|
||||
type: 'json',
|
||||
description: 'Full webhook payload with all email data',
|
||||
},
|
||||
}
|
||||
} as Record<string, TriggerOutput>
|
||||
}
|
||||
|
||||
/**
|
||||
* LinkedIn-specific outputs (includes message content)
|
||||
* Build outputs for email clicked events
|
||||
*/
|
||||
export function buildLinkedInReplyOutputs(): Record<string, TriggerOutput> {
|
||||
export function buildEmailClickedOutputs(): Record<string, TriggerOutput> {
|
||||
return {
|
||||
...buildBaseActivityOutputs(),
|
||||
...buildLeadOutputs(),
|
||||
...coreOutputs,
|
||||
...leadOutputs,
|
||||
...sequenceOutputs,
|
||||
...senderOutputs,
|
||||
messageId: {
|
||||
type: 'string',
|
||||
description: 'LinkedIn message ID',
|
||||
description: 'Email message ID containing the clicked link',
|
||||
},
|
||||
text: {
|
||||
clickedUrl: {
|
||||
type: 'string',
|
||||
description: 'LinkedIn message text content',
|
||||
description: 'URL that was clicked',
|
||||
},
|
||||
sentAt: {
|
||||
type: 'string',
|
||||
description: 'When the message was sent',
|
||||
},
|
||||
webhook: {
|
||||
type: 'json',
|
||||
description: 'Full webhook payload with all LinkedIn data',
|
||||
},
|
||||
}
|
||||
} as Record<string, TriggerOutput>
|
||||
}
|
||||
|
||||
/**
|
||||
* All outputs for generic webhook (activity + lead + all possible fields)
|
||||
* Build outputs for email bounced events
|
||||
*/
|
||||
export function buildAllOutputs(): Record<string, TriggerOutput> {
|
||||
export function buildEmailBouncedOutputs(): Record<string, TriggerOutput> {
|
||||
return {
|
||||
...buildBaseActivityOutputs(),
|
||||
...buildLeadOutputs(),
|
||||
...coreOutputs,
|
||||
...leadOutputs,
|
||||
...sequenceOutputs,
|
||||
...senderOutputs,
|
||||
messageId: {
|
||||
type: 'string',
|
||||
description: 'Message ID (for email/LinkedIn events)',
|
||||
description: 'Email message ID that bounced',
|
||||
},
|
||||
subject: {
|
||||
errorMessage: {
|
||||
type: 'string',
|
||||
description: 'Email subject (for email events)',
|
||||
description: 'Bounce error message',
|
||||
},
|
||||
} as Record<string, TriggerOutput>
|
||||
}
|
||||
|
||||
/**
|
||||
* Build outputs for LinkedIn replied events
|
||||
*/
|
||||
export function buildLinkedInRepliedOutputs(): Record<string, TriggerOutput> {
|
||||
return {
|
||||
...coreOutputs,
|
||||
...leadOutputs,
|
||||
...sequenceOutputs,
|
||||
text: {
|
||||
type: 'string',
|
||||
description: 'Message text content',
|
||||
description: 'LinkedIn message content',
|
||||
},
|
||||
html: {
|
||||
type: 'string',
|
||||
description: 'Message HTML content (for email events)',
|
||||
},
|
||||
sentAt: {
|
||||
type: 'string',
|
||||
description: 'When the message was sent',
|
||||
},
|
||||
webhook: {
|
||||
type: 'json',
|
||||
description: 'Full webhook payload with all data',
|
||||
},
|
||||
}
|
||||
} as Record<string, TriggerOutput>
|
||||
}
|
||||
|
||||
/**
|
||||
* Build outputs for interested/not interested events
|
||||
*/
|
||||
export function buildInterestOutputs(): Record<string, TriggerOutput> {
|
||||
return {
|
||||
...coreOutputs,
|
||||
...leadOutputs,
|
||||
...sequenceOutputs,
|
||||
} as Record<string, TriggerOutput>
|
||||
}
|
||||
|
||||
/**
|
||||
* Build outputs for generic webhook (all events)
|
||||
* Includes all possible fields across event types
|
||||
*/
|
||||
export function buildLemlistOutputs(): Record<string, TriggerOutput> {
|
||||
return {
|
||||
...coreOutputs,
|
||||
...leadOutputs,
|
||||
...sequenceOutputs,
|
||||
...senderOutputs,
|
||||
...emailContentOutputs,
|
||||
clickedUrl: {
|
||||
type: 'string',
|
||||
description: 'URL that was clicked (for emailsClicked events)',
|
||||
},
|
||||
errorMessage: {
|
||||
type: 'string',
|
||||
description: 'Error message (for bounce/failed events)',
|
||||
},
|
||||
} as Record<string, TriggerOutput>
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { LemlistIcon } from '@/components/icons'
|
||||
import { buildTriggerSubBlocks } from '@/triggers'
|
||||
import {
|
||||
buildAllOutputs,
|
||||
buildLemlistExtraFields,
|
||||
buildLemlistOutputs,
|
||||
lemlistSetupInstructions,
|
||||
lemlistTriggerOptions,
|
||||
} from '@/triggers/lemlist/utils'
|
||||
@@ -27,7 +27,7 @@ export const lemlistWebhookTrigger: TriggerConfig = {
|
||||
extraFields: buildLemlistExtraFields('lemlist_webhook'),
|
||||
}),
|
||||
|
||||
outputs: buildAllOutputs(),
|
||||
outputs: buildLemlistOutputs(),
|
||||
|
||||
webhook: {
|
||||
method: 'POST',
|
||||
|
||||
@@ -110,6 +110,7 @@ export const telegramWebhookTrigger: TriggerConfig = {
|
||||
},
|
||||
sender: {
|
||||
id: { type: 'number', description: 'Sender user ID' },
|
||||
username: { type: 'string', description: 'Sender username (if available)' },
|
||||
firstName: { type: 'string', description: 'Sender first name' },
|
||||
lastName: { type: 'string', description: 'Sender last name' },
|
||||
languageCode: { type: 'string', description: 'Sender language code (if available)' },
|
||||
|
||||
@@ -136,6 +136,8 @@ export const typeformWebhookTrigger: TriggerConfig = {
|
||||
'Array of respondent answers (only includes answered questions). Each answer contains type, value, and field reference.',
|
||||
},
|
||||
definition: {
|
||||
description:
|
||||
'Form definition (only included when "Include Form Definition" is enabled in trigger settings)',
|
||||
id: {
|
||||
type: 'string',
|
||||
description: 'Form ID',
|
||||
|
||||
@@ -96,10 +96,6 @@ export const webflowCollectionItemChangedTrigger: TriggerConfig = {
|
||||
type: 'string',
|
||||
description: 'The site ID where the event occurred',
|
||||
},
|
||||
workspaceId: {
|
||||
type: 'string',
|
||||
description: 'The workspace ID where the event occurred',
|
||||
},
|
||||
collectionId: {
|
||||
type: 'string',
|
||||
description: 'The collection ID where the item was changed',
|
||||
|
||||
@@ -109,10 +109,6 @@ export const webflowCollectionItemCreatedTrigger: TriggerConfig = {
|
||||
type: 'string',
|
||||
description: 'The site ID where the event occurred',
|
||||
},
|
||||
workspaceId: {
|
||||
type: 'string',
|
||||
description: 'The workspace ID where the event occurred',
|
||||
},
|
||||
collectionId: {
|
||||
type: 'string',
|
||||
description: 'The collection ID where the item was created',
|
||||
|
||||
@@ -97,10 +97,6 @@ export const webflowCollectionItemDeletedTrigger: TriggerConfig = {
|
||||
type: 'string',
|
||||
description: 'The site ID where the event occurred',
|
||||
},
|
||||
workspaceId: {
|
||||
type: 'string',
|
||||
description: 'The workspace ID where the event occurred',
|
||||
},
|
||||
collectionId: {
|
||||
type: 'string',
|
||||
description: 'The collection ID where the item was deleted',
|
||||
|
||||
@@ -76,9 +76,9 @@ export const webflowFormSubmissionTrigger: TriggerConfig = {
|
||||
type: 'string',
|
||||
description: 'The site ID where the form was submitted',
|
||||
},
|
||||
workspaceId: {
|
||||
formId: {
|
||||
type: 'string',
|
||||
description: 'The workspace ID where the event occurred',
|
||||
description: 'The form ID',
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
|
||||
Reference in New Issue
Block a user