diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index 2701ca3053..790d1da0d0 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -10,7 +10,6 @@ services: environment: - NODE_ENV=development - DATABASE_URL=postgresql://postgres:postgres@db:5432/simstudio - - POSTGRES_URL=postgresql://postgres:postgres@db:5432/simstudio - BETTER_AUTH_URL=http://localhost:3000 - NEXT_PUBLIC_APP_URL=http://localhost:3000 - BUN_INSTALL_CACHE_DIR=/home/bun/.bun/cache diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b1c9e8b753..5262ef8212 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,43 +16,200 @@ jobs: uses: ./.github/workflows/test-build.yml secrets: inherit - # Build and push images (ECR for staging, ECR + GHCR for main) - build-images: - name: Build Images + # Build AMD64 images and push to ECR immediately (+ GHCR for main) + build-amd64: + name: Build AMD64 needs: test-build if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/staging') - uses: ./.github/workflows/images.yml - secrets: inherit + runs-on: blacksmith-4vcpu-ubuntu-2404 permissions: contents: read packages: write id-token: write + strategy: + fail-fast: false + matrix: + include: + - dockerfile: ./docker/app.Dockerfile + ghcr_image: ghcr.io/simstudioai/simstudio + ecr_repo_secret: ECR_APP + - dockerfile: ./docker/db.Dockerfile + ghcr_image: ghcr.io/simstudioai/migrations + ecr_repo_secret: ECR_MIGRATIONS + - dockerfile: ./docker/realtime.Dockerfile + ghcr_image: ghcr.io/simstudioai/realtime + ecr_repo_secret: ECR_REALTIME + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: ${{ github.ref == 'refs/heads/main' && secrets.AWS_ROLE_TO_ASSUME || secrets.STAGING_AWS_ROLE_TO_ASSUME }} + aws-region: ${{ github.ref == 'refs/heads/main' && secrets.AWS_REGION || secrets.STAGING_AWS_REGION }} + + - name: Login to Amazon ECR + id: login-ecr + uses: aws-actions/amazon-ecr-login@v2 + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Login to GHCR + if: github.ref == 'refs/heads/main' + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up Docker Buildx + uses: useblacksmith/setup-docker-builder@v1 + + - name: Generate tags + id: meta + run: | + ECR_REGISTRY="${{ steps.login-ecr.outputs.registry }}" + ECR_REPO="${{ secrets[matrix.ecr_repo_secret] }}" + GHCR_IMAGE="${{ matrix.ghcr_image }}" + + # ECR tags (always build for ECR) + if [ "${{ github.ref }}" = "refs/heads/main" ]; then + ECR_TAG="latest" + else + ECR_TAG="staging" + fi + ECR_IMAGE="${ECR_REGISTRY}/${ECR_REPO}:${ECR_TAG}" - # Deploy Trigger.dev (after builds complete) + # Build tags list + TAGS="${ECR_IMAGE}" + + # Add GHCR tags only for main branch + if [ "${{ github.ref }}" = "refs/heads/main" ]; then + GHCR_AMD64="${GHCR_IMAGE}:latest-amd64" + GHCR_SHA="${GHCR_IMAGE}:${{ github.sha }}-amd64" + TAGS="${TAGS},$GHCR_AMD64,$GHCR_SHA" + fi + + echo "tags=${TAGS}" >> $GITHUB_OUTPUT + + - name: Build and push images + uses: useblacksmith/build-push-action@v2 + with: + context: . + file: ${{ matrix.dockerfile }} + platforms: linux/amd64 + push: true + tags: ${{ steps.meta.outputs.tags }} + provenance: false + sbom: false + + # Build ARM64 images for GHCR (main branch only, runs in parallel) + build-ghcr-arm64: + name: Build ARM64 (GHCR Only) + needs: test-build + runs-on: linux-arm64-8-core + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + permissions: + contents: read + packages: write + strategy: + fail-fast: false + matrix: + include: + - dockerfile: ./docker/app.Dockerfile + image: ghcr.io/simstudioai/simstudio + - dockerfile: ./docker/db.Dockerfile + image: ghcr.io/simstudioai/migrations + - dockerfile: ./docker/realtime.Dockerfile + image: ghcr.io/simstudioai/realtime + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Login to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up Docker Buildx + uses: useblacksmith/setup-docker-builder@v1 + + - name: Generate ARM64 tags + id: meta + run: | + IMAGE="${{ matrix.image }}" + echo "tags=${IMAGE}:latest-arm64,${IMAGE}:${{ github.sha }}-arm64" >> $GITHUB_OUTPUT + + - name: Build and push ARM64 to GHCR + uses: useblacksmith/build-push-action@v2 + with: + context: . + file: ${{ matrix.dockerfile }} + platforms: linux/arm64 + push: true + tags: ${{ steps.meta.outputs.tags }} + provenance: false + sbom: false + + # Create GHCR multi-arch manifests (only for main, after both builds) + create-ghcr-manifests: + name: Create GHCR Manifests + runs-on: blacksmith-4vcpu-ubuntu-2404 + needs: [build-amd64, build-ghcr-arm64] + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + permissions: + packages: write + strategy: + matrix: + include: + - image: ghcr.io/simstudioai/simstudio + - image: ghcr.io/simstudioai/migrations + - image: ghcr.io/simstudioai/realtime + + steps: + - name: Login to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Create and push manifests + run: | + IMAGE_BASE="${{ matrix.image }}" + + # Create latest manifest + docker manifest create "${IMAGE_BASE}:latest" \ + "${IMAGE_BASE}:latest-amd64" \ + "${IMAGE_BASE}:latest-arm64" + docker manifest push "${IMAGE_BASE}:latest" + + # Create SHA manifest + docker manifest create "${IMAGE_BASE}:${{ github.sha }}" \ + "${IMAGE_BASE}:${{ github.sha }}-amd64" \ + "${IMAGE_BASE}:${{ github.sha }}-arm64" + docker manifest push "${IMAGE_BASE}:${{ github.sha }}" + + # Deploy Trigger.dev (after ECR images are pushed, runs in parallel with process-docs) trigger-deploy: name: Deploy Trigger.dev - needs: build-images + needs: build-amd64 if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/staging') uses: ./.github/workflows/trigger-deploy.yml secrets: inherit - # Run database migrations (depends on build completion and trigger deployment) - migrations: - name: Apply Database Migrations - needs: [build-images, trigger-deploy] - if: | - always() && - github.event_name == 'push' && - (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/staging') && - needs.build-images.result == 'success' && - needs.trigger-deploy.result == 'success' - uses: ./.github/workflows/migrations.yml - secrets: inherit - - # Process docs embeddings if needed + # Process docs embeddings (after ECR images are pushed, runs in parallel with trigger-deploy) process-docs: name: Process Docs - needs: migrations + needs: build-amd64 if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/staging') uses: ./.github/workflows/docs-embeddings.yml secrets: inherit diff --git a/.github/workflows/trigger-deploy.yml b/.github/workflows/trigger-deploy.yml index e8a47275d8..ad8b0675af 100644 --- a/.github/workflows/trigger-deploy.yml +++ b/.github/workflows/trigger-deploy.yml @@ -13,6 +13,7 @@ jobs: cancel-in-progress: false env: TRIGGER_ACCESS_TOKEN: ${{ secrets.TRIGGER_ACCESS_TOKEN }} + TRIGGER_PROJECT_ID: ${{ secrets.TRIGGER_PROJECT_ID }} steps: - name: Checkout code @@ -39,4 +40,5 @@ jobs: - name: Deploy to Trigger.dev (Production) if: github.ref == 'refs/heads/main' working-directory: ./apps/sim - run: npx --yes trigger.dev@4.0.4 deploy \ No newline at end of file + run: npx --yes trigger.dev@4.0.4 deploy + diff --git a/apps/docs/content/docs/de/blocks/agent.mdx b/apps/docs/content/docs/de/blocks/agent.mdx index b9c1773576..c926b8b29a 100644 --- a/apps/docs/content/docs/de/blocks/agent.mdx +++ b/apps/docs/content/docs/de/blocks/agent.mdx @@ -175,56 +175,30 @@ Verwenden Sie einen `Memory`Block mit einer konsistenten `id` (zum Beispiel `cha - Lesen Sie den Gesprächsverlauf für den Kontext - Hängen Sie die Antwort des Agenten nach dessen Ausführung an -```yaml -# 1) Add latest user message -- Memory (operation: add) - id: chat - role: user - content: {{input}} - -# 2) Load conversation history -- Memory (operation: get) - id: chat - -# 3) Run the agent with prior messages available -- Agent - System Prompt: ... - User Prompt: | - Use the conversation so far: - {{memory_get.memories}} - Current user message: {{input}} - -# 4) Store the agent reply -- Memory (operation: add) - id: chat - role: assistant - content: {{agent.content}} -``` - -Siehe die `Memory`Block-Referenz für Details: [/tools/memory](/tools/memory). +Siehe die [`Memory`](/tools/memory) Blockreferenz für Details. ## Eingaben und Ausgaben - +
  • - System Prompt: Anweisungen, die das Verhalten und die Rolle des Agenten definieren + System-Prompt: Anweisungen, die das Verhalten und die Rolle des Agenten definieren
  • - User Prompt: Eingabetext oder -daten zur Verarbeitung + Benutzer-Prompt: Eingabetext oder zu verarbeitende Daten
  • - Model: KI-Modellauswahl (OpenAI, Anthropic, Google, etc.) + Modell: KI-Modellauswahl (OpenAI, Anthropic, Google, usw.)
  • - Temperature: Steuerung der Antwort-Zufälligkeit (0-2) + Temperatur: Steuerung der Zufälligkeit der Antwort (0-2)
  • Tools: Array verfügbarer Tools für Funktionsaufrufe
  • - Response Format: JSON-Schema für strukturierte Ausgabe + Antwortformat: JSON-Schema für strukturierte Ausgabe
@@ -261,7 +235,7 @@ Siehe die `Memory`Block-Referenz für Details: [/tools/memory](/tools/memory). ## Beispielanwendungsfälle -### Automatisierung des Kundendienstes +### Automatisierung des Kundenservice

Szenario: Bearbeitung von Kundenanfragen mit Datenbankzugriff

