v0.5.53: hotkey improvements, added redis fallback, fixes for workflow tool

This commit is contained in:
Waleed
2026-01-06 23:34:52 -08:00
committed by GitHub
130 changed files with 2560 additions and 2454 deletions

View File

@@ -0,0 +1,89 @@
---
title: Webhook
---
import { Callout } from 'fumadocs-ui/components/callout'
import { Image } from '@/components/ui/image'
Der Webhook-Block sendet HTTP-POST-Anfragen an externe Webhook-Endpunkte mit automatischen Webhook-Headern und optionaler HMAC-Signierung.
<div className="flex justify-center">
<Image
src="/static/blocks/webhook.png"
alt="Webhook-Block"
width={500}
height={400}
className="my-6"
/>
</div>
## Konfiguration
### Webhook-URL
Der Ziel-Endpunkt für Ihre Webhook-Anfrage. Unterstützt sowohl statische URLs als auch dynamische Werte aus anderen Blöcken.
### Payload
JSON-Daten, die im Anfrage-Body gesendet werden. Verwenden Sie den KI-Zauberstab, um Payloads zu generieren oder auf Workflow-Variablen zu verweisen:
```json
{
"event": "workflow.completed",
"data": {
"result": "<agent.content>",
"timestamp": "<function.result>"
}
}
```
### Signierungsgeheimnis
Optionales Geheimnis für die HMAC-SHA256-Payload-Signierung. Wenn angegeben, wird ein `X-Webhook-Signature`Header hinzugefügt:
```
X-Webhook-Signature: t=1704067200000,v1=5d41402abc4b2a76b9719d911017c592...
```
Um Signaturen zu verifizieren, berechnen Sie `HMAC-SHA256(secret, "${timestamp}.${body}")` und vergleichen Sie mit dem `v1`Wert.
### Zusätzliche Header
Benutzerdefinierte Schlüssel-Wert-Header, die in die Anfrage aufgenommen werden. Diese überschreiben alle automatischen Header mit demselben Namen.
## Automatische Header
Jede Anfrage enthält automatisch diese Header:
| Header | Beschreibung |
|--------|-------------|
| `Content-Type` | `application/json` |
| `X-Webhook-Timestamp` | Unix-Zeitstempel in Millisekunden |
| `X-Delivery-ID` | Eindeutige UUID für diese Zustellung |
| `Idempotency-Key` | Identisch mit `X-Delivery-ID` zur Deduplizierung |
## Ausgaben
| Ausgabe | Typ | Beschreibung |
|--------|------|-------------|
| `data` | json | Antwort-Body vom Endpunkt |
| `status` | number | HTTP-Statuscode |
| `headers` | object | Antwort-Header |
## Beispiel-Anwendungsfälle
**Externe Dienste benachrichtigen** - Workflow-Ergebnisse an Slack, Discord oder benutzerdefinierte Endpunkte senden
```
Agent → Function (format) → Webhook (notify)
```
**Externe Workflows auslösen** - Prozesse in anderen Systemen starten, wenn Bedingungen erfüllt sind
```
Condition (check) → Webhook (trigger) → Response
```
<Callout>
Der Webhook-Block verwendet immer POST. Für andere HTTP-Methoden oder mehr Kontrolle verwenden Sie den [API-Block](/blocks/api).
</Callout>

View File

@@ -1,231 +0,0 @@
---
title: Webhook
description: Empfangen Sie Webhooks von jedem Dienst durch Konfiguration eines
benutzerdefinierten Webhooks.
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
import { Image } from '@/components/ui/image'
<BlockInfoCard
type="generic_webhook"
color="#10B981"
/>
<div className="flex justify-center">
<Image
src="/static/blocks/webhook.png"
alt="Webhook-Block-Konfiguration"
width={500}
height={400}
className="my-6"
/>
</div>
## Übersicht
Der generische Webhook-Block ermöglicht den Empfang von Webhooks von jedem externen Dienst. Dies ist ein flexibler Trigger, der jede JSON-Nutzlast verarbeiten kann und sich daher ideal für die Integration mit Diensten eignet, die keinen dedizierten Sim-Block haben.
## Grundlegende Verwendung
### Einfacher Durchleitungsmodus
Ohne ein definiertes Eingabeformat leitet der Webhook den gesamten Anforderungstext unverändert weiter:
```bash
curl -X POST https://sim.ai/api/webhooks/trigger/{webhook-path} \
-H "Content-Type: application/json" \
-H "X-Sim-Secret: your-secret" \
-d '{
"message": "Test webhook trigger",
"data": {
"key": "value"
}
}'
```
Greifen Sie in nachgelagerten Blöcken auf die Daten zu mit:
- `<webhook1.message>` → "Test webhook trigger"
- `<webhook1.data.key>` → "value"
### Strukturiertes Eingabeformat (optional)
Definieren Sie ein Eingabeschema, um typisierte Felder zu erhalten und erweiterte Funktionen wie Datei-Uploads zu aktivieren:
**Konfiguration des Eingabeformats:**
```json
[
{ "name": "message", "type": "string" },
{ "name": "priority", "type": "number" },
{ "name": "documents", "type": "files" }
]
```
**Webhook-Anfrage:**
```bash
curl -X POST https://sim.ai/api/webhooks/trigger/{webhook-path} \
-H "Content-Type: application/json" \
-H "X-Sim-Secret: your-secret" \
-d '{
"message": "Invoice submission",
"priority": 1,
"documents": [
{
"type": "file",
"data": "data:application/pdf;base64,JVBERi0xLjQK...",
"name": "invoice.pdf",
"mime": "application/pdf"
}
]
}'
```
## Datei-Uploads
### Unterstützte Dateiformate
Der Webhook unterstützt zwei Dateieingabeformate:
#### 1. Base64-kodierte Dateien
Zum direkten Hochladen von Dateiinhalten:
```json
{
"documents": [
{
"type": "file",
"data": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgA...",
"name": "screenshot.png",
"mime": "image/png"
}
]
}
```
- **Maximale Größe**: 20MB pro Datei
- **Format**: Standard-Daten-URL mit Base64-Kodierung
- **Speicherung**: Dateien werden in sicheren Ausführungsspeicher hochgeladen
#### 2. URL-Referenzen
Zum Übergeben vorhandener Datei-URLs:
```json
{
"documents": [
{
"type": "url",
"data": "https://example.com/files/document.pdf",
"name": "document.pdf",
"mime": "application/pdf"
}
]
}
```
### Zugriff auf Dateien in nachgelagerten Blöcken
Dateien werden in `UserFile`Objekte mit den folgenden Eigenschaften verarbeitet:
```typescript
{
id: string, // Unique file identifier
name: string, // Original filename
url: string, // Presigned URL (valid for 5 minutes)
size: number, // File size in bytes
type: string, // MIME type
key: string, // Storage key
uploadedAt: string, // ISO timestamp
expiresAt: string // ISO timestamp (5 minutes)
}
```
**Zugriff in Blöcken:**
- `<webhook1.documents[0].url>` → Download-URL
- `<webhook1.documents[0].name>` → "invoice.pdf"
- `<webhook1.documents[0].size>` → 524288
- `<webhook1.documents[0].type>` → "application/pdf"
### Vollständiges Datei-Upload-Beispiel
```bash
# Create a base64-encoded file
echo "Hello World" | base64
# SGVsbG8gV29ybGQK
# Send webhook with file
curl -X POST https://sim.ai/api/webhooks/trigger/{webhook-path} \
-H "Content-Type: application/json" \
-H "X-Sim-Secret: your-secret" \
-d '{
"subject": "Document for review",
"attachments": [
{
"type": "file",
"data": "data:text/plain;base64,SGVsbG8gV29ybGQK",
"name": "sample.txt",
"mime": "text/plain"
}
]
}'
```
## Authentifizierung
### Authentifizierung konfigurieren (Optional)
In der Webhook-Konfiguration:
1. Aktiviere "Authentifizierung erforderlich"
2. Setze einen geheimen Token
3. Wähle den Header-Typ:
- **Benutzerdefinierter Header**: `X-Sim-Secret: your-token`
- **Authorization Bearer**: `Authorization: Bearer your-token`
### Verwendung der Authentifizierung
```bash
# With custom header
curl -X POST https://sim.ai/api/webhooks/trigger/{webhook-path} \
-H "Content-Type: application/json" \
-H "X-Sim-Secret: your-secret-token" \
-d '{"message": "Authenticated request"}'
# With bearer token
curl -X POST https://sim.ai/api/webhooks/trigger/{webhook-path} \
-H "Content-Type: application/json" \
-H "Authorization: Bearer your-secret-token" \
-d '{"message": "Authenticated request"}'
```
## Best Practices
1. **Eingabeformat für Struktur verwenden**: Definiere ein Eingabeformat, wenn du das erwartete Schema kennst. Dies bietet:
- Typvalidierung
- Bessere Autovervollständigung im Editor
- Datei-Upload-Funktionen
2. **Authentifizierung**: Aktiviere immer die Authentifizierung für Produktions-Webhooks, um unbefugten Zugriff zu verhindern.
3. **Dateigrößenbeschränkungen**: Halte Dateien unter 20 MB. Verwende für größere Dateien URL-Referenzen.
4. **Dateiablauf**: Heruntergeladene Dateien haben URLs mit einer Gültigkeit von 5 Minuten. Verarbeite sie umgehend oder speichere sie an anderer Stelle, wenn sie länger benötigt werden.
5. **Fehlerbehandlung**: Die Webhook-Verarbeitung erfolgt asynchron. Überprüfe die Ausführungsprotokolle auf Fehler.
6. **Testen**: Verwende die Schaltfläche "Webhook testen" im Editor, um deine Konfiguration vor der Bereitstellung zu validieren.
## Anwendungsfälle
- **Formularübermittlungen**: Empfange Daten von benutzerdefinierten Formularen mit Datei-Uploads
- **Drittanbieter-Integrationen**: Verbinde mit Diensten, die Webhooks senden (Stripe, GitHub usw.)
- **Dokumentenverarbeitung**: Akzeptiere Dokumente von externen Systemen zur Verarbeitung
- **Ereignisbenachrichtigungen**: Empfange Ereignisdaten aus verschiedenen Quellen
- **Benutzerdefinierte APIs**: Erstelle benutzerdefinierte API-Endpunkte für deine Anwendungen
## Hinweise
- Kategorie: `triggers`
- Typ: `generic_webhook`
- **Dateiunterstützung**: Verfügbar über Eingabeformat-Konfiguration
- **Maximale Dateigröße**: 20 MB pro Datei

View File

@@ -15,7 +15,7 @@ Der generische Webhook-Block erstellt einen flexiblen Endpunkt, der beliebige Pa
<div className="flex justify-center">
<Image
src="/static/blocks/webhook.png"
src="/static/blocks/webhook-trigger.png"
alt="Generische Webhook-Konfiguration"
width={500}
height={400}

View File

@@ -14,6 +14,7 @@
"router",
"variables",
"wait",
"webhook",
"workflow"
]
}

View File

@@ -0,0 +1,87 @@
---
title: Webhook
---
import { Callout } from 'fumadocs-ui/components/callout'
import { Image } from '@/components/ui/image'
The Webhook block sends HTTP POST requests to external webhook endpoints with automatic webhook headers and optional HMAC signing.
<div className="flex justify-center">
<Image
src="/static/blocks/webhook.png"
alt="Webhook Block"
width={500}
height={400}
className="my-6"
/>
</div>
## Configuration
### Webhook URL
The destination endpoint for your webhook request. Supports both static URLs and dynamic values from other blocks.
### Payload
JSON data to send in the request body. Use the AI wand to generate payloads or reference workflow variables:
```json
{
"event": "workflow.completed",
"data": {
"result": "<agent.content>",
"timestamp": "<function.result>"
}
}
```
### Signing Secret
Optional secret for HMAC-SHA256 payload signing. When provided, adds an `X-Webhook-Signature` header:
```
X-Webhook-Signature: t=1704067200000,v1=5d41402abc4b2a76b9719d911017c592...
```
To verify signatures, compute `HMAC-SHA256(secret, "${timestamp}.${body}")` and compare with the `v1` value.
### Additional Headers
Custom key-value headers to include with the request. These override any automatic headers with the same name.
## Automatic Headers
Every request includes these headers automatically:
| Header | Description |
|--------|-------------|
| `Content-Type` | `application/json` |
| `X-Webhook-Timestamp` | Unix timestamp in milliseconds |
| `X-Delivery-ID` | Unique UUID for this delivery |
| `Idempotency-Key` | Same as `X-Delivery-ID` for deduplication |
## Outputs
| Output | Type | Description |
|--------|------|-------------|
| `data` | json | Response body from the endpoint |
| `status` | number | HTTP status code |
| `headers` | object | Response headers |
## Example Use Cases
**Notify external services** - Send workflow results to Slack, Discord, or custom endpoints
```
Agent → Function (format) → Webhook (notify)
```
**Trigger external workflows** - Start processes in other systems when conditions are met
```
Condition (check) → Webhook (trigger) → Response
```
<Callout>
The Webhook block always uses POST. For other HTTP methods or more control, use the [API block](/blocks/api).
</Callout>

View File

@@ -15,7 +15,7 @@ The Generic Webhook block creates a flexible endpoint that can receive any paylo
<div className="flex justify-center">
<Image
src="/static/blocks/webhook.png"
src="/static/blocks/webhook-trigger.png"
alt="Generic Webhook Configuration"
width={500}
height={400}

View File

@@ -0,0 +1,89 @@
---
title: Webhook
---
import { Callout } from 'fumadocs-ui/components/callout'
import { Image } from '@/components/ui/image'
El bloque Webhook envía solicitudes HTTP POST a endpoints de webhook externos con encabezados de webhook automáticos y firma HMAC opcional.
<div className="flex justify-center">
<Image
src="/static/blocks/webhook.png"
alt="Bloque Webhook"
width={500}
height={400}
className="my-6"
/>
</div>
## Configuración
### URL del webhook
El endpoint de destino para tu solicitud de webhook. Admite tanto URL estáticas como valores dinámicos de otros bloques.
### Carga útil
Datos JSON para enviar en el cuerpo de la solicitud. Usa la varita de IA para generar cargas útiles o referenciar variables del flujo de trabajo:
```json
{
"event": "workflow.completed",
"data": {
"result": "<agent.content>",
"timestamp": "<function.result>"
}
}
```
### Secreto de firma
Secreto opcional para la firma HMAC-SHA256 de la carga útil. Cuando se proporciona, añade un encabezado `X-Webhook-Signature`:
```
X-Webhook-Signature: t=1704067200000,v1=5d41402abc4b2a76b9719d911017c592...
```
Para verificar las firmas, calcula `HMAC-SHA256(secret, "${timestamp}.${body}")` y compara con el valor `v1`.
### Encabezados adicionales
Encabezados personalizados de clave-valor para incluir con la solicitud. Estos sobrescriben cualquier encabezado automático con el mismo nombre.
## Encabezados automáticos
Cada solicitud incluye estos encabezados automáticamente:
| Encabezado | Descripción |
|--------|-------------|
| `Content-Type` | `application/json` |
| `X-Webhook-Timestamp` | Marca de tiempo Unix en milisegundos |
| `X-Delivery-ID` | UUID único para esta entrega |
| `Idempotency-Key` | Igual que `X-Delivery-ID` para deduplicación |
## Salidas
| Salida | Tipo | Descripción |
|--------|------|-------------|
| `data` | json | Cuerpo de respuesta del endpoint |
| `status` | number | Código de estado HTTP |
| `headers` | object | Encabezados de respuesta |
## Ejemplos de casos de uso
**Notificar servicios externos** - Envía resultados del flujo de trabajo a Slack, Discord o endpoints personalizados
```
Agent → Function (format) → Webhook (notify)
```
**Activar flujos de trabajo externos** - Inicia procesos en otros sistemas cuando se cumplan las condiciones
```
Condition (check) → Webhook (trigger) → Response
```
<Callout>
El bloque Webhook siempre usa POST. Para otros métodos HTTP o más control, usa el [bloque API](/blocks/api).
</Callout>

View File

@@ -1,230 +0,0 @@
---
title: Webhook
description: Recibe webhooks de cualquier servicio configurando un webhook personalizado.
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
import { Image } from '@/components/ui/image'
<BlockInfoCard
type="generic_webhook"
color="#10B981"
/>
<div className="flex justify-center">
<Image
src="/static/blocks/webhook.png"
alt="Configuración del bloque Webhook"
width={500}
height={400}
className="my-6"
/>
</div>
## Descripción general
El bloque Webhook genérico te permite recibir webhooks desde cualquier servicio externo. Este es un disparador flexible que puede manejar cualquier carga útil JSON, lo que lo hace ideal para integrarse con servicios que no tienen un bloque Sim dedicado.
## Uso básico
### Modo de paso simple
Sin definir un formato de entrada, el webhook transmite todo el cuerpo de la solicitud tal como está:
```bash
curl -X POST https://sim.ai/api/webhooks/trigger/{webhook-path} \
-H "Content-Type: application/json" \
-H "X-Sim-Secret: your-secret" \
-d '{
"message": "Test webhook trigger",
"data": {
"key": "value"
}
}'
```
Accede a los datos en bloques posteriores usando:
- `<webhook1.message>` → "Test webhook trigger"
- `<webhook1.data.key>` → "value"
### Formato de entrada estructurado (opcional)
Define un esquema de entrada para obtener campos tipados y habilitar funciones avanzadas como cargas de archivos:
**Configuración del formato de entrada:**
```json
[
{ "name": "message", "type": "string" },
{ "name": "priority", "type": "number" },
{ "name": "documents", "type": "files" }
]
```
**Solicitud de webhook:**
```bash
curl -X POST https://sim.ai/api/webhooks/trigger/{webhook-path} \
-H "Content-Type: application/json" \
-H "X-Sim-Secret: your-secret" \
-d '{
"message": "Invoice submission",
"priority": 1,
"documents": [
{
"type": "file",
"data": "data:application/pdf;base64,JVBERi0xLjQK...",
"name": "invoice.pdf",
"mime": "application/pdf"
}
]
}'
```
## Cargas de archivos
### Formatos de archivo compatibles
El webhook admite dos formatos de entrada de archivos:
#### 1. Archivos codificados en Base64
Para cargar contenido de archivos directamente:
```json
{
"documents": [
{
"type": "file",
"data": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgA...",
"name": "screenshot.png",
"mime": "image/png"
}
]
}
```
- **Tamaño máximo**: 20MB por archivo
- **Formato**: URL de datos estándar con codificación base64
- **Almacenamiento**: Los archivos se cargan en almacenamiento seguro de ejecución
#### 2. Referencias URL
Para pasar URLs de archivos existentes:
```json
{
"documents": [
{
"type": "url",
"data": "https://example.com/files/document.pdf",
"name": "document.pdf",
"mime": "application/pdf"
}
]
}
```
### Acceso a archivos en bloques posteriores
Los archivos se procesan en objetos `UserFile` con las siguientes propiedades:
```typescript
{
id: string, // Unique file identifier
name: string, // Original filename
url: string, // Presigned URL (valid for 5 minutes)
size: number, // File size in bytes
type: string, // MIME type
key: string, // Storage key
uploadedAt: string, // ISO timestamp
expiresAt: string // ISO timestamp (5 minutes)
}
```
**Acceso en bloques:**
- `<webhook1.documents[0].url>` → URL de descarga
- `<webhook1.documents[0].name>` → "invoice.pdf"
- `<webhook1.documents[0].size>` → 524288
- `<webhook1.documents[0].type>` → "application/pdf"
### Ejemplo completo de carga de archivos
```bash
# Create a base64-encoded file
echo "Hello World" | base64
# SGVsbG8gV29ybGQK
# Send webhook with file
curl -X POST https://sim.ai/api/webhooks/trigger/{webhook-path} \
-H "Content-Type: application/json" \
-H "X-Sim-Secret: your-secret" \
-d '{
"subject": "Document for review",
"attachments": [
{
"type": "file",
"data": "data:text/plain;base64,SGVsbG8gV29ybGQK",
"name": "sample.txt",
"mime": "text/plain"
}
]
}'
```
## Autenticación
### Configurar autenticación (opcional)
En la configuración del webhook:
1. Habilitar "Requerir autenticación"
2. Establecer un token secreto
3. Elegir tipo de encabezado:
- **Encabezado personalizado**: `X-Sim-Secret: your-token`
- **Autorización Bearer**: `Authorization: Bearer your-token`
### Uso de la autenticación
```bash
# With custom header
curl -X POST https://sim.ai/api/webhooks/trigger/{webhook-path} \
-H "Content-Type: application/json" \
-H "X-Sim-Secret: your-secret-token" \
-d '{"message": "Authenticated request"}'
# With bearer token
curl -X POST https://sim.ai/api/webhooks/trigger/{webhook-path} \
-H "Content-Type: application/json" \
-H "Authorization: Bearer your-secret-token" \
-d '{"message": "Authenticated request"}'
```
## Mejores prácticas
1. **Usar formato de entrada para estructura**: Define un formato de entrada cuando conozcas el esquema esperado. Esto proporciona:
- Validación de tipo
- Mejor autocompletado en el editor
- Capacidades de carga de archivos
2. **Autenticación**: Habilita siempre la autenticación para webhooks en producción para prevenir accesos no autorizados.
3. **Límites de tamaño de archivo**: Mantén los archivos por debajo de 20MB. Para archivos más grandes, usa referencias URL en su lugar.
4. **Caducidad de archivos**: Los archivos descargados tienen URLs con caducidad de 5 minutos. Procésalos rápidamente o almacénalos en otro lugar si los necesitas por más tiempo.
5. **Manejo de errores**: El procesamiento de webhooks es asíncrono. Revisa los registros de ejecución para detectar errores.
6. **Pruebas**: Usa el botón "Probar webhook" en el editor para validar tu configuración antes de implementarla.
## Casos de uso
- **Envíos de formularios**: Recibe datos de formularios personalizados con cargas de archivos
- **Integraciones con terceros**: Conéctate con servicios que envían webhooks (Stripe, GitHub, etc.)
- **Procesamiento de documentos**: Acepta documentos de sistemas externos para procesarlos
- **Notificaciones de eventos**: Recibe datos de eventos de varias fuentes
- **APIs personalizadas**: Construye endpoints de API personalizados para tus aplicaciones
## Notas
- Categoría: `triggers`
- Tipo: `generic_webhook`
- **Soporte de archivos**: disponible a través de la configuración del formato de entrada
- **Tamaño máximo de archivo**: 20MB por archivo