@@ -269,9 +243,9 @@ Siehe die `Memory`Block-Referenz für Details: [/tools/memory](/tools/memory).
  • Benutzer reicht ein Support-Ticket über den API-Block ein
  • Agent prüft Bestellungen/Abonnements in Postgres und durchsucht die Wissensdatenbank nach Anleitungen
  • Falls eine Eskalation erforderlich ist, erstellt der Agent ein Linear-Ticket mit relevantem Kontext
  • -
  • Agent verfasst eine klare E-Mail-Antwort
  • +
  • Agent erstellt eine klare E-Mail-Antwort
  • Gmail sendet die Antwort an den Kunden
  • -
  • Konversation wird im Speicher gesichert, um den Verlauf für zukünftige Nachrichten zu erhalten
  • +
  • Konversation wird im Memory gespeichert, um den Verlauf für zukünftige Nachrichten beizubehalten
  • @@ -287,20 +261,20 @@ Siehe die `Memory`Block-Referenz für Details: [/tools/memory](/tools/memory). -### Werkzeuggestützter Rechercheassistent +### Werkzeuggestützter Forschungsassistent
    -

    Szenario: Rechercheassistent mit Websuche und Dokumentenzugriff

    +

    Szenario: Forschungsassistent mit Websuche und Dokumentenzugriff

    1. Benutzeranfrage über Eingabe erhalten
    2. -
    3. Agent durchsucht das Web mit dem Google Search-Tool
    4. +
    5. Agent durchsucht das Web mit dem Google-Suchwerkzeug
    6. Agent greift auf Notion-Datenbank für interne Dokumente zu
    7. -
    8. Agent erstellt umfassenden Recherchebericht
    9. +
    10. Agent erstellt umfassenden Forschungsbericht
    -## Best Practices +## Bewährte Praktiken -- **Sei spezifisch in System-Prompts**: Definiere die Rolle, den Tonfall und die Einschränkungen des Agenten klar. Je spezifischer deine Anweisungen sind, desto besser kann der Agent seinen vorgesehenen Zweck erfüllen. +- **Sei spezifisch in System-Prompts**: Definiere die Rolle, den Ton und die Einschränkungen des Agenten klar. Je spezifischer deine Anweisungen sind, desto besser kann der Agent seinen vorgesehenen Zweck erfüllen. - **Wähle die richtige Temperatureinstellung**: Verwende niedrigere Temperatureinstellungen (0-0,3), wenn Genauigkeit wichtig ist, oder erhöhe die Temperatur (0,7-2,0) für kreativere oder abwechslungsreichere Antworten -- **Nutze Tools effektiv**: Integriere Tools, die den Zweck des Agenten ergänzen und seine Fähigkeiten erweitern. Sei selektiv bei der Auswahl der Tools, um den Agenten nicht zu überfordern. Für Aufgaben mit wenig Überschneidung verwende einen anderen Agent-Block für die besten Ergebnisse. +- **Nutze Werkzeuge effektiv**: Integriere Werkzeuge, die den Zweck des Agenten ergänzen und seine Fähigkeiten verbessern. Sei selektiv bei der Auswahl der Werkzeuge, um den Agenten nicht zu überfordern. Für Aufgaben mit wenig Überschneidung verwende einen anderen Agent-Block für die besten Ergebnisse. diff --git a/apps/docs/content/docs/de/triggers/api.mdx b/apps/docs/content/docs/de/triggers/api.mdx index b9be5327b0..ccdd8eff9b 100644 --- a/apps/docs/content/docs/de/triggers/api.mdx +++ b/apps/docs/content/docs/de/triggers/api.mdx @@ -24,15 +24,7 @@ Der API-Trigger stellt Ihren Workflow als sicheren HTTP-Endpunkt bereit. Senden Fügen Sie für jeden Parameter ein Feld **Eingabeformat** hinzu. Die Ausgabeschlüssel zur Laufzeit spiegeln das Schema wider und sind auch unter `` verfügbar. -```yaml -- type: string - name: userId - value: demo-user # optional manual test value -- type: number - name: maxTokens -``` - -Manuelle Ausführungen im Editor verwenden die Spalte `value`, damit Sie testen können, ohne eine Anfrage zu senden. Während der Ausführung füllt der Resolver sowohl `` als auch ``. +Manuelle Ausführungen im Editor verwenden die Spalte `value`, damit Sie testen können, ohne eine Anfrage zu senden. Während der Ausführung füllt der Resolver sowohl `` als auch `` aus. ## Anfrage-Beispiel @@ -56,5 +48,5 @@ Erfolgreiche Antworten geben das serialisierte Ausführungsergebnis vom Executor Wenn kein Eingabeformat definiert ist, stellt der Executor das rohe JSON nur unter `` bereit. -Ein Workflow kann nur einen API-Trigger enthalten. Veröffentlichen Sie eine neue Bereitstellung nach Änderungen, damit der Endpunkt aktuell bleibt. +Ein Workflow kann nur einen API-Trigger enthalten. Veröffentlichen Sie nach Änderungen eine neue Bereitstellung, damit der Endpunkt aktuell bleibt. diff --git a/apps/docs/content/docs/de/yaml/blocks/loop.mdx b/apps/docs/content/docs/de/yaml/blocks/loop.mdx index c0e1c54716..0d91ddc906 100644 --- a/apps/docs/content/docs/de/yaml/blocks/loop.mdx +++ b/apps/docs/content/docs/de/yaml/blocks/loop.mdx @@ -10,7 +10,6 @@ type: object required: - type - name - - inputs - connections properties: type: @@ -22,21 +21,23 @@ properties: description: Display name for this loop block inputs: type: object - required: - - loopType + description: Optional. If omitted, defaults will be applied. properties: loopType: type: string enum: [for, forEach] description: Type of loop to execute + default: for iterations: type: number description: Number of iterations (for 'for' loops) + default: 5 minimum: 1 maximum: 1000 collection: type: string description: Collection to iterate over (for 'forEach' loops) + default: "" maxConcurrency: type: number description: Maximum concurrent executions @@ -45,13 +46,10 @@ properties: maximum: 10 connections: type: object - required: - - loop properties: + # Nested format (recommended) loop: type: object - required: - - start properties: start: type: string @@ -59,26 +57,37 @@ properties: end: type: string description: Target block ID for loop completion (optional) + # Direct handle format (alternative) + loop-start-source: + type: string | string[] + description: Target block ID to execute inside the loop (direct format) + loop-end-source: + type: string | string[] + description: Target block ID for loop completion (direct format, optional) error: type: string description: Target block ID for error handling + note: Use either the nested 'loop' format OR the direct 'loop-start-source' format, not both ``` ## Verbindungskonfiguration -Loop-Blöcke verwenden ein spezielles Verbindungsformat mit einem `loop`Abschnitt: +Loop-Blöcke unterstützen zwei Verbindungsformate: + +### Direktes Handle-Format (Alternative) ```yaml connections: - loop: - start: # Target block ID to execute inside the loop - end: # Target block ID after loop completion (optional) + loop-start-source: # Target block ID to execute inside the loop + loop-end-source: # Target block ID after loop completion (optional) error: # Target block ID for error handling (optional) ``` +Beide Formate funktionieren identisch. Verwenden Sie das Format, das Ihnen besser gefällt. + ## Konfiguration von untergeordneten Blöcken -Blöcke innerhalb einer Schleife müssen ihre `parentId` auf die Loop-Block-ID setzen: +Blöcke innerhalb einer Schleife müssen ihre `parentId` auf die Loop-Block-ID gesetzt haben. Die Eigenschaft `extent` wird automatisch auf `'parent'` gesetzt und muss nicht angegeben werden: ```yaml loop-1: @@ -106,7 +115,7 @@ process-item: ## Beispiele -### For-Schleife (feste Iterationen) +### For-Schleife (feste Anzahl von Iterationen) ```yaml countdown-loop: @@ -227,7 +236,7 @@ store-analysis: }; ``` -### Schleife mit paralleler Verarbeitung +### Schleife für parallele Verarbeitung ```yaml parallel-processing-loop: @@ -261,9 +270,62 @@ process-task: success: task-completed ``` +### Beispiel für direktes Handle-Format + +Dieselbe Schleife kann mit dem direkten Handle-Format geschrieben werden: + +```yaml +my-loop: + type: loop + name: "Process Items" + inputs: + loopType: forEach + collection: + connections: + loop-start-source: process-item # Direct handle format + loop-end-source: final-results # Direct handle format + error: handle-error + +process-item: + type: agent + name: "Process Item" + parentId: my-loop + inputs: + systemPrompt: "Process this item" + userPrompt: + model: gpt-4o + apiKey: '{{OPENAI_API_KEY}}' +``` + +### Minimales Schleifenbeispiel (mit Standardwerten) + +Sie können den Abschnitt `inputs` vollständig weglassen, dann werden Standardwerte angewendet: + +```yaml +simple-loop: + type: loop + name: "Simple Loop" + # No inputs section - defaults to loopType: 'for', iterations: 5 + connections: + loop-start-source: process-step + loop-end-source: complete + +process-step: + type: agent + name: "Process Step" + parentId: simple-loop + inputs: + systemPrompt: "Execute step" + userPrompt: "Step " + model: gpt-4o + apiKey: '{{OPENAI_API_KEY}}' +``` + +Diese Schleife führt standardmäßig 5 Iterationen aus. + ## Schleifenvariablen -Innerhalb von untergeordneten Schleifenblöcken sind diese speziellen Variablen verfügbar: +Innerhalb von Schleifenunterblöcken sind diese speziellen Variablen verfügbar: ```yaml # Available in all child blocks of the loop @@ -290,6 +352,6 @@ final-processor: - Verwenden Sie forEach für die Verarbeitung von Sammlungen, for-Schleifen für feste Iterationen - Erwägen Sie die Verwendung von maxConcurrency für I/O-gebundene Operationen - Integrieren Sie Fehlerbehandlung für eine robuste Schleifenausführung -- Verwenden Sie aussagekräftige Namen für Schleifen-Unterblöcke +- Verwenden Sie aussagekräftige Namen für Schleifenunterblöcke - Testen Sie zuerst mit kleinen Sammlungen -- Überwachen Sie die Ausführungszeit bei großen Sammlungen \ No newline at end of file +- Überwachen Sie die Ausführungszeit für große Sammlungen \ No newline at end of file diff --git a/apps/docs/content/docs/es/blocks/agent.mdx b/apps/docs/content/docs/es/blocks/agent.mdx index 5592c01ef7..e8e03ea0c9 100644 --- a/apps/docs/content/docs/es/blocks/agent.mdx +++ b/apps/docs/content/docs/es/blocks/agent.mdx @@ -175,33 +175,7 @@ Utiliza un bloque `Memory` con un `id` consistente (por ejemplo, `chat`) para pe - Lee el historial de conversación para contexto - Añade la respuesta del agente después de que se ejecute -```yaml -# 1) Add latest user message -- Memory (operation: add) - id: chat - role: user - content: {{input}} - -# 2) Load conversation history -- Memory (operation: get) - id: chat - -# 3) Run the agent with prior messages available -- Agent - System Prompt: ... - User Prompt: | - Use the conversation so far: - {{memory_get.memories}} - Current user message: {{input}} - -# 4) Store the agent reply -- Memory (operation: add) - id: chat - role: assistant - content: {{agent.content}} -``` - -Consulta la referencia del bloque `Memory` para más detalles: [/tools/memory](/tools/memory). +Consulta la referencia del bloque [`Memory`](/tools/memory) para más detalles. ## Entradas y salidas @@ -212,7 +186,7 @@ Consulta la referencia del bloque `Memory` para más detalles: [/tools/memory](/ Prompt del sistema: Instrucciones que definen el comportamiento y rol del agente
  • - Prompt del usuario: Texto de entrada o datos a procesar + Prompt del usuario: Texto o datos de entrada para procesar
  • Modelo: Selección del modelo de IA (OpenAI, Anthropic, Google, etc.) @@ -231,13 +205,13 @@ Consulta la referencia del bloque `Memory` para más detalles: [/tools/memory](/
    • - agent.content: Texto de respuesta o datos estructurados del agente + agent.content: Texto de respuesta del agente o datos estructurados
    • - agent.tokens: Objeto de estadísticas de uso de tokens + agent.tokens: Objeto con estadísticas de uso de tokens
    • - agent.tool_calls: Array de detalles de ejecución de herramientas + agent.tool_calls: Array con detalles de ejecución de herramientas
    • agent.cost: Costo estimado de la llamada a la API (si está disponible) @@ -247,7 +221,7 @@ Consulta la referencia del bloque `Memory` para más detalles: [/tools/memory](/
      • - Contenido: Salida de respuesta principal del agente + Contenido: Salida principal de respuesta del agente
      • Metadatos: Estadísticas de uso y detalles de ejecución @@ -267,15 +241,15 @@ Consulta la referencia del bloque `Memory` para más detalles: [/tools/memory](/

        Escenario: Gestionar consultas de clientes con acceso a base de datos

        1. El usuario envía un ticket de soporte a través del bloque API
        2. -
        3. El agente verifica pedidos/suscripciones en Postgres y busca en la base de conocimientos para obtener orientación
        4. +
        5. El agente verifica pedidos/suscripciones en Postgres y busca en la base de conocimientos
        6. Si se necesita escalamiento, el agente crea una incidencia en Linear con el contexto relevante
        7. El agente redacta una respuesta clara por correo electrónico
        8. Gmail envía la respuesta al cliente
        9. -
        10. La conversación se guarda en Memoria para mantener el historial para mensajes futuros
        11. +
        12. La conversación se guarda en Memory para mantener el historial para mensajes futuros
        -### Análisis de contenido multi-modelo +### Análisis de contenido con múltiples modelos

        Escenario: Analizar contenido con diferentes modelos de IA

        @@ -287,13 +261,13 @@ Consulta la referencia del bloque `Memory` para más detalles: [/tools/memory](/
        -### Asistente de investigación con herramientas +### Asistente de investigación potenciado por herramientas

        Escenario: Asistente de investigación con búsqueda web y acceso a documentos

        1. Consulta del usuario recibida a través de entrada
        2. -
        3. El agente busca en la web usando la herramienta de Google Search
        4. +
        5. El agente busca en la web utilizando la herramienta de Google Search
        6. El agente accede a la base de datos de Notion para documentos internos
        7. El agente compila un informe de investigación completo
        @@ -301,6 +275,6 @@ Consulta la referencia del bloque `Memory` para más detalles: [/tools/memory](/ ## Mejores prácticas -- **Sé específico en los prompts del sistema**: Define claramente el rol, tono y limitaciones del agente. Cuanto más específicas sean tus instrucciones, mejor podrá el agente cumplir con su propósito previsto. +- **Sé específico en los prompts del sistema**: Define claramente el rol del agente, el tono y las limitaciones. Cuanto más específicas sean tus instrucciones, mejor podrá el agente cumplir con su propósito previsto. - **Elige la configuración de temperatura adecuada**: Usa configuraciones de temperatura más bajas (0-0.3) cuando la precisión es importante, o aumenta la temperatura (0.7-2.0) para respuestas más creativas o variadas -- **Aprovecha las herramientas de manera efectiva**: Integra herramientas que complementen el propósito del agente y mejoren sus capacidades. Sé selectivo sobre qué herramientas proporcionas para evitar sobrecargar al agente. Para tareas con poca superposición, usa otro bloque de Agente para obtener los mejores resultados. +- **Aprovecha las herramientas de manera efectiva**: Integra herramientas que complementen el propósito del agente y mejoren sus capacidades. Sé selectivo sobre qué herramientas proporcionas para evitar sobrecargar al agente. Para tareas con poco solapamiento, usa otro bloque de Agente para obtener los mejores resultados. diff --git a/apps/docs/content/docs/es/triggers/api.mdx b/apps/docs/content/docs/es/triggers/api.mdx index a41d11bc0e..c05facd2a4 100644 --- a/apps/docs/content/docs/es/triggers/api.mdx +++ b/apps/docs/content/docs/es/triggers/api.mdx @@ -24,14 +24,6 @@ El disparador de API expone tu flujo de trabajo como un punto de conexión HTTP Añade un campo de **Formato de entrada** para cada parámetro. Las claves de salida en tiempo de ejecución reflejan el esquema y también están disponibles bajo ``. -```yaml -- type: string - name: userId - value: demo-user # optional manual test value -- type: number - name: maxTokens -``` - Las ejecuciones manuales en el editor utilizan la columna `value` para que puedas realizar pruebas sin enviar una solicitud. Durante la ejecución, el resolutor completa tanto `` como ``. ## Ejemplo de solicitud @@ -44,17 +36,17 @@ curl -X POST \ -d '{"userId":"demo-user","maxTokens":1024}' ``` -Las respuestas exitosas devuelven el resultado de ejecución serializado del Ejecutor. Los errores muestran fallos de validación, autenticación o del flujo de trabajo. +Las respuestas exitosas devuelven el resultado de ejecución serializado del Ejecutor. Los errores muestran fallos de validación, autenticación o flujo de trabajo. ## Referencia de salida | Referencia | Descripción | |-----------|-------------| -| `` | Campo definido en el formato de entrada | +| `` | Campo definido en el Formato de Entrada | | `` | Cuerpo completo estructurado de la solicitud | -Si no se define un formato de entrada, el ejecutor expone el JSON sin procesar solo en ``. +Si no se define un Formato de Entrada, el ejecutor expone el JSON sin procesar solo en ``. -Un flujo de trabajo puede contener solo un disparador de API. Publica una nueva implementación después de realizar cambios para que el punto de conexión se mantenga actualizado. +Un flujo de trabajo puede contener solo un Disparador de API. Publica una nueva implementación después de realizar cambios para que el punto de conexión se mantenga actualizado. diff --git a/apps/docs/content/docs/es/yaml/blocks/loop.mdx b/apps/docs/content/docs/es/yaml/blocks/loop.mdx index 1326019015..6d2922ed5e 100644 --- a/apps/docs/content/docs/es/yaml/blocks/loop.mdx +++ b/apps/docs/content/docs/es/yaml/blocks/loop.mdx @@ -10,7 +10,6 @@ type: object required: - type - name - - inputs - connections properties: type: @@ -22,21 +21,23 @@ properties: description: Display name for this loop block inputs: type: object - required: - - loopType + description: Optional. If omitted, defaults will be applied. properties: loopType: type: string enum: [for, forEach] description: Type of loop to execute + default: for iterations: type: number description: Number of iterations (for 'for' loops) + default: 5 minimum: 1 maximum: 1000 collection: type: string description: Collection to iterate over (for 'forEach' loops) + default: "" maxConcurrency: type: number description: Maximum concurrent executions @@ -45,13 +46,10 @@ properties: maximum: 10 connections: type: object - required: - - loop properties: + # Nested format (recommended) loop: type: object - required: - - start properties: start: type: string @@ -59,26 +57,37 @@ properties: end: type: string description: Target block ID for loop completion (optional) + # Direct handle format (alternative) + loop-start-source: + type: string | string[] + description: Target block ID to execute inside the loop (direct format) + loop-end-source: + type: string | string[] + description: Target block ID for loop completion (direct format, optional) error: type: string description: Target block ID for error handling + note: Use either the nested 'loop' format OR the direct 'loop-start-source' format, not both ``` ## Configuración de conexión -Los bloques Loop utilizan un formato de conexión especial con una sección `loop`: +Los bloques de bucle admiten dos formatos de conexión: + +### Formato de manejador directo (alternativo) ```yaml connections: - loop: - start: # Target block ID to execute inside the loop - end: # Target block ID after loop completion (optional) + loop-start-source: # Target block ID to execute inside the loop + loop-end-source: # Target block ID after loop completion (optional) error: # Target block ID for error handling (optional) ``` +Ambos formatos funcionan de manera idéntica. Usa el que prefieras. + ## Configuración de bloques secundarios -Los bloques dentro de un bucle deben tener su `parentId` configurado con el ID del bloque loop: +Los bloques dentro de un bucle deben tener su `parentId` configurado con el ID del bloque de bucle. La propiedad `extent` se establece automáticamente como `'parent'` y no necesita ser especificada: ```yaml loop-1: @@ -261,6 +270,59 @@ process-task: success: task-completed ``` +### Ejemplo de formato de manejador directo + +El mismo bucle puede escribirse usando el formato de manejador directo: + +```yaml +my-loop: + type: loop + name: "Process Items" + inputs: + loopType: forEach + collection: + connections: + loop-start-source: process-item # Direct handle format + loop-end-source: final-results # Direct handle format + error: handle-error + +process-item: + type: agent + name: "Process Item" + parentId: my-loop + inputs: + systemPrompt: "Process this item" + userPrompt: + model: gpt-4o + apiKey: '{{OPENAI_API_KEY}}' +``` + +### Ejemplo de bucle mínimo (usando valores predeterminados) + +Puedes omitir completamente la sección `inputs`, y se aplicarán los valores predeterminados: + +```yaml +simple-loop: + type: loop + name: "Simple Loop" + # No inputs section - defaults to loopType: 'for', iterations: 5 + connections: + loop-start-source: process-step + loop-end-source: complete + +process-step: + type: agent + name: "Process Step" + parentId: simple-loop + inputs: + systemPrompt: "Execute step" + userPrompt: "Step " + model: gpt-4o + apiKey: '{{OPENAI_API_KEY}}' +``` + +Este bucle ejecutará 5 iteraciones por defecto. + ## Variables de bucle Dentro de los bloques secundarios del bucle, estas variables especiales están disponibles: @@ -286,10 +348,10 @@ final-processor: ## Mejores prácticas -- Establece límites razonables de iteración para evitar tiempos de ejecución prolongados +- Establece límites de iteración razonables para evitar tiempos de ejecución largos - Usa forEach para procesar colecciones, bucles for para iteraciones fijas - Considera usar maxConcurrency para operaciones limitadas por E/S -- Incluye manejo de errores para una ejecución robusta de bucles +- Incluye manejo de errores para una ejecución robusta del bucle - Usa nombres descriptivos para los bloques secundarios del bucle - Prueba primero con colecciones pequeñas - Monitorea el tiempo de ejecución para colecciones grandes \ No newline at end of file diff --git a/apps/docs/content/docs/fr/blocks/agent.mdx b/apps/docs/content/docs/fr/blocks/agent.mdx index fd9866b30e..f1162ad7e8 100644 --- a/apps/docs/content/docs/fr/blocks/agent.mdx +++ b/apps/docs/content/docs/fr/blocks/agent.mdx @@ -175,33 +175,7 @@ Utilisez un bloc `Memory` avec un `id` cohérent (par exemple, `chat`) pour cons - Lisez l'historique de conversation pour le contexte - Ajoutez la réponse de l'Agent après son exécution -```yaml -# 1) Add latest user message -- Memory (operation: add) - id: chat - role: user - content: {{input}} - -# 2) Load conversation history -- Memory (operation: get) - id: chat - -# 3) Run the agent with prior messages available -- Agent - System Prompt: ... - User Prompt: | - Use the conversation so far: - {{memory_get.memories}} - Current user message: {{input}} - -# 4) Store the agent reply -- Memory (operation: add) - id: chat - role: assistant - content: {{agent.content}} -``` - -Consultez la référence du bloc `Memory` pour plus de détails : [/tools/memory](/tools/memory). +Voir la référence du bloc [`Memory`](/tools/memory) pour plus de détails. ## Entrées et sorties @@ -209,51 +183,51 @@ Consultez la référence du bloc `Memory` pour plus de détails : [/tools/memory
        • - Prompt système : Instructions définissant le comportement et le rôle de l'agent + Prompt système : instructions définissant le comportement et le rôle de l'agent
        • - Prompt utilisateur : Texte d'entrée ou données à traiter + Prompt utilisateur : texte ou données d'entrée à traiter
        • - Modèle : Sélection du modèle d'IA (OpenAI, Anthropic, Google, etc.) + Modèle : sélection du modèle d'IA (OpenAI, Anthropic, Google, etc.)
        • - Température : Contrôle de l'aléatoire des réponses (0-2) + Température : contrôle de l'aléatoire des réponses (0-2)
        • - Outils : Tableau d'outils disponibles pour l'appel de fonctions + Outils : tableau des outils disponibles pour l'appel de fonctions
        • - Format de réponse : Schéma JSON pour une sortie structurée + Format de réponse : schéma JSON pour une sortie structurée
        • - agent.content : Texte de réponse de l'agent ou données structurées + agent.content : texte de réponse de l'agent ou données structurées
        • - agent.tokens : Objet de statistiques d'utilisation des tokens + agent.tokens : objet de statistiques d'utilisation des tokens
        • - agent.tool_calls : Tableau des détails d'exécution des outils + agent.tool_calls : tableau des détails d'exécution des outils
        • - agent.cost : Coût estimé de l'appel API (si disponible) + agent.cost : coût estimé de l'appel API (si disponible)
        • - Contenu : Sortie de réponse principale de l'agent + Contenu : sortie de réponse principale de l'agent
        • - Métadonnées : Statistiques d'utilisation et détails d'exécution + Métadonnées : statistiques d'utilisation et détails d'exécution
        • - Accès : Disponible dans les blocs après l'agent + Accès : disponible dans les blocs après l'agent
        @@ -268,7 +242,7 @@ Consultez la référence du bloc `Memory` pour plus de détails : [/tools/memory
        1. L'utilisateur soumet un ticket de support via le bloc API
        2. L'agent vérifie les commandes/abonnements dans Postgres et recherche des conseils dans la base de connaissances
        3. -
        4. Si une escalade est nécessaire, l'agent crée un ticket Linear avec le contexte pertinent
        5. +
        6. Si une escalade est nécessaire, l'agent crée un problème Linear avec le contexte pertinent
        7. L'agent rédige une réponse par e-mail claire
        8. Gmail envoie la réponse au client
        9. La conversation est enregistrée dans Memory pour conserver l'historique des messages futurs
        10. @@ -278,7 +252,7 @@ Consultez la référence du bloc `Memory` pour plus de détails : [/tools/memory ### Analyse de contenu multi-modèles
          -

          Scénario : analyser du contenu avec différents modèles d'IA

          +

          Scénario : analyser le contenu avec différents modèles d'IA

          1. Le bloc de fonction traite le document téléchargé
          2. L'agent avec GPT-4o effectue une analyse technique
          3. diff --git a/apps/docs/content/docs/fr/triggers/api.mdx b/apps/docs/content/docs/fr/triggers/api.mdx index 4684a58988..bac2d9ab95 100644 --- a/apps/docs/content/docs/fr/triggers/api.mdx +++ b/apps/docs/content/docs/fr/triggers/api.mdx @@ -24,14 +24,6 @@ Le déclencheur d'API expose votre flux de travail en tant que point de terminai Ajoutez un champ **Format d'entrée** pour chaque paramètre. Les clés de sortie d'exécution reflètent le schéma et sont également disponibles sous ``. -```yaml -- type: string - name: userId - value: demo-user # optional manual test value -- type: number - name: maxTokens -``` - Les exécutions manuelles dans l'éditeur utilisent la colonne `value` pour que vous puissiez tester sans envoyer de requête. Pendant l'exécution, le résolveur remplit à la fois `` et ``. ## Exemple de requête @@ -44,17 +36,17 @@ curl -X POST \ -d '{"userId":"demo-user","maxTokens":1024}' ``` -Les réponses réussies renvoient le résultat d'exécution sérialisé de l'Exécuteur. Les erreurs révèlent des problèmes de validation, d'authentification ou d'échec du flux de travail. +Les réponses réussies renvoient le résultat d'exécution sérialisé de l'exécuteur. Les erreurs révèlent des problèmes de validation, d'authentification ou d'échec du workflow. -## Référence de sortie +## Référence des sorties | Référence | Description | |-----------|-------------| -| `` | Champ défini dans le Format d'entrée | -| `` | Corps de la requête structuré complet | +| `` | Champ défini dans le format d'entrée | +| `` | Corps de requête structuré complet | -Si aucun Format d'entrée n'est défini, l'exécuteur expose le JSON brut uniquement à ``. +Si aucun format d'entrée n'est défini, l'exécuteur expose uniquement le JSON brut à ``. -Un flux de travail ne peut contenir qu'un seul déclencheur d'API. Publiez un nouveau déploiement après les modifications pour que le point de terminaison reste à jour. +Un workflow ne peut contenir qu'un seul déclencheur API. Publiez un nouveau déploiement après les modifications pour que le point de terminaison reste à jour. diff --git a/apps/docs/content/docs/fr/yaml/blocks/loop.mdx b/apps/docs/content/docs/fr/yaml/blocks/loop.mdx index 5b65d90c39..23faf65f12 100644 --- a/apps/docs/content/docs/fr/yaml/blocks/loop.mdx +++ b/apps/docs/content/docs/fr/yaml/blocks/loop.mdx @@ -10,7 +10,6 @@ type: object required: - type - name - - inputs - connections properties: type: @@ -22,21 +21,23 @@ properties: description: Display name for this loop block inputs: type: object - required: - - loopType + description: Optional. If omitted, defaults will be applied. properties: loopType: type: string enum: [for, forEach] description: Type of loop to execute + default: for iterations: type: number description: Number of iterations (for 'for' loops) + default: 5 minimum: 1 maximum: 1000 collection: type: string description: Collection to iterate over (for 'forEach' loops) + default: "" maxConcurrency: type: number description: Maximum concurrent executions @@ -45,13 +46,10 @@ properties: maximum: 10 connections: type: object - required: - - loop properties: + # Nested format (recommended) loop: type: object - required: - - start properties: start: type: string @@ -59,26 +57,37 @@ properties: end: type: string description: Target block ID for loop completion (optional) + # Direct handle format (alternative) + loop-start-source: + type: string | string[] + description: Target block ID to execute inside the loop (direct format) + loop-end-source: + type: string | string[] + description: Target block ID for loop completion (direct format, optional) error: type: string description: Target block ID for error handling + note: Use either the nested 'loop' format OR the direct 'loop-start-source' format, not both ``` ## Configuration de connexion -Les blocs Loop utilisent un format de connexion spécial avec une section `loop` : +Les blocs de boucle prennent en charge deux formats de connexion : + +### Format de gestion directe (alternative) ```yaml connections: - loop: - start: # Target block ID to execute inside the loop - end: # Target block ID after loop completion (optional) + loop-start-source: # Target block ID to execute inside the loop + loop-end-source: # Target block ID after loop completion (optional) error: # Target block ID for error handling (optional) ``` +Les deux formats fonctionnent de manière identique. Utilisez celui que vous préférez. + ## Configuration des blocs enfants -Les blocs à l'intérieur d'une boucle doivent avoir leur `parentId` défini sur l'ID du bloc de boucle : +Les blocs à l'intérieur d'une boucle doivent avoir leur `parentId` défini sur l'ID du bloc de boucle. La propriété `extent` est automatiquement définie sur `'parent'` et n'a pas besoin d'être spécifiée : ```yaml loop-1: @@ -261,9 +270,62 @@ process-task: success: task-completed ``` +### Exemple de format de gestion directe + +La même boucle peut être écrite en utilisant le format de gestion directe : + +```yaml +my-loop: + type: loop + name: "Process Items" + inputs: + loopType: forEach + collection: + connections: + loop-start-source: process-item # Direct handle format + loop-end-source: final-results # Direct handle format + error: handle-error + +process-item: + type: agent + name: "Process Item" + parentId: my-loop + inputs: + systemPrompt: "Process this item" + userPrompt: + model: gpt-4o + apiKey: '{{OPENAI_API_KEY}}' +``` + +### Exemple de boucle minimale (utilisant les valeurs par défaut) + +Vous pouvez omettre entièrement la section `inputs`, et les valeurs par défaut seront appliquées : + +```yaml +simple-loop: + type: loop + name: "Simple Loop" + # No inputs section - defaults to loopType: 'for', iterations: 5 + connections: + loop-start-source: process-step + loop-end-source: complete + +process-step: + type: agent + name: "Process Step" + parentId: simple-loop + inputs: + systemPrompt: "Execute step" + userPrompt: "Step " + model: gpt-4o + apiKey: '{{OPENAI_API_KEY}}' +``` + +Cette boucle exécutera 5 itérations par défaut. + ## Variables de boucle -À l'intérieur des blocs enfants de la boucle, ces variables spéciales sont disponibles : +À l'intérieur des blocs enfants de boucle, ces variables spéciales sont disponibles : ```yaml # Available in all child blocks of the loop diff --git a/apps/docs/content/docs/ja/blocks/agent.mdx b/apps/docs/content/docs/ja/blocks/agent.mdx index ca63f1e5e4..28d3fe6953 100644 --- a/apps/docs/content/docs/ja/blocks/agent.mdx +++ b/apps/docs/content/docs/ja/blocks/agent.mdx @@ -172,33 +172,7 @@ When responding to questions about investments, include risk disclaimers. - コンテキストのために会話履歴を読み取る - エージェントの実行後に返信を追加 -```yaml -# 1) Add latest user message -- Memory (operation: add) - id: chat - role: user - content: {{input}} - -# 2) Load conversation history -- Memory (operation: get) - id: chat - -# 3) Run the agent with prior messages available -- Agent - System Prompt: ... - User Prompt: | - Use the conversation so far: - {{memory_get.memories}} - Current user message: {{input}} - -# 4) Store the agent reply -- Memory (operation: add) - id: chat - role: assistant - content: {{agent.content}} -``` - -詳細については`Memory`ブロックリファレンスを参照してください: [/tools/memory](/tools/memory)。 +詳細については[`Memory`](/tools/memory)ブロックリファレンスを参照してください。 ## 入力と出力 @@ -209,7 +183,7 @@ When responding to questions about investments, include risk disclaimers. システムプロンプト: エージェントの動作と役割を定義する指示
          4. - ユーザープロンプト: 処理する入力テキストまたはデータ + ユーザープロンプト: 処理するテキストまたはデータ入力
          5. モデル: AIモデルの選択(OpenAI、Anthropic、Google など) @@ -221,14 +195,14 @@ When responding to questions about investments, include risk disclaimers. ツール: 関数呼び出し用の利用可能なツールの配列
          6. - レスポンス形式: 構造化出力用のJSONスキーマ + レスポンス形式: 構造化された出力のためのJSONスキーマ
      • - agent.content: エージェントのレスポンステキストまたは構造化データ + agent.content: エージェントの応答テキストまたは構造化データ
      • agent.tokens: トークン使用統計オブジェクト @@ -237,14 +211,14 @@ When responding to questions about investments, include risk disclaimers. agent.tool_calls: ツール実行詳細の配列
      • - agent.cost: 推定APIコールコスト(利用可能な場合) + agent.cost: 推定APIコール費用(利用可能な場合)
      • - コンテンツ: エージェントからの主要なレスポンス出力 + コンテンツ: エージェントからの主要な応答出力
      • メタデータ: 使用統計と実行詳細 @@ -261,43 +235,43 @@ When responding to questions about investments, include risk disclaimers. ### カスタマーサポートの自動化
        -

        シナリオ:データベースアクセスによる顧客問い合わせ対応

        +

        シナリオ: データベースアクセスによる顧客問い合わせ対応

          -
        1. ユーザーがAPIブロックを通じてサポートチケットを送信
        2. +
        3. ユーザーがAPIブロック経由でサポートチケットを送信
        4. エージェントがPostgresで注文/サブスクリプションを確認し、ナレッジベースでガイダンスを検索
        5. エスカレーションが必要な場合、エージェントは関連コンテキストを含むLinearの課題を作成
        6. -
        7. エージェントが明確なメール返信を作成
        8. +
        9. エージェントが明確な返信メールを作成
        10. Gmailが顧客に返信を送信
        11. -
        12. 将来のメッセージのために会話履歴を維持するため、会話がメモリに保存される
        13. +
        14. 将来のメッセージのために履歴を維持するため、会話がメモリに保存される
        ### マルチモデルコンテンツ分析
        -

        シナリオ:異なるAIモデルでコンテンツを分析

        +

        シナリオ: 異なるAIモデルでコンテンツを分析

          -
        1. ファンクションブロックがアップロードされた文書を処理
        2. +
        3. 関数ブロックがアップロードされた文書を処理
        4. GPT-4oを搭載したエージェントが技術的分析を実行
        5. -
        6. Claudeを搭載したエージェントが感情やトーンを分析
        7. -
        8. ファンクションブロックが最終レポート用に結果を統合
        9. +
        10. Claudeを搭載したエージェントが感情とトーンを分析
        11. +
        12. 関数ブロックが最終レポート用に結果を統合
        -### ツール活用型リサーチアシスタント +### ツール搭載型リサーチアシスタント
        -

        シナリオ:ウェブ検索と文書アクセス機能を持つリサーチアシスタント

        +

        シナリオ:ウェブ検索とドキュメントアクセス機能を持つリサーチアシスタント

          -
        1. 入力を通じてユーザークエリを受信
        2. +
        3. 入力からユーザークエリを受信
        4. エージェントがGoogle検索ツールを使用してウェブを検索
        5. エージェントが社内文書用のNotionデータベースにアクセス
        6. -
        7. エージェントが包括的な調査レポートをまとめる
        8. +
        9. エージェントが包括的な調査レポートを作成
        ## ベストプラクティス -- **システムプロンプトを具体的に**: エージェントの役割、トーン、制限を明確に定義してください。指示が具体的であればあるほど、エージェントは目的を果たすことができます。 -- **適切な温度設定を選択**: 精度が重要な場合は低い温度設定(0〜0.3)を使用し、よりクリエイティブまたは多様な応答には温度を上げる(0.7〜2.0) -- **ツールを効果的に活用**: エージェントの目的を補完し、その能力を強化するツールを統合してください。エージェントに負担をかけないよう、提供するツールを選択的にしてください。重複の少いタスクには、最良の結果を得るために別のエージェントブロックを使用してください。 +- **システムプロンプトで具体的に指示する**:エージェントの役割、トーン、制限を明確に定義してください。指示が具体的であればあるほど、エージェントは目的を果たすことができます。 +- **適切な温度設定を選択する**:精度が重要な場合は低い温度設定(0〜0.3)を使用し、よりクリエイティブまたは多様な応答を得るには温度を上げる(0.7〜2.0) +- **ツールを効果的に活用する**:エージェントの目的を補完し、その能力を強化するツールを統合してください。エージェントに負担をかけないよう、提供するツールを選択的にしてください。重複の少いタスクには、最良の結果を得るために別のエージェントブロックを使用してください。 diff --git a/apps/docs/content/docs/ja/triggers/api.mdx b/apps/docs/content/docs/ja/triggers/api.mdx index 7355039e67..7f5df702cd 100644 --- a/apps/docs/content/docs/ja/triggers/api.mdx +++ b/apps/docs/content/docs/ja/triggers/api.mdx @@ -24,15 +24,7 @@ APIトリガーは、ワークフローを安全なHTTPエンドポイントと 各パラメータに**入力フォーマット**フィールドを追加します。実行時の出力キーはスキーマを反映し、``でも利用できます。 -```yaml -- type: string - name: userId - value: demo-user # optional manual test value -- type: number - name: maxTokens -``` - -エディタでの手動実行では、リクエストを送信せずにテストできるように`value`列を使用します。実行中、リゾルバは``と``の両方に値を設定します。 +エディタでの手動実行は `value` 列を使用するため、リクエストを送信せずにテストできます。実行中、リゾルバーは `` と `` の両方に値を設定します。 ## リクエスト例 @@ -44,7 +36,7 @@ curl -X POST \ -d '{"userId":"demo-user","maxTokens":1024}' ``` -成功したレスポンスはエグゼキュータからのシリアル化された実行結果を返します。エラーは検証、認証、またはワークフローの失敗を表示します。 +成功したレスポンスはエグゼキュータからシリアル化された実行結果を返します。エラーは検証、認証、またはワークフローの失敗を表示します。 ## 出力リファレンス @@ -53,8 +45,8 @@ curl -X POST \ | `` | 入力フォーマットで定義されたフィールド | | `` | 構造化されたリクエスト本文全体 | -入力フォーマットが定義されていない場合、エグゼキュータは生のJSONを``でのみ公開します。 +入力フォーマットが定義されていない場合、エグゼキュータは生のJSONを `` のみで公開します。 -ワークフローには1つのAPIトリガーしか含めることができません。変更後は新しいデプロイメントを公開して、エンドポイントを最新の状態に保ってください。 +ワークフローには1つのAPIトリガーのみ含めることができます。変更後は新しいデプロイメントを公開して、エンドポイントを最新の状態に保ってください。 diff --git a/apps/docs/content/docs/ja/yaml/blocks/loop.mdx b/apps/docs/content/docs/ja/yaml/blocks/loop.mdx index 11c2a9916c..26a2af28a2 100644 --- a/apps/docs/content/docs/ja/yaml/blocks/loop.mdx +++ b/apps/docs/content/docs/ja/yaml/blocks/loop.mdx @@ -10,7 +10,6 @@ type: object required: - type - name - - inputs - connections properties: type: @@ -22,21 +21,23 @@ properties: description: Display name for this loop block inputs: type: object - required: - - loopType + description: Optional. If omitted, defaults will be applied. properties: loopType: type: string enum: [for, forEach] description: Type of loop to execute + default: for iterations: type: number description: Number of iterations (for 'for' loops) + default: 5 minimum: 1 maximum: 1000 collection: type: string description: Collection to iterate over (for 'forEach' loops) + default: "" maxConcurrency: type: number description: Maximum concurrent executions @@ -45,13 +46,10 @@ properties: maximum: 10 connections: type: object - required: - - loop properties: + # Nested format (recommended) loop: type: object - required: - - start properties: start: type: string @@ -59,26 +57,37 @@ properties: end: type: string description: Target block ID for loop completion (optional) + # Direct handle format (alternative) + loop-start-source: + type: string | string[] + description: Target block ID to execute inside the loop (direct format) + loop-end-source: + type: string | string[] + description: Target block ID for loop completion (direct format, optional) error: type: string description: Target block ID for error handling + note: Use either the nested 'loop' format OR the direct 'loop-start-source' format, not both ``` ## 接続設定 -ループブロックは `loop` セクションを持つ特別な接続形式を使用します: +ループブロックは2つの接続形式をサポートしています: + +### 直接ハンドル形式(代替) ```yaml connections: - loop: - start: # Target block ID to execute inside the loop - end: # Target block ID after loop completion (optional) + loop-start-source: # Target block ID to execute inside the loop + loop-end-source: # Target block ID after loop completion (optional) error: # Target block ID for error handling (optional) ``` -## 子ブロック設定 +両方の形式は同じように機能します。お好みの方を使用してください。 + +## 子ブロックの設定 -ループ内のブロックは、その `parentId` をループブロックIDに設定する必要があります: +ループ内のブロックは、その `parentId` をループブロックIDに設定する必要があります。`extent` プロパティは自動的に `'parent'` に設定されるため、指定する必要はありません: ```yaml loop-1: @@ -261,6 +270,59 @@ process-task: success: task-completed ``` +### 直接ハンドル形式の例 + +同じループは直接ハンドル形式を使用して記述することもできます: + +```yaml +my-loop: + type: loop + name: "Process Items" + inputs: + loopType: forEach + collection: + connections: + loop-start-source: process-item # Direct handle format + loop-end-source: final-results # Direct handle format + error: handle-error + +process-item: + type: agent + name: "Process Item" + parentId: my-loop + inputs: + systemPrompt: "Process this item" + userPrompt: + model: gpt-4o + apiKey: '{{OPENAI_API_KEY}}' +``` + +### 最小限のループ例(デフォルトを使用) + +`inputs` セクションを完全に省略することができ、デフォルト値が適用されます: + +```yaml +simple-loop: + type: loop + name: "Simple Loop" + # No inputs section - defaults to loopType: 'for', iterations: 5 + connections: + loop-start-source: process-step + loop-end-source: complete + +process-step: + type: agent + name: "Process Step" + parentId: simple-loop + inputs: + systemPrompt: "Execute step" + userPrompt: "Step " + model: gpt-4o + apiKey: '{{OPENAI_API_KEY}}' +``` + +このループはデフォルトで5回の反復を実行します。 + ## ループ変数 ループ子ブロック内では、以下の特殊変数が利用可能です: @@ -274,7 +336,7 @@ process-task: ## 出力参照 -ループが完了した後、集約された結果を参照できます: +ループが完了した後、その集計結果を参照できます: ```yaml # In blocks after the loop @@ -286,10 +348,10 @@ final-processor: ## ベストプラクティス -- 長い実行時間を避けるために適切な繰り返し回数の制限を設定する +- 長い実行時間を避けるため、適切な繰り返し制限を設定する - コレクション処理にはforEachを、固定回数の繰り返しにはforループを使用する -- I/O処理の多い操作にはmaxConcurrencyの使用を検討する +- I/O処理が多い操作にはmaxConcurrencyの使用を検討する - 堅牢なループ実行のためにエラー処理を含める -- ループの子ブロックには説明的な名前を使用する +- ループ子ブロックには説明的な名前を使用する - 最初に小さなコレクションでテストする - 大きなコレクションの実行時間を監視する \ No newline at end of file diff --git a/apps/docs/content/docs/zh/blocks/agent.mdx b/apps/docs/content/docs/zh/blocks/agent.mdx index bd49b473d4..ddd6dcdf81 100644 --- a/apps/docs/content/docs/zh/blocks/agent.mdx +++ b/apps/docs/content/docs/zh/blocks/agent.mdx @@ -172,44 +172,18 @@ Agent 模块通过统一的推理接口支持多个 LLM 提供商。可用模型 - 读取对话历史以获取上下文 - 在代理运行后附加其回复 -```yaml -# 1) Add latest user message -- Memory (operation: add) - id: chat - role: user - content: {{input}} - -# 2) Load conversation history -- Memory (operation: get) - id: chat - -# 3) Run the agent with prior messages available -- Agent - System Prompt: ... - User Prompt: | - Use the conversation so far: - {{memory_get.memories}} - Current user message: {{input}} - -# 4) Store the agent reply -- Memory (operation: add) - id: chat - role: assistant - content: {{agent.content}} -``` - -有关详细信息,请参阅 `Memory` 块参考:[工具/内存](/tools/memory)。 +有关详细信息,请参阅 [`Memory`](/tools/memory) 块引用。 ## 输入和输出 - +
        • 系统提示:定义代理行为和角色的指令
        • - 用户提示:要处理的输入文本或数据 + 用户提示:需要处理的输入文本或数据
        • 模型:AI 模型选择(OpenAI、Anthropic、Google 等) @@ -218,7 +192,7 @@ Agent 模块通过统一的推理接口支持多个 LLM 提供商。可用模型 温度:响应随机性控制(0-2)
        • - 工具:可用于函数调用的工具数组 + 工具:可用工具数组,用于功能调用
        • 响应格式:用于结构化输出的 JSON Schema @@ -234,10 +208,10 @@ Agent 模块通过统一的推理接口支持多个 LLM 提供商。可用模型 agent.tokens:令牌使用统计对象
        • - agent.tool_calls:工具执行详细信息数组 + agent.tool_calls:工具执行详情数组
        • - agent.cost:API 调用的估计成本(如果可用) + agent.cost:估算的 API 调用成本(如果可用)
        @@ -247,7 +221,7 @@ Agent 模块通过统一的推理接口支持多个 LLM 提供商。可用模型 内容:代理的主要响应输出
      • - 元数据:使用统计信息和执行详细信息 + 元数据:使用统计和执行详情
      • 访问:在代理之后的块中可用 @@ -261,9 +235,9 @@ Agent 模块通过统一的推理接口支持多个 LLM 提供商。可用模型 ### 客户支持自动化
        -

        场景:通过数据库访问处理客户咨询

        +

        场景:通过数据库访问处理客户查询

          -
        1. 用户通过 API 模块提交支持工单
        2. +
        3. 用户通过 API 块提交支持工单
        4. 代理在 Postgres 中检查订单/订阅,并在知识库中搜索指导
        5. 如果需要升级,代理会创建一个包含相关上下文的 Linear 问题
        6. 代理起草一封清晰的电子邮件回复
        7. @@ -277,10 +251,10 @@ Agent 模块通过统一的推理接口支持多个 LLM 提供商。可用模型

          场景:使用不同的 AI 模型分析内容

            -
          1. 功能模块处理上传的文档
          2. +
          3. 功能块处理上传的文档
          4. 使用 GPT-4o 的代理执行技术分析
          5. 使用 Claude 的代理分析情感和语气
          6. -
          7. 功能模块结合结果生成最终报告
          8. +
          9. 功能块结合结果生成最终报告
          @@ -298,6 +272,6 @@ Agent 模块通过统一的推理接口支持多个 LLM 提供商。可用模型 ## 最佳实践 -- **在系统提示中具体说明**:清晰定义代理的角色、语气和限制。您的指令越具体,代理就越能更好地实现其预期目的。 -- **选择合适的温度设置**:当准确性很重要时,使用较低的温度设置(0-0.3);当需要更具创意或多样化的响应时,增加温度(0.7-2.0)。 -- **有效利用工具**:集成与代理目的互补并增强其能力的工具。选择性地提供工具,以避免让代理不堪重负。对于重叠较少的任务,使用另一个代理模块以获得最佳结果。 +- **在系统提示中具体说明**:清晰定义代理的角色、语气和限制。您的指令越具体,代理越能更好地完成其预期目的。 +- **选择合适的温度设置**:当准确性很重要时,使用较低的温度设置(0-0.3);当需要更具创意或多样化的响应时,可将温度提高(0.7-2.0)。 +- **有效利用工具**:集成与代理目的互补并增强其能力的工具。选择性地提供工具,以避免让代理不堪重负。对于重叠较少的任务,使用另一个代理块以获得最佳效果。 diff --git a/apps/docs/content/docs/zh/triggers/api.mdx b/apps/docs/content/docs/zh/triggers/api.mdx index 060b3db90e..5f4ecb3b71 100644 --- a/apps/docs/content/docs/zh/triggers/api.mdx +++ b/apps/docs/content/docs/zh/triggers/api.mdx @@ -24,15 +24,7 @@ API 触发器将您的工作流公开为一个安全的 HTTP 端点。将 JSON 为每个参数添加一个 **输入格式** 字段。运行时输出键会镜像该模式,并且也可以在 `` 下使用。 -```yaml -- type: string - name: userId - value: demo-user # optional manual test value -- type: number - name: maxTokens -``` - -编辑器中的手动运行使用 `value` 列,因此您可以在不发送请求的情况下进行测试。在执行期间,解析器会填充 `` 和 ``。 +在编辑器中手动运行使用 `value` 列,因此您可以在不发送请求的情况下进行测试。在执行过程中,解析器会填充 `` 和 ``。 ## 请求示例 @@ -44,17 +36,17 @@ curl -X POST \ -d '{"userId":"demo-user","maxTokens":1024}' ``` -成功的响应会返回来自执行器的序列化执行结果。错误会显示验证、身份验证或工作流失败的原因。 +成功的响应会返回来自执行器的序列化执行结果。错误会显示验证、认证或工作流失败的信息。 ## 输出参考 | 参考 | 描述 | |-----------|-------------| -| `` | 在输入格式中定义的字段 | +| `` | 输入格式中定义的字段 | | `` | 整个结构化请求体 | -如果未定义输入格式,执行器仅在 `` 处公开原始 JSON。 +如果未定义输入格式,执行器仅在 `` 处暴露原始 JSON。 -一个工作流只能包含一个 API 触发器。更改后请发布新的部署,以确保端点保持最新。 +一个工作流只能包含一个 API 触发器。更改后发布新的部署,以确保端点保持最新。 diff --git a/apps/docs/content/docs/zh/yaml/blocks/loop.mdx b/apps/docs/content/docs/zh/yaml/blocks/loop.mdx index 6692f8c6ae..589da239bc 100644 --- a/apps/docs/content/docs/zh/yaml/blocks/loop.mdx +++ b/apps/docs/content/docs/zh/yaml/blocks/loop.mdx @@ -10,7 +10,6 @@ type: object required: - type - name - - inputs - connections properties: type: @@ -22,21 +21,23 @@ properties: description: Display name for this loop block inputs: type: object - required: - - loopType + description: Optional. If omitted, defaults will be applied. properties: loopType: type: string enum: [for, forEach] description: Type of loop to execute + default: for iterations: type: number description: Number of iterations (for 'for' loops) + default: 5 minimum: 1 maximum: 1000 collection: type: string description: Collection to iterate over (for 'forEach' loops) + default: "" maxConcurrency: type: number description: Maximum concurrent executions @@ -45,13 +46,10 @@ properties: maximum: 10 connections: type: object - required: - - loop properties: + # Nested format (recommended) loop: type: object - required: - - start properties: start: type: string @@ -59,26 +57,37 @@ properties: end: type: string description: Target block ID for loop completion (optional) + # Direct handle format (alternative) + loop-start-source: + type: string | string[] + description: Target block ID to execute inside the loop (direct format) + loop-end-source: + type: string | string[] + description: Target block ID for loop completion (direct format, optional) error: type: string description: Target block ID for error handling + note: Use either the nested 'loop' format OR the direct 'loop-start-source' format, not both ``` ## 连接配置 -Loop 块使用一种特殊的连接格式,其中包含一个 `loop` 部分: +循环块支持两种连接格式: + +### 直接句柄格式(替代方案) ```yaml connections: - loop: - start: # Target block ID to execute inside the loop - end: # Target block ID after loop completion (optional) + loop-start-source: # Target block ID to execute inside the loop + loop-end-source: # Target block ID after loop completion (optional) error: # Target block ID for error handling (optional) ``` +两种格式的功能完全相同,可根据您的喜好选择使用。 + ## 子块配置 -循环中的块必须将其 `parentId` 设置为循环块的 ID: +循环中的块必须将其 `parentId` 设置为循环块的 ID。`extent` 属性会自动设置为 `'parent'`,无需手动指定: ```yaml loop-1: @@ -261,9 +270,62 @@ process-task: success: task-completed ``` +### 直接句柄格式示例 + +同一个循环可以使用直接句柄格式编写: + +```yaml +my-loop: + type: loop + name: "Process Items" + inputs: + loopType: forEach + collection: + connections: + loop-start-source: process-item # Direct handle format + loop-end-source: final-results # Direct handle format + error: handle-error + +process-item: + type: agent + name: "Process Item" + parentId: my-loop + inputs: + systemPrompt: "Process this item" + userPrompt: + model: gpt-4o + apiKey: '{{OPENAI_API_KEY}}' +``` + +### 最小循环示例(使用默认值) + +您可以完全省略 `inputs` 部分,系统将应用默认值: + +```yaml +simple-loop: + type: loop + name: "Simple Loop" + # No inputs section - defaults to loopType: 'for', iterations: 5 + connections: + loop-start-source: process-step + loop-end-source: complete + +process-step: + type: agent + name: "Process Step" + parentId: simple-loop + inputs: + systemPrompt: "Execute step" + userPrompt: "Step " + model: gpt-4o + apiKey: '{{OPENAI_API_KEY}}' +``` + +此循环默认将执行 5 次迭代。 + ## 循环变量 -在循环的子块中,可以使用以下特殊变量: +在循环子块中,可以使用以下特殊变量: ```yaml # Available in all child blocks of the loop @@ -274,7 +336,7 @@ process-task: ## 输出引用 -循环完成后,可以引用其聚合结果: +循环完成后,您可以引用其聚合结果: ```yaml # In blocks after the loop @@ -286,10 +348,10 @@ final-processor: ## 最佳实践 -- 设置合理的迭代限制以避免长时间的执行 -- 使用 forEach 处理集合,使用 for 循环处理固定迭代 -- 对于 I/O 密集型操作,考虑使用 maxConcurrency +- 设置合理的迭代限制以避免长时间执行 +- 使用 forEach 处理集合,使用 for 循环进行固定迭代 +- 考虑对 I/O 密集型操作使用 maxConcurrency - 包含错误处理以确保循环执行的健壮性 - 为循环子块使用描述性名称 - 先用小集合进行测试 -- 对大集合的执行时间进行监控 \ No newline at end of file +- 对于大集合,监控执行时间 \ No newline at end of file diff --git a/apps/docs/i18n.lock b/apps/docs/i18n.lock index 8dbc54ee68..c8a891bbc2 100644 --- a/apps/docs/i18n.lock +++ b/apps/docs/i18n.lock @@ -3378,19 +3378,18 @@ checksums: content/38: 1b693e51b5b8e31dd088c602663daab4 content/39: 5003cde407a705d39b969eff5bdab18a content/40: 624199f0ed2378588024cfe6055d6b6b - content/41: 8accff30f8b36f6fc562b44d8fe271dd - content/42: fc62aefa5b726f57f2c9a17a276e601f - content/43: d72903dda50a36b12ec050a06ef23a1b - content/44: 19984dc55d279f1ae3226edf4b62aaa3 - content/45: 9c2f91f89a914bf4661512275e461104 - content/46: adc97756961688b2f4cc69b773c961c9 - content/47: 7b0c309be79b5e1ab30e58a98ea0a778 - content/48: ac70442527be4edcc6b0936e2e5dc8c1 - content/49: a298d382850ddaa0b53e19975b9d12d2 - content/50: 27535bb1de08548a7389708045c10714 - content/51: 6f366fdb6389a03bfc4d83c12fa4099d - content/52: b2a4a0c279f47d58a2456f25a1e1c6f9 - content/53: 17af9269613458de7f8e36a81b2a6d30 + content/41: a2c636da376e80aa3427ce26b2dce0fd + content/42: d72903dda50a36b12ec050a06ef23a1b + content/43: 19984dc55d279f1ae3226edf4b62aaa3 + content/44: 9c2f91f89a914bf4661512275e461104 + content/45: adc97756961688b2f4cc69b773c961c9 + content/46: 7b0c309be79b5e1ab30e58a98ea0a778 + content/47: ac70442527be4edcc6b0936e2e5dc8c1 + content/48: a298d382850ddaa0b53e19975b9d12d2 + content/49: 27535bb1de08548a7389708045c10714 + content/50: 6f366fdb6389a03bfc4d83c12fa4099d + content/51: b2a4a0c279f47d58a2456f25a1e1c6f9 + content/52: 17af9269613458de7f8e36a81b2a6d30 fa2a1ea3b95cd7608e0a7d78834b7d49: meta/title: d8df37d5e95512e955c43661de8a40d0 meta/description: d25527b81409cb3d42d9841e8ed318d4 @@ -3567,30 +3566,39 @@ checksums: meta/title: 27e1d8e6df8b8d3ee07124342bcc5599 meta/description: 5a19804a907fe2a0c7ddc8a933e7e147 content/0: 07f0ef1d9ef5ee2993ab113d95797f37 - content/1: c7dc738fa39cff694ecc04dca0aec33e + content/1: 200b847a5b848c11a507cecfcb381e02 content/2: dd7f8a45778d4dddd9bda78c19f046a4 - content/3: 298c570bb45cd493870e9406dc8be50e - content/4: d4346df7c1c5da08e84931d2d449023a - content/5: bacf5914637cc0c1e00dfac72f60cf1f - content/6: 700ac74ffe23bbcc102b001676ee1818 - content/7: d7c2f6c70070e594bffd0af3d20bbccb - content/8: 33b9b1e9744318597da4b925b0995be2 - content/9: caa663af7342e02001aca78c23695b22 - content/10: 1fa44e09185c753fec303e2c73e44eaf - content/11: d794cf2ea75f4aa8e73069f41fe8bc45 - content/12: f9553f38263ad53c261995083622bdde - content/13: e4625b8a75814b2fcfe3c643a47e22cc - content/14: 20ee0fd34f3baab1099a2f8fb06b13cf - content/15: 73a9d04015d0016a994cf1e8fe8d5c12 - content/16: 9a71a905db9dd5d43bdd769f006caf14 - content/17: 18c31983f32539861fd5b4e8dd943169 - content/18: 55300ae3e3c3213c4ad82c1cf21c89b2 - content/19: 8a8aa301371bd07b15c6f568a8e7826f - content/20: a98cce6db23d9a86ac51179100f32529 - content/21: b5a605662dbb6fc20ad37fdb436f0581 - content/22: 2b204164f64dcf034baa6e5367679735 - content/23: b2a4a0c279f47d58a2456f25a1e1c6f9 - content/24: 15ebde5d554a3ec6000f71cf32b16859 + content/3: b1870986cdef32b6cf3c79a4cd56a8b0 + content/4: 5e7c060bf001ead8fb4005385509e857 + content/5: 1de0d605f73842c3464e7fb2e09fb92c + content/6: 1a8e292ce7cc3adb2fe38cf2f5668b43 + content/7: bacf5914637cc0c1e00dfac72f60cf1f + content/8: 336fdb536f9f5654d4d69a5adb1cf071 + content/9: d7c2f6c70070e594bffd0af3d20bbccb + content/10: 33b9b1e9744318597da4b925b0995be2 + content/11: caa663af7342e02001aca78c23695b22 + content/12: 1fa44e09185c753fec303e2c73e44eaf + content/13: d794cf2ea75f4aa8e73069f41fe8bc45 + content/14: f9553f38263ad53c261995083622bdde + content/15: e4625b8a75814b2fcfe3c643a47e22cc + content/16: 20ee0fd34f3baab1099a2f8fb06b13cf + content/17: 73a9d04015d0016a994cf1e8fe8d5c12 + content/18: 9a71a905db9dd5d43bdd769f006caf14 + content/19: b4017a890213e9ac0afd6b2cfc1bdefc + content/20: 479fd4d587cd0a1b8d27dd440e019215 + content/21: db263bbe8b5984777eb738e9e4c3ec71 + content/22: 1128c613d71aad35f668367ba2065a01 + content/23: 12be239d6ea36a71b022996f56d66901 + content/24: aa2240ef8ced8d9b67f7ab50665caae5 + content/25: 5cce1d6a21fae7252b8670a47a2fae9e + content/26: 18c31983f32539861fd5b4e8dd943169 + content/27: 55300ae3e3c3213c4ad82c1cf21c89b2 + content/28: 8a8aa301371bd07b15c6f568a8e7826f + content/29: a98cce6db23d9a86ac51179100f32529 + content/30: b5a605662dbb6fc20ad37fdb436f0581 + content/31: 2b204164f64dcf034baa6e5367679735 + content/32: b2a4a0c279f47d58a2456f25a1e1c6f9 + content/33: 15ebde5d554a3ec6000f71cf32b16859 132869ed8674995bace940b1cefc4241: meta/title: a753d6bd11bc5876c739b95c6d174914 meta/description: 71efdaceb123c4d6b6ee19c085cd9f0f @@ -3958,12 +3966,11 @@ checksums: content/3: b3c762557a1a308f3531ef1f19701807 content/4: bf29da79344f37eeadd4c176aa19b8ff content/5: ae52879ebefa5664a6b7bf8ce5dd57ab - content/6: 5e1cbe37c5714b16c908c7e0fe0b23e3 - content/7: ce487c9bc7a730e7d9da4a87b8eaa0a6 - content/8: e73f4b831f5b77c71d7d86c83abcbf11 - content/9: 07e064793f3e0bbcb02c4dc6083b6daa - content/10: a702b191c3f94458bee880d33853e0cb - content/11: ce110ab5da3ff96f8cbf96ce3376fc51 - content/12: 83f9b3ab46b0501c8eb3989bec3f4f1b - content/13: e00be80effb71b0acb014f9aa53dfbe1 - content/14: 847a381137856ded9faa5994fbc489fb + content/6: ce487c9bc7a730e7d9da4a87b8eaa0a6 + content/7: e73f4b831f5b77c71d7d86c83abcbf11 + content/8: 07e064793f3e0bbcb02c4dc6083b6daa + content/9: a702b191c3f94458bee880d33853e0cb + content/10: ce110ab5da3ff96f8cbf96ce3376fc51 + content/11: 83f9b3ab46b0501c8eb3989bec3f4f1b + content/12: e00be80effb71b0acb014f9aa53dfbe1 + content/13: 847a381137856ded9faa5994fbc489fb diff --git a/apps/sim/app/api/webhooks/trigger/[path]/route.test.ts b/apps/sim/app/api/webhooks/trigger/[path]/route.test.ts index 399ef51804..9d5b9268a2 100644 --- a/apps/sim/app/api/webhooks/trigger/[path]/route.test.ts +++ b/apps/sim/app/api/webhooks/trigger/[path]/route.test.ts @@ -133,6 +133,7 @@ describe('Webhook Trigger API Route', () => { parallels: {}, isFromNormalizedTables: true, }), + blockExistsInDeployment: vi.fn().mockResolvedValue(true), })) hasProcessedMessageMock.mockResolvedValue(false) diff --git a/apps/sim/app/api/webhooks/trigger/[path]/route.ts b/apps/sim/app/api/webhooks/trigger/[path]/route.ts index 0063a80ae0..e341e7098e 100644 --- a/apps/sim/app/api/webhooks/trigger/[path]/route.ts +++ b/apps/sim/app/api/webhooks/trigger/[path]/route.ts @@ -10,6 +10,7 @@ import { queueWebhookExecution, verifyProviderAuth, } from '@/lib/webhooks/processor' +import { blockExistsInDeployment } from '@/lib/workflows/db-helpers' const logger = createLogger('WebhookTriggerAPI') @@ -62,6 +63,16 @@ export async function POST( return usageLimitError } + if (foundWebhook.blockId) { + const blockExists = await blockExistsInDeployment(foundWorkflow.id, foundWebhook.blockId) + if (!blockExists) { + logger.warn( + `[${requestId}] Trigger block ${foundWebhook.blockId} not found in deployment for workflow ${foundWorkflow.id}` + ) + return new NextResponse('Trigger block not deployed', { status: 404 }) + } + } + return queueWebhookExecution(foundWebhook, foundWorkflow, body, request, { requestId, path, diff --git a/apps/sim/app/api/workflows/[id]/autolayout/route.ts b/apps/sim/app/api/workflows/[id]/autolayout/route.ts index d497f92df3..5d9c896143 100644 --- a/apps/sim/app/api/workflows/[id]/autolayout/route.ts +++ b/apps/sim/app/api/workflows/[id]/autolayout/route.ts @@ -8,7 +8,10 @@ import { createLogger } from '@/lib/logs/console/logger' import { getUserEntityPermissions } from '@/lib/permissions/utils' import { generateRequestId } from '@/lib/utils' import { applyAutoLayout } from '@/lib/workflows/autolayout' -import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/db-helpers' +import { + loadWorkflowFromNormalizedTables, + type NormalizedWorkflowData, +} from '@/lib/workflows/db-helpers' export const dynamic = 'force-dynamic' @@ -36,10 +39,14 @@ const AutoLayoutRequestSchema = z.object({ }) .optional() .default({}), + // Optional: if provided, use these blocks instead of loading from DB + // This allows using blocks with live measurements from the UI + blocks: z.record(z.any()).optional(), + edges: z.array(z.any()).optional(), + loops: z.record(z.any()).optional(), + parallels: z.record(z.any()).optional(), }) -type AutoLayoutRequest = z.infer - /** * POST /api/workflows/[id]/autolayout * Apply autolayout to an existing workflow @@ -108,8 +115,23 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{ return NextResponse.json({ error: 'Access denied' }, { status: 403 }) } - // Load current workflow state - const currentWorkflowData = await loadWorkflowFromNormalizedTables(workflowId) + // Use provided blocks/edges if available (with live measurements from UI), + // otherwise load from database + let currentWorkflowData: NormalizedWorkflowData | null + + if (layoutOptions.blocks && layoutOptions.edges) { + logger.info(`[${requestId}] Using provided blocks with live measurements`) + currentWorkflowData = { + blocks: layoutOptions.blocks, + edges: layoutOptions.edges, + loops: layoutOptions.loops || {}, + parallels: layoutOptions.parallels || {}, + isFromNormalizedTables: false, + } + } else { + logger.info(`[${requestId}] Loading blocks from database`) + currentWorkflowData = await loadWorkflowFromNormalizedTables(workflowId) + } if (!currentWorkflowData) { logger.error(`[${requestId}] Could not load workflow ${workflowId} for autolayout`) diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/hooks/use-knowledge-upload.ts b/apps/sim/app/workspace/[workspaceId]/knowledge/hooks/use-knowledge-upload.ts index 070978d367..fd26bdbf26 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/hooks/use-knowledge-upload.ts +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/hooks/use-knowledge-upload.ts @@ -89,7 +89,6 @@ const UPLOAD_CONFIG = { RETRY_DELAY: 2000, // Initial retry delay in ms (2 seconds) RETRY_MULTIPLIER: 2, // Standard exponential backoff (2s, 4s, 8s) CHUNK_SIZE: 5 * 1024 * 1024, - VERCEL_MAX_BODY_SIZE: 4.5 * 1024 * 1024, // Vercel's 4.5MB limit DIRECT_UPLOAD_THRESHOLD: 4 * 1024 * 1024, // Files > 4MB must use presigned URLs LARGE_FILE_THRESHOLD: 50 * 1024 * 1024, // Files > 50MB need multipart upload UPLOAD_TIMEOUT: 60000, // 60 second timeout per upload diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/combobox.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/combobox.tsx index a72e654fba..7ccf40448b 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/combobox.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/combobox.tsx @@ -10,6 +10,7 @@ import { checkTagTrigger, TagDropdown } from '@/components/ui/tag-dropdown' import { createLogger } from '@/lib/logs/console/logger' import { cn } from '@/lib/utils' import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/hooks/use-sub-block-value' +import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes' import type { SubBlockConfig } from '@/blocks/types' import { useTagSelection } from '@/hooks/use-tag-selection' @@ -60,6 +61,7 @@ export function ComboBox({ const [highlightedIndex, setHighlightedIndex] = useState(-1) const emitTagSelection = useTagSelection(blockId, subBlockId) + const accessiblePrefixes = useAccessibleReferencePrefixes(blockId) const inputRef = useRef(null) const overlayRef = useRef(null) @@ -432,7 +434,10 @@ export function ComboBox({ style={{ right: '42px' }} >
          - {formatDisplayText(displayValue)} + {formatDisplayText(displayValue, { + accessiblePrefixes, + highlightAll: !accessiblePrefixes, + })}
        {/* Chevron button */} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/input-mapping/input-mapping.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/input-mapping/input-mapping.tsx index ad4736e9c4..1b48f6f005 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/input-mapping/input-mapping.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/input-mapping/input-mapping.tsx @@ -5,6 +5,7 @@ import { Label } from '@/components/ui/label' import { checkTagTrigger, TagDropdown } from '@/components/ui/tag-dropdown' import { cn } from '@/lib/utils' import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/hooks/use-sub-block-value' +import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' interface InputFormatField { @@ -152,6 +153,8 @@ export function InputMapping({ setMapping(updated) } + const accessiblePrefixes = useAccessibleReferencePrefixes(blockId) + if (!selectedWorkflowId) { return (
        @@ -213,6 +216,7 @@ export function InputMapping({ blockId={blockId} subBlockId={subBlockId} disabled={isPreview || disabled} + accessiblePrefixes={accessiblePrefixes} /> ) })} @@ -229,6 +233,7 @@ function InputMappingField({ blockId, subBlockId, disabled, + accessiblePrefixes, }: { fieldName: string fieldType?: string @@ -237,6 +242,7 @@ function InputMappingField({ blockId: string subBlockId: string disabled: boolean + accessiblePrefixes: Set | undefined }) { const [showTags, setShowTags] = useState(false) const [cursorPosition, setCursorPosition] = useState(0) @@ -318,7 +324,10 @@ function InputMappingField({ className='w-full whitespace-pre' style={{ scrollbarWidth: 'none', minWidth: 'fit-content' }} > - {formatDisplayText(value)} + {formatDisplayText(value, { + accessiblePrefixes, + highlightAll: !accessiblePrefixes, + })}
        diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/knowledge-tag-filters/knowledge-tag-filters.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/knowledge-tag-filters/knowledge-tag-filters.tsx index 63dd78613a..6df8d6582c 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/knowledge-tag-filters/knowledge-tag-filters.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/knowledge-tag-filters/knowledge-tag-filters.tsx @@ -7,6 +7,7 @@ import { formatDisplayText } from '@/components/ui/formatted-text' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { checkTagTrigger, TagDropdown } from '@/components/ui/tag-dropdown' +import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes' import type { SubBlockConfig } from '@/blocks/types' import { useKnowledgeBaseTagDefinitions } from '@/hooks/use-knowledge-base-tag-definitions' import { useTagSelection } from '@/hooks/use-tag-selection' @@ -55,6 +56,9 @@ export function KnowledgeTagFilters({ // Use KB tag definitions hook to get available tags const { tagDefinitions, isLoading } = useKnowledgeBaseTagDefinitions(knowledgeBaseId) + // Get accessible prefixes for variable highlighting + const accessiblePrefixes = useAccessibleReferencePrefixes(blockId) + // State for managing tag dropdown const [activeTagDropdown, setActiveTagDropdown] = useState<{ rowIndex: number @@ -314,7 +318,12 @@ export function KnowledgeTagFilters({ className='w-full border-0 text-transparent caret-foreground placeholder:text-muted-foreground/50 focus-visible:ring-0 focus-visible:ring-offset-0' />
        -
        {formatDisplayText(cellValue)}
        +
        + {formatDisplayText(cellValue, { + accessiblePrefixes, + highlightAll: !accessiblePrefixes, + })} +
        diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/long-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/long-input.tsx index 10c2675312..3261c08385 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/long-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/long-input.tsx @@ -11,6 +11,7 @@ import { createLogger } from '@/lib/logs/console/logger' import { cn } from '@/lib/utils' import { WandPromptBar } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/wand-prompt-bar/wand-prompt-bar' import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/hooks/use-sub-block-value' +import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes' import { useWand } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-wand' import type { SubBlockConfig } from '@/blocks/types' import { useTagSelection } from '@/hooks/use-tag-selection' @@ -92,6 +93,7 @@ export function LongInput({ const overlayRef = useRef(null) const [activeSourceBlockId, setActiveSourceBlockId] = useState(null) const containerRef = useRef(null) + const accessiblePrefixes = useAccessibleReferencePrefixes(blockId) // Use preview value when in preview mode, otherwise use store value or prop value const baseValue = isPreview ? previewValue : propValue !== undefined ? propValue : storeValue @@ -405,7 +407,10 @@ export function LongInput({ height: `${height}px`, }} > - {formatDisplayText(value?.toString() ?? '')} + {formatDisplayText(value?.toString() ?? '', { + accessiblePrefixes, + highlightAll: !accessiblePrefixes, + })} {/* Wand Button */} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/short-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/short-input.tsx index 8fbd00d76b..739b04ddfa 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/short-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/short-input.tsx @@ -11,6 +11,7 @@ import { createLogger } from '@/lib/logs/console/logger' import { cn } from '@/lib/utils' import { WandPromptBar } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/wand-prompt-bar/wand-prompt-bar' import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/hooks/use-sub-block-value' +import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes' import { useWand } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-wand' import type { SubBlockConfig } from '@/blocks/types' import { useTagSelection } from '@/hooks/use-tag-selection' @@ -345,6 +346,8 @@ export function ShortInput({ } } + const accessiblePrefixes = useAccessibleReferencePrefixes(blockId) + return ( <> {password && !isFocused ? '•'.repeat(value?.toString().length ?? 0) - : formatDisplayText(value?.toString() ?? '')} + : formatDisplayText(value?.toString() ?? '', { + accessiblePrefixes, + highlightAll: !accessiblePrefixes, + })} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/starter/input-format.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/starter/input-format.tsx index 44ed4915ee..5741b857ec 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/starter/input-format.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/starter/input-format.tsx @@ -22,6 +22,7 @@ import { checkTagTrigger, TagDropdown } from '@/components/ui/tag-dropdown' import { Textarea } from '@/components/ui/textarea' import { cn } from '@/lib/utils' import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/hooks/use-sub-block-value' +import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes' interface Field { id: string @@ -80,6 +81,7 @@ export function FieldFormat({ const [cursorPosition, setCursorPosition] = useState(0) const [activeFieldId, setActiveFieldId] = useState(null) const [activeSourceBlockId, setActiveSourceBlockId] = useState(null) + const accessiblePrefixes = useAccessibleReferencePrefixes(blockId) // Use preview value when in preview mode, otherwise use store value const value = isPreview ? previewValue : storeValue @@ -471,7 +473,10 @@ export function FieldFormat({ style={{ scrollbarWidth: 'none', minWidth: 'fit-content' }} > {formatDisplayText( - (localValues[field.id] ?? field.value ?? '')?.toString() + (localValues[field.id] ?? field.value ?? '')?.toString(), + accessiblePrefixes + ? { accessiblePrefixes } + : { highlightAll: true } )} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/tool-input/components/mcp-server-modal/mcp-server-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/tool-input/components/mcp-server-modal/mcp-server-modal.tsx index d31a79b190..aba7bdc397 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/tool-input/components/mcp-server-modal/mcp-server-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/tool-input/components/mcp-server-modal/mcp-server-modal.tsx @@ -24,6 +24,7 @@ import { } from '@/components/ui/select' import { createLogger } from '@/lib/logs/console/logger' import type { McpTransport } from '@/lib/mcp/types' +import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes' import { useMcpServerTest } from '@/hooks/use-mcp-server-test' import { useMcpServersStore } from '@/stores/mcp-servers/store' @@ -33,6 +34,7 @@ interface McpServerModalProps { open: boolean onOpenChange: (open: boolean) => void onServerCreated?: () => void + blockId: string } interface McpServerFormData { @@ -42,7 +44,12 @@ interface McpServerFormData { headers?: Record } -export function McpServerModal({ open, onOpenChange, onServerCreated }: McpServerModalProps) { +export function McpServerModal({ + open, + onOpenChange, + onServerCreated, + blockId, +}: McpServerModalProps) { const params = useParams() const workspaceId = params.workspaceId as string const [formData, setFormData] = useState({ @@ -262,6 +269,8 @@ export function McpServerModal({ open, onOpenChange, onServerCreated }: McpServe workspaceId, ]) + const accessiblePrefixes = useAccessibleReferencePrefixes(blockId) + return ( @@ -337,7 +346,10 @@ export function McpServerModal({ open, onOpenChange, onServerCreated }: McpServe className='whitespace-nowrap' style={{ transform: `translateX(-${urlScrollLeft}px)` }} > - {formatDisplayText(formData.url || '')} + {formatDisplayText(formData.url || '', { + accessiblePrefixes, + highlightAll: !accessiblePrefixes, + })} @@ -389,7 +401,10 @@ export function McpServerModal({ open, onOpenChange, onServerCreated }: McpServe transform: `translateX(-${headerScrollLeft[`key-${index}`] || 0}px)`, }} > - {formatDisplayText(key || '')} + {formatDisplayText(key || '', { + accessiblePrefixes, + highlightAll: !accessiblePrefixes, + })} @@ -417,7 +432,10 @@ export function McpServerModal({ open, onOpenChange, onServerCreated }: McpServe transform: `translateX(-${headerScrollLeft[`value-${index}`] || 0}px)`, }} > - {formatDisplayText(value || '')} + {formatDisplayText(value || '', { + accessiblePrefixes, + highlightAll: !accessiblePrefixes, + })} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/tool-input/tool-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/tool-input/tool-input.tsx index 496d21216b..9eef0a2043 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/tool-input/tool-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/tool-input/tool-input.tsx @@ -1977,6 +1977,7 @@ export function ToolInput({ // Refresh MCP tools when a new server is created refreshTools(true) }} + blockId={blockId} /> ) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx index c725e30b63..1cf40e9deb 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx @@ -148,6 +148,7 @@ export function WorkflowBlock({ id, data }: NodeProps) { ) const storeIsWide = useWorkflowStore((state) => state.blocks[id]?.isWide ?? false) const storeBlockHeight = useWorkflowStore((state) => state.blocks[id]?.height ?? 0) + const storeBlockLayout = useWorkflowStore((state) => state.blocks[id]?.layout) const storeBlockAdvancedMode = useWorkflowStore( (state) => state.blocks[id]?.advancedMode ?? false ) @@ -168,6 +169,10 @@ export function WorkflowBlock({ id, data }: NodeProps) { ? (currentWorkflow.blocks[id]?.height ?? 0) : storeBlockHeight + const blockWidth = currentWorkflow.isDiffMode + ? (currentWorkflow.blocks[id]?.layout?.measuredWidth ?? 0) + : (storeBlockLayout?.measuredWidth ?? 0) + // Get per-block webhook status by checking if webhook is configured const activeWorkflowId = useWorkflowRegistry((state) => state.activeWorkflowId) @@ -240,7 +245,7 @@ export function WorkflowBlock({ id, data }: NodeProps) { }, [id, collaborativeSetSubblockValue]) // Workflow store actions - const updateBlockHeight = useWorkflowStore((state) => state.updateBlockHeight) + const updateBlockLayoutMetrics = useWorkflowStore((state) => state.updateBlockLayoutMetrics) // Execution store const isActiveBlock = useExecutionStore((state) => state.activeBlockIds.has(id)) @@ -419,9 +424,9 @@ export function WorkflowBlock({ id, data }: NodeProps) { if (!contentRef.current) return let rafId: number - const debouncedUpdate = debounce((height: number) => { - if (height !== blockHeight) { - updateBlockHeight(id, height) + const debouncedUpdate = debounce((dimensions: { width: number; height: number }) => { + if (dimensions.height !== blockHeight || dimensions.width !== blockWidth) { + updateBlockLayoutMetrics(id, dimensions) updateNodeInternals(id) } }, 100) @@ -435,9 +440,10 @@ export function WorkflowBlock({ id, data }: NodeProps) { // Schedule the update on the next animation frame rafId = requestAnimationFrame(() => { for (const entry of entries) { - const height = - entry.borderBoxSize[0]?.blockSize ?? entry.target.getBoundingClientRect().height - debouncedUpdate(height) + const rect = entry.target.getBoundingClientRect() + const height = entry.borderBoxSize[0]?.blockSize ?? rect.height + const width = entry.borderBoxSize[0]?.inlineSize ?? rect.width + debouncedUpdate({ width, height }) } }) }) @@ -450,7 +456,7 @@ export function WorkflowBlock({ id, data }: NodeProps) { cancelAnimationFrame(rafId) } } - }, [id, blockHeight, updateBlockHeight, updateNodeInternals, lastUpdate]) + }, [id, blockHeight, blockWidth, updateBlockLayoutMetrics, updateNodeInternals, lastUpdate]) // SubBlock layout management function groupSubBlocks(subBlocks: SubBlockConfig[], blockId: string) { diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes.ts new file mode 100644 index 0000000000..06d6dd795c --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes.ts @@ -0,0 +1,64 @@ +import { useMemo } from 'react' +import { shallow } from 'zustand/shallow' +import { BlockPathCalculator } from '@/lib/block-path-calculator' +import { SYSTEM_REFERENCE_PREFIXES } from '@/lib/workflows/references' +import { normalizeBlockName } from '@/stores/workflows/utils' +import { useWorkflowStore } from '@/stores/workflows/workflow/store' +import type { Loop, Parallel } from '@/stores/workflows/workflow/types' + +export function useAccessibleReferencePrefixes(blockId?: string | null): Set | undefined { + const { blocks, edges, loops, parallels } = useWorkflowStore( + (state) => ({ + blocks: state.blocks, + edges: state.edges, + loops: state.loops || {}, + parallels: state.parallels || {}, + }), + shallow + ) + + return useMemo(() => { + if (!blockId) { + return undefined + } + + const graphEdges = edges.map((edge) => ({ source: edge.source, target: edge.target })) + const ancestorIds = BlockPathCalculator.findAllPathNodes(graphEdges, blockId) + const accessibleIds = new Set(ancestorIds) + accessibleIds.add(blockId) + + const starterBlock = Object.values(blocks).find((block) => block.type === 'starter') + if (starterBlock) { + accessibleIds.add(starterBlock.id) + } + + const loopValues = Object.values(loops as Record) + loopValues.forEach((loop) => { + if (!loop?.nodes) return + if (loop.nodes.includes(blockId)) { + loop.nodes.forEach((nodeId) => accessibleIds.add(nodeId)) + } + }) + + const parallelValues = Object.values(parallels as Record) + parallelValues.forEach((parallel) => { + if (!parallel?.nodes) return + if (parallel.nodes.includes(blockId)) { + parallel.nodes.forEach((nodeId) => accessibleIds.add(nodeId)) + } + }) + + const prefixes = new Set() + accessibleIds.forEach((id) => { + prefixes.add(normalizeBlockName(id)) + const block = blocks[id] + if (block?.name) { + prefixes.add(normalizeBlockName(block.name)) + } + }) + + SYSTEM_REFERENCE_PREFIXES.forEach((prefix) => prefixes.add(prefix)) + + return prefixes + }, [blockId, blocks, edges, loops, parallels]) +} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils.ts index 6d0d9e9746..5a3c008c3d 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils.ts @@ -98,18 +98,12 @@ const getBlockDimensions = ( } } - if (block.type === 'workflowBlock') { - const nodeWidth = block.data?.width || block.width - const nodeHeight = block.data?.height || block.height - - if (nodeWidth && nodeHeight) { - return { width: nodeWidth, height: nodeHeight } - } - } - return { - width: block.isWide ? 450 : block.data?.width || block.width || 350, - height: Math.max(block.height || block.data?.height || 150, 100), + width: block.layout?.measuredWidth || (block.isWide ? 450 : block.data?.width || 350), + height: Math.max( + block.layout?.measuredHeight || block.height || block.data?.height || 150, + 100 + ), } } diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/auto-layout.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/auto-layout.ts index e393b09c2b..49ff7b0f33 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/auto-layout.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/auto-layout.ts @@ -78,13 +78,19 @@ export async function applyAutoLayoutToWorkflow( }, } - // Call the autolayout API route which has access to the server-side API key + // Call the autolayout API route, sending blocks with live measurements const response = await fetch(`/api/workflows/${workflowId}/autolayout`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, - body: JSON.stringify(layoutOptions), + body: JSON.stringify({ + ...layoutOptions, + blocks, + edges, + loops, + parallels, + }), }) if (!response.ok) { diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx index 90f43f4e33..c4e1018f6b 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx @@ -781,7 +781,7 @@ const WorkflowContent = React.memo(() => { // Create the trigger block at the center of the viewport const centerPosition = project({ x: window.innerWidth / 2, y: window.innerHeight / 2 }) - const id = `${triggerId}_${Date.now()}` + const id = crypto.randomUUID() // Add the trigger block with trigger mode if specified addBlock( diff --git a/apps/sim/blocks/blocks/api_trigger.ts b/apps/sim/blocks/blocks/api_trigger.ts index 66f7e91904..5a830f9b6c 100644 --- a/apps/sim/blocks/blocks/api_trigger.ts +++ b/apps/sim/blocks/blocks/api_trigger.ts @@ -3,6 +3,7 @@ import type { BlockConfig } from '@/blocks/types' export const ApiTriggerBlock: BlockConfig = { type: 'api_trigger', + triggerAllowed: true, name: 'API', description: 'Expose as HTTP API endpoint', longDescription: diff --git a/apps/sim/blocks/blocks/chat_trigger.ts b/apps/sim/blocks/blocks/chat_trigger.ts index 9dc98f0b1c..7276489ac4 100644 --- a/apps/sim/blocks/blocks/chat_trigger.ts +++ b/apps/sim/blocks/blocks/chat_trigger.ts @@ -7,6 +7,7 @@ const ChatTriggerIcon = (props: SVGProps) => createElement(Messag export const ChatTriggerBlock: BlockConfig = { type: 'chat_trigger', + triggerAllowed: true, name: 'Chat', description: 'Start workflow from a chat deployment', longDescription: 'Chat trigger to run the workflow via deployed chat interfaces.', diff --git a/apps/sim/blocks/blocks/input_trigger.ts b/apps/sim/blocks/blocks/input_trigger.ts index 954c31bc1a..073c38ad7e 100644 --- a/apps/sim/blocks/blocks/input_trigger.ts +++ b/apps/sim/blocks/blocks/input_trigger.ts @@ -7,6 +7,7 @@ const InputTriggerIcon = (props: SVGProps) => createElement(FormI export const InputTriggerBlock: BlockConfig = { type: 'input_trigger', + triggerAllowed: true, name: 'Input Form', description: 'Start workflow manually with a defined input schema', longDescription: diff --git a/apps/sim/blocks/blocks/manual_trigger.ts b/apps/sim/blocks/blocks/manual_trigger.ts index 508ef3f792..4d8a433d6d 100644 --- a/apps/sim/blocks/blocks/manual_trigger.ts +++ b/apps/sim/blocks/blocks/manual_trigger.ts @@ -7,6 +7,7 @@ const ManualTriggerIcon = (props: SVGProps) => createElement(Play export const ManualTriggerBlock: BlockConfig = { type: 'manual_trigger', + triggerAllowed: true, name: 'Manual', description: 'Start workflow manually from the editor', longDescription: diff --git a/apps/sim/blocks/blocks/schedule.ts b/apps/sim/blocks/blocks/schedule.ts index 72a54d6811..c1e8aef79b 100644 --- a/apps/sim/blocks/blocks/schedule.ts +++ b/apps/sim/blocks/blocks/schedule.ts @@ -7,6 +7,7 @@ const ScheduleIcon = (props: SVGProps) => createElement(Clock, pr export const ScheduleBlock: BlockConfig = { type: 'schedule', + triggerAllowed: true, name: 'Schedule', description: 'Trigger workflow execution on a schedule', longDescription: diff --git a/apps/sim/components/ui/formatted-text.tsx b/apps/sim/components/ui/formatted-text.tsx index b61df9be46..b6ac1458ca 100644 --- a/apps/sim/components/ui/formatted-text.tsx +++ b/apps/sim/components/ui/formatted-text.tsx @@ -1,28 +1,50 @@ 'use client' import type { ReactNode } from 'react' +import { normalizeBlockName } from '@/stores/workflows/utils' + +export interface HighlightContext { + accessiblePrefixes?: Set + highlightAll?: boolean +} + +const SYSTEM_PREFIXES = new Set(['start', 'loop', 'parallel', 'variable']) /** * Formats text by highlighting block references (<...>) and environment variables ({{...}}) * Used in code editor, long inputs, and short inputs for consistent syntax highlighting - * - * @param text The text to format */ -export function formatDisplayText(text: string): ReactNode[] { +export function formatDisplayText(text: string, context?: HighlightContext): ReactNode[] { if (!text) return [] - const parts = text.split(/(<[^>]+>|\{\{[^}]+\}\})/g) + const shouldHighlightPart = (part: string): boolean => { + if (!part.startsWith('<') || !part.endsWith('>')) { + return false + } - return parts.map((part, index) => { - if (part.startsWith('<') && part.endsWith('>')) { - return ( - - {part} - - ) + if (context?.highlightAll) { + return true + } + + const inner = part.slice(1, -1) + const [prefix] = inner.split('.') + const normalizedPrefix = normalizeBlockName(prefix) + + if (SYSTEM_PREFIXES.has(normalizedPrefix)) { + return true + } + + if (context?.accessiblePrefixes?.has(normalizedPrefix)) { + return true } - if (part.match(/^\{\{[^}]+\}\}$/)) { + return false + } + + const parts = text.split(/(<[^>]+>|\{\{[^}]+\}\})/g) + + return parts.map((part, index) => { + if (shouldHighlightPart(part) || part.match(/^\{\{[^}]+\}\}$/)) { return ( {part} diff --git a/apps/sim/components/ui/tag-dropdown.tsx b/apps/sim/components/ui/tag-dropdown.tsx index 767271c588..eae8c70bc0 100644 --- a/apps/sim/components/ui/tag-dropdown.tsx +++ b/apps/sim/components/ui/tag-dropdown.tsx @@ -1,13 +1,12 @@ -import type React from 'react' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { ChevronRight } from 'lucide-react' -import { BlockPathCalculator } from '@/lib/block-path-calculator' +import { shallow } from 'zustand/shallow' import { extractFieldsFromSchema, parseResponseFormatSafely } from '@/lib/response-format' import { cn } from '@/lib/utils' import { getBlockOutputPaths, getBlockOutputType } from '@/lib/workflows/block-outputs' +import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes' import { getBlock } from '@/blocks' import type { BlockConfig } from '@/blocks/types' -import { Serializer } from '@/serializer' import { useVariablesStore } from '@/stores/panel/variables/store' import type { Variable } from '@/stores/panel/variables/types' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' @@ -25,6 +24,15 @@ interface BlockTagGroup { distance: number } +interface NestedBlockTagGroup extends BlockTagGroup { + nestedTags: Array<{ + key: string + display: string + fullTag?: string + children?: Array<{ key: string; display: string; fullTag: string }> + }> +} + interface TagDropdownProps { visible: boolean onSelect: (newValue: string) => void @@ -70,6 +78,18 @@ const normalizeVariableName = (variableName: string): string => { return variableName.replace(/\s+/g, '') } +const ensureRootTag = (tags: string[], rootTag: string): string[] => { + if (!rootTag) { + return tags + } + + if (tags.includes(rootTag)) { + return tags + } + + return [rootTag, ...tags] +} + const getSubBlockValue = (blockId: string, property: string): any => { return useSubBlockStore.getState().getValue(blockId, property) } @@ -300,12 +320,27 @@ export const TagDropdown: React.FC = ({ const [parentHovered, setParentHovered] = useState(null) const [submenuHovered, setSubmenuHovered] = useState(false) - const blocks = useWorkflowStore((state) => state.blocks) - const loops = useWorkflowStore((state) => state.loops) - const parallels = useWorkflowStore((state) => state.parallels) - const edges = useWorkflowStore((state) => state.edges) + const { blocks, edges, loops, parallels } = useWorkflowStore( + (state) => ({ + blocks: state.blocks, + edges: state.edges, + loops: state.loops || {}, + parallels: state.parallels || {}, + }), + shallow + ) + const workflowId = useWorkflowRegistry((state) => state.activeWorkflowId) + const rawAccessiblePrefixes = useAccessibleReferencePrefixes(blockId) + + const combinedAccessiblePrefixes = useMemo(() => { + if (!rawAccessiblePrefixes) return new Set() + const normalized = new Set(rawAccessiblePrefixes) + normalized.add(normalizeBlockName(blockId)) + return normalized + }, [rawAccessiblePrefixes, blockId]) + // Subscribe to live subblock values for the active workflow to react to input format changes const workflowSubBlockValues = useSubBlockStore((state) => workflowId ? (state.workflowValues[workflowId] ?? {}) : {} @@ -325,7 +360,6 @@ export const TagDropdown: React.FC = ({ ) const getVariablesByWorkflowId = useVariablesStore((state) => state.getVariablesByWorkflowId) - const variables = useVariablesStore((state) => state.variables) const workflowVariables = workflowId ? getVariablesByWorkflowId(workflowId) : [] const searchTerm = useMemo(() => { @@ -336,8 +370,12 @@ export const TagDropdown: React.FC = ({ const { tags, - variableInfoMap = {}, - blockTagGroups = [], + variableInfoMap, + blockTagGroups: computedBlockTagGroups, + }: { + tags: string[] + variableInfoMap: Record + blockTagGroups: BlockTagGroup[] } = useMemo(() => { if (activeSourceBlockId) { const sourceBlock = blocks[activeSourceBlockId] @@ -481,6 +519,12 @@ export const TagDropdown: React.FC = ({ } } + blockTags = ensureRootTag(blockTags, normalizedBlockName) + const shouldShowRootTag = sourceBlock.type === 'generic_webhook' + if (!shouldShowRootTag) { + blockTags = blockTags.filter((tag) => tag !== normalizedBlockName) + } + const blockTagGroups: BlockTagGroup[] = [ { blockName, @@ -507,18 +551,7 @@ export const TagDropdown: React.FC = ({ } } - const serializer = new Serializer() - const serializedWorkflow = serializer.serializeWorkflow(blocks, edges, loops, parallels) - - const accessibleBlockIds = BlockPathCalculator.findAllPathNodes( - serializedWorkflow.connections, - blockId - ) - const starterBlock = Object.values(blocks).find((block) => block.type === 'starter') - if (starterBlock && !accessibleBlockIds.includes(starterBlock.id)) { - accessibleBlockIds.push(starterBlock.id) - } const blockDistances: Record = {} if (starterBlock) { @@ -623,6 +656,10 @@ export const TagDropdown: React.FC = ({ const blockTagGroups: BlockTagGroup[] = [] const allBlockTags: string[] = [] + // Use the combinedAccessiblePrefixes to iterate through accessible blocks + const accessibleBlockIds = combinedAccessiblePrefixes + ? Array.from(combinedAccessiblePrefixes) + : [] for (const accessibleBlockId of accessibleBlockIds) { const accessibleBlock = blocks[accessibleBlockId] if (!accessibleBlock) continue @@ -648,7 +685,8 @@ export const TagDropdown: React.FC = ({ const normalizedBlockName = normalizeBlockName(blockName) const outputPaths = generateOutputPaths(mockConfig.outputs) - const blockTags = outputPaths.map((path) => `${normalizedBlockName}.${path}`) + let blockTags = outputPaths.map((path) => `${normalizedBlockName}.${path}`) + blockTags = ensureRootTag(blockTags, normalizedBlockName) blockTagGroups.push({ blockName, @@ -750,6 +788,12 @@ export const TagDropdown: React.FC = ({ } } + blockTags = ensureRootTag(blockTags, normalizedBlockName) + const shouldShowRootTag = accessibleBlock.type === 'generic_webhook' + if (!shouldShowRootTag) { + blockTags = blockTags.filter((tag) => tag !== normalizedBlockName) + } + blockTagGroups.push({ blockName, blockId: accessibleBlockId, @@ -781,51 +825,54 @@ export const TagDropdown: React.FC = ({ } return { - tags: [...variableTags, ...contextualTags, ...allBlockTags], + tags: [...allBlockTags, ...variableTags, ...contextualTags], variableInfoMap, blockTagGroups: finalBlockTagGroups, } }, [ + activeSourceBlockId, + combinedAccessiblePrefixes, + blockId, blocks, edges, + getMergedSubBlocks, loops, parallels, - blockId, - activeSourceBlockId, workflowVariables, - workflowSubBlockValues, - getMergedSubBlocks, + workflowId, ]) const filteredTags = useMemo(() => { if (!searchTerm) return tags - return tags.filter((tag: string) => tag.toLowerCase().includes(searchTerm)) + return tags.filter((tag) => tag.toLowerCase().includes(searchTerm)) }, [tags, searchTerm]) const { variableTags, filteredBlockTagGroups } = useMemo(() => { const varTags: string[] = [] - filteredTags.forEach((tag) => { + filteredTags.forEach((tag: string) => { if (tag.startsWith(TAG_PREFIXES.VARIABLE)) { varTags.push(tag) } }) - const filteredBlockTagGroups = blockTagGroups - .map((group) => ({ + const filteredBlockTagGroups = computedBlockTagGroups + .map((group: BlockTagGroup) => ({ ...group, - tags: group.tags.filter((tag) => !searchTerm || tag.toLowerCase().includes(searchTerm)), + tags: group.tags.filter( + (tag: string) => !searchTerm || tag.toLowerCase().includes(searchTerm) + ), })) - .filter((group) => group.tags.length > 0) + .filter((group: BlockTagGroup) => group.tags.length > 0) return { variableTags: varTags, filteredBlockTagGroups, } - }, [filteredTags, blockTagGroups, searchTerm]) + }, [filteredTags, computedBlockTagGroups, searchTerm]) - const nestedBlockTagGroups = useMemo(() => { - return filteredBlockTagGroups.map((group) => { + const nestedBlockTagGroups: NestedBlockTagGroup[] = useMemo(() => { + return filteredBlockTagGroups.map((group: BlockTagGroup) => { const nestedTags: Array<{ key: string display: string @@ -839,7 +886,7 @@ export const TagDropdown: React.FC = ({ > = {} const directTags: Array<{ key: string; display: string; fullTag: string }> = [] - group.tags.forEach((tag) => { + group.tags.forEach((tag: string) => { const tagParts = tag.split('.') if (tagParts.length >= 3) { const parent = tagParts[1] @@ -899,8 +946,8 @@ export const TagDropdown: React.FC = ({ visualTags.push(...variableTags) - nestedBlockTagGroups.forEach((group) => { - group.nestedTags.forEach((nestedTag) => { + nestedBlockTagGroups.forEach((group: NestedBlockTagGroup) => { + group.nestedTags.forEach((nestedTag: any) => { if (nestedTag.children && nestedTag.children.length > 0) { const firstChild = nestedTag.children[0] if (firstChild.fullTag) { @@ -952,8 +999,8 @@ export const TagDropdown: React.FC = ({ if (tag.startsWith(TAG_PREFIXES.VARIABLE)) { const variableName = tag.substring(TAG_PREFIXES.VARIABLE.length) - const variableObj = Object.values(variables).find( - (v) => v.name.replace(/\s+/g, '') === variableName + const variableObj = workflowVariables.find( + (v: Variable) => v.name.replace(/\s+/g, '') === variableName ) if (variableObj) { @@ -985,7 +1032,7 @@ export const TagDropdown: React.FC = ({ onSelect(newValue) onClose?.() }, - [inputValue, cursorPosition, variables, onSelect, onClose] + [inputValue, cursorPosition, workflowVariables, onSelect, onClose] ) useEffect(() => setSelectedIndex(0), [searchTerm]) @@ -1030,7 +1077,7 @@ export const TagDropdown: React.FC = ({ if (selectedIndex < 0 || selectedIndex >= orderedTags.length) return null const selectedTag = orderedTags[selectedIndex] for (let gi = 0; gi < nestedBlockTagGroups.length; gi++) { - const group = nestedBlockTagGroups[gi] + const group = nestedBlockTagGroups[gi]! for (let ni = 0; ni < group.nestedTags.length; ni++) { const nestedTag = group.nestedTags[ni] if (nestedTag.children && nestedTag.children.length > 0) { @@ -1051,16 +1098,16 @@ export const TagDropdown: React.FC = ({ return } - const currentGroup = nestedBlockTagGroups.find((group) => { + const currentGroup = nestedBlockTagGroups.find((group: NestedBlockTagGroup) => { return group.nestedTags.some( - (tag, index) => + (tag: any, index: number) => `${group.blockId}-${tag.key}` === currentHovered.tag && index === currentHovered.index ) }) const currentNestedTag = currentGroup?.nestedTags.find( - (tag, index) => + (tag: any, index: number) => `${currentGroup.blockId}-${tag.key}` === currentHovered.tag && index === currentHovered.index ) @@ -1089,8 +1136,8 @@ export const TagDropdown: React.FC = ({ e.preventDefault() e.stopPropagation() if (submenuIndex >= 0 && submenuIndex < children.length) { - const selectedChild = children[submenuIndex] - handleTagSelect(selectedChild.fullTag, currentGroup) + const selectedChild = children[submenuIndex] as any + handleTagSelect(selectedChild.fullTag, currentGroup as BlockTagGroup | undefined) } break case 'Escape': @@ -1324,7 +1371,7 @@ export const TagDropdown: React.FC = ({ {nestedBlockTagGroups.length > 0 && ( <> {variableTags.length > 0 &&
        } - {nestedBlockTagGroups.map((group) => { + {nestedBlockTagGroups.map((group: NestedBlockTagGroup) => { const blockConfig = getBlock(group.blockType) let blockColor = blockConfig?.bgColor || BLOCK_COLORS.DEFAULT @@ -1340,7 +1387,7 @@ export const TagDropdown: React.FC = ({ {group.blockName}
        - {group.nestedTags.map((nestedTag, index) => { + {group.nestedTags.map((nestedTag: any, index: number) => { const tagIndex = nestedTag.fullTag ? (tagIndexMap.get(nestedTag.fullTag) ?? -1) : -1 @@ -1505,7 +1552,7 @@ export const TagDropdown: React.FC = ({ }} >
        - {nestedTag.children!.map((child, childIndex) => { + {nestedTag.children!.map((child: any, childIndex: number) => { const isKeyboardSelected = inSubmenu && submenuIndex === childIndex const isSelected = isKeyboardSelected diff --git a/apps/sim/executor/handlers/agent/agent-handler.ts b/apps/sim/executor/handlers/agent/agent-handler.ts index c2ad061ba8..8e85ed866a 100644 --- a/apps/sim/executor/handlers/agent/agent-handler.ts +++ b/apps/sim/executor/handlers/agent/agent-handler.ts @@ -1,6 +1,6 @@ -import { getEnv } from '@/lib/env' import { createLogger } from '@/lib/logs/console/logger' import { createMcpToolId } from '@/lib/mcp/utils' +import { getBaseUrl } from '@/lib/urls/utils' import { getAllBlocks } from '@/blocks' import type { BlockOutput } from '@/blocks/types' import { BlockType } from '@/executor/consts' @@ -261,8 +261,7 @@ export class AgentBlockHandler implements BlockHandler { } } - const appUrl = getEnv('NEXT_PUBLIC_APP_URL') - const url = new URL(`${appUrl}/api/mcp/tools/discover`) + const url = new URL('/api/mcp/tools/discover', getBaseUrl()) url.searchParams.set('serverId', serverId) if (context.workspaceId) { url.searchParams.set('workspaceId', context.workspaceId) @@ -316,7 +315,7 @@ export class AgentBlockHandler implements BlockHandler { } } - const execResponse = await fetch(`${appUrl}/api/mcp/tools/execute`, { + const execResponse = await fetch(`${getBaseUrl()}/api/mcp/tools/execute`, { method: 'POST', headers, body: JSON.stringify({ @@ -640,7 +639,7 @@ export class AgentBlockHandler implements BlockHandler { ) { logger.info('Using HTTP provider request (browser environment)') - const url = new URL('/api/providers', getEnv('NEXT_PUBLIC_APP_URL') || '') + const url = new URL('/api/providers', getBaseUrl()) const response = await fetch(url.toString(), { method: 'POST', headers: { 'Content-Type': 'application/json' }, diff --git a/apps/sim/executor/handlers/router/router-handler.ts b/apps/sim/executor/handlers/router/router-handler.ts index dc253a9154..58e27ba45d 100644 --- a/apps/sim/executor/handlers/router/router-handler.ts +++ b/apps/sim/executor/handlers/router/router-handler.ts @@ -1,5 +1,5 @@ -import { env } from '@/lib/env' import { createLogger } from '@/lib/logs/console/logger' +import { getBaseUrl } from '@/lib/urls/utils' import { generateRouterPrompt } from '@/blocks/blocks/router' import type { BlockOutput } from '@/blocks/types' import { BlockType } from '@/executor/consts' @@ -40,8 +40,7 @@ export class RouterBlockHandler implements BlockHandler { const providerId = getProviderFromModel(routerConfig.model) try { - const baseUrl = env.NEXT_PUBLIC_APP_URL || '' - const url = new URL('/api/providers', baseUrl) + const url = new URL('/api/providers', getBaseUrl()) // Create the provider request with proper message formatting const messages = [{ role: 'user', content: routerConfig.prompt }] diff --git a/apps/sim/executor/resolver/resolver.test.ts b/apps/sim/executor/resolver/resolver.test.ts index e03246b7f6..c4831fd679 100644 --- a/apps/sim/executor/resolver/resolver.test.ts +++ b/apps/sim/executor/resolver/resolver.test.ts @@ -1356,7 +1356,7 @@ describe('InputResolver', () => { expect(result.code).toBe('return "Agent response"') }) - it('should reject references to unconnected blocks', () => { + it('should leave references to unconnected blocks as strings', () => { // Create a new block that is added to the workflow but not connected to isolated-block workflowWithConnections.blocks.push({ id: 'test-block', @@ -1402,9 +1402,9 @@ describe('InputResolver', () => { enabled: true, } - expect(() => connectionResolver.resolveInputs(testBlock, contextWithConnections)).toThrow( - /Block "isolated-block" is not connected to this block/ - ) + // Should not throw - inaccessible references remain as strings + const result = connectionResolver.resolveInputs(testBlock, contextWithConnections) + expect(result.code).toBe('return ') // Reference remains as-is }) it('should always allow references to starter block', () => { @@ -1546,7 +1546,7 @@ describe('InputResolver', () => { expect(otherResult).toBe('content: Hello World') }) - it('should provide helpful error messages for unconnected blocks', () => { + it('should not throw for unconnected blocks and leave references as strings', () => { // Create a test block in the workflow first workflowWithConnections.blocks.push({ id: 'test-block-2', @@ -1592,9 +1592,9 @@ describe('InputResolver', () => { enabled: true, } - expect(() => connectionResolver.resolveInputs(testBlock, contextWithConnections)).toThrow( - /Available connected blocks:.*Agent Block.*Start/ - ) + // Should not throw - references to nonexistent blocks remain as strings + const result = connectionResolver.resolveInputs(testBlock, contextWithConnections) + expect(result.code).toBe('return ') // Reference remains as-is }) it('should work with block names and normalized names', () => { @@ -1725,7 +1725,7 @@ describe('InputResolver', () => { extendedResolver.resolveInputs(block1, extendedContext) }).not.toThrow() - // Should fail for indirect connection + // Should not fail for indirect connection - reference remains as string expect(() => { // Add the response block to the workflow so it can be validated properly extendedWorkflow.blocks.push({ @@ -1748,8 +1748,9 @@ describe('InputResolver', () => { outputs: {}, enabled: true, } - extendedResolver.resolveInputs(block2, extendedContext) - }).toThrow(/Block "agent-1" is not connected to this block/) + const result = extendedResolver.resolveInputs(block2, extendedContext) + expect(result.test).toBe('') // Reference remains as-is since agent-1 is not accessible + }).not.toThrow() }) it('should handle blocks in same loop referencing each other', () => { diff --git a/apps/sim/executor/resolver/resolver.ts b/apps/sim/executor/resolver/resolver.ts index addcbaa96c..a1953cc210 100644 --- a/apps/sim/executor/resolver/resolver.ts +++ b/apps/sim/executor/resolver/resolver.ts @@ -1,11 +1,13 @@ import { BlockPathCalculator } from '@/lib/block-path-calculator' import { createLogger } from '@/lib/logs/console/logger' import { VariableManager } from '@/lib/variables/variable-manager' +import { extractReferencePrefixes, SYSTEM_REFERENCE_PREFIXES } from '@/lib/workflows/references' import { TRIGGER_REFERENCE_ALIAS_MAP } from '@/lib/workflows/triggers' import { getBlock } from '@/blocks/index' import type { LoopManager } from '@/executor/loops/loops' import type { ExecutionContext } from '@/executor/types' import type { SerializedBlock, SerializedWorkflow } from '@/serializer/types' +import { normalizeBlockName } from '@/stores/workflows/utils' const logger = createLogger('InputResolver') @@ -461,64 +463,40 @@ export class InputResolver { return value } - const blockMatches = value.match(/<([^>]+)>/g) - if (!blockMatches) return value + const blockMatches = extractReferencePrefixes(value) + if (blockMatches.length === 0) return value - // Filter out patterns that are clearly not variable references (e.g., comparison operators) - const validBlockMatches = blockMatches.filter((match) => this.isValidVariableReference(match)) - - // If no valid matches found after filtering, return original value - if (validBlockMatches.length === 0) { - return value - } - - // If we're in an API block body, check each valid match to see if it looks like XML rather than a reference - if ( - currentBlock.metadata?.id === 'api' && - validBlockMatches.some((match) => { - const innerContent = match.slice(1, -1) - // Patterns that suggest this is XML, not a block reference: - return ( - innerContent.includes(':') || // namespaces like soap:Envelope - innerContent.includes('=') || // attributes like xmlns="http://..." - innerContent.includes(' ') || // any space indicates attributes - innerContent.includes('/') || // self-closing tags - !innerContent.includes('.') - ) // block refs always have dots - }) - ) { - return value // Likely XML content, return unchanged - } + const accessiblePrefixes = this.getAccessiblePrefixes(currentBlock) let resolvedValue = value - // Check if we're in a template literal for function blocks - const isInTemplateLiteral = - currentBlock.metadata?.id === 'function' && - value.includes('${') && - value.includes('}') && - value.includes('`') + for (const match of blockMatches) { + const { raw, prefix } = match + if (!accessiblePrefixes.has(prefix)) { + continue + } - for (const match of validBlockMatches) { - // Skip variables - they've already been processed - if (match.startsWith(' { + const prefixes = new Set() + + const accessibleBlocks = this.getAccessibleBlocks(block.id) + accessibleBlocks.forEach((blockId) => { + prefixes.add(normalizeBlockName(blockId)) + const sourceBlock = this.blockById.get(blockId) + if (sourceBlock?.metadata?.name) { + prefixes.add(normalizeBlockName(sourceBlock.metadata.name)) + } + }) + + SYSTEM_REFERENCE_PREFIXES.forEach((prefix) => prefixes.add(prefix)) + + return prefixes + } } diff --git a/apps/sim/lib/auth-client.ts b/apps/sim/lib/auth-client.ts index fa357631e1..718a526c18 100644 --- a/apps/sim/lib/auth-client.ts +++ b/apps/sim/lib/auth-client.ts @@ -14,19 +14,7 @@ import { isBillingEnabled } from '@/lib/environment' import { SessionContext, type SessionHookResult } from '@/lib/session/session-context' export function getBaseURL() { - let baseURL - - if (env.VERCEL_ENV === 'preview') { - baseURL = `https://${getEnv('NEXT_PUBLIC_VERCEL_URL')}` - } else if (env.VERCEL_ENV === 'development') { - baseURL = `https://${getEnv('NEXT_PUBLIC_VERCEL_URL')}` - } else if (env.VERCEL_ENV === 'production') { - baseURL = env.BETTER_AUTH_URL || getEnv('NEXT_PUBLIC_APP_URL') - } else if (env.NODE_ENV === 'development') { - baseURL = getEnv('NEXT_PUBLIC_APP_URL') || env.BETTER_AUTH_URL || 'http://localhost:3000' - } - - return baseURL + return getEnv('NEXT_PUBLIC_APP_URL') || 'http://localhost:3000' } export const client = createAuthClient({ diff --git a/apps/sim/lib/auth.ts b/apps/sim/lib/auth.ts index b83687ef81..5dd170ad91 100644 --- a/apps/sim/lib/auth.ts +++ b/apps/sim/lib/auth.ts @@ -63,7 +63,6 @@ export const auth = betterAuth({ baseURL: getBaseURL(), trustedOrigins: [ env.NEXT_PUBLIC_APP_URL, - ...(env.NEXT_PUBLIC_VERCEL_URL ? [`https://${env.NEXT_PUBLIC_VERCEL_URL}`] : []), ...(env.NEXT_PUBLIC_SOCKET_URL ? [env.NEXT_PUBLIC_SOCKET_URL] : []), ].filter(Boolean), database: drizzleAdapter(db, { diff --git a/apps/sim/lib/env.ts b/apps/sim/lib/env.ts index b44900203b..bc6c9c6b73 100644 --- a/apps/sim/lib/env.ts +++ b/apps/sim/lib/env.ts @@ -36,7 +36,6 @@ export const env = createEnv({ // Database & Storage - POSTGRES_URL: z.string().url().optional(), // Alternative PostgreSQL connection string REDIS_URL: z.string().url().optional(), // Redis connection string for caching/sessions // Payment & Billing @@ -99,10 +98,10 @@ export const env = createEnv({ // Infrastructure & Deployment NEXT_RUNTIME: z.string().optional(), // Next.js runtime environment - VERCEL_ENV: z.string().optional(), // Vercel deployment environment DOCKER_BUILD: z.boolean().optional(), // Flag indicating Docker build environment // Background Jobs & Scheduling + TRIGGER_PROJECT_ID: z.string().optional(), // Trigger.dev project ID TRIGGER_SECRET_KEY: z.string().min(1).optional(), // Trigger.dev secret key for background jobs TRIGGER_DEV_ENABLED: z.boolean().optional(), // Toggle to enable/disable Trigger.dev for async jobs CRON_SECRET: z.string().optional(), // Secret for authenticating cron job requests @@ -243,7 +242,6 @@ export const env = createEnv({ client: { // Core Application URLs - Required for frontend functionality NEXT_PUBLIC_APP_URL: z.string().url(), // Base URL of the application (e.g., https://app.sim.ai) - NEXT_PUBLIC_VERCEL_URL: z.string().optional(), // Vercel deployment URL for preview/production // Client-side Services NEXT_PUBLIC_SOCKET_URL: z.string().url().optional(), // WebSocket server URL for real-time features @@ -295,7 +293,6 @@ export const env = createEnv({ experimental__runtimeEnv: { NEXT_PUBLIC_APP_URL: process.env.NEXT_PUBLIC_APP_URL, - NEXT_PUBLIC_VERCEL_URL: process.env.NEXT_PUBLIC_VERCEL_URL, NEXT_PUBLIC_BLOB_BASE_URL: process.env.NEXT_PUBLIC_BLOB_BASE_URL, NEXT_PUBLIC_BILLING_ENABLED: process.env.NEXT_PUBLIC_BILLING_ENABLED, NEXT_PUBLIC_GOOGLE_CLIENT_ID: process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID, diff --git a/apps/sim/lib/security/csp.ts b/apps/sim/lib/security/csp.ts index a4d117b03a..d41d656778 100644 --- a/apps/sim/lib/security/csp.ts +++ b/apps/sim/lib/security/csp.ts @@ -38,14 +38,6 @@ export const buildTimeCSPDirectives: CSPDirectives = { "'unsafe-eval'", 'https://*.google.com', 'https://apis.google.com', - 'https://*.vercel-scripts.com', - 'https://*.vercel-insights.com', - 'https://vercel.live', - 'https://*.vercel.live', - 'https://vercel.com', - 'https://*.vercel.app', - 'https://vitals.vercel-insights.com', - 'https://b2bjsstore.s3.us-west-2.amazonaws.com', ], 'style-src': ["'self'", "'unsafe-inline'", 'https://fonts.googleapis.com'], @@ -90,8 +82,6 @@ export const buildTimeCSPDirectives: CSPDirectives = { env.NEXT_PUBLIC_SOCKET_URL || 'http://localhost:3002', env.NEXT_PUBLIC_SOCKET_URL?.replace('http://', 'ws://').replace('https://', 'wss://') || 'ws://localhost:3002', - 'https://*.up.railway.app', - 'wss://*.up.railway.app', 'https://api.browser-use.com', 'https://api.exa.ai', 'https://api.firecrawl.dev', @@ -99,16 +89,8 @@ export const buildTimeCSPDirectives: CSPDirectives = { 'https://*.amazonaws.com', 'https://*.s3.amazonaws.com', 'https://*.blob.core.windows.net', - 'https://*.vercel-insights.com', - 'https://vitals.vercel-insights.com', 'https://*.atlassian.com', 'https://*.supabase.co', - 'https://vercel.live', - 'https://*.vercel.live', - 'https://vercel.com', - 'https://*.vercel.app', - 'wss://*.vercel.app', - 'https://pro.ip-api.com', 'https://api.github.com', 'https://github.com/*', ...getHostnameFromUrl(env.NEXT_PUBLIC_BRAND_LOGO_URL), @@ -168,12 +150,12 @@ export function generateRuntimeCSP(): string { return ` default-src 'self'; - script-src 'self' 'unsafe-inline' 'unsafe-eval' https://*.google.com https://apis.google.com https://*.vercel-scripts.com https://*.vercel-insights.com https://vercel.live https://*.vercel.live https://vercel.com https://*.vercel.app https://vitals.vercel-insights.com https://b2bjsstore.s3.us-west-2.amazonaws.com; + script-src 'self' 'unsafe-inline' 'unsafe-eval' https://*.google.com https://apis.google.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: blob: https://*.googleusercontent.com https://*.google.com https://*.atlassian.com https://cdn.discordapp.com https://*.githubusercontent.com https://*.public.blob.vercel-storage.com ${brandLogoDomain} ${brandFaviconDomain}; media-src 'self' blob:; font-src 'self' https://fonts.gstatic.com; - connect-src 'self' ${appUrl} ${ollamaUrl} ${socketUrl} ${socketWsUrl} https://*.up.railway.app wss://*.up.railway.app https://api.browser-use.com https://api.exa.ai https://api.firecrawl.dev https://*.googleapis.com https://*.amazonaws.com https://*.s3.amazonaws.com https://*.blob.core.windows.net https://api.github.com https://github.com/* https://*.vercel-insights.com https://vitals.vercel-insights.com https://*.atlassian.com https://*.supabase.co https://vercel.live https://*.vercel.live https://vercel.com https://*.vercel.app wss://*.vercel.app https://pro.ip-api.com ${dynamicDomainsStr}; + connect-src 'self' ${appUrl} ${ollamaUrl} ${socketUrl} ${socketWsUrl} https://api.browser-use.com https://api.exa.ai https://api.firecrawl.dev https://*.googleapis.com https://*.amazonaws.com https://*.s3.amazonaws.com https://*.blob.core.windows.net https://api.github.com https://github.com/* https://*.atlassian.com https://*.supabase.co ${dynamicDomainsStr}; frame-src https://drive.google.com https://docs.google.com https://*.google.com; frame-ancestors 'self'; form-action 'self'; diff --git a/apps/sim/lib/urls/utils.ts b/apps/sim/lib/urls/utils.ts index 217426dd2a..3b572a079a 100644 --- a/apps/sim/lib/urls/utils.ts +++ b/apps/sim/lib/urls/utils.ts @@ -6,7 +6,7 @@ import { isProd } from '@/lib/environment' * @returns The base URL string (e.g., 'http://localhost:3000' or 'https://example.com') */ export function getBaseUrl(): string { - if (typeof window !== 'undefined') { + if (typeof window !== 'undefined' && window.location?.origin) { return window.location.origin } diff --git a/apps/sim/lib/workflows/autolayout/containers.ts b/apps/sim/lib/workflows/autolayout/containers.ts index 1c89a65805..6370635222 100644 --- a/apps/sim/lib/workflows/autolayout/containers.ts +++ b/apps/sim/lib/workflows/autolayout/containers.ts @@ -3,7 +3,12 @@ import type { BlockState } from '@/stores/workflows/workflow/types' import { assignLayers, groupByLayer } from './layering' import { calculatePositions } from './positioning' import type { Edge, LayoutOptions } from './types' -import { DEFAULT_CONTAINER_HEIGHT, DEFAULT_CONTAINER_WIDTH, getBlocksByParent } from './utils' +import { + DEFAULT_CONTAINER_HEIGHT, + DEFAULT_CONTAINER_WIDTH, + getBlocksByParent, + prepareBlockMetrics, +} from './utils' const logger = createLogger('AutoLayout:Containers') @@ -45,6 +50,7 @@ export function layoutContainers( } const childNodes = assignLayers(childBlocks, childEdges) + prepareBlockMetrics(childNodes) const childLayers = groupByLayer(childNodes) calculatePositions(childLayers, containerOptions) @@ -57,8 +63,8 @@ export function layoutContainers( for (const node of childNodes.values()) { minX = Math.min(minX, node.position.x) minY = Math.min(minY, node.position.y) - maxX = Math.max(maxX, node.position.x + node.dimensions.width) - maxY = Math.max(maxY, node.position.y + node.dimensions.height) + maxX = Math.max(maxX, node.position.x + node.metrics.width) + maxY = Math.max(maxY, node.position.y + node.metrics.height) } // Adjust all child positions to start at proper padding from container edges diff --git a/apps/sim/lib/workflows/autolayout/incremental.ts b/apps/sim/lib/workflows/autolayout/incremental.ts index 8a8a5ba9de..c56c4f0663 100644 --- a/apps/sim/lib/workflows/autolayout/incremental.ts +++ b/apps/sim/lib/workflows/autolayout/incremental.ts @@ -1,7 +1,7 @@ import { createLogger } from '@/lib/logs/console/logger' import type { BlockState } from '@/stores/workflows/workflow/types' import type { AdjustmentOptions, Edge } from './types' -import { boxesOverlap, createBoundingBox, getBlockDimensions } from './utils' +import { boxesOverlap, createBoundingBox, getBlockMetrics } from './utils' const logger = createLogger('AutoLayout:Incremental') @@ -70,8 +70,8 @@ export function adjustForNewBlock( }) } - const newBlockDims = getBlockDimensions(newBlock) - const newBlockBox = createBoundingBox(newBlock.position, newBlockDims) + const newBlockMetrics = getBlockMetrics(newBlock) + const newBlockBox = createBoundingBox(newBlock.position, newBlockMetrics) const blocksToShift: Array<{ block: BlockState; shiftAmount: number }> = [] @@ -80,11 +80,11 @@ export function adjustForNewBlock( if (block.data?.parentId) continue if (block.position.x >= newBlock.position.x) { - const blockDims = getBlockDimensions(block) - const blockBox = createBoundingBox(block.position, blockDims) + const blockMetrics = getBlockMetrics(block) + const blockBox = createBoundingBox(block.position, blockMetrics) if (boxesOverlap(newBlockBox, blockBox, 50)) { - const requiredShift = newBlock.position.x + newBlockDims.width + 50 - block.position.x + const requiredShift = newBlock.position.x + newBlockMetrics.width + 50 - block.position.x if (requiredShift > 0) { blocksToShift.push({ block, shiftAmount: requiredShift }) } @@ -115,8 +115,8 @@ export function compactHorizontally(blocks: Record, edges: E const prevBlock = blockArray[i - 1] const currentBlock = blockArray[i] - const prevDims = getBlockDimensions(prevBlock) - const expectedX = prevBlock.position.x + prevDims.width + MIN_SPACING + const prevMetrics = getBlockMetrics(prevBlock) + const expectedX = prevBlock.position.x + prevMetrics.width + MIN_SPACING if (currentBlock.position.x > expectedX + 150) { const shift = currentBlock.position.x - expectedX diff --git a/apps/sim/lib/workflows/autolayout/index.ts b/apps/sim/lib/workflows/autolayout/index.ts index fb9b423632..a13ad8a42f 100644 --- a/apps/sim/lib/workflows/autolayout/index.ts +++ b/apps/sim/lib/workflows/autolayout/index.ts @@ -5,7 +5,7 @@ import { adjustForNewBlock as adjustForNewBlockInternal, compactHorizontally } f import { assignLayers, groupByLayer } from './layering' import { calculatePositions } from './positioning' import type { AdjustmentOptions, Edge, LayoutOptions, LayoutResult, Loop, Parallel } from './types' -import { getBlocksByParent } from './utils' +import { getBlocksByParent, prepareBlockMetrics } from './utils' const logger = createLogger('AutoLayout') @@ -39,6 +39,7 @@ export function applyAutoLayout( if (Object.keys(rootBlocks).length > 0) { const nodes = assignLayers(rootBlocks, rootEdges) + prepareBlockMetrics(nodes) const layers = groupByLayer(nodes) calculatePositions(layers, options) @@ -99,4 +100,4 @@ export function adjustForNewBlock( } export type { LayoutOptions, LayoutResult, AdjustmentOptions, Edge, Loop, Parallel } -export { getBlockDimensions, isContainerType } from './utils' +export { getBlockMetrics, isContainerType } from './utils' diff --git a/apps/sim/lib/workflows/autolayout/layering.ts b/apps/sim/lib/workflows/autolayout/layering.ts index 10ad8b3178..59a1d14bf0 100644 --- a/apps/sim/lib/workflows/autolayout/layering.ts +++ b/apps/sim/lib/workflows/autolayout/layering.ts @@ -1,7 +1,7 @@ import { createLogger } from '@/lib/logs/console/logger' import type { BlockState } from '@/stores/workflows/workflow/types' import type { Edge, GraphNode } from './types' -import { getBlockDimensions, isStarterBlock } from './utils' +import { getBlockMetrics } from './utils' const logger = createLogger('AutoLayout:Layering') @@ -15,7 +15,7 @@ export function assignLayers( nodes.set(id, { id, block, - dimensions: getBlockDimensions(block), + metrics: getBlockMetrics(block), incoming: new Set(), outgoing: new Set(), layer: 0, @@ -33,9 +33,9 @@ export function assignLayers( } } - const starterNodes = Array.from(nodes.values()).filter( - (node) => node.incoming.size === 0 || isStarterBlock(node.block) - ) + // Only treat blocks as starters if they have no incoming edges + // This prevents triggers that are mid-flow from being forced to layer 0 + const starterNodes = Array.from(nodes.values()).filter((node) => node.incoming.size === 0) if (starterNodes.length === 0 && nodes.size > 0) { const firstNode = Array.from(nodes.values())[0] @@ -43,35 +43,50 @@ export function assignLayers( logger.warn('No starter blocks found, using first block as starter', { blockId: firstNode.id }) } - const visited = new Set() - const queue: Array<{ nodeId: string; layer: number }> = [] + // Use topological sort to ensure proper layering based on dependencies + // Each node's layer = max(all incoming nodes' layers) + 1 + const inDegreeCount = new Map() - for (const starter of starterNodes) { - starter.layer = 0 - queue.push({ nodeId: starter.id, layer: 0 }) + for (const node of nodes.values()) { + inDegreeCount.set(node.id, node.incoming.size) + if (starterNodes.includes(node)) { + node.layer = 0 + } } - while (queue.length > 0) { - const { nodeId, layer } = queue.shift()! - - if (visited.has(nodeId)) { - continue - } + const queue: string[] = starterNodes.map((n) => n.id) + const processed = new Set() - visited.add(nodeId) + while (queue.length > 0) { + const nodeId = queue.shift()! const node = nodes.get(nodeId)! - node.layer = Math.max(node.layer, layer) + processed.add(nodeId) + + // Calculate this node's layer based on all incoming edges + if (node.incoming.size > 0) { + let maxIncomingLayer = -1 + for (const incomingId of node.incoming) { + const incomingNode = nodes.get(incomingId) + if (incomingNode) { + maxIncomingLayer = Math.max(maxIncomingLayer, incomingNode.layer) + } + } + node.layer = maxIncomingLayer + 1 + } + // Add outgoing nodes to queue when all their dependencies are processed for (const targetId of node.outgoing) { - const targetNode = nodes.get(targetId) - if (targetNode) { - queue.push({ nodeId: targetId, layer: layer + 1 }) + const currentCount = inDegreeCount.get(targetId) || 0 + inDegreeCount.set(targetId, currentCount - 1) + + if (inDegreeCount.get(targetId) === 0 && !processed.has(targetId)) { + queue.push(targetId) } } } for (const node of nodes.values()) { - if (!visited.has(node.id)) { + if (!processed.has(node.id)) { logger.debug('Isolated node detected, assigning to layer 0', { blockId: node.id }) node.layer = 0 } diff --git a/apps/sim/lib/workflows/autolayout/positioning.ts b/apps/sim/lib/workflows/autolayout/positioning.ts index 1a1c1981cf..741d1197e5 100644 --- a/apps/sim/lib/workflows/autolayout/positioning.ts +++ b/apps/sim/lib/workflows/autolayout/positioning.ts @@ -26,7 +26,7 @@ export function calculatePositions( // Calculate total height needed for this layer const totalHeight = nodesInLayer.reduce( - (sum, node, idx) => sum + node.dimensions.height + (idx > 0 ? verticalSpacing : 0), + (sum, node, idx) => sum + node.metrics.height + (idx > 0 ? verticalSpacing : 0), 0 ) @@ -55,7 +55,7 @@ export function calculatePositions( y: yOffset, } - yOffset += node.dimensions.height + verticalSpacing + yOffset += node.metrics.height + verticalSpacing } } @@ -83,8 +83,8 @@ function resolveOverlaps(nodes: GraphNode[], verticalSpacing: number): void { const node1 = sortedNodes[i] const node2 = sortedNodes[j] - const box1 = createBoundingBox(node1.position, node1.dimensions) - const box2 = createBoundingBox(node2.position, node2.dimensions) + const box1 = createBoundingBox(node1.position, node1.metrics) + const box2 = createBoundingBox(node2.position, node2.metrics) // Check for overlap with margin if (boxesOverlap(box1, box2, 30)) { @@ -92,11 +92,11 @@ function resolveOverlaps(nodes: GraphNode[], verticalSpacing: number): void { // If in same layer, shift vertically if (node1.layer === node2.layer) { - const totalHeight = node1.dimensions.height + node2.dimensions.height + verticalSpacing + const totalHeight = node1.metrics.height + node2.metrics.height + verticalSpacing const midpoint = (node1.position.y + node2.position.y) / 2 - node1.position.y = midpoint - node1.dimensions.height / 2 - verticalSpacing / 2 - node2.position.y = midpoint + node2.dimensions.height / 2 + verticalSpacing / 2 + node1.position.y = midpoint - node1.metrics.height / 2 - verticalSpacing / 2 + node2.position.y = midpoint + node2.metrics.height / 2 + verticalSpacing / 2 } else { // Different layers - shift the later one down const requiredSpace = box1.y + box1.height + verticalSpacing diff --git a/apps/sim/lib/workflows/autolayout/types.ts b/apps/sim/lib/workflows/autolayout/types.ts index aec8d11ea7..5dd3930c71 100644 --- a/apps/sim/lib/workflows/autolayout/types.ts +++ b/apps/sim/lib/workflows/autolayout/types.ts @@ -35,9 +35,15 @@ export interface Parallel { parallelType?: 'count' | 'collection' } -export interface BlockDimensions { +export interface BlockMetrics { width: number height: number + minWidth: number + minHeight: number + paddingTop: number + paddingBottom: number + paddingLeft: number + paddingRight: number } export interface BoundingBox { @@ -55,7 +61,7 @@ export interface LayerInfo { export interface GraphNode { id: string block: BlockState - dimensions: BlockDimensions + metrics: BlockMetrics incoming: Set outgoing: Set layer: number diff --git a/apps/sim/lib/workflows/autolayout/utils.ts b/apps/sim/lib/workflows/autolayout/utils.ts index 87cc71cd52..199bce820e 100644 --- a/apps/sim/lib/workflows/autolayout/utils.ts +++ b/apps/sim/lib/workflows/autolayout/utils.ts @@ -1,33 +1,85 @@ +import { TriggerUtils } from '@/lib/workflows/triggers' import type { BlockState } from '@/stores/workflows/workflow/types' -import type { BlockDimensions, BoundingBox } from './types' +import type { BlockMetrics, BoundingBox, GraphNode } from './types' export const DEFAULT_BLOCK_WIDTH = 350 export const DEFAULT_BLOCK_WIDTH_WIDE = 480 export const DEFAULT_BLOCK_HEIGHT = 100 export const DEFAULT_CONTAINER_WIDTH = 500 export const DEFAULT_CONTAINER_HEIGHT = 300 +const DEFAULT_PADDING = 40 + +function resolveNumeric(value: number | undefined, fallback: number): number { + return typeof value === 'number' && Number.isFinite(value) ? value : fallback +} export function isContainerType(blockType: string): boolean { return blockType === 'loop' || blockType === 'parallel' } -export function getBlockDimensions(block: BlockState): BlockDimensions { - if (isContainerType(block.type)) { - return { - width: block.data?.width ? Math.max(block.data.width, 400) : DEFAULT_CONTAINER_WIDTH, - height: block.data?.height ? Math.max(block.data.height, 200) : DEFAULT_CONTAINER_HEIGHT, - } +function getContainerMetrics(block: BlockState): BlockMetrics { + const measuredWidth = block.layout?.measuredWidth + const measuredHeight = block.layout?.measuredHeight + + const containerWidth = Math.max( + measuredWidth ?? 0, + resolveNumeric(block.data?.width, DEFAULT_CONTAINER_WIDTH) + ) + const containerHeight = Math.max( + measuredHeight ?? 0, + resolveNumeric(block.data?.height, DEFAULT_CONTAINER_HEIGHT) + ) + + return { + width: containerWidth, + height: containerHeight, + minWidth: DEFAULT_CONTAINER_WIDTH, + minHeight: DEFAULT_CONTAINER_HEIGHT, + paddingTop: DEFAULT_PADDING, + paddingBottom: DEFAULT_PADDING, + paddingLeft: DEFAULT_PADDING, + paddingRight: DEFAULT_PADDING, } +} + +function getRegularBlockMetrics(block: BlockState): BlockMetrics { + const minWidth = block.isWide ? DEFAULT_BLOCK_WIDTH_WIDE : DEFAULT_BLOCK_WIDTH + const minHeight = DEFAULT_BLOCK_HEIGHT + const measuredH = block.layout?.measuredHeight ?? block.height + const measuredW = block.layout?.measuredWidth + + const width = Math.max(measuredW ?? minWidth, minWidth) + const height = Math.max(measuredH ?? minHeight, minHeight) return { - width: block.isWide ? DEFAULT_BLOCK_WIDTH_WIDE : DEFAULT_BLOCK_WIDTH, - height: Math.max(block.height || DEFAULT_BLOCK_HEIGHT, DEFAULT_BLOCK_HEIGHT), + width, + height, + minWidth, + minHeight, + paddingTop: DEFAULT_PADDING, + paddingBottom: DEFAULT_PADDING, + paddingLeft: DEFAULT_PADDING, + paddingRight: DEFAULT_PADDING, + } +} + +export function getBlockMetrics(block: BlockState): BlockMetrics { + if (isContainerType(block.type)) { + return getContainerMetrics(block) + } + + return getRegularBlockMetrics(block) +} + +export function prepareBlockMetrics(nodes: Map): void { + for (const node of nodes.values()) { + node.metrics = getBlockMetrics(node.block) } } export function createBoundingBox( position: { x: number; y: number }, - dimensions: BlockDimensions + dimensions: Pick ): BoundingBox { return { x: position.x, @@ -70,5 +122,9 @@ export function getBlocksByParent(blocks: Record): { } export function isStarterBlock(block: BlockState): boolean { - return block.type === 'starter' || block.type === 'webhook' || block.type === 'schedule' + if (TriggerUtils.isTriggerBlock({ type: block.type, triggerMode: block.triggerMode })) { + return true + } + + return false } diff --git a/apps/sim/lib/workflows/db-helpers.ts b/apps/sim/lib/workflows/db-helpers.ts index b81b73c80b..b4f6f75897 100644 --- a/apps/sim/lib/workflows/db-helpers.ts +++ b/apps/sim/lib/workflows/db-helpers.ts @@ -36,10 +36,34 @@ export interface NormalizedWorkflowData { isFromNormalizedTables: boolean // Flag to indicate source (true = normalized tables, false = deployed state) } -/** - * Load deployed workflow state for execution - * Returns deployed state if available, otherwise throws error - */ +export async function blockExistsInDeployment( + workflowId: string, + blockId: string +): Promise { + try { + const [result] = await db + .select({ state: workflowDeploymentVersion.state }) + .from(workflowDeploymentVersion) + .where( + and( + eq(workflowDeploymentVersion.workflowId, workflowId), + eq(workflowDeploymentVersion.isActive, true) + ) + ) + .limit(1) + + if (!result?.state) { + return false + } + + const state = result.state as WorkflowState + return !!state.blocks?.[blockId] + } catch (error) { + logger.error(`Error checking block ${blockId} in deployment for workflow ${workflowId}:`, error) + return false + } +} + export async function loadDeployedWorkflowState( workflowId: string ): Promise { diff --git a/apps/sim/lib/workflows/references.ts b/apps/sim/lib/workflows/references.ts new file mode 100644 index 0000000000..d8c8f69698 --- /dev/null +++ b/apps/sim/lib/workflows/references.ts @@ -0,0 +1,73 @@ +import { normalizeBlockName } from '@/stores/workflows/utils' + +export const SYSTEM_REFERENCE_PREFIXES = new Set(['start', 'loop', 'parallel', 'variable']) + +const INVALID_REFERENCE_CHARS = /[+*/=<>!]/ + +export function isLikelyReferenceSegment(segment: string): boolean { + if (!segment.startsWith('<') || !segment.endsWith('>')) { + return false + } + + const inner = segment.slice(1, -1) + + if (inner.startsWith(' ')) { + return false + } + + if (inner.match(/^\s*[<>=!]+\s*$/) || inner.match(/\s[<>=!]+\s/)) { + return false + } + + if (inner.match(/^[<>=!]+\s/)) { + return false + } + + if (inner.includes('.')) { + const dotIndex = inner.indexOf('.') + const beforeDot = inner.substring(0, dotIndex) + const afterDot = inner.substring(dotIndex + 1) + + if (afterDot.includes(' ')) { + return false + } + + if (INVALID_REFERENCE_CHARS.test(beforeDot) || INVALID_REFERENCE_CHARS.test(afterDot)) { + return false + } + } else if (INVALID_REFERENCE_CHARS.test(inner) || inner.match(/^\d/) || inner.match(/\s\d/)) { + return false + } + + return true +} + +export function extractReferencePrefixes(value: string): Array<{ raw: string; prefix: string }> { + if (!value || typeof value !== 'string') { + return [] + } + + const matches = value.match(/<[^>]+>/g) + if (!matches) { + return [] + } + + const references: Array<{ raw: string; prefix: string }> = [] + + for (const match of matches) { + if (!isLikelyReferenceSegment(match)) { + continue + } + + const inner = match.slice(1, -1) + const [rawPrefix] = inner.split('.') + if (!rawPrefix) { + continue + } + + const normalized = normalizeBlockName(rawPrefix) + references.push({ raw: match, prefix: normalized }) + } + + return references +} diff --git a/apps/sim/middleware.ts b/apps/sim/middleware.ts index 52ef03444f..96b9c900bb 100644 --- a/apps/sim/middleware.ts +++ b/apps/sim/middleware.ts @@ -147,6 +147,10 @@ export async function middleware(request: NextRequest) { return NextResponse.next() } + if (url.pathname.startsWith('/chat/')) { + return NextResponse.next() + } + if (url.pathname.startsWith('/workspace')) { if (!hasActiveSession) { return NextResponse.redirect(new URL('/login', request.url)) diff --git a/apps/sim/serializer/index.ts b/apps/sim/serializer/index.ts index ba5a65e2a5..e4a019bf9d 100644 --- a/apps/sim/serializer/index.ts +++ b/apps/sim/serializer/index.ts @@ -1,4 +1,5 @@ import type { Edge } from 'reactflow' +import { BlockPathCalculator } from '@/lib/block-path-calculator' import { createLogger } from '@/lib/logs/console/logger' import { getBlock } from '@/blocks' import type { SubBlockConfig } from '@/blocks/types' @@ -44,22 +45,36 @@ export class Serializer { parallels?: Record, validateRequired = false ): SerializedWorkflow { - // Validate subflow requirements (loops/parallels) before serialization if requested + const safeLoops = loops || {} + const safeParallels = parallels || {} + const accessibleBlocksMap = this.computeAccessibleBlockIds( + blocks, + edges, + safeLoops, + safeParallels + ) + if (validateRequired) { - this.validateSubflowsBeforeExecution(blocks, loops || {}, parallels || {}) + this.validateSubflowsBeforeExecution(blocks, safeLoops, safeParallels) } return { version: '1.0', - blocks: Object.values(blocks).map((block) => this.serializeBlock(block, validateRequired)), + blocks: Object.values(blocks).map((block) => + this.serializeBlock(block, { + validateRequired, + allBlocks: blocks, + accessibleBlocksMap, + }) + ), connections: edges.map((edge) => ({ source: edge.source, target: edge.target, sourceHandle: edge.sourceHandle || undefined, targetHandle: edge.targetHandle || undefined, })), - loops, - parallels, + loops: safeLoops, + parallels: safeParallels, } } @@ -156,7 +171,14 @@ export class Serializer { }) } - private serializeBlock(block: BlockState, validateRequired = false): SerializedBlock { + private serializeBlock( + block: BlockState, + options: { + validateRequired: boolean + allBlocks: Record + accessibleBlocksMap: Map> + } + ): SerializedBlock { // Special handling for subflow blocks (loops, parallels, etc.) if (block.type === 'loop' || block.type === 'parallel') { return { @@ -197,7 +219,7 @@ export class Serializer { } // Validate required fields that only users can provide (before execution starts) - if (validateRequired) { + if (options.validateRequired) { this.validateRequiredFieldsBeforeExecution(block, blockConfig, params) } @@ -541,6 +563,46 @@ export class Serializer { } } + private computeAccessibleBlockIds( + blocks: Record, + edges: Edge[], + loops: Record, + parallels: Record + ): Map> { + const accessibleMap = new Map>() + const simplifiedEdges = edges.map((edge) => ({ source: edge.source, target: edge.target })) + + const starterBlock = Object.values(blocks).find((block) => block.type === 'starter') + + Object.keys(blocks).forEach((blockId) => { + const ancestorIds = BlockPathCalculator.findAllPathNodes(simplifiedEdges, blockId) + const accessibleIds = new Set(ancestorIds) + accessibleIds.add(blockId) + + if (starterBlock) { + accessibleIds.add(starterBlock.id) + } + + Object.values(loops).forEach((loop) => { + if (!loop?.nodes) return + if (loop.nodes.includes(blockId)) { + loop.nodes.forEach((nodeId) => accessibleIds.add(nodeId)) + } + }) + + Object.values(parallels).forEach((parallel) => { + if (!parallel?.nodes) return + if (parallel.nodes.includes(blockId)) { + parallel.nodes.forEach((nodeId) => accessibleIds.add(nodeId)) + } + }) + + accessibleMap.set(blockId, accessibleIds) + }) + + return accessibleMap + } + deserializeWorkflow(workflow: SerializedWorkflow): { blocks: Record edges: Edge[] diff --git a/apps/sim/socket-server/config/socket.ts b/apps/sim/socket-server/config/socket.ts index eeb36dbdb7..3f6f4a7c17 100644 --- a/apps/sim/socket-server/config/socket.ts +++ b/apps/sim/socket-server/config/socket.ts @@ -12,7 +12,6 @@ const logger = createLogger('SocketIOConfig') function getAllowedOrigins(): string[] { const allowedOrigins = [ env.NEXT_PUBLIC_APP_URL, - env.NEXT_PUBLIC_VERCEL_URL, 'http://localhost:3000', 'http://localhost:3001', ...(env.ALLOWED_ORIGINS?.split(',') || []), diff --git a/apps/sim/socket-server/database/operations.ts b/apps/sim/socket-server/database/operations.ts index 0731b44fa6..388d6610a3 100644 --- a/apps/sim/socket-server/database/operations.ts +++ b/apps/sim/socket-server/database/operations.ts @@ -9,8 +9,7 @@ import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/db-helpers' const logger = createLogger('SocketDatabase') -// Create dedicated database connection for socket server with optimized settings -const connectionString = env.POSTGRES_URL ?? env.DATABASE_URL +const connectionString = env.DATABASE_URL const socketDb = drizzle( postgres(connectionString, { prepare: false, diff --git a/apps/sim/socket-server/rooms/manager.ts b/apps/sim/socket-server/rooms/manager.ts index 42c752e29c..b36ac85a77 100644 --- a/apps/sim/socket-server/rooms/manager.ts +++ b/apps/sim/socket-server/rooms/manager.ts @@ -7,8 +7,7 @@ import type { Server } from 'socket.io' import { env } from '@/lib/env' import { createLogger } from '@/lib/logs/console/logger' -// Create dedicated database connection for room manager -const connectionString = env.POSTGRES_URL ?? env.DATABASE_URL +const connectionString = env.DATABASE_URL const db = drizzle( postgres(connectionString, { prepare: false, diff --git a/apps/sim/socket-server/routes/http.ts b/apps/sim/socket-server/routes/http.ts index 2a3cbd958a..0a404abfe6 100644 --- a/apps/sim/socket-server/routes/http.ts +++ b/apps/sim/socket-server/routes/http.ts @@ -16,7 +16,6 @@ interface Logger { */ export function createHttpHandler(roomManager: RoomManager, logger: Logger) { return (req: IncomingMessage, res: ServerResponse) => { - // Handle health check for Railway if (req.method === 'GET' && req.url === '/health') { res.writeHead(200, { 'Content-Type': 'application/json' }) res.end( diff --git a/apps/sim/stores/workflows/workflow/store.ts b/apps/sim/stores/workflows/workflow/store.ts index cd99abec05..8d8a4663da 100644 --- a/apps/sim/stores/workflows/workflow/store.ts +++ b/apps/sim/stores/workflows/workflow/store.ts @@ -185,6 +185,7 @@ export const useWorkflowStore = create()( advancedMode: blockProperties?.advancedMode ?? false, triggerMode: blockProperties?.triggerMode ?? false, height: blockProperties?.height ?? 0, + layout: {}, data: nodeData, }, }, @@ -233,6 +234,11 @@ export const useWorkflowStore = create()( width: dimensions.width, height: dimensions.height, }, + layout: { + ...block.layout, + measuredWidth: dimensions.width, + measuredHeight: dimensions.height, + }, }, }, edges: [...state.edges], @@ -786,20 +792,33 @@ export const useWorkflowStore = create()( // Note: Socket.IO handles real-time sync automatically }, - updateBlockHeight: (id: string, height: number) => { - set((state) => ({ - blocks: { - ...state.blocks, - [id]: { - ...state.blocks[id], - height, + updateBlockLayoutMetrics: (id: string, dimensions: { width: number; height: number }) => { + set((state) => { + const block = state.blocks[id] + if (!block) { + logger.warn(`Cannot update layout metrics: Block ${id} not found in workflow store`) + return state + } + + return { + blocks: { + ...state.blocks, + [id]: { + ...block, + height: dimensions.height, + layout: { + ...block.layout, + measuredWidth: dimensions.width, + measuredHeight: dimensions.height, + }, + }, }, - }, - edges: [...state.edges], - loops: { ...state.loops }, - })) + edges: [...state.edges], + loops: { ...state.loops }, + } + }) get().updateLastSaved() - // No sync needed for height changes, just visual + // No sync needed for layout changes, just visual }, updateLoopCount: (loopId: string, count: number) => diff --git a/apps/sim/stores/workflows/workflow/types.ts b/apps/sim/stores/workflows/workflow/types.ts index dfbd998c45..a9681d1301 100644 --- a/apps/sim/stores/workflows/workflow/types.ts +++ b/apps/sim/stores/workflows/workflow/types.ts @@ -62,6 +62,11 @@ export interface BlockData { type?: string } +export interface BlockLayoutState { + measuredWidth?: number + measuredHeight?: number +} + export interface BlockState { id: string type: string @@ -76,6 +81,7 @@ export interface BlockState { advancedMode?: boolean triggerMode?: boolean data?: BlockData + layout?: BlockLayoutState } export interface SubBlockState { @@ -197,7 +203,7 @@ export interface WorkflowActions { setBlockWide: (id: string, isWide: boolean) => void setBlockAdvancedMode: (id: string, advancedMode: boolean) => void setBlockTriggerMode: (id: string, triggerMode: boolean) => void - updateBlockHeight: (id: string, height: number) => void + updateBlockLayoutMetrics: (id: string, dimensions: { width: number; height: number }) => void triggerUpdate: () => void updateLoopCount: (loopId: string, count: number) => void updateLoopType: (loopId: string, loopType: 'for' | 'forEach') => void diff --git a/apps/sim/trigger.config.ts b/apps/sim/trigger.config.ts index f370e6dacd..f42bd8cc83 100644 --- a/apps/sim/trigger.config.ts +++ b/apps/sim/trigger.config.ts @@ -1,10 +1,11 @@ import { defineConfig } from '@trigger.dev/sdk' +import { env } from './lib/env' export default defineConfig({ - project: 'proj_kufttkwzywcydwtccqhx', + project: env.TRIGGER_PROJECT_ID!, runtime: 'node', logLevel: 'log', - maxDuration: 180, + maxDuration: 600, retries: { enabledInDev: false, default: { diff --git a/apps/sim/vercel.json b/apps/sim/vercel.json deleted file mode 100644 index 38fb972e40..0000000000 --- a/apps/sim/vercel.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "crons": [ - { - "path": "/api/schedules/execute", - "schedule": "*/1 * * * *" - }, - { - "path": "/api/webhooks/poll/gmail", - "schedule": "*/1 * * * *" - }, - { - "path": "/api/webhooks/poll/outlook", - "schedule": "*/1 * * * *" - }, - { - "path": "/api/logs/cleanup", - "schedule": "0 0 * * *" - }, - { - "path": "/api/webhooks/cleanup/idempotency", - "schedule": "0 2 * * *" - } - ] -} diff --git a/bun.lock b/bun.lock index 76869ecb0b..2afd31f06f 100644 --- a/bun.lock +++ b/bun.lock @@ -6,7 +6,6 @@ "dependencies": { "@linear/sdk": "40.0.0", "@t3-oss/env-nextjs": "0.13.4", - "@vercel/analytics": "1.5.0", "drizzle-orm": "^0.44.5", "mongodb": "6.19.0", "postgres": "^3.4.5", diff --git a/docker/app.Dockerfile b/docker/app.Dockerfile index 1e20f6d7ed..a6d012f4ca 100644 --- a/docker/app.Dockerfile +++ b/docker/app.Dockerfile @@ -47,9 +47,7 @@ WORKDIR /app # Provide dummy database URLs during image build so server code that imports @sim/db # can be evaluated without crashing. Runtime environments should override these. ARG DATABASE_URL="postgresql://user:pass@localhost:5432/dummy" -ARG POSTGRES_URL="postgresql://user:pass@localhost:5432/dummy" ENV DATABASE_URL=${DATABASE_URL} -ENV POSTGRES_URL=${POSTGRES_URL} RUN bun run build diff --git a/package.json b/package.json index bfc718474e..cedfa906de 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,6 @@ "dependencies": { "@linear/sdk": "40.0.0", "@t3-oss/env-nextjs": "0.13.4", - "@vercel/analytics": "1.5.0", "drizzle-orm": "^0.44.5", "mongodb": "6.19.0", "postgres": "^3.4.5", diff --git a/packages/db/drizzle.config.ts b/packages/db/drizzle.config.ts index d0cce828f7..ab26be6d82 100644 --- a/packages/db/drizzle.config.ts +++ b/packages/db/drizzle.config.ts @@ -5,6 +5,6 @@ export default { out: './migrations', dialect: 'postgresql', dbCredentials: { - url: process.env.DATABASE_URL || process.env.POSTGRES_URL || '', + url: process.env.DATABASE_URL!, }, } satisfies Config diff --git a/packages/db/index.ts b/packages/db/index.ts index 99853c6c7b..d53999e083 100644 --- a/packages/db/index.ts +++ b/packages/db/index.ts @@ -2,36 +2,19 @@ import { drizzle, type PostgresJsDatabase } from 'drizzle-orm/postgres-js' import postgres from 'postgres' import * as schema from './schema' -// Re-export everything from schema for type consistency export * from './schema' export type { PostgresJsDatabase } -// In production, use the Vercel-generated POSTGRES_URL -// In development, use the direct DATABASE_URL -const connectionString = process.env.POSTGRES_URL ?? process.env.DATABASE_URL ?? '' +const connectionString = process.env.DATABASE_URL! if (!connectionString) { - throw new Error('Missing POSTGRES_URL or DATABASE_URL environment variable') + throw new Error('Missing DATABASE_URL environment variable') } -/** - * Connection Pool Allocation Strategy - * - * Main App: 60 connections per instance - * Socket Server: 25 connections (operations) + 5 connections (room manager) = 30 total - * - * With ~3-4 Vercel serverless instances typically active: - * - Main app: 60 × 4 = 240 connections - * - Socket server: 30 connections total - * - Buffer: 130 connections - * - Total: ~400 connections - * - Supabase limit: 400 connections (16XL instance direct connection pool) - */ - const postgresClient = postgres(connectionString, { prepare: false, idle_timeout: 20, connect_timeout: 30, - max: 60, + max: 80, onnotice: () => {}, }) diff --git a/packages/db/migrations/0095_cheerful_albert_cleary.sql b/packages/db/migrations/0095_cheerful_albert_cleary.sql index 31ab8cb390..58669d737f 100644 --- a/packages/db/migrations/0095_cheerful_albert_cleary.sql +++ b/packages/db/migrations/0095_cheerful_albert_cleary.sql @@ -1,4 +1,4 @@ -CREATE TABLE "sso_provider" ( +CREATE TABLE IF NOT EXISTS "sso_provider" ( "id" text PRIMARY KEY NOT NULL, "issuer" text NOT NULL, "domain" text NOT NULL, @@ -9,9 +9,19 @@ CREATE TABLE "sso_provider" ( "organization_id" text ); --> statement-breakpoint -ALTER TABLE "sso_provider" ADD CONSTRAINT "sso_provider_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint -ALTER TABLE "sso_provider" ADD CONSTRAINT "sso_provider_organization_id_organization_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."organization"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint -CREATE INDEX "sso_provider_provider_id_idx" ON "sso_provider" USING btree ("provider_id");--> statement-breakpoint -CREATE INDEX "sso_provider_domain_idx" ON "sso_provider" USING btree ("domain");--> statement-breakpoint -CREATE INDEX "sso_provider_user_id_idx" ON "sso_provider" USING btree ("user_id");--> statement-breakpoint -CREATE INDEX "sso_provider_organization_id_idx" ON "sso_provider" USING btree ("organization_id"); \ No newline at end of file +DO $$ BEGIN + ALTER TABLE "sso_provider" ADD CONSTRAINT "sso_provider_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "sso_provider" ADD CONSTRAINT "sso_provider_organization_id_organization_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."organization"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "sso_provider_provider_id_idx" ON "sso_provider" USING btree ("provider_id");--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "sso_provider_domain_idx" ON "sso_provider" USING btree ("domain");--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "sso_provider_user_id_idx" ON "sso_provider" USING btree ("user_id");--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "sso_provider_organization_id_idx" ON "sso_provider" USING btree ("organization_id"); \ No newline at end of file diff --git a/packages/db/scripts/register-sso-provider.ts b/packages/db/scripts/register-sso-provider.ts index a978e024d0..3b09c8fd26 100644 --- a/packages/db/scripts/register-sso-provider.ts +++ b/packages/db/scripts/register-sso-provider.ts @@ -134,9 +134,9 @@ const logger = { } // Get database URL from environment -const CONNECTION_STRING = process.env.POSTGRES_URL ?? process.env.DATABASE_URL +const CONNECTION_STRING = process.env.DATABASE_URL if (!CONNECTION_STRING) { - console.error('❌ POSTGRES_URL or DATABASE_URL environment variable is required') + console.error('❌ DATABASE_URL environment variable is required') process.exit(1) } diff --git a/railway.json b/railway.json deleted file mode 100644 index 62d6da767e..0000000000 --- a/railway.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "$schema": "https://railway.app/railway.schema.json", - "build": { - "builder": "NIXPACKS", - "buildCommand": "cd apps/sim && bun install --frozen-lockfile && bun run build" - }, - "deploy": { - "startCommand": "cd apps/sim && NODE_ENV=production bun run socket-server/index.ts", - "healthcheckPath": "/health", - "healthcheckTimeout": 300, - "restartPolicyType": "ON_FAILURE", - "restartPolicyMaxRetries": 10 - }, - "environments": { - "production": { - "variables": { - "NODE_ENV": "production" - } - } - } -}