View File

@@ -15,8 +15,8 @@ El bloque de webhook genérico crea un punto de conexión flexible que puede rec
<div className="flex justify-center">
<Image
src="/static/blocks/webhook.png"
alt="Configuración genérica de webhook"
src="/static/blocks/webhook-trigger.png"
alt="Configuración de webhook genérico"
width={500}
height={400}
className="my-6"

View File

@@ -0,0 +1,89 @@
---
title: Webhook
---
import { Callout } from 'fumadocs-ui/components/callout'
import { Image } from '@/components/ui/image'
Le bloc Webhook envoie des requêtes HTTP POST vers des points de terminaison webhook externes avec des en-têtes webhook automatiques et une signature HMAC optionnelle.
<div className="flex justify-center">
<Image
src="/static/blocks/webhook.png"
alt="Bloc Webhook"
width={500}
height={400}
className="my-6"
/>
</div>
## Configuration
### URL du webhook
Le point de terminaison de destination pour votre requête webhook. Prend en charge les URL statiques et les valeurs dynamiques provenant d'autres blocs.
### Charge utile
Données JSON à envoyer dans le corps de la requête. Utilisez la baguette IA pour générer des charges utiles ou référencer des variables de workflow :
```json
{
"event": "workflow.completed",
"data": {
"result": "<agent.content>",
"timestamp": "<function.result>"
}
}
```
### Secret de signature
Secret optionnel pour la signature HMAC-SHA256 de la charge utile. Lorsqu'il est fourni, ajoute un en-tête `X-Webhook-Signature` :
```
X-Webhook-Signature: t=1704067200000,v1=5d41402abc4b2a76b9719d911017c592...
```
Pour vérifier les signatures, calculez `HMAC-SHA256(secret, "${timestamp}.${body}")` et comparez avec la valeur `v1`.
### En-têtes supplémentaires
En-têtes personnalisés clé-valeur à inclure avec la requête. Ceux-ci remplacent tous les en-têtes automatiques portant le même nom.
## En-têtes automatiques
Chaque requête inclut automatiquement ces en-têtes :
| En-tête | Description |
|--------|-------------|
| `Content-Type` | `application/json` |
| `X-Webhook-Timestamp` | Horodatage Unix en millisecondes |
| `X-Delivery-ID` | UUID unique pour cette livraison |
| `Idempotency-Key` | Identique à `X-Delivery-ID` pour la déduplication |
## Sorties
| Sortie | Type | Description |
|--------|------|-------------|
| `data` | json | Corps de la réponse du point de terminaison |
| `status` | number | Code de statut HTTP |
| `headers` | object | En-têtes de réponse |
## Exemples de cas d'usage
**Notifier des services externes** - Envoyer les résultats du workflow vers Slack, Discord ou des points de terminaison personnalisés
```
Agent → Function (format) → Webhook (notify)
```
**Déclencher des workflows externes** - Démarrer des processus dans d'autres systèmes lorsque des conditions sont remplies
```
Condition (check) → Webhook (trigger) → Response
```
<Callout>
Le bloc Webhook utilise toujours POST. Pour d'autres méthodes HTTP ou plus de contrôle, utilisez le [bloc API](/blocks/api).
</Callout>

View File

@@ -1,231 +0,0 @@
---
title: Webhook
description: Recevez des webhooks de n'importe quel service en configurant un
webhook personnalisé.
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
import { Image } from '@/components/ui/image'
<BlockInfoCard
type="generic_webhook"
color="#10B981"
/>
<div className="flex justify-center">
<Image
src="/static/blocks/webhook.png"
alt="Configuration du bloc Webhook"
width={500}
height={400}
className="my-6"
/>
</div>
## Aperçu
Le bloc Webhook générique vous permet de recevoir des webhooks depuis n'importe quel service externe. C'est un déclencheur flexible qui peut traiter n'importe quelle charge utile JSON, ce qui le rend idéal pour l'intégration avec des services qui n'ont pas de bloc Sim dédié.
## Utilisation de base
### Mode de transmission simple
Sans définir un format d'entrée, le webhook transmet l'intégralité du corps de la requête tel quel :
```bash
curl -X POST https://sim.ai/api/webhooks/trigger/{webhook-path} \
-H "Content-Type: application/json" \
-H "X-Sim-Secret: your-secret" \
-d '{
"message": "Test webhook trigger",
"data": {
"key": "value"
}
}'
```
Accédez aux données dans les blocs en aval en utilisant :
- `<webhook1.message>` → "Test webhook trigger"
- `<webhook1.data.key>` → "value"
### Format d'entrée structuré (optionnel)
Définissez un schéma d'entrée pour obtenir des champs typés et activer des fonctionnalités avancées comme les téléchargements de fichiers :
**Configuration du format d'entrée :**
```json
[
{ "name": "message", "type": "string" },
{ "name": "priority", "type": "number" },
{ "name": "documents", "type": "files" }
]
```
**Requête Webhook :**
```bash
curl -X POST https://sim.ai/api/webhooks/trigger/{webhook-path} \
-H "Content-Type: application/json" \
-H "X-Sim-Secret: your-secret" \
-d '{
"message": "Invoice submission",
"priority": 1,
"documents": [
{
"type": "file",
"data": "data:application/pdf;base64,JVBERi0xLjQK...",
"name": "invoice.pdf",
"mime": "application/pdf"
}
]
}'
```
## Téléchargements de fichiers
### Formats de fichiers pris en charge
Le webhook prend en charge deux formats d'entrée de fichiers :
#### 1. Fichiers encodés en Base64
Pour télécharger directement le contenu du fichier :
```json
{
"documents": [
{
"type": "file",
"data": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgA...",
"name": "screenshot.png",
"mime": "image/png"
}
]
}
```
- **Taille maximale** : 20 Mo par fichier
- **Format** : URL de données standard avec encodage base64
- **Stockage** : Les fichiers sont téléchargés dans un stockage d'exécution sécurisé
#### 2. Références URL
Pour transmettre des URL de fichiers existants :
```json
{
"documents": [
{
"type": "url",
"data": "https://example.com/files/document.pdf",
"name": "document.pdf",
"mime": "application/pdf"
}
]
}
```
### Accès aux fichiers dans les blocs en aval
Les fichiers sont traités en objets `UserFile` avec les propriétés suivantes :
```typescript
{
id: string, // Unique file identifier
name: string, // Original filename
url: string, // Presigned URL (valid for 5 minutes)
size: number, // File size in bytes
type: string, // MIME type
key: string, // Storage key
uploadedAt: string, // ISO timestamp
expiresAt: string // ISO timestamp (5 minutes)
}
```
**Accès dans les blocs :**
- `<webhook1.documents[0].url>` → URL de téléchargement
- `<webhook1.documents[0].name>` → "invoice.pdf"
- `<webhook1.documents[0].size>` → 524288
- `<webhook1.documents[0].type>` → "application/pdf"
### Exemple complet de téléchargement de fichier
```bash
# Create a base64-encoded file
echo "Hello World" | base64
# SGVsbG8gV29ybGQK
# Send webhook with file
curl -X POST https://sim.ai/api/webhooks/trigger/{webhook-path} \
-H "Content-Type: application/json" \
-H "X-Sim-Secret: your-secret" \
-d '{
"subject": "Document for review",
"attachments": [
{
"type": "file",
"data": "data:text/plain;base64,SGVsbG8gV29ybGQK",
"name": "sample.txt",
"mime": "text/plain"
}
]
}'
```
## Authentification
### Configurer l'authentification (optionnel)
Dans la configuration du webhook :
1. Activez "Exiger l'authentification"
2. Définissez un jeton secret
3. Choisissez le type d'en-tête :
- **En-tête personnalisé** : `X-Sim-Secret: your-token`
- **Autorisation Bearer** : `Authorization: Bearer your-token`
### Utilisation de l'authentification
```bash
# With custom header
curl -X POST https://sim.ai/api/webhooks/trigger/{webhook-path} \
-H "Content-Type: application/json" \
-H "X-Sim-Secret: your-secret-token" \
-d '{"message": "Authenticated request"}'
# With bearer token
curl -X POST https://sim.ai/api/webhooks/trigger/{webhook-path} \
-H "Content-Type: application/json" \
-H "Authorization: Bearer your-secret-token" \
-d '{"message": "Authenticated request"}'
```
## Bonnes pratiques
1. **Utiliser le format d'entrée pour la structure** : définissez un format d'entrée lorsque vous connaissez le schéma attendu. Cela fournit :
- Validation de type
- Meilleure autocomplétion dans l'éditeur
- Capacités de téléchargement de fichiers
2. **Authentification** : activez toujours l'authentification pour les webhooks en production afin d'empêcher les accès non autorisés.
3. **Limites de taille de fichier** : gardez les fichiers en dessous de 20 Mo. Pour les fichiers plus volumineux, utilisez plutôt des références URL.
4. **Expiration des fichiers** : les fichiers téléchargés ont des URL d'expiration de 5 minutes. Traitez-les rapidement ou stockez-les ailleurs si vous en avez besoin plus longtemps.
5. **Gestion des erreurs** : le traitement des webhooks est asynchrone. Vérifiez les journaux d'exécution pour les erreurs.
6. **Tests** : utilisez le bouton "Tester le webhook" dans l'éditeur pour valider votre configuration avant le déploiement.
## Cas d'utilisation
- **Soumissions de formulaires** : recevez des données de formulaires personnalisés avec téléchargement de fichiers
- **Intégrations tierces** : connectez-vous avec des services qui envoient des webhooks (Stripe, GitHub, etc.)
- **Traitement de documents** : acceptez des documents de systèmes externes pour traitement
- **Notifications d'événements** : recevez des données d'événements de diverses sources
- **API personnalisées** : créez des points de terminaison API personnalisés pour vos applications
## Remarques
- Catégorie : `triggers`
- Type : `generic_webhook`
- **Support de fichiers** : disponible via la configuration du format d'entrée
- **Taille maximale de fichier** : 20 Mo par fichier

View File

@@ -15,8 +15,8 @@ Le bloc Webhook générique crée un point de terminaison flexible qui peut rece
<div className="flex justify-center">
<Image
src="/static/blocks/webhook.png"
alt="Configuration de webhook générique"
src="/static/blocks/webhook-trigger.png"
alt="Configuration du webhook générique"
width={500}
height={400}
className="my-6"

View File

@@ -0,0 +1,89 @@
---
title: Webhook
---
import { Callout } from 'fumadocs-ui/components/callout'
import { Image } from '@/components/ui/image'
Webhookブロックは、自動的なWebhookヘッダーとオプションのHMAC署名を使用して、外部のWebhookエンドポイントにHTTP POSTリクエストを送信します。
<div className="flex justify-center">
<Image
src="/static/blocks/webhook.png"
alt="Webhookブロック"
width={500}
height={400}
className="my-6"
/>
</div>
## 設定
### Webhook URL
Webhookリクエストの送信先エンドポイントです。静的URLと他のブロックからの動的な値の両方に対応しています。
### ペイロード
リクエストボディで送信するJSONデータです。AIワンドを使用してペイロードを生成したり、ワークフロー変数を参照したりできます。
```json
{
"event": "workflow.completed",
"data": {
"result": "<agent.content>",
"timestamp": "<function.result>"
}
}
```
### 署名シークレット
HMAC-SHA256ペイロード署名用のオプションのシークレットです。指定すると、`X-Webhook-Signature`ヘッダーが追加されます。
```
X-Webhook-Signature: t=1704067200000,v1=5d41402abc4b2a76b9719d911017c592...
```
署名を検証するには、`HMAC-SHA256(secret, "${timestamp}.${body}")`を計算し、`v1`の値と比較します。
### 追加ヘッダー
リクエストに含めるカスタムのキーと値のヘッダーです。同じ名前の自動ヘッダーがある場合は上書きされます。
## 自動ヘッダー
すべてのリクエストには、以下のヘッダーが自動的に含まれます。
| ヘッダー | 説明 |
|--------|-------------|
| `Content-Type` | `application/json` |
| `X-Webhook-Timestamp` | ミリ秒単位のUnixタイムスタンプ |
| `X-Delivery-ID` | この配信の一意のUUID |
| `Idempotency-Key` | 重複排除用の`X-Delivery-ID`と同じ |
## 出力
| 出力 | 型 | 説明 |
|--------|------|-------------|
| `data` | json | エンドポイントからのレスポンスボディ |
| `status` | number | HTTPステータスコード |
| `headers` | object | レスポンスヘッダー |
## 使用例
**外部サービスへの通知** - ワークフローの結果をSlack、Discord、またはカスタムエンドポイントに送信します。
```
Agent → Function (format) → Webhook (notify)
```
**外部ワークフローのトリガー** - 条件が満たされたときに他のシステムでプロセスを開始します。
```
Condition (check) → Webhook (trigger) → Response
```
<Callout>
Webhookブロックは常にPOSTを使用します。他のHTTPメソッドやより詳細な制御が必要な場合は、[APIブロック](/blocks/api)を使用してください。
</Callout>

View File

@@ -1,230 +0,0 @@
---
title: Webhook
description: カスタムウェブフックを設定して、任意のサービスからウェブフックを受信します。
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
import { Image } from '@/components/ui/image'
<BlockInfoCard
type="generic_webhook"
color="#10B981"
/>
<div className="flex justify-center">
<Image
src="/static/blocks/webhook.png"
alt="Webhookブロックの設定"
width={500}
height={400}
className="my-6"
/>
</div>
## 概要
汎用Webhookブロックを使用すると、任意の外部サービスからWebhookを受信できます。これは柔軟なトリガーであり、あらゆるJSONペイロードを処理できるため、専用のSimブロックがないサービスとの統合に最適です。
## 基本的な使用方法
### シンプルなパススルーモード
入力フォーマットを定義しない場合、Webhookはリクエスト本文全体をそのまま渡します
```bash
curl -X POST https://sim.ai/api/webhooks/trigger/{webhook-path} \
-H "Content-Type: application/json" \
-H "X-Sim-Secret: your-secret" \
-d '{
"message": "Test webhook trigger",
"data": {
"key": "value"
}
}'
```
下流のブロックでデータにアクセスする方法:
- `<webhook1.message>` → "Test webhook trigger"
- `<webhook1.data.key>` → "value"
### 構造化入力フォーマット(オプション)
入力スキーマを定義して、型付きフィールドを取得し、ファイルアップロードなどの高度な機能を有効にします:
**入力フォーマット設定:**
```json
[
{ "name": "message", "type": "string" },
{ "name": "priority", "type": "number" },
{ "name": "documents", "type": "files" }
]
```
**Webhookリクエスト**
```bash
curl -X POST https://sim.ai/api/webhooks/trigger/{webhook-path} \
-H "Content-Type: application/json" \
-H "X-Sim-Secret: your-secret" \
-d '{
"message": "Invoice submission",
"priority": 1,
"documents": [
{
"type": "file",
"data": "data:application/pdf;base64,JVBERi0xLjQK...",
"name": "invoice.pdf",
"mime": "application/pdf"
}
]
}'
```
## ファイルアップロード
### サポートされているファイル形式
Webhookは2つのファイル入力形式をサポートしています
#### 1. Base64エンコードファイル
ファイルコンテンツを直接アップロードする場合:
```json
{
"documents": [
{
"type": "file",
"data": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgA...",
"name": "screenshot.png",
"mime": "image/png"
}
]
}
```
- **最大サイズ**: ファイルあたり20MB
- **フォーマット**: Base64エンコーディングを使用した標準データURL
- **ストレージ**: ファイルは安全な実行ストレージにアップロードされます
#### 2. URL参照
既存のファイルURLを渡す場合
```json
{
"documents": [
{
"type": "url",
"data": "https://example.com/files/document.pdf",
"name": "document.pdf",
"mime": "application/pdf"
}
]
}
```
### 下流のブロックでファイルにアクセスする
ファイルは以下のプロパティを持つ `UserFile` オブジェクトに処理されます:
```typescript
{
id: string, // Unique file identifier
name: string, // Original filename
url: string, // Presigned URL (valid for 5 minutes)
size: number, // File size in bytes
type: string, // MIME type
key: string, // Storage key
uploadedAt: string, // ISO timestamp
expiresAt: string // ISO timestamp (5 minutes)
}
```
**ブロック内でのアクセス:**
- `<webhook1.documents[0].url>` → ダウンロードURL
- `<webhook1.documents[0].name>` → "invoice.pdf"
- `<webhook1.documents[0].size>` → 524288
- `<webhook1.documents[0].type>` → "application/pdf"
### ファイルアップロードの完全な例
```bash
# Create a base64-encoded file
echo "Hello World" | base64
# SGVsbG8gV29ybGQK
# Send webhook with file
curl -X POST https://sim.ai/api/webhooks/trigger/{webhook-path} \
-H "Content-Type: application/json" \
-H "X-Sim-Secret: your-secret" \
-d '{
"subject": "Document for review",
"attachments": [
{
"type": "file",
"data": "data:text/plain;base64,SGVsbG8gV29ybGQK",
"name": "sample.txt",
"mime": "text/plain"
}
]
}'
```
## 認証
### 認証の設定(オプション)
ウェブフック設定で:
1. 「認証を要求する」を有効にする
2. シークレットトークンを設定する
3. ヘッダータイプを選択する:
- **カスタムヘッダー**: `X-Sim-Secret: your-token`
- **認証ベアラー**: `Authorization: Bearer your-token`
### 認証の使用
```bash
# With custom header
curl -X POST https://sim.ai/api/webhooks/trigger/{webhook-path} \
-H "Content-Type: application/json" \
-H "X-Sim-Secret: your-secret-token" \
-d '{"message": "Authenticated request"}'
# With bearer token
curl -X POST https://sim.ai/api/webhooks/trigger/{webhook-path} \
-H "Content-Type: application/json" \
-H "Authorization: Bearer your-secret-token" \
-d '{"message": "Authenticated request"}'
```
## ベストプラクティス
1. **構造化のための入力フォーマットの使用**: 予想されるスキーマがわかっている場合は入力フォーマットを定義してください。これにより以下が提供されます:
- 型の検証
- エディタでのより良いオートコンプリート
- ファイルアップロード機能
2. **認証**: 不正アクセスを防ぐため、本番環境のウェブフックには常に認証を有効にしてください。
3. **ファイルサイズの制限**: ファイルは20MB未満に保ってください。より大きなファイルの場合は、代わりにURL参照を使用してください。
4. **ファイルの有効期限**: ダウンロードされたファイルのURLは5分間有効です。すぐに処理するか、長期間必要な場合は別の場所に保存してください。
5. **エラー処理**: ウェブフック処理は非同期です。エラーについては実行ログを確認してください。
6. **テスト**: 設定をデプロイする前に、エディタの「ウェブフックをテスト」ボタンを使用して設定を検証してください。
## ユースケース
- **フォーム送信**: ファイルアップロード機能を持つカスタムフォームからデータを受け取る
- **サードパーティ連携**: ウェブフックを送信するサービスStripe、GitHubなどと接続する
- **ドキュメント処理**: 外部システムからドキュメントを受け取って処理する
- **イベント通知**: さまざまなソースからイベントデータを受け取る
- **カスタムAPI**: アプリケーション用のカスタムAPIエンドポイントを構築する
## 注意事項
- カテゴリ:`triggers`
- タイプ:`generic_webhook`
- **ファイルサポート**:入力フォーマット設定で利用可能
- **最大ファイルサイズ**ファイルあたり20MB

View File

@@ -15,7 +15,7 @@ Webhookを使用すると、外部サービスがHTTPリクエストを送信し
<div className="flex justify-center">
<Image
src="/static/blocks/webhook.png"
src="/static/blocks/webhook-trigger.png"
alt="汎用Webhook設定"
width={500}
height={400}

View File

@@ -0,0 +1,89 @@
---
title: Webhook
---
import { Callout } from 'fumadocs-ui/components/callout'
import { Image } from '@/components/ui/image'
Webhook 模块会向外部 webhook 端点发送 HTTP POST 请求,自动附加 webhook 头部,并可选用 HMAC 签名。
<div className="flex justify-center">
<Image
src="/static/blocks/webhook.png"
alt="Webhook 模块"
width={500}
height={400}
className="my-6"
/>
</div>
## 配置
### Webhook URL
Webhook 请求的目标端点。支持静态 URL 和来自其他模块的动态值。
### 负载
要在请求体中发送的 JSON 数据。可使用 AI 魔杖生成负载,或引用工作流变量:
```json
{
"event": "workflow.completed",
"data": {
"result": "<agent.content>",
"timestamp": "<function.result>"
}
}
```
### 签名密钥
可选的 HMAC-SHA256 负载签名密钥。填写后会添加 `X-Webhook-Signature` 头部:
```
X-Webhook-Signature: t=1704067200000,v1=5d41402abc4b2a76b9719d911017c592...
```
要验证签名,请计算 `HMAC-SHA256(secret, "${timestamp}.${body}")` 并与 `v1` 的值进行比对。
### 额外头部
自定义的键值头部,将随请求一同发送。若与自动头部同名,则会覆盖自动头部。
## 自动头部
每个请求都会自动包含以下头部:
| Header | 说明 |
|--------|------|
| `Content-Type` | `application/json` |
| `X-Webhook-Timestamp` | Unix 时间戳(毫秒) |
| `X-Delivery-ID` | 本次投递的唯一 UUID |
| `Idempotency-Key` | 与 `X-Delivery-ID` 相同,用于去重 |
## 输出
| 输出 | 类型 | 说明 |
|------|------|------|
| `data` | json | 端点返回的响应体 |
| `status` | number | HTTP 状态码 |
| `headers` | object | 响应头部 |
## 示例用例
**通知外部服务** - 将工作流结果发送到 Slack、Discord 或自定义端点
```
Agent → Function (format) → Webhook (notify)
```
**触发外部工作流** - 当满足条件时,在其他系统中启动流程
```
Condition (check) → Webhook (trigger) → Response
```
<Callout>
Webhook 模块始终使用 POST。如需使用其他 HTTP 方法或获得更多控制,请使用 [API 模块](/blocks/api)。
</Callout>

View File

@@ -1,230 +0,0 @@
---
title: Webhook
description: 通过配置自定义 webhook从任何服务接收 webhook。
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
import { Image } from '@/components/ui/image'
<BlockInfoCard
type="generic_webhook"
color="#10B981"
/>
<div className="flex justify-center">
<Image
src="/static/blocks/webhook.png"
alt="Webhook Block Configuration"
width={500}
height={400}
className="my-6"
/>
</div>
## 概述
通用 Webhook 模块允许您接收来自任何外部服务的 webhook。这是一个灵活的触发器可以处理任何 JSON 负载,非常适合与没有专用 Sim 模块的服务集成。
## 基本用法
### 简单直通模式
在未定义输入格式的情况下webhook 会按原样传递整个请求正文:
```bash
curl -X POST https://sim.ai/api/webhooks/trigger/{webhook-path} \
-H "Content-Type: application/json" \
-H "X-Sim-Secret: your-secret" \
-d '{
"message": "Test webhook trigger",
"data": {
"key": "value"
}
}'
```
在下游模块中使用以下方式访问数据:
- `<webhook1.message>` → "测试 webhook 触发器"
- `<webhook1.data.key>` → "值"
### 结构化输入格式(可选)
定义输入模式以获取类型化字段,并启用高级功能,例如文件上传:
**输入格式配置:**
```json
[
{ "name": "message", "type": "string" },
{ "name": "priority", "type": "number" },
{ "name": "documents", "type": "files" }
]
```
**Webhook 请求:**
```bash
curl -X POST https://sim.ai/api/webhooks/trigger/{webhook-path} \
-H "Content-Type: application/json" \
-H "X-Sim-Secret: your-secret" \
-d '{
"message": "Invoice submission",
"priority": 1,
"documents": [
{
"type": "file",
"data": "data:application/pdf;base64,JVBERi0xLjQK...",
"name": "invoice.pdf",
"mime": "application/pdf"
}
]
}'
```
## 文件上传
### 支持的文件格式
webhook 支持两种文件输入格式:
#### 1. Base64 编码文件
用于直接上传文件内容:
```json
{
"documents": [
{
"type": "file",
"data": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgA...",
"name": "screenshot.png",
"mime": "image/png"
}
]
}
```
- **最大大小**:每个文件 20MB
- **格式**:带有 base64 编码的标准数据 URL
- **存储**:文件上传到安全的执行存储
#### 2. URL 引用
用于传递现有文件 URL
```json
{
"documents": [
{
"type": "url",
"data": "https://example.com/files/document.pdf",
"name": "document.pdf",
"mime": "application/pdf"
}
]
}
```
### 在下游模块中访问文件
文件被处理为具有以下属性的 `UserFile` 对象:
```typescript
{
id: string, // Unique file identifier
name: string, // Original filename
url: string, // Presigned URL (valid for 5 minutes)
size: number, // File size in bytes
type: string, // MIME type
key: string, // Storage key
uploadedAt: string, // ISO timestamp
expiresAt: string // ISO timestamp (5 minutes)
}
```
**分块访问:**
- `<webhook1.documents[0].url>` → 下载 URL
- `<webhook1.documents[0].name>` → "invoice.pdf"
- `<webhook1.documents[0].size>` → 524288
- `<webhook1.documents[0].type>` → "application/pdf"
### 完整文件上传示例
```bash
# Create a base64-encoded file
echo "Hello World" | base64
# SGVsbG8gV29ybGQK
# Send webhook with file
curl -X POST https://sim.ai/api/webhooks/trigger/{webhook-path} \
-H "Content-Type: application/json" \
-H "X-Sim-Secret: your-secret" \
-d '{
"subject": "Document for review",
"attachments": [
{
"type": "file",
"data": "data:text/plain;base64,SGVsbG8gV29ybGQK",
"name": "sample.txt",
"mime": "text/plain"
}
]
}'
```
## 身份验证
### 配置身份验证(可选)
在 webhook 配置中:
1. 启用“需要身份验证”
2. 设置一个密钥令牌
3. 选择头类型:
- **自定义头**: `X-Sim-Secret: your-token`
- **授权 Bearer**: `Authorization: Bearer your-token`
### 使用身份验证
```bash
# With custom header
curl -X POST https://sim.ai/api/webhooks/trigger/{webhook-path} \
-H "Content-Type: application/json" \
-H "X-Sim-Secret: your-secret-token" \
-d '{"message": "Authenticated request"}'
# With bearer token
curl -X POST https://sim.ai/api/webhooks/trigger/{webhook-path} \
-H "Content-Type: application/json" \
-H "Authorization: Bearer your-secret-token" \
-d '{"message": "Authenticated request"}'
```
## 最佳实践
1. **使用输入格式定义结构**:当您知道预期的模式时,定义输入格式。这提供:
- 类型验证
- 编辑器中的更好自动完成
- 文件上传功能
2. **身份验证**:在生产环境的 webhook 中始终启用身份验证,以防止未经授权的访问。
3. **文件大小限制**:将文件保持在 20MB 以下。对于更大的文件,请使用 URL 引用。
4. **文件过期**:下载的文件具有 5 分钟的 URL 过期时间。请及时处理,或如果需要更长时间,请将其存储在其他地方。
5. **错误处理**Webhook 处理是异步的。请检查执行日志以获取错误信息。
6. **测试**:在部署前,使用编辑器中的“测试 Webhook”按钮验证您的配置。
## 使用场景
- **表单提交**:接收带有文件上传的自定义表单数据
- **第三方集成**:与发送 webhook 的服务(如 Stripe、GitHub 等)连接
- **文档处理**:接受来自外部系统的文档进行处理
- **事件通知**:接收来自各种来源的事件数据
- **自定义 API**:为您的应用程序构建自定义 API 端点
## 注意事项
- 类别:`triggers`
- 类型:`generic_webhook`
- **文件支持**:通过输入格式配置可用
- **最大文件大小**:每个文件 20MB

View File

@@ -15,7 +15,7 @@ Webhook 允许外部服务通过向您的工作流发送 HTTP 请求来触发工
<div className="flex justify-center">
<Image
src="/static/blocks/webhook.png"
src="/static/blocks/webhook-trigger.png"
alt="通用 Webhook 配置"
width={500}
height={400}

View File

@@ -169,7 +169,7 @@ checksums:
content/1: 9d1b6de2021f809cc43502d19a19bd15
content/2: f4c40c45a45329eca670aca4fcece6f3
content/3: b03a97486cc185beb7b51644b548875a
content/4: a77222cf7a57362fc7eb5ebf7cc652c6
content/4: 01c24bef59948dbecc1ae19794019d5f
content/5: ba18ac99184b17d7e49bd1abdc814437
content/6: 171c4e97e509427ca63acccf136779b3
content/7: 98e1babdd0136267807b7e94ae7da6c7
@@ -50197,3 +50197,31 @@ checksums:
content/7: 7b29d23aec8fda839f3934c5fc71c6d3
content/8: b3f310d5ef115bea5a8b75bf25d7ea9a
content/9: 79ecd09a7bedc128285814d8b439ed40
2bf1f583bd3a431e459e5a0142a82efd:
meta/title: 70f95b2c27f2c3840b500fcaf79ee83c
content/0: eb0ed7078f192304703144f4cac3442f
content/1: 1bc1f971556fb854666c22551215d3c2
content/2: 5127a30fba20289720806082df2eae87
content/3: 0441638444240cd20a6c69ea1d3afbb1
content/4: 0b5805c0201ed427ba1b56b9814ee0cb
content/5: cf5305db38e782a1001f5208cdf6b5f1
content/6: 575a2fc0f65f0d24a9d75fac8e8bf5f8
content/7: 1acea0b3685c12e5c3d73c7afa9c5582
content/8: 4464a6c6f5ccc67b95309ba6399552e9
content/9: 336794d9cf3e900c1b5aba0071944f1c
content/10: bf46b631598a496c37560e074454f5ec
content/11: 3d6a55b18007832eb2ed751638e968ca
content/12: 3f97586d23efe56c4ab94c03a0b91706
content/13: f2caee00e0e386a5e5257862209aaaef
content/14: 15c9ed641ef776a33a945b6e0ddb908c
content/15: db087c66ef8c0ab22775072b10655d05
content/16: e148c1c6e1345e9ee95657c5ba40ebf4
content/17: 9feca6cbb058fb8070b23d139d2d96e6
content/18: 987932038f4e9442bd89f0f8ed3c5319
content/19: 8e0258b3891544d355fa4a92f2ae96e4
content/20: 9c2f91f89a914bf4661512275e461104
content/21: a5cc8d50937a37d5ae7e921fc85a71f1
content/22: 51b2fdf484e8d6b07cdf8434034dc872
content/23: 59da7694b8be001fec8b9f9d7b604faf
content/24: 8fb6954068c6687d44121e21a95cf1b6
content/25: 9e7b1a1a453340d20adf4cacbd532018

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 38 KiB

View File

@@ -20,7 +20,7 @@ interface NavProps {
}
export default function Nav({ hideAuthButtons = false, variant = 'landing' }: NavProps = {}) {
const [githubStars, setGithubStars] = useState('24.4k')
const [githubStars, setGithubStars] = useState('25.1k')
const [isHovered, setIsHovered] = useState(false)
const [isLoginHovered, setIsLoginHovered] = useState(false)
const router = useRouter()

View File

@@ -14,10 +14,6 @@ import {
} from '@/app/api/__test-utils__/utils'
const {
hasProcessedMessageMock,
markMessageAsProcessedMock,
closeRedisConnectionMock,
acquireLockMock,
generateRequestHashMock,
validateSlackSignatureMock,
handleWhatsAppVerificationMock,
@@ -28,10 +24,6 @@ const {
processWebhookMock,
executeMock,
} = vi.hoisted(() => ({
hasProcessedMessageMock: vi.fn().mockResolvedValue(false),
markMessageAsProcessedMock: vi.fn().mockResolvedValue(true),
closeRedisConnectionMock: vi.fn().mockResolvedValue(undefined),
acquireLockMock: vi.fn().mockResolvedValue(true),
generateRequestHashMock: vi.fn().mockResolvedValue('test-hash-123'),
validateSlackSignatureMock: vi.fn().mockResolvedValue(true),
handleWhatsAppVerificationMock: vi.fn().mockResolvedValue(null),
@@ -73,13 +65,6 @@ vi.mock('@/background/logs-webhook-delivery', () => ({
logsWebhookDelivery: {},
}))
vi.mock('@/lib/redis', () => ({
hasProcessedMessage: hasProcessedMessageMock,
markMessageAsProcessed: markMessageAsProcessedMock,
closeRedisConnection: closeRedisConnectionMock,
acquireLock: acquireLockMock,
}))
vi.mock('@/lib/webhooks/utils', () => ({
handleWhatsAppVerification: handleWhatsAppVerificationMock,
handleSlackChallenge: handleSlackChallengeMock,
@@ -201,9 +186,6 @@ describe('Webhook Trigger API Route', () => {
workspaceId: 'test-workspace-id',
})
hasProcessedMessageMock.mockResolvedValue(false)
markMessageAsProcessedMock.mockResolvedValue(true)
acquireLockMock.mockResolvedValue(true)
handleWhatsAppVerificationMock.mockResolvedValue(null)
processGenericDeduplicationMock.mockResolvedValue(null)
processWebhookMock.mockResolvedValue(new Response('Webhook processed', { status: 200 }))

View File

@@ -117,7 +117,7 @@ export default function ChatClient({ identifier }: { identifier: string }) {
const [error, setError] = useState<string | null>(null)
const messagesEndRef = useRef<HTMLDivElement>(null)
const messagesContainerRef = useRef<HTMLDivElement>(null)
const [starCount, setStarCount] = useState('24.4k')
const [starCount, setStarCount] = useState('25.1k')
const [conversationId, setConversationId] = useState('')
const [showScrollButton, setShowScrollButton] = useState(false)

View File

@@ -164,7 +164,7 @@ function getBlockIconAndColor(
return { icon: ParallelTool.icon, bgColor: ParallelTool.bgColor }
}
if (lowerType === 'workflow') {
return { icon: WorkflowIcon, bgColor: '#705335' }
return { icon: WorkflowIcon, bgColor: '#6366F1' }
}
// Look up from block registry (model maps to agent)

View File

@@ -256,24 +256,13 @@ export function InputMapping({
if (!selectedWorkflowId) {
return (
<div className='flex flex-col items-center justify-center rounded-[4px] border border-[var(--border-1)] bg-[var(--surface-3)] p-8 text-center dark:bg-[#1F1F1F]'>
<svg
className='mb-3 h-10 w-10 text-[var(--text-tertiary)]'
fill='none'
viewBox='0 0 24 24'
stroke='currentColor'
>
<path
strokeLinecap='round'
strokeLinejoin='round'
strokeWidth={1.5}
d='M13 10V3L4 14h7v7l9-11h-7z'
/>
</svg>
<p className='font-medium text-[var(--text-tertiary)] text-sm'>No workflow selected</p>
<p className='mt-1 text-[var(--text-tertiary)]/80 text-xs'>
Select a workflow above to configure inputs
</p>
<div className='flex h-32 items-center justify-center rounded-[4px] border border-[var(--border-1)] border-dashed bg-[var(--surface-3)] dark:bg-[#1F1F1F]'>
<div className='text-center'>
<p className='font-medium text-[var(--text-secondary)] text-sm'>No workflow selected</p>
<p className='mt-1 text-[var(--text-muted)] text-xs'>
Select a workflow above to configure inputs
</p>
</div>
</div>
)
}

View File

@@ -95,7 +95,9 @@ export function FieldFormat({
}: FieldFormatProps) {
const [storeValue, setStoreValue] = useSubBlockValue<Field[]>(blockId, subBlockId)
const valueInputRefs = useRef<Record<string, HTMLInputElement | HTMLTextAreaElement>>({})
const nameInputRefs = useRef<Record<string, HTMLInputElement>>({})
const overlayRefs = useRef<Record<string, HTMLDivElement>>({})
const nameOverlayRefs = useRef<Record<string, HTMLDivElement>>({})
const accessiblePrefixes = useAccessibleReferencePrefixes(blockId)
const inputController = useSubBlockInput({
@@ -158,6 +160,97 @@ export function FieldFormat({
if (overlay) overlay.scrollLeft = scrollLeft
}
/**
* Syncs scroll position between name input and overlay for text highlighting
*/
const syncNameOverlayScroll = (fieldId: string, scrollLeft: number) => {
const overlay = nameOverlayRefs.current[fieldId]
if (overlay) overlay.scrollLeft = scrollLeft
}
/**
* Generates a unique field key for name inputs to avoid collision with value inputs
*/
const getNameFieldKey = (fieldId: string) => `name-${fieldId}`
/**
* Renders the name input field with tag dropdown support
*/
const renderNameInput = (field: Field) => {
const nameFieldKey = getNameFieldKey(field.id)
const fieldValue = field.name ?? ''
const fieldState = inputController.fieldHelpers.getFieldState(nameFieldKey)
const handlers = inputController.fieldHelpers.createFieldHandlers(
nameFieldKey,
fieldValue,
(newValue) => updateField(field.id, 'name', newValue)
)
const tagSelectHandler = inputController.fieldHelpers.createTagSelectHandler(
nameFieldKey,
fieldValue,
(newValue) => updateField(field.id, 'name', newValue)
)
const inputClassName = cn('text-transparent caret-foreground')
return (
<>
<Input
ref={(el) => {
if (el) nameInputRefs.current[field.id] = el
}}
name='name'
value={fieldValue}
onChange={handlers.onChange}
onKeyDown={handlers.onKeyDown}
onDrop={handlers.onDrop}
onDragOver={handlers.onDragOver}
onScroll={(e) => syncNameOverlayScroll(field.id, e.currentTarget.scrollLeft)}
onPaste={() =>
setTimeout(() => {
const input = nameInputRefs.current[field.id]
input && syncNameOverlayScroll(field.id, input.scrollLeft)
}, 0)
}
placeholder={placeholder}
disabled={isReadOnly}
autoComplete='off'
className={cn('allow-scroll w-full overflow-auto', inputClassName)}
style={{ overflowX: 'auto' }}
/>
<div
ref={(el) => {
if (el) nameOverlayRefs.current[field.id] = el
}}
className='pointer-events-none absolute inset-0 flex items-center overflow-x-auto bg-transparent px-[8px] py-[6px] font-medium font-sans text-sm'
style={{ overflowX: 'auto' }}
>
<div
className='w-full whitespace-pre'
style={{ scrollbarWidth: 'none', minWidth: 'fit-content' }}
>
{formatDisplayText(
fieldValue,
accessiblePrefixes ? { accessiblePrefixes } : { highlightAll: true }
)}
</div>
</div>
{fieldState.showTags && (
<TagDropdown
visible={fieldState.showTags}
onSelect={tagSelectHandler}
blockId={blockId}
activeSourceBlockId={fieldState.activeSourceBlockId}
inputValue={fieldValue}
cursorPosition={fieldState.cursorPosition}
onClose={() => inputController.fieldHelpers.hideFieldDropdowns(nameFieldKey)}
inputRef={{ current: nameInputRefs.current[field.id] || null }}
/>
)}
</>
)
}
/**
* Renders the field header with name, type badge, and action buttons
*/
@@ -417,14 +510,7 @@ export function FieldFormat({
<div className='flex flex-col gap-[8px] border-[var(--border-1)] border-t px-[10px] pt-[6px] pb-[10px]'>
<div className='flex flex-col gap-[6px]'>
<Label className='text-[13px]'>Name</Label>
<Input
name='name'
value={field.name}
onChange={(e) => updateField(field.id, 'name', e.target.value)}
placeholder={placeholder}
disabled={isReadOnly}
autoComplete='off'
/>
<div className='relative'>{renderNameInput(field)}</div>
</div>
{showType && (

View File

@@ -50,6 +50,7 @@ import {
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/custom-tool-modal/custom-tool-modal'
import { ToolCredentialSelector } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tool-credential-selector'
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
import { useChildDeployment } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/hooks/use-child-deployment'
import { getAllBlocks } from '@/blocks'
import {
type CustomTool as CustomToolDefinition,
@@ -582,6 +583,8 @@ function WorkflowSelectorSyncWrapper({
onChange={onChange}
placeholder={uiComponent.placeholder || 'Select workflow'}
disabled={disabled || isLoading}
searchable
searchPlaceholder='Search workflows...'
/>
)
}
@@ -752,6 +755,81 @@ function CodeEditorSyncWrapper({
)
}
/**
* Badge component showing deployment status for workflow tools
*/
function WorkflowToolDeployBadge({
workflowId,
onDeploySuccess,
}: {
workflowId: string
onDeploySuccess?: () => void
}) {
const { isDeployed, needsRedeploy, isLoading, refetch } = useChildDeployment(workflowId)
const [isDeploying, setIsDeploying] = useState(false)
const deployWorkflow = useCallback(async () => {
if (isDeploying || !workflowId) return
try {
setIsDeploying(true)
const response = await fetch(`/api/workflows/${workflowId}/deploy`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
deployChatEnabled: false,
}),
})
if (response.ok) {
refetch()
onDeploySuccess?.()
} else {
logger.error('Failed to deploy workflow')
}
} catch (error) {
logger.error('Error deploying workflow:', error)
} finally {
setIsDeploying(false)
}
}, [isDeploying, workflowId, refetch, onDeploySuccess])
if (isLoading || (isDeployed && !needsRedeploy)) {
return null
}
if (typeof isDeployed !== 'boolean') {
return null
}
return (
<Tooltip.Root>
<Tooltip.Trigger asChild>
<Badge
variant={!isDeployed ? 'red' : 'amber'}
className='cursor-pointer'
size='sm'
dot
onClick={(e: React.MouseEvent) => {
e.stopPropagation()
e.preventDefault()
if (!isDeploying) {
deployWorkflow()
}
}}
>
{isDeploying ? 'Deploying...' : !isDeployed ? 'undeployed' : 'redeploy'}
</Badge>
</Tooltip.Trigger>
<Tooltip.Content>
<span className='text-sm'>{!isDeployed ? 'Click to deploy' : 'Click to redeploy'}</span>
</Tooltip.Content>
</Tooltip.Root>
)
}
/**
* Set of built-in tool types that are core platform tools.
*
@@ -2219,10 +2297,15 @@ export function ToolInput({
{getIssueBadgeLabel(issue)}
</Badge>
</Tooltip.Trigger>
<Tooltip.Content>{issue.message}: click to open settings</Tooltip.Content>
<Tooltip.Content>
<span className='text-sm'>{issue.message}: click to open settings</span>
</Tooltip.Content>
</Tooltip.Root>
)
})()}
{tool.type === 'workflow' && tool.params?.workflowId && (
<WorkflowToolDeployBadge workflowId={tool.params.workflowId} />
)}
</div>
<div className='flex flex-shrink-0 items-center gap-[8px]'>
{supportsToolControl && !(isMcpTool && isMcpToolUnavailable(tool)) && (

View File

@@ -361,9 +361,9 @@ export function TriggerSave({
onClick={handleSave}
disabled={disabled || isProcessing}
className={cn(
'h-[32px] flex-1 rounded-[8px] px-[12px] transition-all duration-200',
saveStatus === 'saved' && 'bg-green-600 hover:bg-green-700',
saveStatus === 'error' && 'bg-red-600 hover:bg-red-700'
'flex-1',
saveStatus === 'saved' && '!bg-green-600 !text-white hover:!bg-green-700',
saveStatus === 'error' && '!bg-red-600 !text-white hover:!bg-red-700'
)}
>
{saveStatus === 'saving' && 'Saving...'}
@@ -373,12 +373,7 @@ export function TriggerSave({
</Button>
{webhookId && (
<Button
variant='default'
onClick={handleDeleteClick}
disabled={disabled || isProcessing}
className='h-[32px] rounded-[8px] px-[12px]'
>
<Button variant='default' onClick={handleDeleteClick} disabled={disabled || isProcessing}>
<Trash className='h-[14px] w-[14px]' />
</Button>
)}

View File

@@ -1947,11 +1947,26 @@ const WorkflowContent = React.memo(() => {
const handleKeyUp = (e: KeyboardEvent) => {
if (e.key === 'Shift') setIsShiftPressed(false)
}
const handleFocusLoss = () => {
setIsShiftPressed(false)
setIsSelectionDragActive(false)
}
const handleVisibilityChange = () => {
if (document.hidden) {
handleFocusLoss()
}
}
window.addEventListener('keydown', handleKeyDown)
window.addEventListener('keyup', handleKeyUp)
window.addEventListener('blur', handleFocusLoss)
document.addEventListener('visibilitychange', handleVisibilityChange)
return () => {
window.removeEventListener('keydown', handleKeyDown)
window.removeEventListener('keyup', handleKeyUp)
window.removeEventListener('blur', handleFocusLoss)
document.removeEventListener('visibilitychange', handleVisibilityChange)
}
}, [])

View File

@@ -352,53 +352,6 @@ describe('Blocks Module', () => {
expect(typeof block?.tools.config?.tool).toBe('function')
})
})
describe('WebhookBlock', () => {
const block = getBlock('webhook')
it('should have correct metadata', () => {
expect(block?.type).toBe('webhook')
expect(block?.name).toBe('Webhook')
expect(block?.category).toBe('triggers')
expect(block?.authMode).toBe(AuthMode.OAuth)
expect(block?.triggerAllowed).toBe(true)
expect(block?.hideFromToolbar).toBe(true)
})
it('should have webhookProvider dropdown with multiple providers', () => {
const providerSubBlock = block?.subBlocks.find((sb) => sb.id === 'webhookProvider')
expect(providerSubBlock).toBeDefined()
expect(providerSubBlock?.type).toBe('dropdown')
const options = providerSubBlock?.options as Array<{ label: string; id: string }>
expect(options?.map((o) => o.id)).toContain('slack')
expect(options?.map((o) => o.id)).toContain('generic')
expect(options?.map((o) => o.id)).toContain('github')
})
it('should have conditional OAuth inputs', () => {
const gmailCredentialSubBlock = block?.subBlocks.find((sb) => sb.id === 'gmailCredential')
expect(gmailCredentialSubBlock).toBeDefined()
expect(gmailCredentialSubBlock?.type).toBe('oauth-input')
expect(gmailCredentialSubBlock?.condition).toEqual({
field: 'webhookProvider',
value: 'gmail',
})
const outlookCredentialSubBlock = block?.subBlocks.find(
(sb) => sb.id === 'outlookCredential'
)
expect(outlookCredentialSubBlock).toBeDefined()
expect(outlookCredentialSubBlock?.type).toBe('oauth-input')
expect(outlookCredentialSubBlock?.condition).toEqual({
field: 'webhookProvider',
value: 'outlook',
})
})
it('should have empty tools access', () => {
expect(block?.tools.access).toEqual([])
})
})
})
describe('SubBlock Validation', () => {
@@ -545,8 +498,8 @@ describe('Blocks Module', () => {
})
it('should handle blocks with triggerAllowed flag', () => {
const webhookBlock = getBlock('webhook')
expect(webhookBlock?.triggerAllowed).toBe(true)
const gmailBlock = getBlock('gmail')
expect(gmailBlock?.triggerAllowed).toBe(true)
const functionBlock = getBlock('function')
expect(functionBlock?.triggerAllowed).toBeUndefined()
@@ -663,16 +616,6 @@ describe('Blocks Module', () => {
expect(temperatureSubBlock?.min).toBe(0)
expect(temperatureSubBlock?.max).toBe(2)
})
it('should have required scopes on OAuth inputs', () => {
const webhookBlock = getBlock('webhook')
const gmailCredentialSubBlock = webhookBlock?.subBlocks.find(
(sb) => sb.id === 'gmailCredential'
)
expect(gmailCredentialSubBlock?.requiredScopes).toBeDefined()
expect(Array.isArray(gmailCredentialSubBlock?.requiredScopes)).toBe(true)
expect((gmailCredentialSubBlock?.requiredScopes?.length ?? 0) > 0).toBe(true)
})
})
describe('Block Consistency', () => {

View File

@@ -10,6 +10,7 @@ export const HumanInTheLoopBlock: BlockConfig<ResponseBlockOutput> = {
'Combines response and start functionality. Sends structured responses and allows workflow to resume from this point.',
category: 'blocks',
bgColor: '#10B981',
docsLink: 'https://docs.sim.ai/blocks/human-in-the-loop',
icon: HumanInTheLoopIcon,
subBlocks: [
// Operation dropdown hidden - block defaults to human approval mode

View File

@@ -9,7 +9,7 @@ export const TtsBlock: BlockConfig<TtsBlockResponse> = {
authMode: AuthMode.ApiKey,
longDescription:
'Generate natural-sounding speech from text using state-of-the-art AI voices from OpenAI, Deepgram, ElevenLabs, Cartesia, Google Cloud, Azure, and PlayHT. Supports multiple voices, languages, and audio formats.',
docsLink: 'https://docs.sim.ai/blocks/tts',
docsLink: 'https://docs.sim.ai/tools/tts',
category: 'tools',
bgColor: '#181C1E',
icon: TTSIcon,

View File

@@ -1,132 +0,0 @@
import {
AirtableIcon,
DiscordIcon,
GithubIcon,
GmailIcon,
MicrosoftTeamsIcon,
OutlookIcon,
SignalIcon,
SlackIcon,
StripeIcon,
TelegramIcon,
WebhookIcon,
WhatsAppIcon,
} from '@/components/icons'
import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
const getWebhookProviderIcon = (provider: string) => {
const iconMap: Record<string, React.ComponentType<{ className?: string }>> = {
slack: SlackIcon,
gmail: GmailIcon,
outlook: OutlookIcon,
airtable: AirtableIcon,
telegram: TelegramIcon,
generic: SignalIcon,
whatsapp: WhatsAppIcon,
github: GithubIcon,
discord: DiscordIcon,
stripe: StripeIcon,
microsoftteams: MicrosoftTeamsIcon,
}
return iconMap[provider.toLowerCase()]
}
export const WebhookBlock: BlockConfig = {
type: 'webhook',
name: 'Webhook',
description: 'Trigger workflow execution from external webhooks',
authMode: AuthMode.OAuth,
category: 'triggers',
icon: WebhookIcon,
bgColor: '#10B981', // Green color for triggers
docsLink: 'https://docs.sim.ai/triggers/webhook',
triggerAllowed: true,
hideFromToolbar: true, // Hidden for backwards compatibility - use generic webhook trigger instead
subBlocks: [
{
id: 'webhookProvider',
title: 'Webhook Provider',
type: 'dropdown',
options: [
'slack',
'gmail',
'outlook',
'airtable',
'telegram',
'generic',
'whatsapp',
'github',
'discord',
'stripe',
'microsoftteams',
].map((provider) => {
const providerLabels = {
slack: 'Slack',
gmail: 'Gmail',
outlook: 'Outlook',
airtable: 'Airtable',
telegram: 'Telegram',
generic: 'Generic',
whatsapp: 'WhatsApp',
github: 'GitHub',
discord: 'Discord',
stripe: 'Stripe',
microsoftteams: 'Microsoft Teams',
}
const icon = getWebhookProviderIcon(provider)
return {
label: providerLabels[provider as keyof typeof providerLabels],
id: provider,
...(icon && { icon }),
}
}),
value: () => 'generic',
},
{
id: 'gmailCredential',
title: 'Gmail Account',
type: 'oauth-input',
serviceId: 'gmail',
requiredScopes: [
'https://www.googleapis.com/auth/gmail.modify',
'https://www.googleapis.com/auth/gmail.labels',
],
placeholder: 'Select Gmail account',
condition: { field: 'webhookProvider', value: 'gmail' },
required: true,
},
{
id: 'outlookCredential',
title: 'Microsoft Account',
type: 'oauth-input',
serviceId: 'outlook',
requiredScopes: [
'Mail.ReadWrite',
'Mail.ReadBasic',
'Mail.Read',
'Mail.Send',
'offline_access',
],
placeholder: 'Select Microsoft account',
condition: { field: 'webhookProvider', value: 'outlook' },
required: true,
},
{
id: 'webhookConfig',
title: 'Webhook Configuration',
type: 'webhook-config',
},
],
tools: {
access: [], // No external tools needed
},
inputs: {}, // No inputs - webhook triggers receive data externally
outputs: {}, // No outputs - webhook data is injected directly into workflow context
}

View File

@@ -32,7 +32,7 @@ export const WorkflowBlock: BlockConfig = {
description:
'This is a core workflow block. Execute another workflow as a block in your workflow. Enter the input variable to pass to the child workflow.',
category: 'blocks',
bgColor: '#705335',
bgColor: '#6366F1',
icon: WorkflowIcon,
subBlocks: [
{

View File

@@ -2,7 +2,6 @@ import { WorkflowIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
// Helper: list workflows excluding self
const getAvailableWorkflows = (): Array<{ label: string; id: string }> => {
try {
const { workflows, activeWorkflowId } = useWorkflowRegistry.getState()
@@ -15,7 +14,6 @@ const getAvailableWorkflows = (): Array<{ label: string; id: string }> => {
}
}
// New workflow block variant that visualizes child Input Trigger schema for mapping
export const WorkflowInputBlock: BlockConfig = {
type: 'workflow_input',
name: 'Workflow',
@@ -26,6 +24,7 @@ export const WorkflowInputBlock: BlockConfig = {
- Remember, that the start point of the child workflow is the Start block.
`,
category: 'blocks',
docsLink: 'https://docs.sim.ai/blocks/workflow',
bgColor: '#6366F1', // Indigo - modern and professional
icon: WorkflowIcon,
subBlocks: [

View File

@@ -130,7 +130,6 @@ import { VisionBlock } from '@/blocks/blocks/vision'
import { WaitBlock } from '@/blocks/blocks/wait'
import { WealthboxBlock } from '@/blocks/blocks/wealthbox'
import { WebflowBlock } from '@/blocks/blocks/webflow'
import { WebhookBlock } from '@/blocks/blocks/webhook'
import { WebhookRequestBlock } from '@/blocks/blocks/webhook_request'
import { WhatsAppBlock } from '@/blocks/blocks/whatsapp'
import { WikipediaBlock } from '@/blocks/blocks/wikipedia'
@@ -281,7 +280,6 @@ export const registry: Record<string, BlockConfig> = {
wait: WaitBlock,
wealthbox: WealthboxBlock,
webflow: WebflowBlock,
webhook: WebhookBlock,
webhook_request: WebhookRequestBlock,
whatsapp: WhatsAppBlock,
wikipedia: WikipediaBlock,
@@ -296,11 +294,9 @@ export const registry: Record<string, BlockConfig> = {
}
export const getBlock = (type: string): BlockConfig | undefined => {
// Direct lookup first
if (registry[type]) {
return registry[type]
}
// Fallback: normalize hyphens to underscores (e.g., 'microsoft-teams' -> 'microsoft_teams')
const normalized = type.replace(/-/g, '_')
return registry[normalized]
}

View File

@@ -1,7 +1,6 @@
import { createLogger } from '@sim/logger'
import type { BlockOutput } from '@/blocks/types'
import { BlockType, HTTP, REFERENCE } from '@/executor/constants'
import type { BlockHandler, ExecutionContext } from '@/executor/types'
import type { BlockHandler, ExecutionContext, NormalizedBlockOutput } from '@/executor/types'
import type { SerializedBlock } from '@/serializer/types'
const logger = createLogger('ResponseBlockHandler')
@@ -23,7 +22,7 @@ export class ResponseBlockHandler implements BlockHandler {
ctx: ExecutionContext,
block: SerializedBlock,
inputs: Record<string, any>
): Promise<BlockOutput> {
): Promise<NormalizedBlockOutput> {
logger.info(`Executing response block: ${block.id}`)
try {
@@ -38,23 +37,19 @@ export class ResponseBlockHandler implements BlockHandler {
})
return {
response: {
data: responseData,
status: statusCode,
headers: responseHeaders,
},
data: responseData,
status: statusCode,
headers: responseHeaders,
}
} catch (error: any) {
logger.error('Response block execution failed:', error)
return {
response: {
data: {
error: 'Response block execution failed',
message: error.message || 'Unknown error',
},
status: HTTP.STATUS.SERVER_ERROR,
headers: { 'Content-Type': HTTP.CONTENT_TYPE.JSON },
data: {
error: 'Response block execution failed',
message: error.message || 'Unknown error',
},
status: HTTP.STATUS.SERVER_ERROR,
headers: { 'Content-Type': HTTP.CONTENT_TYPE.JSON },
}
}
}

View File

@@ -293,6 +293,532 @@ describe('BlockResolver', () => {
})
})
describe('Response block backwards compatibility', () => {
it.concurrent('should resolve new format: <responseBlock.data>', () => {
const workflow = createTestWorkflow([
{ id: 'response-block', name: 'Response', type: 'response' },
])
const resolver = new BlockResolver(workflow)
const ctx = createTestContext('current', {
'response-block': {
data: { message: 'hello', userId: 123 },
status: 200,
headers: { 'Content-Type': 'application/json' },
},
})
expect(resolver.resolve('<response.data>', ctx)).toEqual({ message: 'hello', userId: 123 })
expect(resolver.resolve('<response.data.message>', ctx)).toBe('hello')
expect(resolver.resolve('<response.data.userId>', ctx)).toBe(123)
})
it.concurrent('should resolve new format: <responseBlock.status>', () => {
const workflow = createTestWorkflow([
{ id: 'response-block', name: 'Response', type: 'response' },
])
const resolver = new BlockResolver(workflow)
const ctx = createTestContext('current', {
'response-block': {
data: { message: 'hello' },
status: 201,
headers: {},
},
})
expect(resolver.resolve('<response.status>', ctx)).toBe(201)
})
it.concurrent('should resolve new format: <responseBlock.headers>', () => {
const workflow = createTestWorkflow([
{ id: 'response-block', name: 'Response', type: 'response' },
])
const resolver = new BlockResolver(workflow)
const ctx = createTestContext('current', {
'response-block': {
data: {},
status: 200,
headers: { 'X-Custom-Header': 'custom-value', 'Content-Type': 'application/json' },
},
})
expect(resolver.resolve('<response.headers>', ctx)).toEqual({
'X-Custom-Header': 'custom-value',
'Content-Type': 'application/json',
})
})
it.concurrent(
'should resolve old format (backwards compat): <responseBlock.response.data>',
() => {
const workflow = createTestWorkflow([
{ id: 'response-block', name: 'Response', type: 'response' },
])
const resolver = new BlockResolver(workflow)
const ctx = createTestContext('current', {
'response-block': {
data: { message: 'hello', userId: 123 },
status: 200,
headers: { 'Content-Type': 'application/json' },
},
})
// Old format: <responseBlock.response.data> should strip 'response.' and resolve to data
expect(resolver.resolve('<response.response.data>', ctx)).toEqual({
message: 'hello',
userId: 123,
})
expect(resolver.resolve('<response.response.data.message>', ctx)).toBe('hello')
expect(resolver.resolve('<response.response.data.userId>', ctx)).toBe(123)
}
)
it.concurrent(
'should resolve old format (backwards compat): <responseBlock.response.status>',
() => {
const workflow = createTestWorkflow([
{ id: 'response-block', name: 'Response', type: 'response' },
])
const resolver = new BlockResolver(workflow)
const ctx = createTestContext('current', {
'response-block': {
data: { message: 'hello' },
status: 404,
headers: {},
},
})
// Old format: <responseBlock.response.status> should strip 'response.' and resolve to status
expect(resolver.resolve('<response.response.status>', ctx)).toBe(404)
}
)
it.concurrent(
'should resolve old format (backwards compat): <responseBlock.response.headers>',
() => {
const workflow = createTestWorkflow([
{ id: 'response-block', name: 'Response', type: 'response' },
])
const resolver = new BlockResolver(workflow)
const ctx = createTestContext('current', {
'response-block': {
data: {},
status: 200,
headers: { 'X-Request-Id': 'abc-123' },
},
})
// Old format: <responseBlock.response.headers> should strip 'response.' and resolve to headers
expect(resolver.resolve('<response.response.headers>', ctx)).toEqual({
'X-Request-Id': 'abc-123',
})
}
)
it.concurrent('should resolve entire Response block output with new format', () => {
const workflow = createTestWorkflow([
{ id: 'response-block', name: 'My Response', type: 'response' },
])
const resolver = new BlockResolver(workflow)
const fullOutput = {
data: { result: 'success' },
status: 200,
headers: { 'Content-Type': 'application/json' },
}
const ctx = createTestContext('current', { 'response-block': fullOutput })
expect(resolver.resolve('<myresponse>', ctx)).toEqual(fullOutput)
})
it.concurrent(
'should only strip response prefix for response block type, not other blocks',
() => {
// For non-response blocks, 'response' is a valid property name that should NOT be stripped
const workflow = createTestWorkflow([{ id: 'agent-block', name: 'Agent', type: 'agent' }])
const resolver = new BlockResolver(workflow)
const ctx = createTestContext('current', {
'agent-block': {
response: { content: 'AI generated text' },
tokens: { input: 100, output: 50 },
},
})
// For agent blocks, 'response' is a valid property and should be accessed normally
expect(resolver.resolve('<agent.response.content>', ctx)).toBe('AI generated text')
}
)
it.concurrent(
'should NOT strip response prefix if output actually has response key (edge case)',
() => {
// Edge case: What if a Response block somehow has a 'response' key in its output?
// This shouldn't happen in practice, but if it does, we should respect it.
const workflow = createTestWorkflow([
{ id: 'response-block', name: 'Response', type: 'response' },
])
const resolver = new BlockResolver(workflow)
// Hypothetical edge case where output has an actual 'response' property
const ctx = createTestContext('current', {
'response-block': {
response: { legacyData: 'some value' },
data: { newData: 'other value' },
},
})
// Since output.response exists, we should NOT strip it - access the actual 'response' property
expect(resolver.resolve('<response.response.legacyData>', ctx)).toBe('some value')
expect(resolver.resolve('<response.data.newData>', ctx)).toBe('other value')
}
)
})
describe('Workflow block with child Response block backwards compatibility', () => {
it.concurrent('should resolve new format: <workflowBlock.result.data>', () => {
const workflow = createTestWorkflow([
{ id: 'workflow-block', name: 'My Workflow', type: 'workflow' },
])
const resolver = new BlockResolver(workflow)
// After our change, child workflow with Response block returns { data, status, headers }
// Workflow block wraps it in { success, result: { data, status, headers }, ... }
const ctx = createTestContext('current', {
'workflow-block': {
success: true,
childWorkflowName: 'Child Workflow',
result: {
data: { userId: 456, name: 'Test User' },
status: 200,
headers: { 'Content-Type': 'application/json' },
},
},
})
expect(resolver.resolve('<myworkflow.result.data>', ctx)).toEqual({
userId: 456,
name: 'Test User',
})
expect(resolver.resolve('<myworkflow.result.data.userId>', ctx)).toBe(456)
expect(resolver.resolve('<myworkflow.result.data.name>', ctx)).toBe('Test User')
})
it.concurrent('should resolve new format: <workflowBlock.result.status>', () => {
const workflow = createTestWorkflow([
{ id: 'workflow-block', name: 'My Workflow', type: 'workflow' },
])
const resolver = new BlockResolver(workflow)
const ctx = createTestContext('current', {
'workflow-block': {
success: true,
childWorkflowName: 'Child Workflow',
result: {
data: { message: 'created' },
status: 201,
headers: {},
},
},
})
expect(resolver.resolve('<myworkflow.result.status>', ctx)).toBe(201)
})
it.concurrent('should resolve new format: <workflowBlock.result.headers>', () => {
const workflow = createTestWorkflow([
{ id: 'workflow-block', name: 'My Workflow', type: 'workflow' },
])
const resolver = new BlockResolver(workflow)
const ctx = createTestContext('current', {
'workflow-block': {
success: true,
childWorkflowName: 'Child Workflow',
result: {
data: {},
status: 200,
headers: { 'X-Trace-Id': 'trace-abc-123' },
},
},
})
expect(resolver.resolve('<myworkflow.result.headers>', ctx)).toEqual({
'X-Trace-Id': 'trace-abc-123',
})
})
it.concurrent(
'should resolve old format (backwards compat): <workflowBlock.result.response.data>',
() => {
const workflow = createTestWorkflow([
{ id: 'workflow-block', name: 'My Workflow', type: 'workflow' },
])
const resolver = new BlockResolver(workflow)
const ctx = createTestContext('current', {
'workflow-block': {
success: true,
childWorkflowName: 'Child Workflow',
result: {
data: { userId: 456, name: 'Test User' },
status: 200,
headers: { 'Content-Type': 'application/json' },
},
},
})
// Old format: <workflowBlock.result.response.data> should strip 'response.' and resolve to result.data
expect(resolver.resolve('<myworkflow.result.response.data>', ctx)).toEqual({
userId: 456,
name: 'Test User',
})
expect(resolver.resolve('<myworkflow.result.response.data.userId>', ctx)).toBe(456)
expect(resolver.resolve('<myworkflow.result.response.data.name>', ctx)).toBe('Test User')
}
)
it.concurrent(
'should resolve old format (backwards compat): <workflowBlock.result.response.status>',
() => {
const workflow = createTestWorkflow([
{ id: 'workflow-block', name: 'My Workflow', type: 'workflow' },
])
const resolver = new BlockResolver(workflow)
const ctx = createTestContext('current', {
'workflow-block': {
success: true,
childWorkflowName: 'Child Workflow',
result: {
data: { message: 'error' },
status: 500,
headers: {},
},
},
})
// Old format: <workflowBlock.result.response.status> should strip 'response.' and resolve to result.status
expect(resolver.resolve('<myworkflow.result.response.status>', ctx)).toBe(500)
}
)
it.concurrent(
'should resolve old format (backwards compat): <workflowBlock.result.response.headers>',
() => {
const workflow = createTestWorkflow([
{ id: 'workflow-block', name: 'My Workflow', type: 'workflow' },
])
const resolver = new BlockResolver(workflow)
const ctx = createTestContext('current', {
'workflow-block': {
success: true,
childWorkflowName: 'Child Workflow',
result: {
data: {},
status: 200,
headers: { 'Cache-Control': 'no-cache' },
},
},
})
// Old format: <workflowBlock.result.response.headers> should strip 'response.' and resolve to result.headers
expect(resolver.resolve('<myworkflow.result.response.headers>', ctx)).toEqual({
'Cache-Control': 'no-cache',
})
}
)
it.concurrent('should resolve workflow block success and other properties', () => {
const workflow = createTestWorkflow([
{ id: 'workflow-block', name: 'My Workflow', type: 'workflow' },
])
const resolver = new BlockResolver(workflow)
const ctx = createTestContext('current', {
'workflow-block': {
success: true,
childWorkflowName: 'Child Workflow',
result: { data: {}, status: 200, headers: {} },
},
})
expect(resolver.resolve('<myworkflow.success>', ctx)).toBe(true)
expect(resolver.resolve('<myworkflow.childWorkflowName>', ctx)).toBe('Child Workflow')
})
it.concurrent('should handle workflow block with failed child workflow', () => {
const workflow = createTestWorkflow([
{ id: 'workflow-block', name: 'My Workflow', type: 'workflow' },
])
const resolver = new BlockResolver(workflow)
const ctx = createTestContext('current', {
'workflow-block': {
success: false,
childWorkflowName: 'Child Workflow',
result: {},
error: 'Child workflow execution failed',
},
})
expect(resolver.resolve('<myworkflow.success>', ctx)).toBe(false)
expect(resolver.resolve('<myworkflow.error>', ctx)).toBe('Child workflow execution failed')
})
it.concurrent('should handle workflow block where child has non-Response final block', () => {
// When child workflow does NOT have a Response block as final block,
// the result structure will be different (not data/status/headers)
const workflow = createTestWorkflow([
{ id: 'workflow-block', name: 'My Workflow', type: 'workflow' },
])
const resolver = new BlockResolver(workflow)
const ctx = createTestContext('current', {
'workflow-block': {
success: true,
childWorkflowName: 'Child Workflow',
result: {
content: 'AI generated response',
tokens: { input: 100, output: 50 },
},
},
})
// No backwards compat needed here since child didn't have Response block
expect(resolver.resolve('<myworkflow.result.content>', ctx)).toBe('AI generated response')
expect(resolver.resolve('<myworkflow.result.tokens.input>', ctx)).toBe(100)
})
it.concurrent('should not apply workflow backwards compat for non-workflow blocks', () => {
// For non-workflow blocks, 'result.response' is a valid path that should NOT be modified
const workflow = createTestWorkflow([
{ id: 'function-block', name: 'Function', type: 'function' },
])
const resolver = new BlockResolver(workflow)
const ctx = createTestContext('current', {
'function-block': {
result: {
response: { apiData: 'test' },
other: 'value',
},
},
})
// For function blocks, 'result.response' is a valid nested property
expect(resolver.resolve('<function.result.response.apiData>', ctx)).toBe('test')
})
it.concurrent(
'should NOT strip result.response if child actually has response property (edge case)',
() => {
// Edge case: Child workflow's final output legitimately has a 'response' property
// (e.g., child ended with an Agent block that outputs response data)
const workflow = createTestWorkflow([
{ id: 'workflow-block', name: 'My Workflow', type: 'workflow' },
])
const resolver = new BlockResolver(workflow)
const ctx = createTestContext('current', {
'workflow-block': {
success: true,
childWorkflowName: 'Child Workflow',
result: {
// Child workflow ended with Agent block, not Response block
content: 'AI generated text',
response: { apiCallData: 'from external API' }, // legitimate 'response' property
},
},
})
// Since output.result.response exists, we should NOT strip it - access the actual property
expect(resolver.resolve('<myworkflow.result.response.apiCallData>', ctx)).toBe(
'from external API'
)
expect(resolver.resolve('<myworkflow.result.content>', ctx)).toBe('AI generated text')
}
)
it.concurrent('should handle mixed scenarios correctly', () => {
// Test that new format works when child workflow had Response block
const workflow = createTestWorkflow([
{ id: 'workflow-block', name: 'My Workflow', type: 'workflow' },
])
const resolver = new BlockResolver(workflow)
// Scenario 1: Child had Response block (new format - no 'response' key in result)
const ctx1 = createTestContext('current', {
'workflow-block': {
success: true,
result: { data: { id: 1 }, status: 200, headers: {} },
},
})
// New format works
expect(resolver.resolve('<myworkflow.result.data.id>', ctx1)).toBe(1)
// Old format also works (backwards compat kicks in because result.response is undefined)
expect(resolver.resolve('<myworkflow.result.response.data.id>', ctx1)).toBe(1)
// Scenario 2: Child had Agent block with 'response' property
const ctx2 = createTestContext('current', {
'workflow-block': {
success: true,
result: {
content: 'text',
response: { external: 'data' }, // actual 'response' property
},
},
})
// Access the actual 'response' property - no stripping
expect(resolver.resolve('<myworkflow.result.response.external>', ctx2)).toBe('data')
})
it.concurrent(
'real-world scenario: parent workflow referencing child Response block via <workflow1.result.response.data>',
() => {
/**
* This test simulates the exact scenario from user workflows:
*
* Child workflow (vibrant-cliff):
* Start → Function 1 (returns "fuck") → Response 1
* Response 1 outputs: { data: { hi: "fuck" }, status: 200, headers: {...} }
*
* Parent workflow (flying-glacier):
* Start → Workflow 1 (calls vibrant-cliff) → Function 1
* Function 1 code: return <workflow1.result.response.data>
*
* After our changes:
* - Child Response block outputs { data, status, headers } (no wrapper)
* - Workflow block wraps it in { success, result: { data, status, headers }, ... }
* - Parent uses OLD reference <workflow1.result.response.data>
* - Backwards compat should strip 'response.' and resolve to result.data
*/
const workflow = createTestWorkflow([
{ id: 'workflow-block', name: 'Workflow 1', type: 'workflow' },
])
const resolver = new BlockResolver(workflow)
// Simulate the workflow block output after child (vibrant-cliff) executes
// Child's Response block now outputs { data, status, headers } directly (no wrapper)
// Workflow block wraps it in { success, result: <child_output>, ... }
const ctx = createTestContext('current', {
'workflow-block': {
success: true,
childWorkflowName: 'vibrant-cliff',
result: {
// This is what Response block outputs after our changes (no 'response' wrapper)
data: { hi: 'fuck' },
status: 200,
headers: { 'Content-Type': 'application/json' },
},
},
})
// OLD reference pattern: <workflow1.result.response.data>
// Should work via backwards compatibility (strips 'response.')
expect(resolver.resolve('<workflow1.result.response.data>', ctx)).toEqual({ hi: 'fuck' })
expect(resolver.resolve('<workflow1.result.response.data.hi>', ctx)).toBe('fuck')
expect(resolver.resolve('<workflow1.result.response.status>', ctx)).toBe(200)
// NEW reference pattern: <workflow1.result.data>
// Should work directly
expect(resolver.resolve('<workflow1.result.data>', ctx)).toEqual({ hi: 'fuck' })
expect(resolver.resolve('<workflow1.result.data.hi>', ctx)).toBe('fuck')
expect(resolver.resolve('<workflow1.result.status>', ctx)).toBe(200)
// Other workflow block properties should still work
expect(resolver.resolve('<workflow1.success>', ctx)).toBe(true)
expect(resolver.resolve('<workflow1.childWorkflowName>', ctx)).toBe('vibrant-cliff')
}
)
})
describe('edge cases', () => {
it.concurrent('should handle case-insensitive block name matching', () => {
const workflow = createTestWorkflow([{ id: 'block-1', name: 'My Block' }])

View File

@@ -48,7 +48,6 @@ export class BlockResolver implements Resolver {
}
const output = this.getBlockOutput(blockId, context)
if (output === undefined) {
return undefined
}
@@ -56,16 +55,62 @@ export class BlockResolver implements Resolver {
return output
}
const result = navigatePath(output, pathParts)
// Try the original path first
let result = navigatePath(output, pathParts)
if (result === undefined) {
const availableKeys = output && typeof output === 'object' ? Object.keys(output) : []
throw new Error(
`No value found at path "${pathParts.join('.')}" in block "${blockName}". Available fields: ${availableKeys.join(', ')}`
)
// If successful, return it immediately
if (result !== undefined) {
return result
}
return result
// If failed, check if we should try backwards compatibility fallback
const block = this.workflow.blocks.find((b) => b.id === blockId)
// Response block backwards compatibility:
// Old: <responseBlock.response.data> -> New: <responseBlock.data>
// Only apply fallback if:
// 1. Block type is 'response'
// 2. Path starts with 'response.'
// 3. Output doesn't have a 'response' key (confirming it's the new format)
if (
block?.metadata?.id === 'response' &&
pathParts[0] === 'response' &&
output?.response === undefined
) {
const adjustedPathParts = pathParts.slice(1)
if (adjustedPathParts.length === 0) {
return output
}
result = navigatePath(output, adjustedPathParts)
if (result !== undefined) {
return result
}
}
// Workflow block backwards compatibility:
// Old: <workflowBlock.result.response.data> -> New: <workflowBlock.result.data>
// Only apply fallback if:
// 1. Block type is 'workflow'
// 2. Path starts with 'result.response.'
// 3. output.result.response doesn't exist (confirming child used new format)
if (
block?.metadata?.id === 'workflow' &&
pathParts[0] === 'result' &&
pathParts[1] === 'response' &&
output?.result?.response === undefined
) {
const adjustedPathParts = ['result', ...pathParts.slice(2)]
result = navigatePath(output, adjustedPathParts)
if (result !== undefined) {
return result
}
}
// If still undefined, throw error with original path
const availableKeys = output && typeof output === 'object' ? Object.keys(output) : []
throw new Error(
`No value found at path "${pathParts.join('.')}" in block "${blockName}". Available fields: ${availableKeys.join(', ')}`
)
}
private getBlockOutput(blockId: string, context: ResolutionContext): any {

View File

@@ -61,54 +61,6 @@ export function getRedisClient(): Redis | null {
}
}
/**
* Check if Redis is ready for commands.
* Use for health checks only - commands should be sent regardless (ioredis queues them).
*/
export function isRedisConnected(): boolean {
return globalRedisClient?.status === 'ready'
}
/**
* Get Redis connection status for diagnostics.
*/
export function getRedisStatus(): string {
return globalRedisClient?.status ?? 'not initialized'
}
const MESSAGE_ID_PREFIX = 'processed:'
const MESSAGE_ID_EXPIRY = 60 * 60 * 24 * 7
/**
* Check if a message has been processed (for idempotency).
* Requires Redis - throws if Redis is not available.
*/
export async function hasProcessedMessage(key: string): Promise<boolean> {
const redis = getRedisClient()
if (!redis) {
throw new Error('Redis not available for message deduplication')
}
const result = await redis.exists(`${MESSAGE_ID_PREFIX}${key}`)
return result === 1
}
/**
* Mark a message as processed (for idempotency).
* Requires Redis - throws if Redis is not available.
*/
export async function markMessageAsProcessed(
key: string,
expirySeconds: number = MESSAGE_ID_EXPIRY
): Promise<void> {
const redis = getRedisClient()
if (!redis) {
throw new Error('Redis not available for message deduplication')
}
await redis.set(`${MESSAGE_ID_PREFIX}${key}`, '1', 'EX', expirySeconds)
}
/**
* Lua script for safe lock release.
* Only deletes the key if the value matches (ownership verification).
@@ -125,7 +77,10 @@ end
/**
* Acquire a distributed lock using Redis SET NX.
* Returns true if lock acquired, false if already held.
* Requires Redis - throws if Redis is not available.
*
* When Redis is not available, returns true (lock "acquired") to allow
* single-replica deployments to function without Redis. In multi-replica
* deployments without Redis, the idempotency layer prevents duplicate processing.
*/
export async function acquireLock(
lockKey: string,
@@ -134,36 +89,24 @@ export async function acquireLock(
): Promise<boolean> {
const redis = getRedisClient()
if (!redis) {
throw new Error('Redis not available for distributed locking')
return true // No-op when Redis unavailable; idempotency layer handles duplicates
}
const result = await redis.set(lockKey, value, 'EX', expirySeconds, 'NX')
return result === 'OK'
}
/**
* Get the value of a lock key.
* Requires Redis - throws if Redis is not available.
*/
export async function getLockValue(key: string): Promise<string | null> {
const redis = getRedisClient()
if (!redis) {
throw new Error('Redis not available')
}
return redis.get(key)
}
/**
* Release a distributed lock safely.
* Only releases if the caller owns the lock (value matches).
* Returns true if lock was released, false if not owned or already expired.
* Requires Redis - throws if Redis is not available.
*
* When Redis is not available, returns true (no-op) since no lock was held.
*/
export async function releaseLock(lockKey: string, value: string): Promise<boolean> {
const redis = getRedisClient()
if (!redis) {
throw new Error('Redis not available for distributed locking')
return true // No-op when Redis unavailable; no lock was actually held
}
const result = await redis.eval(RELEASE_LOCK_SCRIPT, 1, lockKey, value)

View File

@@ -42,11 +42,7 @@ interface StreamingState {
}
function extractOutputValue(output: any, path: string): any {
let value = traverseObjectPath(output, path)
if (value === undefined && output?.response) {
value = traverseObjectPath(output.response, path)
}
return value
return traverseObjectPath(output, path)
}
function isDangerousKey(key: string): boolean {

View File

@@ -136,12 +136,7 @@ export async function updateWorkflowRunCounts(workflowId: string, runs = 1) {
}
export const workflowHasResponseBlock = (executionResult: ExecutionResult): boolean => {
if (
!executionResult?.logs ||
!Array.isArray(executionResult.logs) ||
!executionResult.success ||
!executionResult.output.response
) {
if (!executionResult?.logs || !Array.isArray(executionResult.logs) || !executionResult.success) {
return false
}
@@ -154,8 +149,7 @@ export const workflowHasResponseBlock = (executionResult: ExecutionResult): bool
// Create a HTTP response from response block
export const createHttpResponseFromBlock = (executionResult: ExecutionResult): NextResponse => {
const output = executionResult.output.response
const { data = {}, status = 200, headers = {} } = output
const { data = {}, status = 200, headers = {} } = executionResult.output
const responseHeaders = new Headers({
'Content-Type': 'application/json',

View File

@@ -0,0 +1,230 @@
import { describe, expect, it } from 'vitest'
import { workflowExecutorTool } from '@/tools/workflow/executor'
describe('workflowExecutorTool', () => {
describe('request.body', () => {
const buildBody = workflowExecutorTool.request.body!
it.concurrent('should pass through object inputMapping unchanged (LLM-provided args)', () => {
const params = {
workflowId: 'test-workflow-id',
inputMapping: { firstName: 'John', lastName: 'Doe', age: 30 },
}
const result = buildBody(params)
expect(result).toEqual({
input: { firstName: 'John', lastName: 'Doe', age: 30 },
triggerType: 'api',
useDraftState: false,
})
})
it.concurrent('should parse JSON string inputMapping (UI-provided via tool-input)', () => {
const params = {
workflowId: 'test-workflow-id',
inputMapping: '{"firstName": "John", "lastName": "Doe"}',
}
const result = buildBody(params)
expect(result).toEqual({
input: { firstName: 'John', lastName: 'Doe' },
triggerType: 'api',
useDraftState: false,
})
})
it.concurrent('should handle nested objects in JSON string inputMapping', () => {
const params = {
workflowId: 'test-workflow-id',
inputMapping: '{"user": {"name": "John", "email": "john@example.com"}, "count": 5}',
}
const result = buildBody(params)
expect(result).toEqual({
input: { user: { name: 'John', email: 'john@example.com' }, count: 5 },
triggerType: 'api',
useDraftState: false,
})
})
it.concurrent('should handle arrays in JSON string inputMapping', () => {
const params = {
workflowId: 'test-workflow-id',
inputMapping: '{"tags": ["a", "b", "c"], "ids": [1, 2, 3]}',
}
const result = buildBody(params)
expect(result).toEqual({
input: { tags: ['a', 'b', 'c'], ids: [1, 2, 3] },
triggerType: 'api',
useDraftState: false,
})
})
it.concurrent('should default to empty object when inputMapping is undefined', () => {
const params = {
workflowId: 'test-workflow-id',
inputMapping: undefined,
}
const result = buildBody(params)
expect(result).toEqual({
input: {},
triggerType: 'api',
useDraftState: false,
})
})
it.concurrent('should default to empty object when inputMapping is null', () => {
const params = {
workflowId: 'test-workflow-id',
inputMapping: null as any,
}
const result = buildBody(params)
expect(result).toEqual({
input: {},
triggerType: 'api',
useDraftState: false,
})
})
it.concurrent('should fallback to empty object for invalid JSON string', () => {
const params = {
workflowId: 'test-workflow-id',
inputMapping: 'not valid json {',
}
const result = buildBody(params)
expect(result).toEqual({
input: {},
triggerType: 'api',
useDraftState: false,
})
})
it.concurrent('should fallback to empty object for empty string', () => {
const params = {
workflowId: 'test-workflow-id',
inputMapping: '',
}
const result = buildBody(params)
expect(result).toEqual({
input: {},
triggerType: 'api',
useDraftState: false,
})
})
it.concurrent('should handle empty object inputMapping', () => {
const params = {
workflowId: 'test-workflow-id',
inputMapping: {},
}
const result = buildBody(params)
expect(result).toEqual({
input: {},
triggerType: 'api',
useDraftState: false,
})
})
it.concurrent('should handle empty JSON object string', () => {
const params = {
workflowId: 'test-workflow-id',
inputMapping: '{}',
}
const result = buildBody(params)
expect(result).toEqual({
input: {},
triggerType: 'api',
useDraftState: false,
})
})
it.concurrent('should preserve special characters in string values', () => {
const params = {
workflowId: 'test-workflow-id',
inputMapping: '{"message": "Hello\\nWorld", "path": "C:\\\\Users"}',
}
const result = buildBody(params)
expect(result).toEqual({
input: { message: 'Hello\nWorld', path: 'C:\\Users' },
triggerType: 'api',
useDraftState: false,
})
})
it.concurrent('should handle unicode characters in JSON string', () => {
const params = {
workflowId: 'test-workflow-id',
inputMapping: '{"greeting": "こんにちは", "emoji": "👋"}',
}
const result = buildBody(params)
expect(result).toEqual({
input: { greeting: 'こんにちは', emoji: '👋' },
triggerType: 'api',
useDraftState: false,
})
})
it.concurrent('should not modify object with string values that look like JSON', () => {
const params = {
workflowId: 'test-workflow-id',
inputMapping: { data: '{"nested": "json"}' },
}
const result = buildBody(params)
expect(result).toEqual({
input: { data: '{"nested": "json"}' },
triggerType: 'api',
useDraftState: false,
})
})
})
describe('request.url', () => {
it.concurrent('should build correct URL with workflowId', () => {
const url = workflowExecutorTool.request.url as (params: any) => string
expect(url({ workflowId: 'abc-123' })).toBe('/api/workflows/abc-123/execute')
expect(url({ workflowId: 'my-workflow' })).toBe('/api/workflows/my-workflow/execute')
})
})
describe('tool metadata', () => {
it.concurrent('should have correct id', () => {
expect(workflowExecutorTool.id).toBe('workflow_executor')
})
it.concurrent('should have required workflowId param', () => {
expect(workflowExecutorTool.params.workflowId.required).toBe(true)
})
it.concurrent('should have optional inputMapping param', () => {
expect(workflowExecutorTool.params.inputMapping.required).toBe(false)
})
it.concurrent('should use POST method', () => {
expect(workflowExecutorTool.request.method).toBe('POST')
})
})
})

View File

@@ -33,11 +33,21 @@ export const workflowExecutorTool: ToolConfig<
url: (params: WorkflowExecutorParams) => `/api/workflows/${params.workflowId}/execute`,
method: 'POST',
headers: () => ({ 'Content-Type': 'application/json' }),
body: (params: WorkflowExecutorParams) => ({
input: params.inputMapping || {},
triggerType: 'api',
useDraftState: false,
}),
body: (params: WorkflowExecutorParams) => {
let inputData = params.inputMapping || {}
if (typeof inputData === 'string') {
try {
inputData = JSON.parse(inputData)
} catch {
inputData = {}
}
}
return {
input: inputData,
triggerType: 'api',
useDraftState: false,
}
},
},
transformResponse: async (response: Response) => {
const data = await response.json()

View File

@@ -2,7 +2,8 @@ import type { ToolResponse } from '@/tools/types'
export interface WorkflowExecutorParams {
workflowId: string
inputMapping?: Record<string, any>
/** Can be a JSON string (from tool-input UI) or an object (from LLM args) */
inputMapping?: Record<string, any> | string
}
export interface WorkflowExecutorResponse extends ToolResponse {

View File

@@ -47,6 +47,14 @@ export const airtableWebhookTrigger: TriggerConfig = {
defaultValue: false,
mode: 'trigger',
},
{
id: 'triggerSave',
title: '',
type: 'trigger-save',
hideFromPreview: true,
mode: 'trigger',
triggerId: 'airtable_webhook',
},
{
id: 'triggerInstructions',
title: 'Setup Instructions',
@@ -67,14 +75,6 @@ export const airtableWebhookTrigger: TriggerConfig = {
.join(''),
mode: 'trigger',
},
{
id: 'triggerSave',
title: '',
type: 'trigger-save',
hideFromPreview: true,
mode: 'trigger',
triggerId: 'airtable_webhook',
},
],
outputs: {

View File

@@ -38,6 +38,18 @@ export const calendlyInviteeCanceledTrigger: TriggerConfig = {
value: 'calendly_invitee_canceled',
},
},
{
id: 'triggerSave',
title: '',
type: 'trigger-save',
hideFromPreview: true,
mode: 'trigger',
triggerId: 'calendly_invitee_canceled',
condition: {
field: 'selectedTriggerId',
value: 'calendly_invitee_canceled',
},
},
{
id: 'triggerInstructions',
title: 'Setup Instructions',
@@ -61,18 +73,6 @@ export const calendlyInviteeCanceledTrigger: TriggerConfig = {
value: 'calendly_invitee_canceled',
},
},
{
id: 'triggerSave',
title: '',
type: 'trigger-save',
hideFromPreview: true,
mode: 'trigger',
triggerId: 'calendly_invitee_canceled',
condition: {
field: 'selectedTriggerId',
value: 'calendly_invitee_canceled',
},
},
],
outputs: buildInviteeOutputs(),

View File

@@ -47,6 +47,18 @@ export const calendlyInviteeCreatedTrigger: TriggerConfig = {
value: 'calendly_invitee_created',
},
},
{
id: 'triggerSave',
title: '',
type: 'trigger-save',
hideFromPreview: true,
mode: 'trigger',
triggerId: 'calendly_invitee_created',
condition: {
field: 'selectedTriggerId',
value: 'calendly_invitee_created',
},
},
{
id: 'triggerInstructions',
title: 'Setup Instructions',
@@ -70,18 +82,6 @@ export const calendlyInviteeCreatedTrigger: TriggerConfig = {
value: 'calendly_invitee_created',
},
},
{
id: 'triggerSave',
title: '',
type: 'trigger-save',
hideFromPreview: true,
mode: 'trigger',
triggerId: 'calendly_invitee_created',
condition: {
field: 'selectedTriggerId',
value: 'calendly_invitee_created',
},
},
],
outputs: buildInviteeOutputs(),

View File

@@ -38,6 +38,18 @@ export const calendlyRoutingFormSubmittedTrigger: TriggerConfig = {
value: 'calendly_routing_form_submitted',
},
},
{
id: 'triggerSave',
title: '',
type: 'trigger-save',
hideFromPreview: true,
mode: 'trigger',
triggerId: 'calendly_routing_form_submitted',
condition: {
field: 'selectedTriggerId',
value: 'calendly_routing_form_submitted',
},
},
{
id: 'triggerInstructions',
title: 'Setup Instructions',
@@ -61,18 +73,6 @@ export const calendlyRoutingFormSubmittedTrigger: TriggerConfig = {
value: 'calendly_routing_form_submitted',
},
},
{
id: 'triggerSave',
title: '',
type: 'trigger-save',
hideFromPreview: true,
mode: 'trigger',
triggerId: 'calendly_routing_form_submitted',
condition: {
field: 'selectedTriggerId',
value: 'calendly_routing_form_submitted',
},
},
],
outputs: buildRoutingFormOutputs(),

View File

@@ -37,6 +37,18 @@ export const calendlyWebhookTrigger: TriggerConfig = {
value: 'calendly_webhook',
},
},
{
id: 'triggerSave',
title: '',
type: 'trigger-save',
hideFromPreview: true,
mode: 'trigger',
triggerId: 'calendly_webhook',
condition: {
field: 'selectedTriggerId',
value: 'calendly_webhook',
},
},
{
id: 'triggerInstructions',
title: 'Setup Instructions',
@@ -60,18 +72,6 @@ export const calendlyWebhookTrigger: TriggerConfig = {
value: 'calendly_webhook',
},
},
{
id: 'triggerSave',
title: '',
type: 'trigger-save',
hideFromPreview: true,
mode: 'trigger',
triggerId: 'calendly_webhook',
condition: {
field: 'selectedTriggerId',
value: 'calendly_webhook',
},
},
],
outputs: {

View File

@@ -39,18 +39,6 @@ export const circlebackMeetingCompletedTrigger: TriggerConfig = {
value: 'circleback_meeting_completed',
},
},
{
id: 'triggerInstructions',
title: 'Setup Instructions',
hideFromPreview: true,
type: 'text',
defaultValue: circlebackSetupInstructions('All meeting data'),
mode: 'trigger',
condition: {
field: 'selectedTriggerId',
value: 'circleback_meeting_completed',
},
},
{
id: 'triggerSave',
title: '',
@@ -63,6 +51,18 @@ export const circlebackMeetingCompletedTrigger: TriggerConfig = {
value: 'circleback_meeting_completed',
},
},
{
id: 'triggerInstructions',
title: 'Setup Instructions',
hideFromPreview: true,
type: 'text',
defaultValue: circlebackSetupInstructions('All meeting data'),
mode: 'trigger',
condition: {
field: 'selectedTriggerId',
value: 'circleback_meeting_completed',
},
},
],
outputs: buildMeetingOutputs(),

View File

@@ -39,18 +39,6 @@ export const circlebackMeetingNotesTrigger: TriggerConfig = {
value: 'circleback_meeting_notes',
},
},
{
id: 'triggerInstructions',
title: 'Setup Instructions',
hideFromPreview: true,
type: 'text',
defaultValue: circlebackSetupInstructions('Meeting notes and action items'),
mode: 'trigger',
condition: {
field: 'selectedTriggerId',
value: 'circleback_meeting_notes',
},
},
{
id: 'triggerSave',
title: '',
@@ -63,6 +51,18 @@ export const circlebackMeetingNotesTrigger: TriggerConfig = {
value: 'circleback_meeting_notes',
},
},
{
id: 'triggerInstructions',
title: 'Setup Instructions',
hideFromPreview: true,
type: 'text',
defaultValue: circlebackSetupInstructions('Meeting notes and action items'),
mode: 'trigger',
condition: {
field: 'selectedTriggerId',
value: 'circleback_meeting_notes',
},
},
],
outputs: buildMeetingOutputs(),

View File

@@ -48,18 +48,6 @@ export const circlebackWebhookTrigger: TriggerConfig = {
value: 'circleback_webhook',
},
},
{
id: 'triggerInstructions',
title: 'Setup Instructions',
hideFromPreview: true,
type: 'text',
defaultValue: circlebackSetupInstructions('All events'),
mode: 'trigger',
condition: {
field: 'selectedTriggerId',
value: 'circleback_webhook',
},
},
{
id: 'triggerSave',
title: '',
@@ -72,6 +60,18 @@ export const circlebackWebhookTrigger: TriggerConfig = {
value: 'circleback_webhook',
},
},
{
id: 'triggerInstructions',
title: 'Setup Instructions',
hideFromPreview: true,
type: 'text',
defaultValue: circlebackSetupInstructions('All events'),
mode: 'trigger',
condition: {
field: 'selectedTriggerId',
value: 'circleback_webhook',
},
},
],
outputs: buildGenericOutputs(),

View File

@@ -56,6 +56,14 @@ export const genericWebhookTrigger: TriggerConfig = {
'Define the expected JSON input schema for this webhook (optional). Use type "files" for file uploads.',
mode: 'trigger',
},
{
id: 'triggerSave',
title: '',
type: 'trigger-save',
hideFromPreview: true,
mode: 'trigger',
triggerId: 'generic_webhook',
},
{
id: 'triggerInstructions',
title: 'Setup Instructions',
@@ -76,14 +84,6 @@ export const genericWebhookTrigger: TriggerConfig = {
.join(''),
mode: 'trigger',
},
{
id: 'triggerSave',
title: '',
type: 'trigger-save',
hideFromPreview: true,
mode: 'trigger',
triggerId: 'generic_webhook',
},
],
outputs: {},

View File

@@ -75,6 +75,18 @@ export const githubIssueClosedTrigger: TriggerConfig = {
value: 'github_issue_closed',
},
},
{
id: 'triggerSave',
title: '',
type: 'trigger-save',
hideFromPreview: true,
mode: 'trigger',
triggerId: 'github_issue_closed',
condition: {
field: 'selectedTriggerId',
value: 'github_issue_closed',
},
},
{
id: 'triggerInstructions',
title: 'Setup Instructions',
@@ -101,18 +113,6 @@ export const githubIssueClosedTrigger: TriggerConfig = {
value: 'github_issue_closed',
},
},
{
id: 'triggerSave',
title: '',
type: 'trigger-save',
hideFromPreview: true,
mode: 'trigger',
triggerId: 'github_issue_closed',
condition: {
field: 'selectedTriggerId',
value: 'github_issue_closed',
},
},
],
outputs: {

View File

@@ -75,6 +75,18 @@ export const githubIssueCommentTrigger: TriggerConfig = {
value: 'github_issue_comment',
},
},
{
id: 'triggerSave',
title: '',
type: 'trigger-save',
hideFromPreview: true,
mode: 'trigger',
triggerId: 'github_issue_comment',
condition: {
field: 'selectedTriggerId',
value: 'github_issue_comment',
},
},
{
id: 'triggerInstructions',
title: 'Setup Instructions',
@@ -102,18 +114,6 @@ export const githubIssueCommentTrigger: TriggerConfig = {
value: 'github_issue_comment',
},
},
{
id: 'triggerSave',
title: '',
type: 'trigger-save',
hideFromPreview: true,
mode: 'trigger',
triggerId: 'github_issue_comment',
condition: {
field: 'selectedTriggerId',
value: 'github_issue_comment',
},
},
],
outputs: {

View File

@@ -96,6 +96,18 @@ export const githubIssueOpenedTrigger: TriggerConfig = {
value: 'github_issue_opened',
},
},
{
id: 'triggerSave',
title: '',
type: 'trigger-save',
hideFromPreview: true,
mode: 'trigger',
triggerId: 'github_issue_opened',
condition: {
field: 'selectedTriggerId',
value: 'github_issue_opened',
},
},
{
id: 'triggerInstructions',
title: 'Setup Instructions',
@@ -122,18 +134,6 @@ export const githubIssueOpenedTrigger: TriggerConfig = {
value: 'github_issue_opened',
},
},
{
id: 'triggerSave',
title: '',
type: 'trigger-save',
hideFromPreview: true,
mode: 'trigger',
triggerId: 'github_issue_opened',
condition: {
field: 'selectedTriggerId',
value: 'github_issue_opened',
},
},
],
outputs: {

View File

@@ -76,6 +76,18 @@ export const githubPRClosedTrigger: TriggerConfig = {
value: 'github_pr_closed',
},
},
{
id: 'triggerSave',
title: '',
type: 'trigger-save',
hideFromPreview: true,
mode: 'trigger',
triggerId: 'github_pr_closed',
condition: {
field: 'selectedTriggerId',
value: 'github_pr_closed',
},
},
{
id: 'triggerInstructions',
title: 'Setup Instructions',
@@ -102,18 +114,6 @@ export const githubPRClosedTrigger: TriggerConfig = {
value: 'github_pr_closed',
},
},
{
id: 'triggerSave',
title: '',
type: 'trigger-save',
hideFromPreview: true,
mode: 'trigger',
triggerId: 'github_pr_closed',
condition: {
field: 'selectedTriggerId',
value: 'github_pr_closed',
},
},
],
outputs: {

View File

@@ -75,6 +75,18 @@ export const githubPRCommentTrigger: TriggerConfig = {
value: 'github_pr_comment',
},
},
{
id: 'triggerSave',
title: '',
type: 'trigger-save',
hideFromPreview: true,
mode: 'trigger',
triggerId: 'github_pr_comment',
condition: {
field: 'selectedTriggerId',
value: 'github_pr_comment',
},
},
{
id: 'triggerInstructions',
title: 'Setup Instructions',
@@ -102,18 +114,6 @@ export const githubPRCommentTrigger: TriggerConfig = {
value: 'github_pr_comment',
},
},
{
id: 'triggerSave',
title: '',
type: 'trigger-save',
hideFromPreview: true,
mode: 'trigger',
triggerId: 'github_pr_comment',
condition: {
field: 'selectedTriggerId',
value: 'github_pr_comment',
},
},
],
outputs: {

View File

@@ -75,6 +75,18 @@ export const githubPRMergedTrigger: TriggerConfig = {
value: 'github_pr_merged',
},
},
{
id: 'triggerSave',
title: '',
type: 'trigger-save',
hideFromPreview: true,
mode: 'trigger',
triggerId: 'github_pr_merged',
condition: {
field: 'selectedTriggerId',
value: 'github_pr_merged',
},
},
{
id: 'triggerInstructions',
title: 'Setup Instructions',
@@ -101,18 +113,6 @@ export const githubPRMergedTrigger: TriggerConfig = {
value: 'github_pr_merged',
},
},
{
id: 'triggerSave',
title: '',
type: 'trigger-save',
hideFromPreview: true,
mode: 'trigger',
triggerId: 'github_pr_merged',
condition: {
field: 'selectedTriggerId',
value: 'github_pr_merged',
},
},
],
outputs: {

View File

@@ -75,6 +75,18 @@ export const githubPROpenedTrigger: TriggerConfig = {
value: 'github_pr_opened',
},
},
{
id: 'triggerSave',
title: '',
type: 'trigger-save',
hideFromPreview: true,
mode: 'trigger',
triggerId: 'github_pr_opened',
condition: {
field: 'selectedTriggerId',
value: 'github_pr_opened',
},
},
{
id: 'triggerInstructions',
title: 'Setup Instructions',
@@ -101,18 +113,6 @@ export const githubPROpenedTrigger: TriggerConfig = {
value: 'github_pr_opened',
},
},
{
id: 'triggerSave',
title: '',
type: 'trigger-save',
hideFromPreview: true,
mode: 'trigger',
triggerId: 'github_pr_opened',
condition: {
field: 'selectedTriggerId',
value: 'github_pr_opened',
},
},
],
outputs: {

View File

@@ -76,6 +76,18 @@ export const githubPRReviewedTrigger: TriggerConfig = {
value: 'github_pr_reviewed',
},
},
{
id: 'triggerSave',
title: '',
type: 'trigger-save',
hideFromPreview: true,
mode: 'trigger',
triggerId: 'github_pr_reviewed',
condition: {
field: 'selectedTriggerId',
value: 'github_pr_reviewed',
},
},
{
id: 'triggerInstructions',
title: 'Setup Instructions',
@@ -102,18 +114,6 @@ export const githubPRReviewedTrigger: TriggerConfig = {
value: 'github_pr_reviewed',
},
},
{
id: 'triggerSave',
title: '',
type: 'trigger-save',
hideFromPreview: true,
mode: 'trigger',
triggerId: 'github_pr_reviewed',
condition: {
field: 'selectedTriggerId',
value: 'github_pr_reviewed',
},
},
],
outputs: {

View File

@@ -75,6 +75,18 @@ export const githubPushTrigger: TriggerConfig = {
value: 'github_push',
},
},
{
id: 'triggerSave',
title: '',
type: 'trigger-save',
hideFromPreview: true,
mode: 'trigger',
triggerId: 'github_push',
condition: {
field: 'selectedTriggerId',
value: 'github_push',
},
},
{
id: 'triggerInstructions',
title: 'Setup Instructions',
@@ -101,18 +113,6 @@ export const githubPushTrigger: TriggerConfig = {
value: 'github_push',
},
},
{
id: 'triggerSave',
title: '',
type: 'trigger-save',
hideFromPreview: true,
mode: 'trigger',
triggerId: 'github_push',
condition: {
field: 'selectedTriggerId',
value: 'github_push',
},
},
],
outputs: {

View File

@@ -75,6 +75,18 @@ export const githubReleasePublishedTrigger: TriggerConfig = {
value: 'github_release_published',
},
},
{
id: 'triggerSave',
title: '',
type: 'trigger-save',
hideFromPreview: true,
mode: 'trigger',
triggerId: 'github_release_published',
condition: {
field: 'selectedTriggerId',
value: 'github_release_published',
},
},
{
id: 'triggerInstructions',
title: 'Setup Instructions',
@@ -101,18 +113,6 @@ export const githubReleasePublishedTrigger: TriggerConfig = {
value: 'github_release_published',
},
},
{
id: 'triggerSave',
title: '',
type: 'trigger-save',
hideFromPreview: true,
mode: 'trigger',
triggerId: 'github_release_published',
condition: {
field: 'selectedTriggerId',
value: 'github_release_published',
},
},
],
outputs: {

View File

@@ -72,6 +72,18 @@ export const githubWebhookTrigger: TriggerConfig = {
value: 'github_webhook',
},
},
{
id: 'triggerSave',
title: '',
type: 'trigger-save',
hideFromPreview: true,
mode: 'trigger',
triggerId: 'github_webhook',
condition: {
field: 'selectedTriggerId',
value: 'github_webhook',
},
},
{
id: 'triggerInstructions',
title: 'Setup Instructions',
@@ -98,18 +110,6 @@ export const githubWebhookTrigger: TriggerConfig = {
value: 'github_webhook',
},
},
{
id: 'triggerSave',
title: '',
type: 'trigger-save',
hideFromPreview: true,
mode: 'trigger',
triggerId: 'github_webhook',
condition: {
field: 'selectedTriggerId',
value: 'github_webhook',
},
},
],
outputs: {

View File

@@ -76,6 +76,18 @@ export const githubWorkflowRunTrigger: TriggerConfig = {
value: 'github_workflow_run',
},
},
{
id: 'triggerSave',
title: '',
type: 'trigger-save',
hideFromPreview: true,
mode: 'trigger',
triggerId: 'github_workflow_run',
condition: {
field: 'selectedTriggerId',
value: 'github_workflow_run',
},
},
{
id: 'triggerInstructions',
title: 'Setup Instructions',
@@ -102,18 +114,6 @@ export const githubWorkflowRunTrigger: TriggerConfig = {
value: 'github_workflow_run',
},
},
{
id: 'triggerSave',
title: '',
type: 'trigger-save',
hideFromPreview: true,
mode: 'trigger',
triggerId: 'github_workflow_run',
condition: {
field: 'selectedTriggerId',
value: 'github_workflow_run',
},
},
],
outputs: {

View File

@@ -128,6 +128,14 @@ Return ONLY the Gmail search query, no explanations or markdown.`,
required: false,
mode: 'trigger',
},
{
id: 'triggerSave',
title: '',
type: 'trigger-save',
hideFromPreview: true,
mode: 'trigger',
triggerId: 'gmail_poller',
},
{
id: 'triggerInstructions',
title: 'Setup Instructions',
@@ -145,14 +153,6 @@ Return ONLY the Gmail search query, no explanations or markdown.`,
.join(''),
mode: 'trigger',
},
{
id: 'triggerSave',
title: '',
type: 'trigger-save',
hideFromPreview: true,
mode: 'trigger',
triggerId: 'gmail_poller',
},
],
outputs: {

View File

@@ -59,6 +59,14 @@ export const googleFormsWebhookTrigger: TriggerConfig = {
defaultValue: true,
mode: 'trigger',
},
{
id: 'triggerSave',
title: '',
type: 'trigger-save',
hideFromPreview: true,
mode: 'trigger',
triggerId: 'google_forms_webhook',
},
{
id: 'triggerInstructions',
title: 'Setup Instructions',
@@ -86,12 +94,12 @@ export const googleFormsWebhookTrigger: TriggerConfig = {
const script = `function onFormSubmit(e) {
const WEBHOOK_URL = "{{WEBHOOK_URL}}";
const SHARED_SECRET = "{{SHARED_SECRET}}";
try {
const form = FormApp.getActiveForm();
const formResponse = e.response;
const itemResponses = formResponse.getItemResponses();
// Build answers object
const answers = {};
for (var i = 0; i < itemResponses.length; i++) {
@@ -100,7 +108,7 @@ export const googleFormsWebhookTrigger: TriggerConfig = {
const answer = itemResponse.getResponse();
answers[question] = answer;
}
// Build payload
const payload = {
provider: "google_forms",
@@ -110,7 +118,7 @@ export const googleFormsWebhookTrigger: TriggerConfig = {
lastSubmittedTime: formResponse.getTimestamp().toISOString(),
answers: answers
};
// Send to webhook
const options = {
method: "post",
@@ -121,9 +129,9 @@ export const googleFormsWebhookTrigger: TriggerConfig = {
payload: JSON.stringify(payload),
muteHttpExceptions: true
};
const response = UrlFetchApp.fetch(WEBHOOK_URL, options);
if (response.getResponseCode() !== 200) {
Logger.log("Webhook failed: " + response.getContentText());
} else {
@@ -145,14 +153,6 @@ export const googleFormsWebhookTrigger: TriggerConfig = {
description: 'Copy this code and paste it into your Google Forms Apps Script editor',
mode: 'trigger',
},
{
id: 'triggerSave',
title: '',
type: 'trigger-save',
hideFromPreview: true,
mode: 'trigger',
triggerId: 'google_forms_webhook',
},
],
outputs: {

View File

@@ -34,18 +34,6 @@ export const grainHighlightCreatedTrigger: TriggerConfig = {
value: 'grain_highlight_created',
},
},
{
id: 'triggerInstructions',
title: 'Setup Instructions',
hideFromPreview: true,
type: 'text',
defaultValue: grainSetupInstructions('Highlight (new)'),
mode: 'trigger',
condition: {
field: 'selectedTriggerId',
value: 'grain_highlight_created',
},
},
{
id: 'triggerSave',
title: '',
@@ -58,6 +46,18 @@ export const grainHighlightCreatedTrigger: TriggerConfig = {
value: 'grain_highlight_created',
},
},
{
id: 'triggerInstructions',
title: 'Setup Instructions',
hideFromPreview: true,
type: 'text',
defaultValue: grainSetupInstructions('Highlight (new)'),
mode: 'trigger',
condition: {
field: 'selectedTriggerId',
value: 'grain_highlight_created',
},
},
],
outputs: buildHighlightOutputs(),

View File

@@ -34,18 +34,6 @@ export const grainHighlightUpdatedTrigger: TriggerConfig = {
value: 'grain_highlight_updated',
},
},
{
id: 'triggerInstructions',
title: 'Setup Instructions',
hideFromPreview: true,
type: 'text',
defaultValue: grainSetupInstructions('Highlight (updated)'),
mode: 'trigger',
condition: {
field: 'selectedTriggerId',
value: 'grain_highlight_updated',
},
},
{
id: 'triggerSave',
title: '',
@@ -58,6 +46,18 @@ export const grainHighlightUpdatedTrigger: TriggerConfig = {
value: 'grain_highlight_updated',
},
},
{
id: 'triggerInstructions',
title: 'Setup Instructions',
hideFromPreview: true,
type: 'text',
defaultValue: grainSetupInstructions('Highlight (updated)'),
mode: 'trigger',
condition: {
field: 'selectedTriggerId',
value: 'grain_highlight_updated',
},
},
],
outputs: buildHighlightOutputs(),

View File

@@ -34,18 +34,6 @@ export const grainRecordingCreatedTrigger: TriggerConfig = {
value: 'grain_recording_created',
},
},
{
id: 'triggerInstructions',
title: 'Setup Instructions',
hideFromPreview: true,
type: 'text',
defaultValue: grainSetupInstructions('Recording (new)'),
mode: 'trigger',
condition: {
field: 'selectedTriggerId',
value: 'grain_recording_created',
},
},
{
id: 'triggerSave',
title: '',
@@ -58,6 +46,18 @@ export const grainRecordingCreatedTrigger: TriggerConfig = {
value: 'grain_recording_created',
},
},
{
id: 'triggerInstructions',
title: 'Setup Instructions',
hideFromPreview: true,
type: 'text',
defaultValue: grainSetupInstructions('Recording (new)'),
mode: 'trigger',
condition: {
field: 'selectedTriggerId',
value: 'grain_recording_created',
},
},
],
outputs: buildRecordingOutputs(),

View File

@@ -34,18 +34,6 @@ export const grainRecordingUpdatedTrigger: TriggerConfig = {
value: 'grain_recording_updated',
},
},
{
id: 'triggerInstructions',
title: 'Setup Instructions',
hideFromPreview: true,
type: 'text',
defaultValue: grainSetupInstructions('Recording (updated)'),
mode: 'trigger',
condition: {
field: 'selectedTriggerId',
value: 'grain_recording_updated',
},
},
{
id: 'triggerSave',
title: '',
@@ -58,6 +46,18 @@ export const grainRecordingUpdatedTrigger: TriggerConfig = {
value: 'grain_recording_updated',
},
},
{
id: 'triggerInstructions',
title: 'Setup Instructions',
hideFromPreview: true,
type: 'text',
defaultValue: grainSetupInstructions('Recording (updated)'),
mode: 'trigger',
condition: {
field: 'selectedTriggerId',
value: 'grain_recording_updated',
},
},
],
outputs: buildRecordingOutputs(),

View File

@@ -34,18 +34,6 @@ export const grainStoryCreatedTrigger: TriggerConfig = {
value: 'grain_story_created',
},
},
{
id: 'triggerInstructions',
title: 'Setup Instructions',
hideFromPreview: true,
type: 'text',
defaultValue: grainSetupInstructions('Story (new)'),
mode: 'trigger',
condition: {
field: 'selectedTriggerId',
value: 'grain_story_created',
},
},
{
id: 'triggerSave',
title: '',
@@ -58,6 +46,18 @@ export const grainStoryCreatedTrigger: TriggerConfig = {
value: 'grain_story_created',
},
},
{
id: 'triggerInstructions',
title: 'Setup Instructions',
hideFromPreview: true,
type: 'text',
defaultValue: grainSetupInstructions('Story (new)'),
mode: 'trigger',
condition: {
field: 'selectedTriggerId',
value: 'grain_story_created',
},
},
],
outputs: buildStoryOutputs(),

View File

@@ -34,18 +34,6 @@ export const grainWebhookTrigger: TriggerConfig = {
value: 'grain_webhook',
},
},
{
id: 'triggerInstructions',
title: 'Setup Instructions',
hideFromPreview: true,
type: 'text',
defaultValue: grainSetupInstructions('All events'),
mode: 'trigger',
condition: {
field: 'selectedTriggerId',
value: 'grain_webhook',
},
},
{
id: 'triggerSave',
title: '',
@@ -58,6 +46,18 @@ export const grainWebhookTrigger: TriggerConfig = {
value: 'grain_webhook',
},
},
{
id: 'triggerInstructions',
title: 'Setup Instructions',
hideFromPreview: true,
type: 'text',
defaultValue: grainSetupInstructions('All events'),
mode: 'trigger',
condition: {
field: 'selectedTriggerId',
value: 'grain_webhook',
},
},
],
outputs: buildGenericOutputs(),

View File

@@ -93,6 +93,17 @@ export const hubspotCompanyCreatedTrigger: TriggerConfig = {
value: 'hubspot_company_created',
},
},
{
id: 'triggerSave',
title: '',
type: 'trigger-save',
mode: 'trigger',
triggerId: 'hubspot_company_created',
condition: {
field: 'selectedTriggerId',
value: 'hubspot_company_created',
},
},
{
id: 'triggerInstructions',
title: 'Setup Instructions',
@@ -156,17 +167,6 @@ export const hubspotCompanyCreatedTrigger: TriggerConfig = {
value: 'hubspot_company_created',
},
},
{
id: 'triggerSave',
title: '',
type: 'trigger-save',
mode: 'trigger',
triggerId: 'hubspot_company_created',
condition: {
field: 'selectedTriggerId',
value: 'hubspot_company_created',
},
},
{
id: 'samplePayload',
title: 'Event Payload Example',

View File

@@ -93,6 +93,17 @@ export const hubspotCompanyDeletedTrigger: TriggerConfig = {
value: 'hubspot_company_deleted',
},
},
{
id: 'triggerSave',
title: '',
type: 'trigger-save',
mode: 'trigger',
triggerId: 'hubspot_company_deleted',
condition: {
field: 'selectedTriggerId',
value: 'hubspot_company_deleted',
},
},
{
id: 'triggerInstructions',
title: 'Setup Instructions',
@@ -156,17 +167,6 @@ export const hubspotCompanyDeletedTrigger: TriggerConfig = {
value: 'hubspot_company_deleted',
},
},
{
id: 'triggerSave',
title: '',
type: 'trigger-save',
mode: 'trigger',
triggerId: 'hubspot_company_deleted',
condition: {
field: 'selectedTriggerId',
value: 'hubspot_company_deleted',
},
},
{
id: 'samplePayload',
title: 'Event Payload Example',

View File

@@ -107,6 +107,17 @@ export const hubspotCompanyPropertyChangedTrigger: TriggerConfig = {
value: 'hubspot_company_property_changed',
},
},
{
id: 'triggerSave',
title: '',
type: 'trigger-save',
mode: 'trigger',
triggerId: 'hubspot_company_property_changed',
condition: {
field: 'selectedTriggerId',
value: 'hubspot_company_property_changed',
},
},
{
id: 'triggerInstructions',
title: 'Setup Instructions',
@@ -170,17 +181,6 @@ export const hubspotCompanyPropertyChangedTrigger: TriggerConfig = {
value: 'hubspot_company_property_changed',
},
},
{
id: 'triggerSave',
title: '',
type: 'trigger-save',
mode: 'trigger',
triggerId: 'hubspot_company_property_changed',
condition: {
field: 'selectedTriggerId',
value: 'hubspot_company_property_changed',
},
},
{
id: 'samplePayload',
title: 'Event Payload Example',

View File

@@ -93,6 +93,17 @@ export const hubspotContactCreatedTrigger: TriggerConfig = {
value: 'hubspot_contact_created',
},
},
{
id: 'triggerSave',
title: '',
type: 'trigger-save',
mode: 'trigger',
triggerId: 'hubspot_contact_created',
condition: {
field: 'selectedTriggerId',
value: 'hubspot_contact_created',
},
},
{
id: 'triggerInstructions',
title: 'Setup Instructions',
@@ -156,17 +167,6 @@ export const hubspotContactCreatedTrigger: TriggerConfig = {
value: 'hubspot_contact_created',
},
},
{
id: 'triggerSave',
title: '',
type: 'trigger-save',
mode: 'trigger',
triggerId: 'hubspot_contact_created',
condition: {
field: 'selectedTriggerId',
value: 'hubspot_contact_created',
},
},
{
id: 'samplePayload',
title: 'Event Payload Example',

View File

@@ -93,6 +93,17 @@ export const hubspotContactDeletedTrigger: TriggerConfig = {
value: 'hubspot_contact_deleted',
},
},
{
id: 'triggerSave',
title: '',
type: 'trigger-save',
mode: 'trigger',
triggerId: 'hubspot_contact_deleted',
condition: {
field: 'selectedTriggerId',
value: 'hubspot_contact_deleted',
},
},
{
id: 'triggerInstructions',
title: 'Setup Instructions',
@@ -156,17 +167,6 @@ export const hubspotContactDeletedTrigger: TriggerConfig = {
value: 'hubspot_contact_deleted',
},
},
{
id: 'triggerSave',
title: '',
type: 'trigger-save',
mode: 'trigger',
triggerId: 'hubspot_contact_deleted',
condition: {
field: 'selectedTriggerId',
value: 'hubspot_contact_deleted',
},
},
{
id: 'samplePayload',
title: 'Event Payload Example',

View File

@@ -94,6 +94,17 @@ export const hubspotContactPrivacyDeletedTrigger: TriggerConfig = {
value: 'hubspot_contact_privacy_deleted',
},
},
{
id: 'triggerSave',
title: '',
type: 'trigger-save',
mode: 'trigger',
triggerId: 'hubspot_contact_privacy_deleted',
condition: {
field: 'selectedTriggerId',
value: 'hubspot_contact_privacy_deleted',
},
},
{
id: 'triggerInstructions',
title: 'Setup Instructions',
@@ -157,17 +168,6 @@ export const hubspotContactPrivacyDeletedTrigger: TriggerConfig = {
value: 'hubspot_contact_privacy_deleted',
},
},
{
id: 'triggerSave',
title: '',
type: 'trigger-save',
mode: 'trigger',
triggerId: 'hubspot_contact_privacy_deleted',
condition: {
field: 'selectedTriggerId',
value: 'hubspot_contact_privacy_deleted',
},
},
{
id: 'samplePayload',
title: 'Event Payload Example',

View File

@@ -107,6 +107,17 @@ export const hubspotContactPropertyChangedTrigger: TriggerConfig = {
value: 'hubspot_contact_property_changed',
},
},
{
id: 'triggerSave',
title: '',
type: 'trigger-save',
mode: 'trigger',
triggerId: 'hubspot_contact_property_changed',
condition: {
field: 'selectedTriggerId',
value: 'hubspot_contact_property_changed',
},
},
{
id: 'triggerInstructions',
title: 'Setup Instructions',
@@ -170,17 +181,6 @@ export const hubspotContactPropertyChangedTrigger: TriggerConfig = {
value: 'hubspot_contact_property_changed',
},
},
{
id: 'triggerSave',
title: '',
type: 'trigger-save',
mode: 'trigger',
triggerId: 'hubspot_contact_property_changed',
condition: {
field: 'selectedTriggerId',
value: 'hubspot_contact_property_changed',
},
},
{
id: 'samplePayload',
title: 'Event Payload Example',

View File

@@ -93,6 +93,17 @@ export const hubspotConversationCreationTrigger: TriggerConfig = {
value: 'hubspot_conversation_creation',
},
},
{
id: 'triggerSave',
title: '',
type: 'trigger-save',
mode: 'trigger',
triggerId: 'hubspot_conversation_creation',
condition: {
field: 'selectedTriggerId',
value: 'hubspot_conversation_creation',
},
},
{
id: 'triggerInstructions',
title: 'Setup Instructions',
@@ -156,17 +167,6 @@ export const hubspotConversationCreationTrigger: TriggerConfig = {
value: 'hubspot_conversation_creation',
},
},
{
id: 'triggerSave',
title: '',
type: 'trigger-save',
mode: 'trigger',
triggerId: 'hubspot_conversation_creation',
condition: {
field: 'selectedTriggerId',
value: 'hubspot_conversation_creation',
},
},
{
id: 'samplePayload',
title: 'Event Payload Example',

View File

@@ -93,6 +93,17 @@ export const hubspotConversationDeletionTrigger: TriggerConfig = {
value: 'hubspot_conversation_deletion',
},
},
{
id: 'triggerSave',
title: '',
type: 'trigger-save',
mode: 'trigger',
triggerId: 'hubspot_conversation_deletion',
condition: {
field: 'selectedTriggerId',
value: 'hubspot_conversation_deletion',
},
},
{
id: 'triggerInstructions',
title: 'Setup Instructions',
@@ -156,17 +167,6 @@ export const hubspotConversationDeletionTrigger: TriggerConfig = {
value: 'hubspot_conversation_deletion',
},
},
{
id: 'triggerSave',
title: '',
type: 'trigger-save',
mode: 'trigger',
triggerId: 'hubspot_conversation_deletion',
condition: {
field: 'selectedTriggerId',
value: 'hubspot_conversation_deletion',
},
},
{
id: 'samplePayload',
title: 'Event Payload Example',

View File

@@ -93,6 +93,17 @@ export const hubspotConversationNewMessageTrigger: TriggerConfig = {
value: 'hubspot_conversation_new_message',
},
},
{
id: 'triggerSave',
title: '',
type: 'trigger-save',
mode: 'trigger',
triggerId: 'hubspot_conversation_new_message',
condition: {
field: 'selectedTriggerId',
value: 'hubspot_conversation_new_message',
},
},
{
id: 'triggerInstructions',
title: 'Setup Instructions',
@@ -156,17 +167,6 @@ export const hubspotConversationNewMessageTrigger: TriggerConfig = {
value: 'hubspot_conversation_new_message',
},
},
{
id: 'triggerSave',
title: '',
type: 'trigger-save',
mode: 'trigger',
triggerId: 'hubspot_conversation_new_message',
condition: {
field: 'selectedTriggerId',
value: 'hubspot_conversation_new_message',
},
},
{
id: 'samplePayload',
title: 'Event Payload Example',

View File

@@ -94,6 +94,17 @@ export const hubspotConversationPrivacyDeletionTrigger: TriggerConfig = {
value: 'hubspot_conversation_privacy_deletion',
},
},
{
id: 'triggerSave',
title: '',
type: 'trigger-save',
mode: 'trigger',
triggerId: 'hubspot_conversation_privacy_deletion',
condition: {
field: 'selectedTriggerId',
value: 'hubspot_conversation_privacy_deletion',
},
},
{
id: 'triggerInstructions',
title: 'Setup Instructions',
@@ -157,17 +168,6 @@ export const hubspotConversationPrivacyDeletionTrigger: TriggerConfig = {
value: 'hubspot_conversation_privacy_deletion',
},
},
{
id: 'triggerSave',
title: '',
type: 'trigger-save',
mode: 'trigger',
triggerId: 'hubspot_conversation_privacy_deletion',
condition: {
field: 'selectedTriggerId',
value: 'hubspot_conversation_privacy_deletion',
},
},
{
id: 'samplePayload',
title: 'Event Payload Example',

View File

@@ -107,6 +107,17 @@ export const hubspotConversationPropertyChangedTrigger: TriggerConfig = {
value: 'hubspot_conversation_property_changed',
},
},
{
id: 'triggerSave',
title: '',
type: 'trigger-save',
mode: 'trigger',
triggerId: 'hubspot_conversation_property_changed',
condition: {
field: 'selectedTriggerId',
value: 'hubspot_conversation_property_changed',
},
},
{
id: 'triggerInstructions',
title: 'Setup Instructions',
@@ -170,17 +181,6 @@ export const hubspotConversationPropertyChangedTrigger: TriggerConfig = {
value: 'hubspot_conversation_property_changed',
},
},
{
id: 'triggerSave',
title: '',
type: 'trigger-save',
mode: 'trigger',
triggerId: 'hubspot_conversation_property_changed',
condition: {
field: 'selectedTriggerId',
value: 'hubspot_conversation_property_changed',
},
},
{
id: 'samplePayload',
title: 'Event Payload Example',

View File

@@ -93,6 +93,17 @@ export const hubspotDealCreatedTrigger: TriggerConfig = {
value: 'hubspot_deal_created',
},
},
{
id: 'triggerSave',
title: '',
type: 'trigger-save',
mode: 'trigger',
triggerId: 'hubspot_deal_created',
condition: {
field: 'selectedTriggerId',
value: 'hubspot_deal_created',
},
},
{
id: 'triggerInstructions',
title: 'Setup Instructions',
@@ -156,17 +167,6 @@ export const hubspotDealCreatedTrigger: TriggerConfig = {
value: 'hubspot_deal_created',
},
},
{
id: 'triggerSave',
title: '',
type: 'trigger-save',
mode: 'trigger',
triggerId: 'hubspot_deal_created',
condition: {
field: 'selectedTriggerId',
value: 'hubspot_deal_created',
},
},
{
id: 'samplePayload',
title: 'Event Payload Example',

View File

@@ -93,6 +93,17 @@ export const hubspotDealDeletedTrigger: TriggerConfig = {
value: 'hubspot_deal_deleted',
},
},
{
id: 'triggerSave',
title: '',
type: 'trigger-save',
mode: 'trigger',
triggerId: 'hubspot_deal_deleted',
condition: {
field: 'selectedTriggerId',
value: 'hubspot_deal_deleted',
},
},
{
id: 'triggerInstructions',
title: 'Setup Instructions',
@@ -156,17 +167,6 @@ export const hubspotDealDeletedTrigger: TriggerConfig = {
value: 'hubspot_deal_deleted',
},
},
{
id: 'triggerSave',
title: '',
type: 'trigger-save',
mode: 'trigger',
triggerId: 'hubspot_deal_deleted',
condition: {
field: 'selectedTriggerId',
value: 'hubspot_deal_deleted',
},
},
{
id: 'samplePayload',
title: 'Event Payload Example',

View File

@@ -107,6 +107,17 @@ export const hubspotDealPropertyChangedTrigger: TriggerConfig = {
value: 'hubspot_deal_property_changed',
},
},
{
id: 'triggerSave',
title: '',
type: 'trigger-save',
mode: 'trigger',
triggerId: 'hubspot_deal_property_changed',
condition: {
field: 'selectedTriggerId',
value: 'hubspot_deal_property_changed',
},
},
{
id: 'triggerInstructions',
title: 'Setup Instructions',
@@ -170,17 +181,6 @@ export const hubspotDealPropertyChangedTrigger: TriggerConfig = {
value: 'hubspot_deal_property_changed',
},
},
{
id: 'triggerSave',
title: '',
type: 'trigger-save',
mode: 'trigger',
triggerId: 'hubspot_deal_property_changed',
condition: {
field: 'selectedTriggerId',
value: 'hubspot_deal_property_changed',
},
},
{
id: 'samplePayload',
title: 'Event Payload Example',

View File

@@ -93,6 +93,17 @@ export const hubspotTicketCreatedTrigger: TriggerConfig = {
value: 'hubspot_ticket_created',
},
},
{
id: 'triggerSave',
title: '',
type: 'trigger-save',
mode: 'trigger',
triggerId: 'hubspot_ticket_created',
condition: {
field: 'selectedTriggerId',
value: 'hubspot_ticket_created',
},
},
{
id: 'triggerInstructions',
title: 'Setup Instructions',
@@ -156,17 +167,6 @@ export const hubspotTicketCreatedTrigger: TriggerConfig = {
value: 'hubspot_ticket_created',
},
},
{
id: 'triggerSave',
title: '',
type: 'trigger-save',
mode: 'trigger',
triggerId: 'hubspot_ticket_created',
condition: {
field: 'selectedTriggerId',
value: 'hubspot_ticket_created',
},
},
{
id: 'samplePayload',
title: 'Event Payload Example',

View File

@@ -93,6 +93,17 @@ export const hubspotTicketDeletedTrigger: TriggerConfig = {
value: 'hubspot_ticket_deleted',
},
},
{
id: 'triggerSave',
title: '',
type: 'trigger-save',
mode: 'trigger',
triggerId: 'hubspot_ticket_deleted',
condition: {
field: 'selectedTriggerId',
value: 'hubspot_ticket_deleted',
},
},
{
id: 'triggerInstructions',
title: 'Setup Instructions',
@@ -156,17 +167,6 @@ export const hubspotTicketDeletedTrigger: TriggerConfig = {
value: 'hubspot_ticket_deleted',
},
},
{
id: 'triggerSave',
title: '',
type: 'trigger-save',
mode: 'trigger',
triggerId: 'hubspot_ticket_deleted',
condition: {
field: 'selectedTriggerId',
value: 'hubspot_ticket_deleted',
},
},
{
id: 'samplePayload',
title: 'Event Payload Example',

View File

@@ -107,6 +107,17 @@ export const hubspotTicketPropertyChangedTrigger: TriggerConfig = {
value: 'hubspot_ticket_property_changed',
},
},
{
id: 'triggerSave',
title: '',
type: 'trigger-save',
mode: 'trigger',
triggerId: 'hubspot_ticket_property_changed',
condition: {
field: 'selectedTriggerId',
value: 'hubspot_ticket_property_changed',
},
},
{
id: 'triggerInstructions',
title: 'Setup Instructions',
@@ -170,17 +181,6 @@ export const hubspotTicketPropertyChangedTrigger: TriggerConfig = {
value: 'hubspot_ticket_property_changed',
},
},
{
id: 'triggerSave',
title: '',
type: 'trigger-save',
mode: 'trigger',
triggerId: 'hubspot_ticket_property_changed',
condition: {
field: 'selectedTriggerId',
value: 'hubspot_ticket_property_changed',
},
},
{
id: 'samplePayload',
title: 'Event Payload Example',

View File

@@ -82,7 +82,7 @@ export function hubspotSetupInstructions(eventType: string, additionalNotes?: st
'<strong>Step 3: Configure OAuth Settings</strong><br/>After creating your app via CLI, configure it to add the OAuth Redirect URL: <code>https://www.sim.ai/api/auth/oauth2/callback/hubspot</code>. Then retrieve your <strong>Client ID</strong> and <strong>Client Secret</strong> from your app configuration and enter them in the fields above.',
"<strong>Step 4: Get App ID and Developer API Key</strong><br/>In your HubSpot developer account, find your <strong>App ID</strong> (shown below your app name) and your <strong>Developer API Key</strong> (in app settings). You'll need both for the next steps.",
'<strong>Step 5: Set Required Scopes</strong><br/>Configure your app to include the required OAuth scope: <code>crm.objects.contacts.read</code>',
'<strong>Step 6: Save Configuration in Sim</strong><br/>Click the <strong>"Save Configuration"</strong> button below. This will generate your unique webhook URL.',
'<strong>Step 6: Save Configuration in Sim</strong><br/>Click the <strong>"Save Configuration"</strong> button above. This will generate your unique webhook URL.',
'<strong>Step 7: Configure Webhook in HubSpot via API</strong><br/>After saving above, copy the <strong>Webhook URL</strong> and run the two curl commands below (replace <code>{YOUR_APP_ID}</code>, <code>{YOUR_DEVELOPER_API_KEY}</code>, and <code>{YOUR_WEBHOOK_URL_FROM_ABOVE}</code> with your actual values).',
"<strong>Step 8: Test Your Webhook</strong><br/>Create or modify a contact in HubSpot to trigger the webhook. Check your workflow execution logs in Sim to verify it's working.",
]

View File

@@ -202,6 +202,14 @@ Return ONLY valid JSON, no explanations or markdown.`,
mode: 'trigger',
},
// Instructions
{
id: 'triggerSave',
title: '',
type: 'trigger-save',
hideFromPreview: true,
mode: 'trigger',
triggerId: 'imap_poller',
},
{
id: 'triggerInstructions',
title: 'Setup Instructions',
@@ -222,14 +230,6 @@ Return ONLY valid JSON, no explanations or markdown.`,
.join(''),
mode: 'trigger',
},
{
id: 'triggerSave',
title: '',
type: 'trigger-save',
hideFromPreview: true,
mode: 'trigger',
triggerId: 'imap_poller',
},
],
outputs: {

View File

@@ -56,18 +56,6 @@ export const jiraIssueCommentedTrigger: TriggerConfig = {
value: 'jira_issue_commented',
},
},
{
id: 'triggerInstructions',
title: 'Setup Instructions',
hideFromPreview: true,
type: 'text',
defaultValue: jiraSetupInstructions('comment_created'),
mode: 'trigger',
condition: {
field: 'selectedTriggerId',
value: 'jira_issue_commented',
},
},
{
id: 'triggerSave',
title: '',
@@ -80,6 +68,18 @@ export const jiraIssueCommentedTrigger: TriggerConfig = {
value: 'jira_issue_commented',
},
},
{
id: 'triggerInstructions',
title: 'Setup Instructions',
hideFromPreview: true,
type: 'text',
defaultValue: jiraSetupInstructions('comment_created'),
mode: 'trigger',
condition: {
field: 'selectedTriggerId',
value: 'jira_issue_commented',
},
},
],
outputs: buildCommentOutputs(),

View File

@@ -65,18 +65,6 @@ export const jiraIssueCreatedTrigger: TriggerConfig = {
value: 'jira_issue_created',
},
},
{
id: 'triggerInstructions',
title: 'Setup Instructions',
hideFromPreview: true,
type: 'text',
defaultValue: jiraSetupInstructions('jira:issue_created'),
mode: 'trigger',
condition: {
field: 'selectedTriggerId',
value: 'jira_issue_created',
},
},
{
id: 'triggerSave',
title: '',
@@ -89,6 +77,18 @@ export const jiraIssueCreatedTrigger: TriggerConfig = {
value: 'jira_issue_created',
},
},
{
id: 'triggerInstructions',
title: 'Setup Instructions',
hideFromPreview: true,
type: 'text',
defaultValue: jiraSetupInstructions('jira:issue_created'),
mode: 'trigger',
condition: {
field: 'selectedTriggerId',
value: 'jira_issue_created',
},
},
],
outputs: buildIssueOutputs(),

View File

@@ -56,18 +56,6 @@ export const jiraIssueDeletedTrigger: TriggerConfig = {
value: 'jira_issue_deleted',
},
},
{
id: 'triggerInstructions',
title: 'Setup Instructions',
hideFromPreview: true,
type: 'text',
defaultValue: jiraSetupInstructions('jira:issue_deleted'),
mode: 'trigger',
condition: {
field: 'selectedTriggerId',
value: 'jira_issue_deleted',
},
},
{
id: 'triggerSave',
title: '',
@@ -80,6 +68,18 @@ export const jiraIssueDeletedTrigger: TriggerConfig = {
value: 'jira_issue_deleted',
},
},
{
id: 'triggerInstructions',
title: 'Setup Instructions',
hideFromPreview: true,
type: 'text',
defaultValue: jiraSetupInstructions('jira:issue_deleted'),
mode: 'trigger',
condition: {
field: 'selectedTriggerId',
value: 'jira_issue_deleted',
},
},
],
outputs: buildIssueOutputs(),

View File

@@ -70,18 +70,6 @@ export const jiraIssueUpdatedTrigger: TriggerConfig = {
value: 'jira_issue_updated',
},
},
{
id: 'triggerInstructions',
title: 'Setup Instructions',
hideFromPreview: true,
type: 'text',
defaultValue: jiraSetupInstructions('jira:issue_updated'),
mode: 'trigger',
condition: {
field: 'selectedTriggerId',
value: 'jira_issue_updated',
},
},
{
id: 'triggerSave',
title: '',
@@ -94,6 +82,18 @@ export const jiraIssueUpdatedTrigger: TriggerConfig = {
value: 'jira_issue_updated',
},
},
{
id: 'triggerInstructions',
title: 'Setup Instructions',
hideFromPreview: true,
type: 'text',
defaultValue: jiraSetupInstructions('jira:issue_updated'),
mode: 'trigger',
condition: {
field: 'selectedTriggerId',
value: 'jira_issue_updated',
},
},
],
outputs: buildIssueUpdatedOutputs(),

View File

@@ -43,18 +43,6 @@ export const jiraWebhookTrigger: TriggerConfig = {
value: 'jira_webhook',
},
},
{
id: 'triggerInstructions',
title: 'Setup Instructions',
hideFromPreview: true,
type: 'text',
defaultValue: jiraSetupInstructions('All Events'),
mode: 'trigger',
condition: {
field: 'selectedTriggerId',
value: 'jira_webhook',
},
},
{
id: 'triggerSave',
title: '',
@@ -67,6 +55,18 @@ export const jiraWebhookTrigger: TriggerConfig = {
value: 'jira_webhook',
},
},
{
id: 'triggerInstructions',
title: 'Setup Instructions',
hideFromPreview: true,
type: 'text',
defaultValue: jiraSetupInstructions('All Events'),
mode: 'trigger',
condition: {
field: 'selectedTriggerId',
value: 'jira_webhook',
},
},
],
outputs: {

Some files were not shown because too many files have changed in this diff Show More